diff --git a/res/layout/mpris_control.xml b/res/layout/mpris_control.xml
index ec1c186c..f098bc38 100644
--- a/res/layout/mpris_control.xml
+++ b/res/layout/mpris_control.xml
@@ -1,180 +1,171 @@
-
-
diff --git a/res/values/strings.xml b/res/values/strings.xml
index bbee6f91..d19cc157 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1,362 +1,362 @@
KDE Connect
Not connected to any device
Connected to: %s
Send Clipboard
Telephony notifier
Send notifications for incoming calls
Battery report
Periodically report battery status
Filesystem expose
Allows to browse this device\'s filesystem remotely
Clipboard sync
Share the clipboard content
Clipboard Sent
Remote input
Use your phone or tablet as a touchpad and keyboard
Slideshow remote
Use your device to change slides in a presentation
Receive remote keypresses
Receive keypress events from remote devices
Multimedia controls
Provides a remote control for your media player
Run Command
Trigger remote commands from your phone or tablet
Contacts Synchronizer
Allow synchronizing the device\'s contacts book
Ping
Send and receive pings
Notification sync
Access your notifications from other devices
Receive notifications
Receive notifications from the other device and display them on Android
Share and receive
Share files and URLs between devices
No devices
OK
OK :(
Cancel
Open settings
You need to grant permission to access notifications
To be able to control your media players you need to grant access to the notifications
To receive keypresses you need to activate the KDE Connect Remote Keyboard
Send ping
Multimedia control
remotekeyboard_editing_only
Handle remote keys only when editing
There is no active remote keyboard connection, establish one in kdeconnect
Remote keyboard connection is active
There is more than one remote keyboard connection, select the device to configure
Remote input
Move a finger on the screen to move the mouse cursor. Tap for a click, and use two/three fingers for right and middle buttons. Use 2 fingers to scroll. Use a long press to drag\'n drop.
Set two finger tap action
Set three finger tap action
Set touchpad sensitivity
Set pointer acceleration
mousepad_double_tap_key
mousepad_triple_tap_key
mousepad_sensitivity_key
mousepad_acceleration_profile_key
Reverse Scrolling Direction
mousepad_scroll_direction
- Right click
- Middle click
- Nothing
right
middle
default
medium
- right
- middle
- none
- Slowest
- Above Slowest
- Default
- Above Default
- Fastest
- No Acceleration
- Weakest
- Weaker
- Medium
- Stronger
- Strongest
- slowest
- aboveSlowest
- default
- aboveDefault
- fastest
- noacceleration
- weaker
- weak
- medium
- strong
- stronger
Connected devices
Available devices
Remembered devices
Plugin settings
Unpair
Pair new device
Unknown device
Device not reachable
Device already paired
Could not send package
Timed out
Canceled by user
Canceled by other peer
Encryption Info
The other device doesn\'t use a recent version of KDE Connect, using the legacy encryption method.
SHA1 fingerprint of your device certificate is:
SHA1 fingerprint of remote device certificate is:
Pair requested
Pairing request from %1s
Receiving file from %1s>
- Receiving %1$d file from %2$s
- Receiving %1$d files from %2$s
- File: %1s
- (File %2$d of %3$d) : %1$s
- Sending %1$d file to %2$s
- Sending %1$d files to %2$s
- File: %1$s
- (File %2$d of %3$d) : %1$s
- Received file from %1$s
- Received %2$d files from %1$s
- Failed receiving file from %1$s
- Failed receiving %2$d of %3$d files from %1$s
- Sent file to %1$s
- Sent %2$d files to %1$s"
- Failed sending file to %1$s
- Failed sending %2$d of %3$d files to %1$s
Tap to open \'%1s\'
Cannot create file %s
Tap to answer
Send Right Click
Send Middle Click
Show Keyboard
Device not paired
Request pairing
Accept
Reject
Settings
Play
Pause
Previous
Rewind
Fast-forward
Next
Volume
Forward/rewind buttons
Adjust the time to fast forward/rewind when pressed
mpris_interval_time
- 10 seconds
- 20 seconds
- 30 seconds
- 1 minute
- 2 minutes
10000000
- 10000000
- 20000000
- 30000000
- 60000000
- 120000000
Show media control notification
Allow controlling your media players without opening KDE Connect
mpris_notification_enabled
Share To…
This device uses a newer protocol version
%s settings
Invalid device name
Received text, saved to clipboard
Custom device list
Add devices by IP
Custom device deleted
If your device is not automatically detected you can add its IP address or hostname by clicking on the Floating Action Button
Add a device
Undo
Noisy notifications
Vibrate and play a sound when receiving a file
Customize destination directory
Received files will appear in Downloads
Files will be stored in the directory below
Destination directory
Share
Share \"%s\"
Notification filter
Notifications will be synchronized for the selected apps.
SD card %d
SD card
(read only)
Camera pictures
Add device
Hostname or IP address
Detected SD cards
Edit SD card
Configured storage locations
Add storage location
Edit storage location
Add camera folder shortcut
Add a shortcut to the camera folder
Do not add a shortcut to the camera folder
key_sftp_preference_category
key_sftp_add_storage
key_sftp_add_camera_shotcut
key_sftp_storage_info%d"
key_sftp_storage_info_list
Storage location
This location has already been configured
click to select
Display name
This display name is already used
Display name cannot be empty
Delete
No SD card detected
No storage locations configured
To access files remotely you have to configure storage locations
No players found
Send files
KDE Connect Devices
Other devices running KDE Connect in your same network should appear here.
Rename device
Rename
Refresh
This paired device is not reachable. Make sure it is connected to your same network.
You\'re not connected to a Wi-Fi network, so you may not be able to see any devices. Click here to enable Wi-Fi.
Not on a trusted network: autodiscovery is disabled.
There are no file browsers installed.
Send SMS
Send text messages from your desktop
Find my phone
Find my tablet
Find my TV
Rings this device so you can find it
Found it
Open
Close
Some Plugins need permissions to work (tap for more info):
This plugin needs permissions to work
You need to grant extra permissions to enable all functions
Some plugins have features disabled because of lack of permission (tap for more info):
To share files between your phone and your desktop you need to give access to the phone\'s storage
To read and write SMS from your desktop you need to give permission to SMS
To see phone calls on the desktop you need to give permission to phone call logs and phone state
To see a contact name instead of a phone number you need to give access to the phone\'s contacts
To share your contacts book with the desktop, you need to give contacts permission
Select a ringtone
Blocked numbers
Don\'t show calls and SMS from these numbers. Please specify one number per line
Cover art of current media
Device icon
Settings icon
Fullscreen
Exit presentation
You can lock your device and use the volume keys to go to the previous/next slide
Add a command
There are no commands registered
You can add new commands in the KDE Connect System Settings
You can add commands on the desktop
Media Player Control
Control your phone\'s media players from another device
Other notifications
Persistent indicator
Media control
File transfer
High priority
Stop the current player
Copy URL to clipboard
Copied to clipboard
Device is not reachable
Device is not paired
There is no such device
This device does not have the Run Command Plugin enabled
Find remote device
Ring your remote device
Ring
System volume
Control the system volume of the remote device
Mute
All
Devices
Device name
Dark theme
More settings
Per-device settings can be found under \'Plugin settings\' from within a device.
Show persistent notification
Persistent notification
Tap to enable/disable in Notification settings
Extra options
Privacy options
Set your privacy options
Block contents of notifications
Block images in notifications
Notifications from other devices
Launch camera
Launch the camera app to ease taking and transferring pictures
findmyphone_ringtone
No suitable app found to open this file
KDE Connect Remote Keyboard
Pointer
Trusted networks
Restrict autodiscovery to known networks
Add %1s
You haven\'t added any trusted network yet
Allow all
Permission required
Android requires the Location permission to identify your WiFi network
Android 10 has removed clipboard access to all apps. This plugin will be disabled.
- Open URL
+ Continue playing here
Can\'t open URL
Can\'t format URL for seek
diff --git a/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisActivity.java b/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisActivity.java
index 345356ef..79556b4b 100644
--- a/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisActivity.java
+++ b/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisActivity.java
@@ -1,475 +1,493 @@
/*
* Copyright 2014 Albert Vaca Cintora
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License or (at your option) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
package org.kde.kdeconnect.Plugins.MprisPlugin;
+import android.app.Activity;
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.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.SeekBar;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
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 org.kde.kdeconnect.UserInterface.ThemeUtil;
import org.kde.kdeconnect_tp.R;
import java.net.MalformedURLException;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.fragment.app.FragmentManager;
import butterknife.BindView;
import butterknife.ButterKnife;
public class MprisActivity extends AppCompatActivity {
private String deviceId;
private final Handler positionSeekUpdateHandler = new Handler();
private Runnable positionSeekUpdateRunnable = null;
private MprisPlugin.MprisPlayer targetPlayer = null;
@BindView(R.id.play_button)
ImageButton playButton;
@BindView(R.id.prev_button)
ImageButton prevButton;
@BindView(R.id.next_button)
ImageButton nextButton;
@BindView(R.id.rew_button)
ImageButton rewButton;
@BindView(R.id.ff_button)
ImageButton ffButton;
- @BindView(R.id.open_url_button)
- ImageButton openUrlButton;
-
@BindView(R.id.time_textview)
TextView timeText;
@BindView(R.id.album_art)
ImageView albumArtView;
@BindView(R.id.player_spinner)
Spinner playerSpinner;
@BindView(R.id.no_players)
TextView noPlayers;
@BindView(R.id.now_playing_textview)
TextView nowPlayingText;
@BindView(R.id.positionSeek)
SeekBar positionBar;
@BindView(R.id.progress_slider)
LinearLayout progressSlider;
@BindView(R.id.volume_seek)
SeekBar volumeSeek;
@BindView(R.id.volume_layout)
LinearLayout volumeLayout;
@BindView(R.id.stop_button)
ImageButton stopButton;
@BindView(R.id.progress_textview)
TextView progressText;
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);
addSytemvolumeFragment();
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 playerList = mpris.getPlayerList();
final ArrayAdapter adapter = new ArrayAdapter<>(MprisActivity.this,
android.R.layout.simple_spinner_item,
playerList.toArray(new String[0])
);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
runOnUiThread(() -> {
playerSpinner.setAdapter(adapter);
if (playerList.isEmpty()) {
noPlayers.setVisibility(View.VISIBLE);
playerSpinner.setVisibility(View.GONE);
nowPlayingText.setText("");
} else {
noPlayers.setVisibility(View.GONE);
playerSpinner.setVisibility(View.VISIBLE);
}
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) {
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));
playerSpinner.setSelection(0);
}
updatePlayerStatus(mpris);
});
}
});
});
}
private void addSytemvolumeFragment() {
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 (!nowPlayingText.getText().toString().equals(song)) {
nowPlayingText.setText(song);
}
Bitmap albumArt = playerStatus.getAlbumArt();
if (albumArt == null) {
Drawable placeholder_art = DrawableCompat.wrap(getResources().getDrawable(R.drawable.ic_album_art_placeholder));
DrawableCompat.setTint(placeholder_art, getResources().getColor(R.color.primary));
albumArtView.setImageDrawable(placeholder_art);
} else {
albumArtView.setImageBitmap(albumArt);
}
if (playerStatus.isSeekAllowed()) {
timeText.setText(milisToProgress(playerStatus.getLength()));
positionBar.setMax((int) (playerStatus.getLength()));
positionBar.setProgress((int) (playerStatus.getPosition()));
progressSlider.setVisibility(View.VISIBLE);
} else {
progressSlider.setVisibility(View.GONE);
}
int volume = playerStatus.getVolume();
volumeSeek.setProgress(volume);
boolean isPlaying = playerStatus.isPlaying();
if (isPlaying) {
playButton.setImageResource(R.drawable.ic_pause_black);
playButton.setEnabled(playerStatus.isPauseAllowed());
} else {
playButton.setImageResource(R.drawable.ic_play_black);
playButton.setEnabled(playerStatus.isPlayAllowed());
}
volumeLayout.setVisibility(playerStatus.isSetVolumeAllowed() ? View.VISIBLE : View.GONE);
rewButton.setVisibility(playerStatus.isSeekAllowed() ? View.VISIBLE : View.GONE);
ffButton.setVisibility(playerStatus.isSeekAllowed() ? View.VISIBLE : View.GONE);
- openUrlButton.setVisibility("".equals(playerStatus.getUrl()) ? View.GONE : View.VISIBLE);
+
+ invalidateOptionsMenu();
//Show and hide previous/next buttons simultaneously
if (playerStatus.isGoPreviousAllowed() || playerStatus.isGoNextAllowed()) {
prevButton.setVisibility(View.VISIBLE);
prevButton.setEnabled(playerStatus.isGoPreviousAllowed());
nextButton.setVisibility(View.VISIBLE);
nextButton.setEnabled(playerStatus.isGoNextAllowed());
} else {
prevButton.setVisibility(View.GONE);
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
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_VOLUME_UP:
updateVolume(5);
return true;
case KeyEvent.KEYCODE_VOLUME_DOWN:
updateVolume(-5);
return true;
default:
return super.onKeyDown(keyCode, event);
}
}
@Override
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:
return super.onKeyUp(keyCode, event);
}
}
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);
setContentView(R.layout.activity_mpris);
ButterKnife.bind(this);
final String targetPlayerName = getIntent().getStringExtra("player");
getIntent().removeExtra("player");
deviceId = getIntent().getStringExtra("deviceId");
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);
BackgroundService.RunCommand(MprisActivity.this, service -> service.addConnectionListener(connectionReceiver));
connectToPlugin(targetPlayerName);
performActionOnClick(playButton, MprisPlugin.MprisPlayer::playPause);
performActionOnClick(prevButton, MprisPlugin.MprisPlayer::previous);
performActionOnClick(rewButton, p -> targetPlayer.seek(interval_time * -1));
performActionOnClick(ffButton, p -> p.seek(interval_time));
- performActionOnClick(openUrlButton, p -> {
- String url = p.getUrl();
- try {
- url = VideoUrlsHelper.formatUriWithSeek(p.getUrl(), p.getPosition()).toString();
- } catch (MalformedURLException e) {
- e.printStackTrace();
- Toast.makeText(getApplicationContext(), String.format("%s '%s'", getString(R.string.cant_format_seek_uri), p.getUrl()), Toast.LENGTH_LONG).show();
- }
- try {
- Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
- startActivity(browserIntent);
- } catch (ActivityNotFoundException e) {
- e.printStackTrace();
- Toast.makeText(getApplicationContext(), String.format("%s '%s'", getString(R.string.cant_open_url), p.getUrl()), Toast.LENGTH_LONG).show();
- }
- });
-
performActionOnClick(nextButton, MprisPlugin.MprisPlayer::next);
performActionOnClick(stopButton, MprisPlugin.MprisPlayer::stop);
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) {
positionBar.setProgress((int) (targetPlayer.getPosition()));
}
positionSeekUpdateHandler.removeCallbacks(positionSeekUpdateRunnable);
positionSeekUpdateHandler.postDelayed(positionSeekUpdateRunnable, 1000);
});
positionSeekUpdateHandler.postDelayed(positionSeekUpdateRunnable, 200);
positionBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean byUser) {
progressText.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);
});
}
});
nowPlayingText.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) {
+ String url = targetPlayer.getUrl();
+ try {
+ url = VideoUrlsHelper.formatUriWithSeek(url, targetPlayer.getPosition()).toString();
+ } catch (MalformedURLException e) {
+ e.printStackTrace();
+ Toast.makeText(getApplicationContext(), String.format("%s '%s'", getString(R.string.cant_format_seek_uri), url), Toast.LENGTH_LONG).show();
+ }
+ try {
+ Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+ startActivity(browserIntent);
+ } catch (ActivityNotFoundException e) {
+ e.printStackTrace();
+ Toast.makeText(getApplicationContext(), String.format("%s '%s'", getString(R.string.cant_open_url), url), Toast.LENGTH_LONG).show();
+ }
+
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
}
diff --git a/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisPlugin.java b/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisPlugin.java
index f6205b8f..86cbe91f 100644
--- a/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisPlugin.java
+++ b/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisPlugin.java
@@ -1,505 +1,504 @@
/*
* Copyright 2014 Albert Vaca Cintora
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License or (at your option) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
package org.kde.kdeconnect.Plugins.MprisPlugin;
-import android.annotation.NonNull;
import android.app.Activity;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
+import androidx.core.content.ContextCompat;
+
import org.kde.kdeconnect.NetworkPacket;
import org.kde.kdeconnect.Plugins.Plugin;
import org.kde.kdeconnect.Plugins.PluginFactory;
import org.kde.kdeconnect_tp.R;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
-import androidx.core.content.ContextCompat;
-
@PluginFactory.LoadablePlugin
public class MprisPlugin extends Plugin {
public class MprisPlayer {
private String player = "";
private boolean playing = false;
private String currentSong = "";
private String title = "";
private String artist = "";
private String album = "";
private String albumArtUrl = "";
private String url = "";
private int volume = 50;
private long length = -1;
private long lastPosition = 0;
private long lastPositionTime;
private boolean playAllowed = true;
private boolean pauseAllowed = true;
private boolean goNextAllowed = true;
private boolean goPreviousAllowed = true;
private boolean seekAllowed = true;
MprisPlayer() {
lastPositionTime = System.currentTimeMillis();
}
public String getCurrentSong() {
return currentSong;
}
public String getTitle() {
return title;
}
public String getArtist() {
return artist;
}
public String getAlbum() {
return album;
}
public String getPlayer() {
return player;
}
boolean isSpotify() {
return getPlayer().toLowerCase().equals("spotify");
}
public int getVolume() {
return volume;
}
public long getLength() {
return length;
}
public boolean isPlaying() {
return playing;
}
public boolean isPlayAllowed() {
return playAllowed;
}
public boolean isPauseAllowed() {
return pauseAllowed;
}
public boolean isGoNextAllowed() {
return goNextAllowed;
}
public boolean isGoPreviousAllowed() {
return goPreviousAllowed;
}
public boolean isSeekAllowed() {
return seekAllowed && getLength() >= 0 && getPosition() >= 0 && !isSpotify();
}
public boolean hasAlbumArt() {
return !albumArtUrl.isEmpty();
}
/**
* Returns the album art (if available). Note that this can return null even if hasAlbumArt() returns true.
*
* @return The album art, or null if not available
*/
public Bitmap getAlbumArt() {
return AlbumArtCache.getAlbumArt(albumArtUrl, MprisPlugin.this, player);
}
- @NonNull
+ //@NonNull
public String getUrl() {
return url;
}
public boolean isSetVolumeAllowed() {
return !isSpotify();
}
public long getPosition() {
if (playing) {
return lastPosition + (System.currentTimeMillis() - lastPositionTime);
} else {
return lastPosition;
}
}
public void playPause() {
if (isPauseAllowed() || isPlayAllowed()) {
MprisPlugin.this.sendCommand(getPlayer(), "action", "PlayPause");
}
}
public void play() {
if (isPlayAllowed()) {
MprisPlugin.this.sendCommand(getPlayer(), "action", "Play");
}
}
public void pause() {
if (isPauseAllowed()) {
MprisPlugin.this.sendCommand(getPlayer(), "action", "Pause");
}
}
public void stop() {
MprisPlugin.this.sendCommand(getPlayer(), "action", "Stop");
}
public void previous() {
if (isGoPreviousAllowed()) {
MprisPlugin.this.sendCommand(getPlayer(), "action", "Previous");
}
}
public void next() {
if (isGoNextAllowed()) {
MprisPlugin.this.sendCommand(getPlayer(), "action", "Next");
}
}
public void setVolume(int volume) {
if (isSetVolumeAllowed()) {
MprisPlugin.this.sendCommand(getPlayer(), "setVolume", volume);
}
}
public void setPosition(int position) {
if (isSeekAllowed()) {
MprisPlugin.this.sendCommand(getPlayer(), "SetPosition", position);
lastPosition = position;
lastPositionTime = System.currentTimeMillis();
}
}
public void seek(int offset) {
if (isSeekAllowed()) {
MprisPlugin.this.sendCommand(getPlayer(), "Seek", offset);
}
}
}
private final static String PACKET_TYPE_MPRIS = "kdeconnect.mpris";
private final static String PACKET_TYPE_MPRIS_REQUEST = "kdeconnect.mpris.request";
private final HashMap players = new HashMap<>();
private boolean supportAlbumArtPayload = false;
private final HashMap playerStatusUpdated = new HashMap<>();
private final HashMap playerListUpdated = new HashMap<>();
@Override
public String getDisplayName() {
return context.getResources().getString(R.string.pref_plugin_mpris);
}
@Override
public String getDescription() {
return context.getResources().getString(R.string.pref_plugin_mpris_desc);
}
@Override
public Drawable getIcon() {
return ContextCompat.getDrawable(context, R.drawable.mpris_plugin_action);
}
@Override
public boolean hasSettings() {
return true;
}
@Override
public boolean onCreate() {
MprisMediaSession.getInstance().onCreate(context.getApplicationContext(), this, device.getDeviceId());
//Always request the player list so the data is up-to-date
requestPlayerList();
AlbumArtCache.initializeDiskCache(context);
AlbumArtCache.registerPlugin(this);
return true;
}
@Override
public void onDestroy() {
players.clear();
AlbumArtCache.deregisterPlugin(this);
MprisMediaSession.getInstance().onDestroy(this, device.getDeviceId());
}
private void sendCommand(String player, String method, String value) {
NetworkPacket np = new NetworkPacket(PACKET_TYPE_MPRIS_REQUEST);
np.set("player", player);
np.set(method, value);
device.sendPacket(np);
}
private void sendCommand(String player, String method, int value) {
NetworkPacket np = new NetworkPacket(PACKET_TYPE_MPRIS_REQUEST);
np.set("player", player);
np.set(method, value);
device.sendPacket(np);
}
@Override
public boolean onPacketReceived(NetworkPacket np) {
if (np.getBoolean("transferringAlbumArt", false)) {
AlbumArtCache.payloadToDiskCache(np.getString("albumArtUrl"), np.getPayload());
return true;
}
if (np.has("player")) {
MprisPlayer playerStatus = players.get(np.getString("player"));
if (playerStatus != null) {
playerStatus.currentSong = np.getString("nowPlaying", playerStatus.currentSong);
//Note: title, artist and album will not be available for all desktop clients
playerStatus.title = np.getString("title", playerStatus.title);
playerStatus.artist = np.getString("artist", playerStatus.artist);
playerStatus.album = np.getString("album", playerStatus.album);
playerStatus.url = np.getString("url", playerStatus.url);
playerStatus.volume = np.getInt("volume", playerStatus.volume);
playerStatus.length = np.getLong("length", playerStatus.length);
if (np.has("pos")) {
playerStatus.lastPosition = np.getLong("pos", playerStatus.lastPosition);
playerStatus.lastPositionTime = System.currentTimeMillis();
}
playerStatus.playing = np.getBoolean("isPlaying", playerStatus.playing);
playerStatus.playAllowed = np.getBoolean("canPlay", playerStatus.playAllowed);
playerStatus.pauseAllowed = np.getBoolean("canPause", playerStatus.pauseAllowed);
playerStatus.goNextAllowed = np.getBoolean("canGoNext", playerStatus.goNextAllowed);
playerStatus.goPreviousAllowed = np.getBoolean("canGoPrevious", playerStatus.goPreviousAllowed);
playerStatus.seekAllowed = np.getBoolean("canSeek", playerStatus.seekAllowed);
String newAlbumArtUrlstring = np.getString("albumArtUrl", playerStatus.albumArtUrl);
try {
//Turn the url into canonical form (and check its validity)
URL newAlbumArtUrl = new URL(newAlbumArtUrlstring);
playerStatus.albumArtUrl = newAlbumArtUrl.toString();
} catch (MalformedURLException ignored) {
playerStatus.albumArtUrl = "";
}
for (String key : playerStatusUpdated.keySet()) {
try {
playerStatusUpdated.get(key).dispatchMessage(new Message());
} catch (Exception e) {
Log.e("MprisControl", "Exception", e);
playerStatusUpdated.remove(key);
}
}
}
}
//Remember if the connected device support album art payloads
supportAlbumArtPayload = np.getBoolean("supportAlbumArtPayload", supportAlbumArtPayload);
List newPlayerList = np.getStringList("playerList");
if (newPlayerList != null) {
boolean equals = true;
for (String newPlayer : newPlayerList) {
if (!players.containsKey(newPlayer)) {
equals = false;
MprisPlayer player = new MprisPlayer();
player.player = newPlayer;
players.put(newPlayer, player);
//Immediately ask for the data of this player
requestPlayerStatus(newPlayer);
}
}
Iterator> iter = players.entrySet().iterator();
while (iter.hasNext()) {
String oldPlayer = iter.next().getKey();
boolean found = false;
for (String newPlayer : newPlayerList) {
if (newPlayer.equals(oldPlayer)) {
found = true;
break;
}
}
if (!found) {
iter.remove();
equals = false;
}
}
if (!equals) {
for (String key : playerListUpdated.keySet()) {
try {
playerListUpdated.get(key).dispatchMessage(new Message());
} catch (Exception e) {
Log.e("MprisControl", "Exception", e);
playerListUpdated.remove(key);
}
}
}
}
return true;
}
@Override
public String[] getSupportedPacketTypes() {
return new String[]{PACKET_TYPE_MPRIS};
}
@Override
public String[] getOutgoingPacketTypes() {
return new String[]{PACKET_TYPE_MPRIS_REQUEST};
}
public void setPlayerStatusUpdatedHandler(String id, Handler h) {
playerStatusUpdated.put(id, h);
h.dispatchMessage(new Message());
}
public void removePlayerStatusUpdatedHandler(String id) {
playerStatusUpdated.remove(id);
}
public void setPlayerListUpdatedHandler(String id, Handler h) {
playerListUpdated.put(id, h);
h.dispatchMessage(new Message());
}
public void removePlayerListUpdatedHandler(String id) {
playerListUpdated.remove(id);
}
public List getPlayerList() {
List playerlist = new ArrayList<>(players.keySet());
Collections.sort(playerlist);
return playerlist;
}
public MprisPlayer getPlayerStatus(String player) {
return players.get(player);
}
public MprisPlayer getEmptyPlayer() {
return new MprisPlayer();
}
/**
* Returns a playing mpris player, if any exist
*
* @return null if no players are playing, a playing player otherwise
*/
public MprisPlayer getPlayingPlayer() {
for (MprisPlayer player : players.values()) {
if (player.isPlaying()) {
return player;
}
}
return null;
}
boolean hasPlayer(MprisPlayer player) {
return players.containsValue(player);
}
private void requestPlayerList() {
NetworkPacket np = new NetworkPacket(PACKET_TYPE_MPRIS_REQUEST);
np.set("requestPlayerList", true);
device.sendPacket(np);
}
private void requestPlayerStatus(String player) {
NetworkPacket np = new NetworkPacket(PACKET_TYPE_MPRIS_REQUEST);
np.set("player", player);
np.set("requestNowPlaying", true);
np.set("requestVolume", true);
device.sendPacket(np);
}
@Override
public boolean hasMainActivity() {
return true;
}
@Override
public void startMainActivity(Activity parentActivity) {
Intent intent = new Intent(parentActivity, MprisActivity.class);
intent.putExtra("deviceId", device.getDeviceId());
parentActivity.startActivity(intent);
}
@Override
public String getActionName() {
return context.getString(R.string.open_mpris_controls);
}
public void fetchedAlbumArt(String url) {
boolean doEmitUpdate = false;
for (MprisPlayer player : players.values()) {
if (url.equals(player.albumArtUrl)) {
doEmitUpdate = true;
}
}
if (doEmitUpdate) {
for (String key : playerStatusUpdated.keySet()) {
try {
playerStatusUpdated.get(key).dispatchMessage(new Message());
} catch (Exception e) {
Log.e("MprisControl", "Exception", e);
playerStatusUpdated.remove(key);
}
}
}
}
public boolean askTransferAlbumArt(String url, String playerName) {
//First check if the remote supports transferring album art
if (!supportAlbumArtPayload) return false;
if (url.isEmpty()) return false;
MprisPlayer player = getPlayerStatus(playerName);
if (player == null) return false;
if (player.albumArtUrl.equals(url)) {
NetworkPacket np = new NetworkPacket(PACKET_TYPE_MPRIS_REQUEST);
np.set("player", player.getPlayer());
np.set("albumArtUrl", url);
device.sendPacket(np);
return true;
}
return false;
}
}