mirror of
https://github.com/KDE/kdeconnect-android
synced 2025-08-22 18:07:55 +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:name="org.kde.kdeconnect.Plugins.MprisPlugin.MprisActivity"
|
||||||
android:label="@string/open_mpris_controls"
|
android:label="@string/open_mpris_controls"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
|
android:theme="@style/KdeConnectTheme.NoActionBar"
|
||||||
android:parentActivityName="org.kde.kdeconnect.UserInterface.MainActivity">
|
android:parentActivityName="org.kde.kdeconnect.UserInterface.MainActivity">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<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_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical">
|
||||||
android:paddingBottom="15dp"
|
|
||||||
android:paddingLeft="25dp"
|
|
||||||
android:paddingRight="25dp"
|
|
||||||
android:paddingTop="25dp">
|
|
||||||
|
|
||||||
<ImageView
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
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"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
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>
|
</LinearLayout>
|
||||||
|
@ -1,44 +1,58 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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: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_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical">
|
app:cardCornerRadius="8dp"
|
||||||
|
app:cardElevation="0dp"
|
||||||
<TextView
|
app:contentPadding="8dp"
|
||||||
android:id="@+id/systemvolume_label"
|
app:strokeColor="@color/card_stroke_color"
|
||||||
android:layout_width="match_parent"
|
app:strokeWidth="1dp">
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="5dp"
|
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Medium" />
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/systemvolume_layout"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:gravity="center"
|
||||||
android:layout_marginTop="8dip"
|
android:orientation="vertical">
|
||||||
android:orientation="horizontal">
|
|
||||||
|
|
||||||
<ImageButton
|
<RadioButton
|
||||||
android:id="@+id/systemvolume_mute"
|
android:id="@+id/systemvolume_label"
|
||||||
android:layout_width="30dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="30dp"
|
android:layout_height="wrap_content"
|
||||||
android:background="@android:color/transparent"
|
android:layout_gravity="start"
|
||||||
android:contentDescription="@string/mute"
|
android:ellipsize="end"
|
||||||
android:scaleType="fitXY"
|
android:maxLines="1"
|
||||||
android:src="@drawable/ic_volume_black"
|
android:paddingLeft="14dp"
|
||||||
app:tint="?attr/colorHighContrast" />
|
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
|
||||||
|
tools:text="Device name" />
|
||||||
|
|
||||||
<SeekBar
|
<LinearLayout
|
||||||
android:id="@+id/systemvolume_seek"
|
android:id="@+id/systemvolume_layout"
|
||||||
android:layout_width="0dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:layout_weight="1"
|
android:layout_marginTop="8dip"
|
||||||
android:max="100" />
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/systemvolume_mute"
|
||||||
|
android:layout_width="30dp"
|
||||||
|
android:layout_height="30dp"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/mute"
|
||||||
|
android:scaleType="fitXY"
|
||||||
|
android:src="@drawable/ic_volume_black"
|
||||||
|
app:tint="?attr/colorHighContrast" />
|
||||||
|
|
||||||
|
<SeekBar
|
||||||
|
android:id="@+id/systemvolume_seek"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:max="100" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
</LinearLayout>
|
|
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_primary">@android:color/white</color>
|
||||||
<color name="text_color">@android:color/white</color>
|
<color name="text_color">@android:color/white</color>
|
||||||
<color name="toolbar_color">#222222</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
|
<!-- 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
|
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_primary">@android:color/black</color>
|
||||||
<color name="text_color">@android:color/black</color>
|
<color name="text_color">@android:color/black</color>
|
||||||
<color name="toolbar_color">#F67400</color>
|
<color name="toolbar_color">#F67400</color>
|
||||||
|
<color name="card_stroke_color">#C8C8C8</color>
|
||||||
</resources>
|
</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;
|
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.Bundle;
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Message;
|
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.view.KeyEvent;
|
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.NonNull;
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.core.graphics.drawable.DrawableCompat;
|
import androidx.fragment.app.FragmentActivity;
|
||||||
import androidx.fragment.app.FragmentManager;
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.ArrayUtils;
|
import com.google.android.material.tabs.TabLayoutMediator;
|
||||||
import org.kde.kdeconnect.Backends.BaseLink;
|
|
||||||
import org.kde.kdeconnect.Backends.BaseLinkProvider;
|
import org.kde.kdeconnect.Plugins.SystemVolumePlugin.SystemVolumeFragment;
|
||||||
import org.kde.kdeconnect.BackgroundService;
|
|
||||||
import org.kde.kdeconnect.Helpers.VideoUrlsHelper;
|
|
||||||
import org.kde.kdeconnect.NetworkPacket;
|
|
||||||
import org.kde.kdeconnect.Plugins.SystemvolumePlugin.SystemvolumeFragment;
|
|
||||||
import org.kde.kdeconnect.UserInterface.ThemeUtil;
|
import org.kde.kdeconnect.UserInterface.ThemeUtil;
|
||||||
import org.kde.kdeconnect_tp.R;
|
import org.kde.kdeconnect_tp.R;
|
||||||
import org.kde.kdeconnect_tp.databinding.ActivityMprisBinding;
|
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 {
|
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 ActivityMprisBinding activityMprisBinding;
|
||||||
private MprisControlBinding mprisControlBinding;
|
private MprisPagerAdapter mprisPagerAdapter;
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||||
switch (keyCode) {
|
switch (keyCode) {
|
||||||
case KeyEvent.KEYCODE_VOLUME_UP:
|
case KeyEvent.KEYCODE_VOLUME_UP:
|
||||||
updateVolume(5);
|
if (activityMprisBinding != null && mprisPagerAdapter != null) {
|
||||||
|
int pagePosition = activityMprisBinding.mprisTabs.getSelectedTabPosition();
|
||||||
|
mprisPagerAdapter.onVolumeUp(pagePosition);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
case KeyEvent.KEYCODE_VOLUME_DOWN:
|
case KeyEvent.KEYCODE_VOLUME_DOWN:
|
||||||
updateVolume(-5);
|
if (activityMprisBinding != null && mprisPagerAdapter != null) {
|
||||||
|
int pagePosition = activityMprisBinding.mprisTabs.getSelectedTabPosition();
|
||||||
|
mprisPagerAdapter.onVolumeDown(pagePosition);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
return super.onKeyDown(keyCode, event);
|
return super.onKeyDown(keyCode, event);
|
||||||
@ -287,7 +51,6 @@ public class MprisActivity extends AppCompatActivity {
|
|||||||
public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) {
|
public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) {
|
||||||
switch (keyCode) {
|
switch (keyCode) {
|
||||||
case KeyEvent.KEYCODE_VOLUME_UP:
|
case KeyEvent.KEYCODE_VOLUME_UP:
|
||||||
return true;
|
|
||||||
case KeyEvent.KEYCODE_VOLUME_DOWN:
|
case KeyEvent.KEYCODE_VOLUME_DOWN:
|
||||||
return true;
|
return true;
|
||||||
default:
|
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
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
ThemeUtil.setUserPreferredTheme(this);
|
ThemeUtil.setUserPreferredTheme(this);
|
||||||
|
|
||||||
activityMprisBinding = ActivityMprisBinding.inflate(getLayoutInflater());
|
activityMprisBinding = ActivityMprisBinding.inflate(getLayoutInflater());
|
||||||
mprisControlBinding = activityMprisBinding.mprisControl;
|
|
||||||
|
|
||||||
setContentView(activityMprisBinding.getRoot());
|
setContentView(activityMprisBinding.getRoot());
|
||||||
|
|
||||||
String targetPlayerName = getIntent().getStringExtra("player");
|
String deviceId = getIntent().getStringExtra(MprisPlugin.DEVICE_ID_KEY);
|
||||||
getIntent().removeExtra("player");
|
|
||||||
|
|
||||||
if (TextUtils.isEmpty(targetPlayerName)) {
|
mprisPagerAdapter = new MprisPagerAdapter(this, deviceId);
|
||||||
if (savedInstanceState != null) {
|
activityMprisBinding.mprisPager.setAdapter(mprisPagerAdapter);
|
||||||
targetPlayerName = savedInstanceState.getString("targetPlayer");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deviceId = getIntent().getStringExtra("deviceId");
|
TabLayoutMediator tabLayoutMediator = new TabLayoutMediator(
|
||||||
|
activityMprisBinding.mprisTabs,
|
||||||
|
activityMprisBinding.mprisPager,
|
||||||
|
(tab, position) -> {
|
||||||
|
tab.setText(mprisPagerAdapter.getTitle(position));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
activityMprisBinding.mprisTabs.getSelectedTabPosition();
|
||||||
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(MprisActivity.this, service -> service.addConnectionListener(connectionReceiver));
|
tabLayoutMediator.attach();
|
||||||
connectToPlugin(targetPlayerName);
|
|
||||||
|
|
||||||
performActionOnClick(mprisControlBinding.playButton, MprisPlugin.MprisPlayer::playPause);
|
setSupportActionBar(activityMprisBinding.toolbar);
|
||||||
|
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||||
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(MprisActivity.this, service -> {
|
|
||||||
if (targetPlayer == null) return;
|
|
||||||
targetPlayer.setVolume(seekBar.getProgress());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStopTrackingTouch(final SeekBar seekBar) {
|
|
||||||
BackgroundService.RunCommand(MprisActivity.this, service -> {
|
|
||||||
if (targetPlayer != null) {
|
|
||||||
targetPlayer.setPosition(seekBar.getProgress());
|
|
||||||
}
|
|
||||||
positionSeekUpdateHandler.postDelayed(positionSeekUpdateRunnable, 200);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
mprisControlBinding.nowPlayingTextview.setSelected(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final static int MENU_OPEN_URL = Menu.FIRST;
|
static class MprisPagerAdapter extends ExtendedFragmentAdapter {
|
||||||
|
|
||||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
private final String deviceId;
|
||||||
menu.clear();
|
|
||||||
if(targetPlayer != null && !"".equals(targetPlayer.getUrl())) {
|
public MprisPagerAdapter(@NonNull FragmentActivity fragmentActivity, String deviceId) {
|
||||||
menu.add(0, MENU_OPEN_URL, Menu.NONE, R.string.mpris_open_url);
|
super(fragmentActivity);
|
||||||
|
this.deviceId = deviceId;
|
||||||
}
|
}
|
||||||
return super.onPrepareOptionsMenu(menu);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@NonNull
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
@Override
|
||||||
if (targetPlayer != null && item.getItemId() == MENU_OPEN_URL) {
|
public Fragment createFragment(int position) {
|
||||||
try {
|
if (position == 1) {
|
||||||
String url = VideoUrlsHelper.formatUriWithSeek(targetPlayer.getUrl(), targetPlayer.getPosition()).toString();
|
return SystemVolumeFragment.newInstance(deviceId);
|
||||||
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
} else {
|
||||||
startActivity(browserIntent);
|
return MprisNowPlayingFragment.newInstance(deviceId);
|
||||||
targetPlayer.pause();
|
|
||||||
return true;
|
|
||||||
} catch (MalformedURLException | ActivityNotFoundException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
Toast.makeText(getApplicationContext(), getString(R.string.cant_open_url), Toast.LENGTH_LONG).show();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
public int getItemCount() {
|
||||||
if (targetPlayer != null) {
|
return 2;
|
||||||
outState.putString("targetPlayer", targetPlayer.getPlayer());
|
}
|
||||||
|
|
||||||
|
@StringRes
|
||||||
|
int getTitle(int position) {
|
||||||
|
if (position == 1) {
|
||||||
|
return R.string.devices;
|
||||||
|
} else {
|
||||||
|
return R.string.mpris_play;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onVolumeUp(int page) {
|
||||||
|
Fragment requestedFragment = getFragment(page);
|
||||||
|
if (requestedFragment == null) return;
|
||||||
|
|
||||||
|
if (requestedFragment instanceof VolumeKeyListener) {
|
||||||
|
((VolumeKeyListener) requestedFragment).onVolumeUp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onVolumeDown(int page) {
|
||||||
|
Fragment requestedFragment = getFragment(page);
|
||||||
|
if (requestedFragment == null) return;
|
||||||
|
|
||||||
|
if (requestedFragment instanceof VolumeKeyListener) {
|
||||||
|
((VolumeKeyListener) requestedFragment).onVolumeDown();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
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 = "kdeconnect.mpris";
|
||||||
private final static String PACKET_TYPE_MPRIS_REQUEST = "kdeconnect.mpris.request";
|
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
|
* 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.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
@ -23,6 +23,7 @@ class Sink {
|
|||||||
private String name;
|
private String name;
|
||||||
private boolean mute;
|
private boolean mute;
|
||||||
private int maxVolume;
|
private int maxVolume;
|
||||||
|
private boolean enabled;
|
||||||
|
|
||||||
private final List<UpdateListener> listeners;
|
private final List<UpdateListener> listeners;
|
||||||
|
|
||||||
@ -33,6 +34,7 @@ class Sink {
|
|||||||
mute = obj.getBoolean("muted");
|
mute = obj.getBoolean("muted");
|
||||||
description = obj.getString("description");
|
description = obj.getString("description");
|
||||||
maxVolume = obj.getInt("maxVolume");
|
maxVolume = obj.getInt("maxVolume");
|
||||||
|
enabled = obj.optBoolean("enabled", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
int getVolume() {
|
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) {
|
void addListener(UpdateListener l) {
|
||||||
|
|
||||||
if (!listeners.contains(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
|
* 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;
|
import android.util.Log;
|
||||||
|
|
||||||
@ -79,6 +79,9 @@ public class SystemVolumePlugin extends Plugin {
|
|||||||
if (np.has("muted")) {
|
if (np.has("muted")) {
|
||||||
sinks.get(name).setMute(np.getBoolean("muted"));
|
sinks.get(name).setMute(np.getBoolean("muted"));
|
||||||
}
|
}
|
||||||
|
if (np.has("enabled")) {
|
||||||
|
sinks.get(name).setDefault(np.getBoolean("enabled"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@ -98,6 +101,13 @@ public class SystemVolumePlugin extends Plugin {
|
|||||||
device.sendPacket(np);
|
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() {
|
void requestSinkList() {
|
||||||
NetworkPacket np = new NetworkPacket(PACKET_TYPE_SYSTEMVOLUME_REQUEST);
|
NetworkPacket np = new NetworkPacket(PACKET_TYPE_SYSTEMVOLUME_REQUEST);
|
||||||
np.set("requestSinks", true);
|
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