Use hardware volume keys from anywhere inside Kore (#453)

* Allow the RemoteActivity and BaseMediaActivity to show a volume controller dialog when hardware volume keys are pressed
** The dialog handles the key events and makes callbacks to the activities to handle showing the dialog itself

* Minor improvements
** Make HighlightButton render in the IDE GUI Editor
** Minor code cleanup
** set currentActivePlayerId = -1; on playerOnStop in BaseMediaActivity to be more consistent

resolves xbmc/Kore#235
This commit is contained in:
Tamás Varga 2017-10-08 20:48:00 +02:00 committed by Synced Synapse
parent 8fb24ee806
commit 108fb88b9f
9 changed files with 400 additions and 65 deletions

View File

@ -28,20 +28,15 @@ import org.xbmc.kore.utils.UIUtils;
*/
public abstract class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
@Override
protected void onCreate(Bundle savedInstanceState) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
setTheme(UIUtils.getThemeResourceId(
prefs.getString(Settings.KEY_PREF_THEME, Settings.DEFAULT_PREF_THEME)));
super.onCreate(savedInstanceState);
}
@Override
public void onPause() {
super.onPause();
}
// @Override
// @Override
// public boolean onCreateOptionsMenu(Menu menu) {
// getMenuInflater().inflate(R.menu.global, menu);
// return super.onCreateOptionsMenu(menu);
@ -50,8 +45,8 @@ public abstract class BaseActivity extends AppCompatActivity {
// @Override
// public boolean onOptionsItemSelected(MenuItem item) {
// switch (item.getItemId()) {
// case R.id.action_settings:
// return true;
// case R.id.action_settings:
// return true;
// default:
// break;
// }

View File

@ -28,6 +28,7 @@ import android.support.v7.app.ActionBar;
import android.support.v7.widget.Toolbar;
import android.text.TextUtils;
import android.transition.TransitionInflater;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
@ -48,6 +49,9 @@ import org.xbmc.kore.jsonrpc.type.ListType;
import org.xbmc.kore.jsonrpc.type.PlayerType;
import org.xbmc.kore.ui.generic.NavigationDrawerFragment;
import org.xbmc.kore.ui.sections.remote.RemoteActivity;
import org.xbmc.kore.ui.volumecontrollers.OnHardwareVolumeKeyPressedCallback;
import org.xbmc.kore.ui.volumecontrollers.VolumeControllerDialogFragmentListener;
import org.xbmc.kore.ui.volumecontrollers.VolumeKeyActionHandler;
import org.xbmc.kore.ui.widgets.MediaProgressIndicator;
import org.xbmc.kore.ui.widgets.NowPlayingPanel;
import org.xbmc.kore.ui.widgets.VolumeLevelIndicator;
@ -63,7 +67,8 @@ public abstract class BaseMediaActivity extends BaseActivity
implements HostConnectionObserver.ApplicationEventsObserver,
HostConnectionObserver.PlayerEventsObserver,
NowPlayingPanel.OnPanelButtonsClickListener,
MediaProgressIndicator.OnProgressChangeListener {
MediaProgressIndicator.OnProgressChangeListener,
OnHardwareVolumeKeyPressedCallback {
private static final String TAG = LogUtils.makeLogTag(BaseMediaActivity.class);
private static final String NAVICON_ISARROW = "navstate";
@ -79,6 +84,7 @@ public abstract class BaseMediaActivity extends BaseActivity
private HostManager hostManager;
private HostConnectionObserver hostConnectionObserver;
private VolumeKeyActionHandler volumeKeyActionHandler;
private boolean showNowPlayingPanel;
@ -180,7 +186,6 @@ public abstract class BaseMediaActivity extends BaseActivity
.getBoolean(Settings.KEY_PREF_SHOW_NOW_PLAYING_PANEL,
Settings.DEFAULT_PREF_SHOW_NOW_PLAYING_PANEL);
if(showNowPlayingPanel) {
setupNowPlayingPanel();
} else {
@ -205,6 +210,30 @@ public abstract class BaseMediaActivity extends BaseActivity
hostConnectionObserver.unregisterPlayerObserver(this);
}
/**
* Override hardware volume keys and send to Kodi
*/
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (volumeKeyActionHandler == null) {
volumeKeyActionHandler = new VolumeKeyActionHandler(hostManager, this, this);
}
return volumeKeyActionHandler.handleDispatchKeyEvent(event) || super.dispatchKeyEvent(
event);
}
@Override
public void onHardwareVolumeKeyPressed() {
showVolumeChangeDialog();
}
private void showVolumeChangeDialog() {
VolumeControllerDialogFragmentListener volumeControllerDialogFragment =
new VolumeControllerDialogFragmentListener();
volumeControllerDialogFragment.show(getSupportFragmentManager(),
VolumeControllerDialogFragmentListener.class.getName());
}
public boolean getDrawerIndicatorIsArrow() {
return drawerIndicatorIsArrow;
}
@ -296,6 +325,7 @@ public abstract class BaseMediaActivity extends BaseActivity
@Override
public void playerOnStop() {
currentActivePlayerId = -1;
//We delay hiding the panel to prevent hiding the panel when playing
// the next item in a playlist
callbackHandler.removeCallbacks(hidePanelRunnable);

View File

@ -49,7 +49,6 @@ import org.xbmc.kore.jsonrpc.method.Player;
import org.xbmc.kore.jsonrpc.method.Playlist;
import org.xbmc.kore.jsonrpc.method.System;
import org.xbmc.kore.jsonrpc.method.VideoLibrary;
import org.xbmc.kore.jsonrpc.type.GlobalType;
import org.xbmc.kore.jsonrpc.type.ListType;
import org.xbmc.kore.jsonrpc.type.PlayerType;
import org.xbmc.kore.jsonrpc.type.PlaylistType;
@ -59,6 +58,9 @@ import org.xbmc.kore.ui.generic.NavigationDrawerFragment;
import org.xbmc.kore.ui.generic.SendTextDialogFragment;
import org.xbmc.kore.ui.sections.hosts.AddHostActivity;
import org.xbmc.kore.ui.views.CirclePageIndicator;
import org.xbmc.kore.ui.volumecontrollers.OnHardwareVolumeKeyPressedCallback;
import org.xbmc.kore.ui.volumecontrollers.VolumeControllerDialogFragmentListener;
import org.xbmc.kore.ui.volumecontrollers.VolumeKeyActionHandler;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.TabsAdapter;
import org.xbmc.kore.utils.UIUtils;
@ -77,8 +79,8 @@ import butterknife.InjectView;
public class RemoteActivity extends BaseActivity
implements HostConnectionObserver.PlayerEventsObserver,
NowPlayingFragment.NowPlayingListener,
SendTextDialogFragment.SendTextDialogListener {
private static final String TAG = LogUtils.makeLogTag(RemoteActivity.class);
SendTextDialogFragment.SendTextDialogListener, OnHardwareVolumeKeyPressedCallback {
private static final String TAG = LogUtils.makeLogTag(RemoteActivity.class);
private static final int NOWPLAYING_FRAGMENT_ID = 1;
@ -97,6 +99,8 @@ public class RemoteActivity extends BaseActivity
private NavigationDrawerFragment navigationDrawerFragment;
private VolumeKeyActionHandler volumeKeyActionHandler;
@InjectView(R.id.background_image) ImageView backgroundImage;
@InjectView(R.id.pager_indicator) CirclePageIndicator pageIndicator;
@InjectView(R.id.pager) ViewPager viewPager;
@ -201,54 +205,20 @@ public class RemoteActivity extends BaseActivity
hostConnectionObserver = null;
}
/**
* Override hardware volume keys and send to Kodi
*/
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
// Check whether we should intercept this
boolean useVolumeKeys = PreferenceManager
.getDefaultSharedPreferences(this)
.getBoolean(Settings.KEY_PREF_USE_HARDWARE_VOLUME_KEYS,
Settings.DEFAULT_PREF_USE_HARDWARE_VOLUME_KEYS);
if (useVolumeKeys) {
int action = event.getAction();
int keyCode = event.getKeyCode();
switch (keyCode) {
case KeyEvent.KEYCODE_VOLUME_UP:
if (action == KeyEvent.ACTION_DOWN) {
new Application
.SetVolume(GlobalType.IncrementDecrement.INCREMENT)
.execute(hostManager.getConnection(), null, null);
}
return true;
case KeyEvent.KEYCODE_VOLUME_DOWN:
if (action == KeyEvent.ACTION_DOWN) {
new Application
.SetVolume(GlobalType.IncrementDecrement.DECREMENT)
.execute(hostManager.getConnection(), null, null);
}
return true;
}
}
return super.dispatchKeyEvent(event);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
if (!navigationDrawerFragment.isDrawerOpen()) {
// Only show items in the action bar relevant to this screen if the drawer is not showing.
// Otherwise, let the drawer decide what to show in the action bar.
if (!navigationDrawerFragment.isDrawerOpen()) {
// Only show items in the action bar relevant to this screen if the drawer is not showing.
// Otherwise, let the drawer decide what to show in the action bar.
getMenuInflater().inflate(R.menu.remote, menu);
}
}
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here.
switch (item.getItemId()) {
switch (item.getItemId()) {
case R.id.action_wake_up:
UIUtils.sendWolAsync(this, hostManager.getHostInfo());
return true;
@ -298,11 +268,30 @@ public class RemoteActivity extends BaseActivity
AudioLibrary.Scan actionScanAudio = new AudioLibrary.Scan();
actionScanAudio.execute(hostManager.getConnection(), null, null);
return true;
default:
break;
}
default:
break;
}
return super.onOptionsItemSelected(item);
return super.onOptionsItemSelected(item);
}
/**
* Override hardware volume keys and send to Kodi
*/
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (volumeKeyActionHandler == null) {
volumeKeyActionHandler = new VolumeKeyActionHandler(hostManager, this, this);
}
return volumeKeyActionHandler.handleDispatchKeyEvent(event) || super.dispatchKeyEvent(
event);
}
private void showVolumeChangeDialog() {
VolumeControllerDialogFragmentListener volumeControllerDialogFragment =
new VolumeControllerDialogFragmentListener();
volumeControllerDialogFragment.show(getSupportFragmentManager(),
VolumeControllerDialogFragmentListener.class.getName());
}
/**
@ -704,4 +693,16 @@ public class RemoteActivity extends BaseActivity
playlistFragment.forceRefreshPlaylist();
}
}
@Override
public void onHardwareVolumeKeyPressed() {
int currentPage = viewPager.getCurrentItem();
if (!isPageWithVolumeController(currentPage)) {
showVolumeChangeDialog();
}
}
private boolean isPageWithVolumeController(int currentPage) {
return currentPage == 0;
}
}

View File

@ -0,0 +1,5 @@
package org.xbmc.kore.ui.volumecontrollers;
public interface OnHardwareVolumeKeyPressedCallback {
void onHardwareVolumeKeyPressed();
}

View File

@ -0,0 +1,183 @@
package org.xbmc.kore.ui.volumecontrollers;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.Nullable;
import android.support.v4.app.FragmentManager;
import android.support.v7.app.AppCompatDialogFragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.xbmc.kore.R;
import org.xbmc.kore.host.HostConnectionObserver;
import org.xbmc.kore.host.HostManager;
import org.xbmc.kore.jsonrpc.ApiCallback;
import org.xbmc.kore.jsonrpc.ApiMethod;
import org.xbmc.kore.jsonrpc.method.Application;
import org.xbmc.kore.ui.widgets.HighlightButton;
import org.xbmc.kore.ui.widgets.VolumeLevelIndicator;
import org.xbmc.kore.utils.LogUtils;
import butterknife.ButterKnife;
import butterknife.InjectView;
public class VolumeControllerDialogFragmentListener extends AppCompatDialogFragment
implements HostConnectionObserver.ApplicationEventsObserver,
OnHardwareVolumeKeyPressedCallback, VolumeLevelIndicator.VolumeBarTouchTrackerListener {
private static final String TAG = LogUtils.makeLogTag(VolumeControllerDialogFragmentListener.class);
private static final int AUTO_DISMISS_DELAY = 2000;
@InjectView(R.id.npp_volume_mute)
HighlightButton volumeMuteButton;
@InjectView(R.id.npp_volume_muted_indicator)
HighlightButton volumeMutedIndicatorButton;
@InjectView(R.id.npp_volume_level_indicator)
VolumeLevelIndicator volumeLevelIndicator;
private Handler callbackHandler = new Handler();
private VolumeKeyActionHandler volumeKeyActionHandler;
private HostManager hostManager = null;
private ApiCallback<Integer> defaultIntActionCallback = ApiMethod.getDefaultActionCallback();
private View.OnClickListener onMuteToggleOnClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
cancelDismissDialog();
Application.SetMute action = new Application.SetMute();
action.execute(hostManager.getConnection(), new ApiCallback<Boolean>() {
@Override
public void onSuccess(Boolean result) {
//We depend on the listener to correct the mute button state
}
@Override
public void onError(int errorCode, String description) {
LogUtils.LOGE(TAG,
"Got an error calling Application.SetMute. Error code: " + errorCode +
", description: " + description);
}
}, callbackHandler);
}
};
private long lastVolumeChangeInteractionEvent;
private Runnable dismissDialog = new Runnable() {
@Override
public void run() {
long timeSinceLastEvent = System.currentTimeMillis() - lastVolumeChangeInteractionEvent;
if (timeSinceLastEvent >= AUTO_DISMISS_DELAY) {
Dialog dialog = getDialog();
if (dialog != null && dialog.isShowing()) {
dialog.dismiss();
}
}
}
};
@Override
public void onResume() {
super.onResume();
if (volumeKeyActionHandler == null) {
volumeKeyActionHandler = new VolumeKeyActionHandler(hostManager, getContext(), this);
}
getDialog().setOnKeyListener(new DialogInterface.OnKeyListener() {
@Override
public boolean onKey(android.content.DialogInterface dialog, int keyCode,
android.view.KeyEvent event) {
return volumeKeyActionHandler.handleDispatchKeyEvent(event);
}
});
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.volume_controller_dialog, container, false);
ButterKnife.inject(this, rootView);
return rootView;
}
@Override
public void show(FragmentManager manager, String tag) {
super.show(manager, tag);
delayedDismissDialog();
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
hostManager = HostManager.getInstance(getContext());
setListeners();
registerObserver();
// for orientation change
delayedDismissDialog();
}
private void registerObserver() {
HostConnectionObserver hostConnectionObserver = hostManager.getHostConnectionObserver();
if (hostConnectionObserver == null) {
return;
}
hostConnectionObserver.registerApplicationObserver(this, true);
hostConnectionObserver.forceRefreshResults();
}
private void setListeners() {
volumeMuteButton.setOnClickListener(onMuteToggleOnClickListener);
volumeMutedIndicatorButton.setOnClickListener(onMuteToggleOnClickListener);
volumeLevelIndicator.setOnVolumeChangeListener(
new VolumeLevelIndicator.OnVolumeChangeListener() {
@Override
public void onVolumeChanged(int volume) {
cancelDismissDialog();
new Application.SetVolume(volume).execute(hostManager.getConnection(),
defaultIntActionCallback, callbackHandler);
}
});
volumeLevelIndicator.setVolumeBarTouchTrackerListener(this);
}
@Override
public void applicationOnVolumeChanged(int volume, boolean muted) {
volumeLevelIndicator.setVolume(muted, volume);
volumeMutedIndicatorButton.setVisibility(muted ? View.VISIBLE : View.GONE);
volumeMutedIndicatorButton.setHighlight(muted);
volumeMuteButton.setHighlight(muted);
}
@Override
public void onHardwareVolumeKeyPressed() {
delayedDismissDialog();
}
private void delayedDismissDialog() {
cancelDismissDialog();
callbackHandler.postDelayed(dismissDialog, AUTO_DISMISS_DELAY);
}
private void cancelDismissDialog() {
lastVolumeChangeInteractionEvent = System.currentTimeMillis();
callbackHandler.removeCallbacks(dismissDialog);
}
@Override
public void onStartTrackingTouch() {
cancelDismissDialog();
}
@Override
public void onStopTrackingTouch() {
delayedDismissDialog();
}
}

View File

@ -0,0 +1,62 @@
package org.xbmc.kore.ui.volumecontrollers;
import android.content.Context;
import android.support.annotation.Nullable;
import android.view.KeyEvent;
import org.xbmc.kore.Settings;
import org.xbmc.kore.host.HostManager;
import org.xbmc.kore.jsonrpc.method.Application;
import org.xbmc.kore.jsonrpc.type.GlobalType;
public class VolumeKeyActionHandler {
private final HostManager hostManager;
private final Context context;
private final OnHardwareVolumeKeyPressedCallback onHardwareVolumeKeyPressedCallback;
public VolumeKeyActionHandler(HostManager hostManager, Context context,
@Nullable OnHardwareVolumeKeyPressedCallback onHardwareVolumeKeyPressedCallback) {
this.hostManager = hostManager;
this.context = context;
this.onHardwareVolumeKeyPressedCallback = onHardwareVolumeKeyPressedCallback;
}
public boolean handleDispatchKeyEvent(KeyEvent event) {
if (shouldInterceptKey()) {
int action = event.getAction();
int keyCode = event.getKeyCode();
if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
if (action == KeyEvent.ACTION_DOWN) {
notifyCallback();
setVolume(GlobalType.IncrementDecrement.INCREMENT);
}
return true;
}
else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
if (action == KeyEvent.ACTION_DOWN) {
notifyCallback();
setVolume(GlobalType.IncrementDecrement.DECREMENT);
}
return true;
}
}
return false;
}
private void setVolume(String volume) {
new Application.SetVolume(volume).execute(hostManager.getConnection(), null, null);
}
private boolean shouldInterceptKey() {
return android.support.v7.preference.PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(Settings.KEY_PREF_USE_HARDWARE_VOLUME_KEYS,
Settings.DEFAULT_PREF_USE_HARDWARE_VOLUME_KEYS);
}
private void notifyCallback() {
if (onHardwareVolumeKeyPressedCallback != null) {
onHardwareVolumeKeyPressedCallback.onHardwareVolumeKeyPressed();
}
}
}

View File

@ -23,6 +23,7 @@ import android.util.AttributeSet;
import org.xbmc.kore.R;
public class HighlightButton extends AppCompatImageButton {
private int colorFilter;
private boolean highlight;
@ -56,10 +57,12 @@ public class HighlightButton extends AppCompatImageButton {
}
private void setStyle(Context context) {
TypedArray styledAttributes = context.getTheme().obtainStyledAttributes(new int[]{
R.attr.colorAccent});
colorFilter = styledAttributes.getColor(styledAttributes.getIndex(0),
context.getResources().getColor(R.color.accent_default));
styledAttributes.recycle();
if (!this.isInEditMode()) {
TypedArray styledAttributes = context.getTheme().obtainStyledAttributes(
new int[]{R.attr.colorAccent});
colorFilter = styledAttributes.getColor(styledAttributes.getIndex(0),
context.getResources().getColor(R.color.accent_default));
styledAttributes.recycle();
}
}
}

View File

@ -33,11 +33,17 @@ public class VolumeLevelIndicator extends LinearLayout {
@InjectView(R.id.vli_volume_text) TextView volumeTextView;
private OnVolumeChangeListener onVolumeChangeListener;
private VolumeBarTouchTrackerListener volumeBarTouchTrackerListener;
public interface OnVolumeChangeListener {
void onVolumeChanged(int volume);
}
public interface VolumeBarTouchTrackerListener {
void onStartTrackingTouch();
void onStopTrackingTouch();
}
public VolumeLevelIndicator(Context context) {
super(context);
initializeView(context);
@ -67,13 +73,19 @@ public class VolumeLevelIndicator extends LinearLayout {
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
if (volumeBarTouchTrackerListener != null) {
volumeBarTouchTrackerListener.onStartTrackingTouch();
}
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
if (onVolumeChangeListener != null)
if (onVolumeChangeListener != null) {
onVolumeChangeListener.onVolumeChanged(seekBar.getProgress());
if (volumeBarTouchTrackerListener != null) {
volumeBarTouchTrackerListener.onStopTrackingTouch();
}
}
}
});
}
@ -82,6 +94,11 @@ public class VolumeLevelIndicator extends LinearLayout {
this.onVolumeChangeListener = onVolumeChangeListener;
}
public void setVolumeBarTouchTrackerListener(
VolumeBarTouchTrackerListener volumeBarTouchTrackerListener) {
this.volumeBarTouchTrackerListener = volumeBarTouchTrackerListener;
}
/**
* Sets UI volume state
* @param muted

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/contentBackgroundColor"
android:minWidth="200dp"
tools:background="@color/dark_content_background_dim_70pct">
<org.xbmc.kore.ui.widgets.HighlightButton
android:id="@+id/npp_volume_mute"
style="@style/Widget.Button.Borderless"
android:layout_width="@dimen/default_icon_size"
android:layout_height="@dimen/default_icon_size"
android:contentDescription="@string/volume_mute"
android:src="?attr/iconVolumeMute"
tools:src="@drawable/ic_volume_off_white_24dp" />
<org.xbmc.kore.ui.widgets.HighlightButton
android:id="@+id/npp_volume_muted_indicator"
style="@style/Widget.Button.Borderless"
android:layout_width="@dimen/default_icon_size"
android:layout_height="@dimen/default_icon_size"
android:contentDescription="@string/volume_mute"
android:src="?attr/iconVolumeMute"
android:visibility="gone"
tools:src="@drawable/ic_volume_off_white_24dp" />
<org.xbmc.kore.ui.widgets.VolumeLevelIndicator
android:id="@+id/npp_volume_level_indicator"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_toRightOf="@id/npp_volume_mute"
android:orientation="vertical">
</org.xbmc.kore.ui.widgets.VolumeLevelIndicator>
</RelativeLayout>