mirror of
https://github.com/KDE/kdeconnect-android
synced 2025-08-22 09:58:08 +00:00
Added switching between audio outputs and split player and devices into two pages
This commit is contained in:
parent
2f71fab62c
commit
a4b3da7a14
@ -174,6 +174,7 @@
|
||||
android:name="org.kde.kdeconnect.Plugins.MprisPlugin.MprisActivity"
|
||||
android:label="@string/open_mpris_controls"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/KdeConnectTheme.NoActionBar"
|
||||
android:parentActivityName="org.kde.kdeconnect.UserInterface.MainActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
|
5
res/drawable/sink_item_background.xml
Normal file
5
res/drawable/sink_item_background.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<corners android:radius="8dp"/>
|
||||
<stroke android:width="3dp" android:color="?colorAccent" />
|
||||
</shape>
|
@ -1,32 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="15dp"
|
||||
android:paddingLeft="25dp"
|
||||
android:paddingRight="25dp"
|
||||
android:paddingTop="25dp">
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/album_art"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginBottom="25dp"
|
||||
android:layout_weight="4"
|
||||
android:contentDescription="@string/mpris_coverart_description"
|
||||
android:scaleType="fitCenter" />
|
||||
|
||||
<include
|
||||
layout="@layout/mpris_control"
|
||||
android:id="@+id/mpris_control"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<fragment android:name="org.kde.kdeconnect.Plugins.SystemvolumePlugin.SystemvolumeFragment"
|
||||
android:id="@+id/systemvolume_fragment"
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="5dp"/>
|
||||
android:elevation="8dp"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize" />
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/mpris_tabs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/toolbar_color"
|
||||
app:tabIndicatorColor="?android:textColorPrimary"
|
||||
app:tabSelectedTextColor="?android:textColorPrimary" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/mpris_pager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</LinearLayout>
|
||||
|
@ -1,16 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/sink_card"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="0dp"
|
||||
app:contentPadding="8dp"
|
||||
app:strokeColor="@color/card_stroke_color"
|
||||
app:strokeWidth="1dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
<RadioButton
|
||||
android:id="@+id/systemvolume_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="5dp"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Medium" />
|
||||
android:layout_gravity="start"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:paddingLeft="14dp"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
|
||||
tools:text="Device name" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/systemvolume_layout"
|
||||
@ -32,13 +48,11 @@
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/systemvolume_seek"
|
||||
android:layout_width="0dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_weight="1"
|
||||
android:max="100" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
24
res/layout/mpris_now_playing.xml
Normal file
24
res/layout/mpris_now_playing.xml
Normal file
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="@dimen/activity_horizontal_margin"
|
||||
android:paddingVertical="@dimen/activity_vertical_margin">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/album_art"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginBottom="25dp"
|
||||
android:layout_weight="4"
|
||||
android:contentDescription="@string/mpris_coverart_description"
|
||||
android:scaleType="fitCenter" />
|
||||
|
||||
<include
|
||||
android:id="@+id/mpris_control"
|
||||
layout="@layout/mpris_control"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
11
res/layout/system_volume_fragment.xml
Normal file
11
res/layout/system_volume_fragment.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/audio_devices_recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="@dimen/activity_horizontal_margin"
|
||||
android:paddingVertical="@dimen/activity_vertical_margin">
|
||||
|
||||
</androidx.recyclerview.widget.RecyclerView>
|
@ -9,6 +9,7 @@
|
||||
<color name="text_color_primary">@android:color/white</color>
|
||||
<color name="text_color">@android:color/white</color>
|
||||
<color name="toolbar_color">#222222</color>
|
||||
<color name="card_stroke_color">#8C8C8C</color>
|
||||
|
||||
<!-- This is for dark theme. In dark theme both selected and unselected text in the
|
||||
navigation bar should be white. This is different from the light theme as both states have
|
||||
|
@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="KdeConnectTheme.NoActionBar" parent="KdeConnectThemeBase.NoActionBar">
|
||||
<item name="android:statusBarColor">#65000000</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
@ -9,4 +9,5 @@
|
||||
<color name="text_color_primary">@android:color/black</color>
|
||||
<color name="text_color">@android:color/black</color>
|
||||
<color name="toolbar_color">#F67400</color>
|
||||
<color name="card_stroke_color">#C8C8C8</color>
|
||||
</resources>
|
@ -0,0 +1,54 @@
|
||||
package org.kde.kdeconnect.Plugins.MprisPlugin;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.collection.LongSparseArray;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
/**
|
||||
* Provides access to adapter fragments
|
||||
*/
|
||||
public abstract class ExtendedFragmentAdapter extends FragmentStateAdapter {
|
||||
|
||||
public ExtendedFragmentAdapter(@NonNull FragmentActivity fragmentActivity) {
|
||||
super(fragmentActivity);
|
||||
}
|
||||
|
||||
public ExtendedFragmentAdapter(@NonNull Fragment fragment) {
|
||||
super(fragment);
|
||||
}
|
||||
|
||||
public ExtendedFragmentAdapter(@NonNull FragmentManager fragmentManager, @NonNull Lifecycle lifecycle) {
|
||||
super(fragmentManager, lifecycle);
|
||||
}
|
||||
|
||||
protected LongSparseArray<Fragment> getFragments() {
|
||||
try {
|
||||
Field fragmentsField = FragmentStateAdapter.class.getDeclaredField("mFragments");
|
||||
fragmentsField.setAccessible(true);
|
||||
Object fieldData = fragmentsField.get(this);
|
||||
if (fieldData instanceof LongSparseArray) {
|
||||
//noinspection unchecked
|
||||
return (LongSparseArray<Fragment>) fieldData;
|
||||
}
|
||||
} catch (NoSuchFieldException e) {
|
||||
e.printStackTrace();
|
||||
} catch (IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected Fragment getFragment(int position) {
|
||||
LongSparseArray<Fragment> adapterFragments = getFragments();
|
||||
if (adapterFragments == null) return null;
|
||||
|
||||
return adapterFragments.get(position);
|
||||
}
|
||||
}
|
@ -6,277 +6,41 @@
|
||||
|
||||
package org.kde.kdeconnect.Plugins.MprisPlugin;
|
||||
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.text.TextUtils;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.graphics.drawable.DrawableCompat;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.kde.kdeconnect.Backends.BaseLink;
|
||||
import org.kde.kdeconnect.Backends.BaseLinkProvider;
|
||||
import org.kde.kdeconnect.BackgroundService;
|
||||
import org.kde.kdeconnect.Helpers.VideoUrlsHelper;
|
||||
import org.kde.kdeconnect.NetworkPacket;
|
||||
import org.kde.kdeconnect.Plugins.SystemvolumePlugin.SystemvolumeFragment;
|
||||
import com.google.android.material.tabs.TabLayoutMediator;
|
||||
|
||||
import org.kde.kdeconnect.Plugins.SystemVolumePlugin.SystemVolumeFragment;
|
||||
import org.kde.kdeconnect.UserInterface.ThemeUtil;
|
||||
import org.kde.kdeconnect_tp.R;
|
||||
import org.kde.kdeconnect_tp.databinding.ActivityMprisBinding;
|
||||
import org.kde.kdeconnect_tp.databinding.MprisControlBinding;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.util.List;
|
||||
|
||||
public class MprisActivity extends AppCompatActivity {
|
||||
|
||||
private String deviceId;
|
||||
private final Handler positionSeekUpdateHandler = new Handler();
|
||||
private Runnable positionSeekUpdateRunnable = null;
|
||||
private MprisPlugin.MprisPlayer targetPlayer = null;
|
||||
|
||||
private ActivityMprisBinding activityMprisBinding;
|
||||
private MprisControlBinding mprisControlBinding;
|
||||
|
||||
private static String milisToProgress(long milis) {
|
||||
int length = (int) (milis / 1000); //From milis to seconds
|
||||
StringBuilder text = new StringBuilder();
|
||||
int minutes = length / 60;
|
||||
if (minutes > 60) {
|
||||
int hours = minutes / 60;
|
||||
minutes = minutes % 60;
|
||||
text.append(hours).append(':');
|
||||
if (minutes < 10) text.append('0');
|
||||
}
|
||||
text.append(minutes).append(':');
|
||||
int seconds = (length % 60);
|
||||
if (seconds < 10)
|
||||
text.append('0'); // needed to show length properly (eg 4:05 instead of 4:5)
|
||||
text.append(seconds);
|
||||
return text.toString();
|
||||
}
|
||||
|
||||
private void connectToPlugin(final String targetPlayerName) {
|
||||
BackgroundService.RunWithPlugin(this, deviceId, MprisPlugin.class, mpris -> {
|
||||
targetPlayer = mpris.getPlayerStatus(targetPlayerName);
|
||||
|
||||
addSystemVolumeFragment();
|
||||
|
||||
mpris.setPlayerStatusUpdatedHandler("activity", new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
runOnUiThread(() -> updatePlayerStatus(mpris));
|
||||
}
|
||||
});
|
||||
|
||||
mpris.setPlayerListUpdatedHandler("activity", new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
final List<String> playerList = mpris.getPlayerList();
|
||||
final ArrayAdapter<String> adapter = new ArrayAdapter<>(MprisActivity.this,
|
||||
android.R.layout.simple_spinner_item,
|
||||
playerList.toArray(ArrayUtils.EMPTY_STRING_ARRAY)
|
||||
);
|
||||
|
||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
runOnUiThread(() -> {
|
||||
mprisControlBinding.playerSpinner.setAdapter(adapter);
|
||||
|
||||
if (playerList.isEmpty()) {
|
||||
mprisControlBinding.noPlayers.setVisibility(View.VISIBLE);
|
||||
mprisControlBinding.playerSpinner.setVisibility(View.GONE);
|
||||
mprisControlBinding.nowPlayingTextview.setText("");
|
||||
} else {
|
||||
mprisControlBinding.noPlayers.setVisibility(View.GONE);
|
||||
mprisControlBinding.playerSpinner.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
mprisControlBinding.playerSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> arg0, View arg1, int pos, long id) {
|
||||
|
||||
if (pos >= playerList.size()) return;
|
||||
|
||||
String player = playerList.get(pos);
|
||||
if (targetPlayer != null && player.equals(targetPlayer.getPlayer())) {
|
||||
return; //Player hasn't actually changed
|
||||
}
|
||||
targetPlayer = mpris.getPlayerStatus(player);
|
||||
updatePlayerStatus(mpris);
|
||||
|
||||
if (targetPlayer.isPlaying()) {
|
||||
MprisMediaSession.getInstance().playerSelected(targetPlayer);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> arg0) {
|
||||
targetPlayer = null;
|
||||
}
|
||||
});
|
||||
|
||||
if (targetPlayer == null) {
|
||||
//If no player is selected, try to select a playing player
|
||||
targetPlayer = mpris.getPlayingPlayer();
|
||||
}
|
||||
//Try to select the specified player
|
||||
if (targetPlayer != null) {
|
||||
int targetIndex = adapter.getPosition(targetPlayer.getPlayer());
|
||||
if (targetIndex >= 0) {
|
||||
mprisControlBinding.playerSpinner.setSelection(targetIndex);
|
||||
} else {
|
||||
targetPlayer = null;
|
||||
}
|
||||
}
|
||||
//If no player selected, select the first one (if any)
|
||||
if (targetPlayer == null && !playerList.isEmpty()) {
|
||||
targetPlayer = mpris.getPlayerStatus(playerList.get(0));
|
||||
mprisControlBinding.playerSpinner.setSelection(0);
|
||||
}
|
||||
updatePlayerStatus(mpris);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void addSystemVolumeFragment() {
|
||||
if (findViewById(R.id.systemvolume_fragment) == null)
|
||||
return;
|
||||
|
||||
FragmentManager fragmentManager = getSupportFragmentManager();
|
||||
((SystemvolumeFragment) fragmentManager.findFragmentById(R.id.systemvolume_fragment)).connectToPlugin(deviceId);
|
||||
}
|
||||
|
||||
private final BaseLinkProvider.ConnectionReceiver connectionReceiver = new BaseLinkProvider.ConnectionReceiver() {
|
||||
@Override
|
||||
public void onConnectionReceived(NetworkPacket identityPacket, BaseLink link) {
|
||||
connectToPlugin(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionLost(BaseLink link) {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
BackgroundService.RunCommand(MprisActivity.this, service -> service.removeConnectionListener(connectionReceiver));
|
||||
}
|
||||
|
||||
private void updatePlayerStatus(MprisPlugin mpris) {
|
||||
MprisPlugin.MprisPlayer playerStatus = targetPlayer;
|
||||
if (playerStatus == null) {
|
||||
//No player with that name found, just display "empty" data
|
||||
playerStatus = mpris.getEmptyPlayer();
|
||||
}
|
||||
String song = playerStatus.getCurrentSong();
|
||||
|
||||
if (!mprisControlBinding.nowPlayingTextview.getText().toString().equals(song)) {
|
||||
mprisControlBinding.nowPlayingTextview.setText(song);
|
||||
}
|
||||
|
||||
Bitmap albumArt = playerStatus.getAlbumArt();
|
||||
if (albumArt == null) {
|
||||
final Drawable drawable = ContextCompat.getDrawable(this, R.drawable.ic_album_art_placeholder);
|
||||
assert drawable != null;
|
||||
Drawable placeholder_art = DrawableCompat.wrap(drawable);
|
||||
DrawableCompat.setTint(placeholder_art, ContextCompat.getColor(this, R.color.primary));
|
||||
activityMprisBinding.albumArt.setImageDrawable(placeholder_art);
|
||||
} else {
|
||||
activityMprisBinding.albumArt.setImageBitmap(albumArt);
|
||||
}
|
||||
|
||||
if (playerStatus.isSeekAllowed()) {
|
||||
mprisControlBinding.timeTextview.setText(milisToProgress(playerStatus.getLength()));
|
||||
mprisControlBinding.positionSeek.setMax((int) (playerStatus.getLength()));
|
||||
mprisControlBinding.positionSeek.setProgress((int) (playerStatus.getPosition()));
|
||||
mprisControlBinding.progressSlider.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mprisControlBinding.progressSlider.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
int volume = playerStatus.getVolume();
|
||||
mprisControlBinding.volumeSeek.setProgress(volume);
|
||||
|
||||
boolean isPlaying = playerStatus.isPlaying();
|
||||
if (isPlaying) {
|
||||
mprisControlBinding.playButton.setImageResource(R.drawable.ic_pause_black);
|
||||
mprisControlBinding.playButton.setEnabled(playerStatus.isPauseAllowed());
|
||||
} else {
|
||||
mprisControlBinding.playButton.setImageResource(R.drawable.ic_play_black);
|
||||
mprisControlBinding.playButton.setEnabled(playerStatus.isPlayAllowed());
|
||||
}
|
||||
|
||||
mprisControlBinding.volumeLayout.setVisibility(playerStatus.isSetVolumeAllowed() ? View.VISIBLE : View.GONE);
|
||||
mprisControlBinding.rewButton.setVisibility(playerStatus.isSeekAllowed() ? View.VISIBLE : View.GONE);
|
||||
mprisControlBinding.ffButton.setVisibility(playerStatus.isSeekAllowed() ? View.VISIBLE : View.GONE);
|
||||
|
||||
invalidateOptionsMenu();
|
||||
|
||||
//Show and hide previous/next buttons simultaneously
|
||||
if (playerStatus.isGoPreviousAllowed() || playerStatus.isGoNextAllowed()) {
|
||||
mprisControlBinding.prevButton.setVisibility(View.VISIBLE);
|
||||
mprisControlBinding.prevButton.setEnabled(playerStatus.isGoPreviousAllowed());
|
||||
mprisControlBinding.nextButton.setVisibility(View.VISIBLE);
|
||||
mprisControlBinding.nextButton.setEnabled(playerStatus.isGoNextAllowed());
|
||||
} else {
|
||||
mprisControlBinding.prevButton.setVisibility(View.GONE);
|
||||
mprisControlBinding.nextButton.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change current volume with provided step.
|
||||
*
|
||||
* @param step step size volume change
|
||||
*/
|
||||
private void updateVolume(int step) {
|
||||
if (targetPlayer == null) {
|
||||
return;
|
||||
}
|
||||
final int currentVolume = targetPlayer.getVolume();
|
||||
|
||||
if (currentVolume <= 100 && currentVolume >= 0) {
|
||||
int newVolume = currentVolume + step;
|
||||
if (newVolume > 100) {
|
||||
newVolume = 100;
|
||||
} else if (newVolume < 0) {
|
||||
newVolume = 0;
|
||||
}
|
||||
targetPlayer.setVolume(newVolume);
|
||||
}
|
||||
}
|
||||
private MprisPagerAdapter mprisPagerAdapter;
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
switch (keyCode) {
|
||||
case KeyEvent.KEYCODE_VOLUME_UP:
|
||||
updateVolume(5);
|
||||
if (activityMprisBinding != null && mprisPagerAdapter != null) {
|
||||
int pagePosition = activityMprisBinding.mprisTabs.getSelectedTabPosition();
|
||||
mprisPagerAdapter.onVolumeUp(pagePosition);
|
||||
}
|
||||
return true;
|
||||
case KeyEvent.KEYCODE_VOLUME_DOWN:
|
||||
updateVolume(-5);
|
||||
if (activityMprisBinding != null && mprisPagerAdapter != null) {
|
||||
int pagePosition = activityMprisBinding.mprisTabs.getSelectedTabPosition();
|
||||
mprisPagerAdapter.onVolumeDown(pagePosition);
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
return super.onKeyDown(keyCode, event);
|
||||
@ -287,7 +51,6 @@ public class MprisActivity extends AppCompatActivity {
|
||||
public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) {
|
||||
switch (keyCode) {
|
||||
case KeyEvent.KEYCODE_VOLUME_UP:
|
||||
return true;
|
||||
case KeyEvent.KEYCODE_VOLUME_DOWN:
|
||||
return true;
|
||||
default:
|
||||
@ -295,142 +58,85 @@ public class MprisActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
private interface MprisPlayerCallback {
|
||||
void performAction(MprisPlugin.MprisPlayer player);
|
||||
}
|
||||
|
||||
private void performActionOnClick(View v, MprisPlayerCallback l) {
|
||||
v.setOnClickListener(view -> BackgroundService.RunCommand(MprisActivity.this, service -> {
|
||||
if (targetPlayer == null) return;
|
||||
l.performAction(targetPlayer);
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
ThemeUtil.setUserPreferredTheme(this);
|
||||
|
||||
activityMprisBinding = ActivityMprisBinding.inflate(getLayoutInflater());
|
||||
mprisControlBinding = activityMprisBinding.mprisControl;
|
||||
|
||||
setContentView(activityMprisBinding.getRoot());
|
||||
|
||||
String targetPlayerName = getIntent().getStringExtra("player");
|
||||
getIntent().removeExtra("player");
|
||||
String deviceId = getIntent().getStringExtra(MprisPlugin.DEVICE_ID_KEY);
|
||||
|
||||
if (TextUtils.isEmpty(targetPlayerName)) {
|
||||
if (savedInstanceState != null) {
|
||||
targetPlayerName = savedInstanceState.getString("targetPlayer");
|
||||
mprisPagerAdapter = new MprisPagerAdapter(this, deviceId);
|
||||
activityMprisBinding.mprisPager.setAdapter(mprisPagerAdapter);
|
||||
|
||||
TabLayoutMediator tabLayoutMediator = new TabLayoutMediator(
|
||||
activityMprisBinding.mprisTabs,
|
||||
activityMprisBinding.mprisPager,
|
||||
(tab, position) -> {
|
||||
tab.setText(mprisPagerAdapter.getTitle(position));
|
||||
}
|
||||
);
|
||||
|
||||
activityMprisBinding.mprisTabs.getSelectedTabPosition();
|
||||
|
||||
tabLayoutMediator.attach();
|
||||
|
||||
setSupportActionBar(activityMprisBinding.toolbar);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
|
||||
deviceId = getIntent().getStringExtra("deviceId");
|
||||
static class MprisPagerAdapter extends ExtendedFragmentAdapter {
|
||||
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
String interval_time_str = prefs.getString(getString(R.string.mpris_time_key),
|
||||
getString(R.string.mpris_time_default));
|
||||
final int interval_time = Integer.parseInt(interval_time_str);
|
||||
private final String deviceId;
|
||||
|
||||
BackgroundService.RunCommand(MprisActivity.this, service -> service.addConnectionListener(connectionReceiver));
|
||||
connectToPlugin(targetPlayerName);
|
||||
public MprisPagerAdapter(@NonNull FragmentActivity fragmentActivity, String deviceId) {
|
||||
super(fragmentActivity);
|
||||
this.deviceId = deviceId;
|
||||
}
|
||||
|
||||
performActionOnClick(mprisControlBinding.playButton, MprisPlugin.MprisPlayer::playPause);
|
||||
|
||||
performActionOnClick(mprisControlBinding.prevButton, MprisPlugin.MprisPlayer::previous);
|
||||
|
||||
performActionOnClick(mprisControlBinding.rewButton, p -> targetPlayer.seek(interval_time * -1));
|
||||
|
||||
performActionOnClick(mprisControlBinding.ffButton, p -> p.seek(interval_time));
|
||||
|
||||
performActionOnClick(mprisControlBinding.nextButton, MprisPlugin.MprisPlayer::next);
|
||||
|
||||
performActionOnClick(mprisControlBinding.stopButton, MprisPlugin.MprisPlayer::stop);
|
||||
|
||||
mprisControlBinding.volumeSeek.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
@NonNull
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
|
||||
public Fragment createFragment(int position) {
|
||||
if (position == 1) {
|
||||
return SystemVolumeFragment.newInstance(deviceId);
|
||||
} else {
|
||||
return MprisNowPlayingFragment.newInstance(deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
public int getItemCount() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(final SeekBar seekBar) {
|
||||
BackgroundService.RunCommand(MprisActivity.this, service -> {
|
||||
if (targetPlayer == null) return;
|
||||
targetPlayer.setVolume(seekBar.getProgress());
|
||||
});
|
||||
@StringRes
|
||||
int getTitle(int position) {
|
||||
if (position == 1) {
|
||||
return R.string.devices;
|
||||
} else {
|
||||
return R.string.mpris_play;
|
||||
}
|
||||
});
|
||||
|
||||
positionSeekUpdateRunnable = () -> BackgroundService.RunCommand(MprisActivity.this, service -> {
|
||||
if (targetPlayer != null) {
|
||||
mprisControlBinding.positionSeek.setProgress((int) (targetPlayer.getPosition()));
|
||||
}
|
||||
positionSeekUpdateHandler.removeCallbacks(positionSeekUpdateRunnable);
|
||||
positionSeekUpdateHandler.postDelayed(positionSeekUpdateRunnable, 1000);
|
||||
});
|
||||
positionSeekUpdateHandler.postDelayed(positionSeekUpdateRunnable, 200);
|
||||
|
||||
mprisControlBinding.positionSeek.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean byUser) {
|
||||
mprisControlBinding.progressTextview.setText(milisToProgress(progress));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
positionSeekUpdateHandler.removeCallbacks(positionSeekUpdateRunnable);
|
||||
void onVolumeUp(int page) {
|
||||
Fragment requestedFragment = getFragment(page);
|
||||
if (requestedFragment == null) return;
|
||||
|
||||
if (requestedFragment instanceof VolumeKeyListener) {
|
||||
((VolumeKeyListener) requestedFragment).onVolumeUp();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(final SeekBar seekBar) {
|
||||
BackgroundService.RunCommand(MprisActivity.this, service -> {
|
||||
if (targetPlayer != null) {
|
||||
targetPlayer.setPosition(seekBar.getProgress());
|
||||
}
|
||||
positionSeekUpdateHandler.postDelayed(positionSeekUpdateRunnable, 200);
|
||||
});
|
||||
}
|
||||
});
|
||||
void onVolumeDown(int page) {
|
||||
Fragment requestedFragment = getFragment(page);
|
||||
if (requestedFragment == null) return;
|
||||
|
||||
mprisControlBinding.nowPlayingTextview.setSelected(true);
|
||||
}
|
||||
|
||||
final static int MENU_OPEN_URL = Menu.FIRST;
|
||||
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
menu.clear();
|
||||
if(targetPlayer != null && !"".equals(targetPlayer.getUrl())) {
|
||||
menu.add(0, MENU_OPEN_URL, Menu.NONE, R.string.mpris_open_url);
|
||||
}
|
||||
return super.onPrepareOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (targetPlayer != null && item.getItemId() == MENU_OPEN_URL) {
|
||||
try {
|
||||
String url = VideoUrlsHelper.formatUriWithSeek(targetPlayer.getUrl(), targetPlayer.getPosition()).toString();
|
||||
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||
startActivity(browserIntent);
|
||||
targetPlayer.pause();
|
||||
return true;
|
||||
} catch (MalformedURLException | ActivityNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
Toast.makeText(getApplicationContext(), getString(R.string.cant_open_url), Toast.LENGTH_LONG).show();
|
||||
if (requestedFragment instanceof VolumeKeyListener) {
|
||||
((VolumeKeyListener) requestedFragment).onVolumeDown();
|
||||
}
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
if (targetPlayer != null) {
|
||||
outState.putString("targetPlayer", targetPlayer.getPlayer());
|
||||
}
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,426 @@
|
||||
package org.kde.kdeconnect.Plugins.MprisPlugin;
|
||||
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.graphics.drawable.DrawableCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.kde.kdeconnect.Backends.BaseLink;
|
||||
import org.kde.kdeconnect.Backends.BaseLinkProvider;
|
||||
import org.kde.kdeconnect.BackgroundService;
|
||||
import org.kde.kdeconnect.Helpers.VideoUrlsHelper;
|
||||
import org.kde.kdeconnect.NetworkPacket;
|
||||
import org.kde.kdeconnect_tp.R;
|
||||
import org.kde.kdeconnect_tp.databinding.MprisControlBinding;
|
||||
import org.kde.kdeconnect_tp.databinding.MprisNowPlayingBinding;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.util.List;
|
||||
|
||||
public class MprisNowPlayingFragment extends Fragment implements VolumeKeyListener {
|
||||
|
||||
final static int MENU_OPEN_URL = Menu.FIRST;
|
||||
private final Handler positionSeekUpdateHandler = new Handler();
|
||||
MprisControlBinding mprisControlBinding;
|
||||
private MprisNowPlayingBinding activityMprisBinding;
|
||||
private String deviceId;
|
||||
private Runnable positionSeekUpdateRunnable = null;
|
||||
private MprisPlugin.MprisPlayer targetPlayer = null;
|
||||
private final BaseLinkProvider.ConnectionReceiver connectionReceiver = new BaseLinkProvider.ConnectionReceiver() {
|
||||
@Override
|
||||
public void onConnectionReceived(NetworkPacket identityPacket, BaseLink link) {
|
||||
connectToPlugin(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionLost(BaseLink link) {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
public static MprisNowPlayingFragment newInstance(String deviceId) {
|
||||
MprisNowPlayingFragment mprisNowPlayingFragment = new MprisNowPlayingFragment();
|
||||
|
||||
Bundle arguments = new Bundle();
|
||||
arguments.putString(MprisPlugin.DEVICE_ID_KEY, deviceId);
|
||||
|
||||
mprisNowPlayingFragment.setArguments(arguments);
|
||||
|
||||
return mprisNowPlayingFragment;
|
||||
}
|
||||
|
||||
private static String milisToProgress(long milis) {
|
||||
int length = (int) (milis / 1000); //From milis to seconds
|
||||
StringBuilder text = new StringBuilder();
|
||||
int minutes = length / 60;
|
||||
if (minutes > 60) {
|
||||
int hours = minutes / 60;
|
||||
minutes = minutes % 60;
|
||||
text.append(hours).append(':');
|
||||
if (minutes < 10) text.append('0');
|
||||
}
|
||||
text.append(minutes).append(':');
|
||||
int seconds = (length % 60);
|
||||
if (seconds < 10)
|
||||
text.append('0'); // needed to show length properly (eg 4:05 instead of 4:5)
|
||||
text.append(seconds);
|
||||
return text.toString();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
|
||||
if (activityMprisBinding == null) {
|
||||
activityMprisBinding = MprisNowPlayingBinding.inflate(inflater);
|
||||
mprisControlBinding = activityMprisBinding.mprisControl;
|
||||
|
||||
String targetPlayerName = "";
|
||||
Intent activityIntent = requireActivity().getIntent();
|
||||
activityIntent.getStringExtra("player");
|
||||
activityIntent.removeExtra("player");
|
||||
|
||||
if (TextUtils.isEmpty(targetPlayerName)) {
|
||||
if (savedInstanceState != null) {
|
||||
targetPlayerName = savedInstanceState.getString("targetPlayer");
|
||||
}
|
||||
}
|
||||
|
||||
deviceId = requireArguments().getString(MprisPlugin.DEVICE_ID_KEY);
|
||||
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(requireContext());
|
||||
String interval_time_str = prefs.getString(getString(R.string.mpris_time_key),
|
||||
getString(R.string.mpris_time_default));
|
||||
final int interval_time = Integer.parseInt(interval_time_str);
|
||||
|
||||
BackgroundService.RunCommand(requireContext(), service -> service.addConnectionListener(connectionReceiver));
|
||||
connectToPlugin(targetPlayerName);
|
||||
|
||||
performActionOnClick(mprisControlBinding.playButton, MprisPlugin.MprisPlayer::playPause);
|
||||
|
||||
performActionOnClick(mprisControlBinding.prevButton, MprisPlugin.MprisPlayer::previous);
|
||||
|
||||
performActionOnClick(mprisControlBinding.rewButton, p -> targetPlayer.seek(interval_time * -1));
|
||||
|
||||
performActionOnClick(mprisControlBinding.ffButton, p -> p.seek(interval_time));
|
||||
|
||||
performActionOnClick(mprisControlBinding.nextButton, MprisPlugin.MprisPlayer::next);
|
||||
|
||||
performActionOnClick(mprisControlBinding.stopButton, MprisPlugin.MprisPlayer::stop);
|
||||
|
||||
mprisControlBinding.volumeSeek.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(final SeekBar seekBar) {
|
||||
BackgroundService.RunCommand(requireContext(), service -> {
|
||||
if (targetPlayer == null) return;
|
||||
targetPlayer.setVolume(seekBar.getProgress());
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
positionSeekUpdateRunnable = () -> {
|
||||
Context context = getContext();
|
||||
if (context == null) return; // Fragment was already detached
|
||||
BackgroundService.RunCommand(context, service -> {
|
||||
if (targetPlayer != null) {
|
||||
mprisControlBinding.positionSeek.setProgress((int) (targetPlayer.getPosition()));
|
||||
}
|
||||
positionSeekUpdateHandler.removeCallbacks(positionSeekUpdateRunnable);
|
||||
positionSeekUpdateHandler.postDelayed(positionSeekUpdateRunnable, 1000);
|
||||
});
|
||||
};
|
||||
positionSeekUpdateHandler.postDelayed(positionSeekUpdateRunnable, 200);
|
||||
|
||||
mprisControlBinding.positionSeek.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean byUser) {
|
||||
mprisControlBinding.progressTextview.setText(milisToProgress(progress));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
positionSeekUpdateHandler.removeCallbacks(positionSeekUpdateRunnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(final SeekBar seekBar) {
|
||||
BackgroundService.RunCommand(requireContext(), service -> {
|
||||
if (targetPlayer != null) {
|
||||
targetPlayer.setPosition(seekBar.getProgress());
|
||||
}
|
||||
positionSeekUpdateHandler.postDelayed(positionSeekUpdateRunnable, 200);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
mprisControlBinding.nowPlayingTextview.setSelected(true);
|
||||
|
||||
}
|
||||
|
||||
return activityMprisBinding.getRoot();
|
||||
}
|
||||
|
||||
private void connectToPlugin(final String targetPlayerName) {
|
||||
BackgroundService.RunWithPlugin(requireContext(), deviceId, MprisPlugin.class, mpris -> {
|
||||
targetPlayer = mpris.getPlayerStatus(targetPlayerName);
|
||||
|
||||
mpris.setPlayerStatusUpdatedHandler("activity", new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
requireActivity().runOnUiThread(() -> updatePlayerStatus(mpris));
|
||||
}
|
||||
});
|
||||
|
||||
mpris.setPlayerListUpdatedHandler("activity", new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
final List<String> playerList = mpris.getPlayerList();
|
||||
final ArrayAdapter<String> adapter = new ArrayAdapter<>(requireContext(),
|
||||
android.R.layout.simple_spinner_item,
|
||||
playerList.toArray(ArrayUtils.EMPTY_STRING_ARRAY)
|
||||
);
|
||||
|
||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
requireActivity().runOnUiThread(() -> {
|
||||
mprisControlBinding.playerSpinner.setAdapter(adapter);
|
||||
|
||||
if (playerList.isEmpty()) {
|
||||
mprisControlBinding.noPlayers.setVisibility(View.VISIBLE);
|
||||
mprisControlBinding.playerSpinner.setVisibility(View.GONE);
|
||||
mprisControlBinding.nowPlayingTextview.setText("");
|
||||
} else {
|
||||
mprisControlBinding.noPlayers.setVisibility(View.GONE);
|
||||
mprisControlBinding.playerSpinner.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
mprisControlBinding.playerSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> arg0, View arg1, int pos, long id) {
|
||||
|
||||
if (pos >= playerList.size()) return;
|
||||
|
||||
String player = playerList.get(pos);
|
||||
if (targetPlayer != null && player.equals(targetPlayer.getPlayer())) {
|
||||
return; //Player hasn't actually changed
|
||||
}
|
||||
targetPlayer = mpris.getPlayerStatus(player);
|
||||
updatePlayerStatus(mpris);
|
||||
|
||||
if (targetPlayer.isPlaying()) {
|
||||
MprisMediaSession.getInstance().playerSelected(targetPlayer);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> arg0) {
|
||||
targetPlayer = null;
|
||||
}
|
||||
});
|
||||
|
||||
if (targetPlayer == null) {
|
||||
//If no player is selected, try to select a playing player
|
||||
targetPlayer = mpris.getPlayingPlayer();
|
||||
}
|
||||
//Try to select the specified player
|
||||
if (targetPlayer != null) {
|
||||
int targetIndex = adapter.getPosition(targetPlayer.getPlayer());
|
||||
if (targetIndex >= 0) {
|
||||
mprisControlBinding.playerSpinner.setSelection(targetIndex);
|
||||
} else {
|
||||
targetPlayer = null;
|
||||
}
|
||||
}
|
||||
//If no player selected, select the first one (if any)
|
||||
if (targetPlayer == null && !playerList.isEmpty()) {
|
||||
targetPlayer = mpris.getPlayerStatus(playerList.get(0));
|
||||
mprisControlBinding.playerSpinner.setSelection(0);
|
||||
}
|
||||
updatePlayerStatus(mpris);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
BackgroundService.RunCommand(requireContext(), service -> service.removeConnectionListener(connectionReceiver));
|
||||
}
|
||||
|
||||
private void performActionOnClick(View v, MprisPlayerCallback l) {
|
||||
v.setOnClickListener(view -> BackgroundService.RunCommand(requireContext(), service -> {
|
||||
if (targetPlayer == null) return;
|
||||
l.performAction(targetPlayer);
|
||||
}));
|
||||
}
|
||||
|
||||
private void updatePlayerStatus(MprisPlugin mpris) {
|
||||
MprisPlugin.MprisPlayer playerStatus = targetPlayer;
|
||||
if (playerStatus == null) {
|
||||
//No player with that name found, just display "empty" data
|
||||
playerStatus = mpris.getEmptyPlayer();
|
||||
}
|
||||
String song = playerStatus.getCurrentSong();
|
||||
|
||||
if (!mprisControlBinding.nowPlayingTextview.getText().toString().equals(song)) {
|
||||
mprisControlBinding.nowPlayingTextview.setText(song);
|
||||
}
|
||||
|
||||
Bitmap albumArt = playerStatus.getAlbumArt();
|
||||
if (albumArt == null) {
|
||||
final Drawable drawable = ContextCompat.getDrawable(requireContext(), R.drawable.ic_album_art_placeholder);
|
||||
assert drawable != null;
|
||||
Drawable placeholder_art = DrawableCompat.wrap(drawable);
|
||||
DrawableCompat.setTint(placeholder_art, ContextCompat.getColor(requireContext(), R.color.primary));
|
||||
activityMprisBinding.albumArt.setImageDrawable(placeholder_art);
|
||||
} else {
|
||||
activityMprisBinding.albumArt.setImageBitmap(albumArt);
|
||||
}
|
||||
|
||||
if (playerStatus.isSeekAllowed()) {
|
||||
mprisControlBinding.timeTextview.setText(milisToProgress(playerStatus.getLength()));
|
||||
mprisControlBinding.positionSeek.setMax((int) (playerStatus.getLength()));
|
||||
mprisControlBinding.positionSeek.setProgress((int) (playerStatus.getPosition()));
|
||||
mprisControlBinding.progressSlider.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mprisControlBinding.progressSlider.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
int volume = playerStatus.getVolume();
|
||||
mprisControlBinding.volumeSeek.setProgress(volume);
|
||||
|
||||
boolean isPlaying = playerStatus.isPlaying();
|
||||
if (isPlaying) {
|
||||
mprisControlBinding.playButton.setImageResource(R.drawable.ic_pause_black);
|
||||
mprisControlBinding.playButton.setEnabled(playerStatus.isPauseAllowed());
|
||||
} else {
|
||||
mprisControlBinding.playButton.setImageResource(R.drawable.ic_play_black);
|
||||
mprisControlBinding.playButton.setEnabled(playerStatus.isPlayAllowed());
|
||||
}
|
||||
|
||||
mprisControlBinding.volumeLayout.setVisibility(playerStatus.isSetVolumeAllowed() ? View.VISIBLE : View.GONE);
|
||||
mprisControlBinding.rewButton.setVisibility(playerStatus.isSeekAllowed() ? View.VISIBLE : View.GONE);
|
||||
mprisControlBinding.ffButton.setVisibility(playerStatus.isSeekAllowed() ? View.VISIBLE : View.GONE);
|
||||
|
||||
requireActivity().invalidateOptionsMenu();
|
||||
|
||||
//Show and hide previous/next buttons simultaneously
|
||||
if (playerStatus.isGoPreviousAllowed() || playerStatus.isGoNextAllowed()) {
|
||||
mprisControlBinding.prevButton.setVisibility(View.VISIBLE);
|
||||
mprisControlBinding.prevButton.setEnabled(playerStatus.isGoPreviousAllowed());
|
||||
mprisControlBinding.nextButton.setVisibility(View.VISIBLE);
|
||||
mprisControlBinding.nextButton.setEnabled(playerStatus.isGoNextAllowed());
|
||||
} else {
|
||||
mprisControlBinding.prevButton.setVisibility(View.GONE);
|
||||
mprisControlBinding.nextButton.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change current volume with provided step.
|
||||
*
|
||||
* @param step step size volume change
|
||||
*/
|
||||
private void updateVolume(int step) {
|
||||
Log.e("NowPlayingVolume", String.valueOf(step));
|
||||
if (targetPlayer == null) {
|
||||
return;
|
||||
}
|
||||
Log.e("NowPlayingVolumePlayer", targetPlayer.getTitle());
|
||||
Log.e("NowPlayingVolumeP", String.valueOf(targetPlayer.getVolume()));
|
||||
|
||||
final int currentVolume = targetPlayer.getVolume();
|
||||
|
||||
if (currentVolume <= 100 && currentVolume >= 0) {
|
||||
int newVolume = currentVolume + step;
|
||||
if (newVolume > 100) {
|
||||
newVolume = 100;
|
||||
} else if (newVolume < 0) {
|
||||
newVolume = 0;
|
||||
}
|
||||
|
||||
Log.e("NowPlayingVolumeN", String.valueOf(newVolume));
|
||||
targetPlayer.setVolume(newVolume);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVolumeUp() {
|
||||
updateVolume(5);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVolumeDown() {
|
||||
updateVolume(-5);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepareOptionsMenu(@NonNull Menu menu) {
|
||||
menu.clear();
|
||||
if (targetPlayer != null && !"".equals(targetPlayer.getUrl())) {
|
||||
menu.add(0, MENU_OPEN_URL, Menu.NONE, R.string.mpris_open_url);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||
if (targetPlayer != null && item.getItemId() == MENU_OPEN_URL) {
|
||||
try {
|
||||
String url = VideoUrlsHelper.formatUriWithSeek(targetPlayer.getUrl(), targetPlayer.getPosition()).toString();
|
||||
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||
startActivity(browserIntent);
|
||||
targetPlayer.pause();
|
||||
return true;
|
||||
} catch (MalformedURLException | ActivityNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
Toast.makeText(requireContext(), getString(R.string.cant_open_url), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
if (targetPlayer != null) {
|
||||
outState.putString("targetPlayer", targetPlayer.getPlayer());
|
||||
}
|
||||
}
|
||||
|
||||
private interface MprisPlayerCallback {
|
||||
void performAction(MprisPlugin.MprisPlayer player);
|
||||
}
|
||||
}
|
@ -197,6 +197,7 @@ public class MprisPlugin extends Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
public final static String DEVICE_ID_KEY = "deviceId";
|
||||
private final static String PACKET_TYPE_MPRIS = "kdeconnect.mpris";
|
||||
private final static String PACKET_TYPE_MPRIS_REQUEST = "kdeconnect.mpris.request";
|
||||
|
||||
|
@ -0,0 +1,9 @@
|
||||
package org.kde.kdeconnect.Plugins.MprisPlugin;
|
||||
|
||||
public interface VolumeKeyListener {
|
||||
|
||||
void onVolumeUp();
|
||||
|
||||
void onVolumeDown();
|
||||
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package org.kde.kdeconnect.Plugins.SystemVolumePlugin;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public class ItemGapDecoration extends RecyclerView.ItemDecoration {
|
||||
|
||||
private final int gap;
|
||||
|
||||
ItemGapDecoration(int gap) {
|
||||
this.gap = gap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getItemOffsets(
|
||||
@NonNull Rect outRect,
|
||||
@NonNull View view,
|
||||
@NonNull RecyclerView parent,
|
||||
@NonNull RecyclerView.State state
|
||||
) {
|
||||
super.getItemOffsets(outRect, view, parent, state);
|
||||
|
||||
int itemPosition = parent.getChildAdapterPosition(view);
|
||||
RecyclerView.Adapter<?> adapter = parent.getAdapter();
|
||||
|
||||
if (adapter == null) return;
|
||||
|
||||
if (itemPosition >= 0 && itemPosition < adapter.getItemCount() - 1) {
|
||||
outRect.bottom = gap;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
package org.kde.kdeconnect.Plugins.SystemvolumePlugin;
|
||||
package org.kde.kdeconnect.Plugins.SystemVolumePlugin;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
@ -23,6 +23,7 @@ class Sink {
|
||||
private String name;
|
||||
private boolean mute;
|
||||
private int maxVolume;
|
||||
private boolean enabled;
|
||||
|
||||
private final List<UpdateListener> listeners;
|
||||
|
||||
@ -33,6 +34,7 @@ class Sink {
|
||||
mute = obj.getBoolean("muted");
|
||||
description = obj.getString("description");
|
||||
maxVolume = obj.getInt("maxVolume");
|
||||
enabled = obj.optBoolean("enabled", false);
|
||||
}
|
||||
|
||||
int getVolume() {
|
||||
@ -66,6 +68,17 @@ class Sink {
|
||||
}
|
||||
}
|
||||
|
||||
boolean isDefault() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
void setDefault(boolean enable) {
|
||||
this.enabled = enable;
|
||||
for (UpdateListener l : listeners) {
|
||||
l.updateSink(this);
|
||||
}
|
||||
}
|
||||
|
||||
void addListener(UpdateListener l) {
|
||||
|
||||
if (!listeners.contains(l)) {
|
@ -0,0 +1,21 @@
|
||||
package org.kde.kdeconnect.Plugins.SystemVolumePlugin;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
|
||||
public class SinkItemCallback extends DiffUtil.ItemCallback<Sink> {
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull Sink oldItem, @NonNull Sink newItem) {
|
||||
return oldItem.getName().equals(newItem.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull Sink oldItem, @NonNull Sink newItem) {
|
||||
return oldItem.getVolume() == newItem.getVolume()
|
||||
&& oldItem.isMute() == newItem.isMute()
|
||||
&& oldItem.isDefault() == newItem.isDefault()
|
||||
&& oldItem.getMaxVolume() == newItem.getMaxVolume() // should this be checked?
|
||||
&& oldItem.getDescription().equals(newItem.getDescription()); // should this be checked?
|
||||
}
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
package org.kde.kdeconnect.Plugins.SystemVolumePlugin;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.kde.kdeconnect.BackgroundService;
|
||||
import org.kde.kdeconnect_tp.R;
|
||||
import org.kde.kdeconnect_tp.databinding.ListItemSystemvolumeBinding;
|
||||
|
||||
class SinkItemHolder extends RecyclerView.ViewHolder
|
||||
implements
|
||||
SeekBar.OnSeekBarChangeListener,
|
||||
ImageButton.OnClickListener,
|
||||
CompoundButton.OnCheckedChangeListener,
|
||||
View.OnLongClickListener {
|
||||
|
||||
private final ListItemSystemvolumeBinding viewBinding;
|
||||
private final SystemVolumePlugin plugin;
|
||||
private final Consumer<Boolean> seekBarTracking;
|
||||
|
||||
private Sink sink;
|
||||
|
||||
public SinkItemHolder(
|
||||
@NonNull ListItemSystemvolumeBinding viewBinding,
|
||||
@NonNull SystemVolumePlugin plugin,
|
||||
@NonNull Consumer<Boolean> seekBarTracking
|
||||
) {
|
||||
super(viewBinding.getRoot());
|
||||
this.viewBinding = viewBinding;
|
||||
this.plugin = plugin;
|
||||
this.seekBarTracking = seekBarTracking;
|
||||
|
||||
viewBinding.sinkCard.setOnLongClickListener(this);
|
||||
viewBinding.systemvolumeLabel.setOnLongClickListener(this);
|
||||
|
||||
viewBinding.systemvolumeLabel.setOnCheckedChangeListener(this);
|
||||
viewBinding.systemvolumeMute.setOnClickListener(this);
|
||||
viewBinding.systemvolumeSeek.setOnSeekBarChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgressChanged(final SeekBar seekBar, int i, boolean b) {
|
||||
BackgroundService.RunCommand(seekBar.getContext(),
|
||||
service -> plugin.sendVolume(sink.getName(), seekBar.getProgress()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
seekBarTracking.accept(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(final SeekBar seekBar) {
|
||||
seekBarTracking.accept(false);
|
||||
BackgroundService.RunCommand(seekBar.getContext(),
|
||||
service -> plugin.sendVolume(sink.getName(), seekBar.getProgress()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
plugin.sendMute(sink.getName(), !sink.isMute());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||
if (isChecked) {
|
||||
plugin.sendEnable(sink.getName());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
Toast.makeText(v.getContext(), sink.getDescription(), Toast.LENGTH_SHORT).show();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void bind(Sink sink) {
|
||||
this.sink = sink;
|
||||
|
||||
final RadioButton radioButton = viewBinding.systemvolumeLabel;
|
||||
radioButton.setChecked(sink.isDefault());
|
||||
radioButton.setText(sink.getDescription());
|
||||
|
||||
final SeekBar seekBar = viewBinding.systemvolumeSeek;
|
||||
seekBar.setMax(sink.getMaxVolume());
|
||||
seekBar.setProgress(sink.getVolume());
|
||||
|
||||
int iconRes = sink.isMute() ? R.drawable.ic_volume_mute_black : R.drawable.ic_volume_black;
|
||||
|
||||
ImageButton button = viewBinding.systemvolumeMute;
|
||||
button.setImageResource(iconRes);
|
||||
}
|
||||
}
|
@ -0,0 +1,189 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2018 Nicolas Fella <nicolas.fella@gmx.de>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
package org.kde.kdeconnect.Plugins.SystemVolumePlugin;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.asynclayoutinflater.view.AsyncLayoutInflater;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.ListAdapter;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.kde.kdeconnect.BackgroundService;
|
||||
import org.kde.kdeconnect.Plugins.MprisPlugin.MprisPlugin;
|
||||
import org.kde.kdeconnect.Plugins.MprisPlugin.VolumeKeyListener;
|
||||
import org.kde.kdeconnect_tp.R;
|
||||
import org.kde.kdeconnect_tp.databinding.ListItemSystemvolumeBinding;
|
||||
import org.kde.kdeconnect_tp.databinding.SystemVolumeFragmentBinding;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public class SystemVolumeFragment
|
||||
extends Fragment
|
||||
implements Sink.UpdateListener, SystemVolumePlugin.SinkListener, VolumeKeyListener {
|
||||
|
||||
private SystemVolumePlugin plugin;
|
||||
private RecyclerSinkAdapter recyclerAdapter;
|
||||
private boolean tracking;
|
||||
private final Consumer<Boolean> trackingConsumer = aBoolean -> tracking = aBoolean;
|
||||
private SystemVolumeFragmentBinding systemVolumeFragmentBinding;
|
||||
|
||||
public static SystemVolumeFragment newInstance(String deviceId) {
|
||||
SystemVolumeFragment systemvolumeFragment = new SystemVolumeFragment();
|
||||
|
||||
Bundle arguments = new Bundle();
|
||||
arguments.putString(MprisPlugin.DEVICE_ID_KEY, deviceId);
|
||||
|
||||
systemvolumeFragment.setArguments(arguments);
|
||||
|
||||
return systemvolumeFragment;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(
|
||||
@NonNull LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState
|
||||
) {
|
||||
|
||||
if (systemVolumeFragmentBinding == null) {
|
||||
systemVolumeFragmentBinding = SystemVolumeFragmentBinding.inflate(inflater);
|
||||
|
||||
RecyclerView recyclerView = systemVolumeFragmentBinding.audioDevicesRecycler;
|
||||
|
||||
int gap = requireContext().getResources().getDimensionPixelSize(R.dimen.activity_vertical_margin);
|
||||
recyclerView.addItemDecoration(new ItemGapDecoration(gap));
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||
|
||||
recyclerAdapter = new RecyclerSinkAdapter();
|
||||
recyclerView.setAdapter(recyclerAdapter);
|
||||
}
|
||||
|
||||
return systemVolumeFragmentBinding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
connectToPlugin(getDeviceId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
super.onDetach();
|
||||
disconnectFromPlugin(getDeviceId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateSink(final Sink sink) {
|
||||
|
||||
// Don't set progress while the slider is moved
|
||||
if (!tracking) {
|
||||
|
||||
requireActivity().runOnUiThread(() -> recyclerAdapter.notifyDataSetChanged());
|
||||
}
|
||||
}
|
||||
|
||||
private void connectToPlugin(final String deviceId) {
|
||||
BackgroundService.RunWithPlugin(requireActivity(), deviceId, SystemVolumePlugin.class, plugin -> {
|
||||
this.plugin = plugin;
|
||||
plugin.addSinkListener(SystemVolumeFragment.this);
|
||||
plugin.requestSinkList();
|
||||
});
|
||||
}
|
||||
|
||||
private void disconnectFromPlugin(final String deviceId) {
|
||||
BackgroundService.RunWithPlugin(requireActivity(), deviceId, SystemVolumePlugin.class, plugin ->
|
||||
plugin.removeSinkListener(SystemVolumeFragment.this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sinksChanged() {
|
||||
|
||||
for (Sink sink : plugin.getSinks()) {
|
||||
sink.addListener(SystemVolumeFragment.this);
|
||||
}
|
||||
|
||||
requireActivity().runOnUiThread(() -> {
|
||||
List<Sink> newSinks = new ArrayList<>(plugin.getSinks());
|
||||
recyclerAdapter.submitList(newSinks);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVolumeUp() {
|
||||
updateDefaultSinkVolume(5);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVolumeDown() {
|
||||
updateDefaultSinkVolume(-5);
|
||||
}
|
||||
|
||||
private void updateDefaultSinkVolume(int percent) {
|
||||
|
||||
if (percent < -100) percent = -100;
|
||||
if (percent > 100) percent = 100;
|
||||
|
||||
Optional<Sink> foundSink = plugin.getSinks().stream().filter(Sink::isDefault).findFirst();
|
||||
if (foundSink.isPresent()) {
|
||||
Sink defaultSink = foundSink.get();
|
||||
|
||||
int step = defaultSink.getMaxVolume() * percent / 100;
|
||||
|
||||
int newVolume = defaultSink.getVolume() + step;
|
||||
|
||||
if (newVolume > defaultSink.getMaxVolume()) {
|
||||
newVolume = defaultSink.getMaxVolume();
|
||||
} else if (newVolume < 0) {
|
||||
newVolume = 0;
|
||||
}
|
||||
|
||||
if (defaultSink.getVolume() == newVolume) return;
|
||||
|
||||
plugin.sendVolume(defaultSink.getName(), newVolume);
|
||||
}
|
||||
}
|
||||
|
||||
private String getDeviceId() {
|
||||
return requireArguments().getString(MprisPlugin.DEVICE_ID_KEY);
|
||||
}
|
||||
|
||||
private class RecyclerSinkAdapter extends ListAdapter<Sink, SinkItemHolder> {
|
||||
|
||||
public RecyclerSinkAdapter() {
|
||||
super(new SinkItemCallback());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public SinkItemHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
|
||||
LayoutInflater inflater = getLayoutInflater();
|
||||
ListItemSystemvolumeBinding viewBinding = ListItemSystemvolumeBinding.inflate(inflater, parent, false);
|
||||
|
||||
return new SinkItemHolder(viewBinding, plugin, trackingConsumer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull SinkItemHolder holder, int position) {
|
||||
holder.bind(getItem(position));
|
||||
}
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
package org.kde.kdeconnect.Plugins.SystemvolumePlugin;
|
||||
package org.kde.kdeconnect.Plugins.SystemVolumePlugin;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
@ -79,6 +79,9 @@ public class SystemVolumePlugin extends Plugin {
|
||||
if (np.has("muted")) {
|
||||
sinks.get(name).setMute(np.getBoolean("muted"));
|
||||
}
|
||||
if (np.has("enabled")) {
|
||||
sinks.get(name).setDefault(np.getBoolean("enabled"));
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@ -98,6 +101,13 @@ public class SystemVolumePlugin extends Plugin {
|
||||
device.sendPacket(np);
|
||||
}
|
||||
|
||||
void sendEnable(String name) {
|
||||
NetworkPacket np = new NetworkPacket(PACKET_TYPE_SYSTEMVOLUME_REQUEST);
|
||||
np.set("enabled", true);
|
||||
np.set("name", name);
|
||||
device.sendPacket(np);
|
||||
}
|
||||
|
||||
void requestSinkList() {
|
||||
NetworkPacket np = new NetworkPacket(PACKET_TYPE_SYSTEMVOLUME_REQUEST);
|
||||
np.set("requestSinks", true);
|
@ -1,139 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2018 Nicolas Fella <nicolas.fella@gmx.de>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
package org.kde.kdeconnect.Plugins.SystemvolumePlugin;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.kde.kdeconnect.BackgroundService;
|
||||
import org.kde.kdeconnect_tp.R;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.ListFragment;
|
||||
|
||||
public class SystemvolumeFragment extends ListFragment implements Sink.UpdateListener, SystemVolumePlugin.SinkListener {
|
||||
|
||||
private SystemVolumePlugin plugin;
|
||||
private Activity activity;
|
||||
private SinkAdapter adapter;
|
||||
private Context context;
|
||||
private boolean tracking;
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
getListView().setDivider(null);
|
||||
setListAdapter(new SinkAdapter(getContext(), new Sink[0]));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateSink(final Sink sink) {
|
||||
|
||||
// Don't set progress while the slider is moved
|
||||
if (!tracking) {
|
||||
|
||||
activity.runOnUiThread(() -> adapter.notifyDataSetChanged());
|
||||
}
|
||||
}
|
||||
|
||||
public void connectToPlugin(final String deviceId) {
|
||||
BackgroundService.RunWithPlugin(activity, deviceId, SystemVolumePlugin.class, plugin -> {
|
||||
this.plugin = plugin;
|
||||
plugin.addSinkListener(SystemvolumeFragment.this);
|
||||
plugin.requestSinkList();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
activity = getActivity();
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sinksChanged() {
|
||||
|
||||
for (Sink sink : plugin.getSinks()) {
|
||||
sink.addListener(SystemvolumeFragment.this);
|
||||
}
|
||||
|
||||
activity.runOnUiThread(() -> {
|
||||
adapter = new SinkAdapter(context, plugin.getSinks().toArray(new Sink[0]));
|
||||
setListAdapter(adapter);
|
||||
});
|
||||
}
|
||||
|
||||
private class SinkAdapter extends ArrayAdapter<Sink> {
|
||||
|
||||
private SinkAdapter(@NonNull Context context, @NonNull Sink[] objects) {
|
||||
super(context, R.layout.list_item_systemvolume, objects);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public View getView(final int position, @Nullable View convertView, @NonNull ViewGroup parent) {
|
||||
|
||||
View view = getLayoutInflater().inflate(R.layout.list_item_systemvolume, parent, false);
|
||||
|
||||
UIListener listener = new UIListener(getItem(position));
|
||||
|
||||
((TextView) view.findViewById(R.id.systemvolume_label)).setText(getItem(position).getDescription());
|
||||
|
||||
final SeekBar seekBar = view.findViewById(R.id.systemvolume_seek);
|
||||
seekBar.setMax(getItem(position).getMaxVolume());
|
||||
seekBar.setProgress(getItem(position).getVolume());
|
||||
seekBar.setOnSeekBarChangeListener(listener);
|
||||
|
||||
ImageButton button = view.findViewById(R.id.systemvolume_mute);
|
||||
int iconRes = getItem(position).isMute() ? R.drawable.ic_volume_mute_black : R.drawable.ic_volume_black;
|
||||
button.setImageResource(iconRes);
|
||||
button.setOnClickListener(listener);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class UIListener implements SeekBar.OnSeekBarChangeListener, ImageButton.OnClickListener {
|
||||
|
||||
private final Sink sink;
|
||||
|
||||
private UIListener(Sink sink) {
|
||||
this.sink = sink;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgressChanged(final SeekBar seekBar, int i, boolean b) {
|
||||
BackgroundService.RunCommand(activity, service -> plugin.sendVolume(sink.getName(), seekBar.getProgress()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
tracking = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(final SeekBar seekBar) {
|
||||
tracking = false;
|
||||
BackgroundService.RunCommand(activity, service -> plugin.sendVolume(sink.getName(), seekBar.getProgress()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
plugin.sendMute(sink.getName(), !sink.isMute());
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user