diff --git a/res/drawable/ic_arrow_black.xml b/res/drawable/ic_arrow_black.xml
new file mode 100644
index 00000000..b9b41c17
--- /dev/null
+++ b/res/drawable/ic_arrow_black.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/res/layout/mpris_control.xml b/res/layout/mpris_control.xml
index 1feab65e..ec1c186c 100644
--- a/res/layout/mpris_control.xml
+++ b/res/layout/mpris_control.xml
@@ -101,6 +101,14 @@
android:src="@drawable/ic_next_black"
android:theme="@style/DisableableButton" />
+
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
+ Can\'t open URL
+ Can\'t format URL for seek
diff --git a/src/org/kde/kdeconnect/Helpers/VideoUrlsHelper.java b/src/org/kde/kdeconnect/Helpers/VideoUrlsHelper.java
new file mode 100644
index 00000000..5d2fc9d6
--- /dev/null
+++ b/src/org/kde/kdeconnect/Helpers/VideoUrlsHelper.java
@@ -0,0 +1,145 @@
+package org.kde.kdeconnect.Helpers;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Locale;
+
+public class VideoUrlsHelper {
+ private static final int SECONDS_IN_MINUTE = 60;
+ private static final int MINUTES_IN_HOUR = 60;
+ private static final int SECONDS_IN_HOUR = SECONDS_IN_MINUTE * MINUTES_IN_HOUR;
+
+ public static URL formatUriWithSeek(String address, long position)
+ throws MalformedURLException {
+ URL url = new URL(address);
+ position /= 1000; // Convert ms to seconds
+ if (position <= 0) {
+ return url; // nothing to do
+ }
+ String host = url.getHost().toLowerCase();
+
+ // Most common settings as defaults:
+ String parameter = "t="; // Characters before timestamp
+ String timestamp = Long.toString(position); // Timestamp itself
+ String trailer = ""; // Characters after timestamp
+ // true - search/add to query URL part (between ? and # signs),
+ // false - search/add timestamp to ref (anchor) URL part (after # sign),
+ boolean inQuery = true;
+ // true - We know how to format URL with seek timestamp, false - not
+ boolean seekUrl = false;
+
+ // Override defaults if necessary
+ if (host.contains("youtube.com")
+ || host.contains("youtu.be")
+ || host.contains("pornhub.com")) {
+ seekUrl = true;
+ url = stripTimestampS(url, parameter, trailer, inQuery);
+ } else if (host.contains("vimeo.com")) {
+ seekUrl = true;
+ trailer = "s";
+ url = stripTimestampS(url, parameter, trailer, inQuery);
+ } else if (host.contains("dailymotion.com")) {
+ seekUrl = true;
+ parameter = "start=";
+ url = stripTimestampS(url, parameter, trailer, inQuery);
+ } else if (host.contains("twitch.tv")) {
+ seekUrl = true;
+ timestamp = formatTimestampHMS(position, true);
+ url = stripTimestampHMS(url, parameter, trailer, inQuery);
+ }
+
+ if (seekUrl) {
+ url = formatUrlWithSeek(url, timestamp, parameter, trailer, inQuery);
+ }
+ return url;
+ }
+
+ // Returns timestamp in 1h2m34s or 01h02m34s (according to padWithZeroes)
+ private static String formatTimestampHMS(long seconds, boolean padWithZeroes) {
+ if (seconds == 0) {
+ return "0s";
+ }
+
+ int sec = (int) (seconds % SECONDS_IN_MINUTE);
+ int min = (int) ((seconds / SECONDS_IN_MINUTE) % MINUTES_IN_HOUR);
+ int hour = (int) (seconds / SECONDS_IN_HOUR);
+
+ String hours = hour > 0 ? hour + "h" : "";
+ String mins = min > 0 || hour > 0 ? min + "m" : "";
+ String secs = sec + "s";
+
+ String value;
+ if (padWithZeroes) {
+ String hoursPad = hour > 9 ? "" : "0";
+ String minsPad = min > 9 ? "" : "0";
+ String secsPad = sec > 9 ? "" : "0";
+ value = hoursPad + hours + minsPad + mins + secsPad + secs;
+ } else {
+ value = hours + mins + secs;
+ }
+ return value;
+
+ }
+
+ // Remove timestamp in 01h02m34s or 1h2m34s or 02m34s or 2m34s or 01s or 1s format.
+ // Can also nandle rimestamps in 1234s format if called with 's' trailer
+ private static URL stripTimestampHMS(URL url, String parameter, String trailer, boolean inQuery)
+ throws MalformedURLException {
+ String regex = parameter + "([\\d]+[hH])?([\\d]+[mM])?[\\d]+[sS]" + trailer + "&?";
+ return stripTimestampCommon(url, inQuery, regex);
+ }
+
+
+ // Remove timestamp in 1234 format
+ private static URL stripTimestampS(URL url, String parameter, String trailer, boolean inQuery)
+ throws MalformedURLException {
+ String regex = parameter + "[\\d]+" + trailer + "&?";
+ return stripTimestampCommon(url, inQuery, regex);
+ }
+
+ private static URL stripTimestampCommon(URL url, boolean inQuery, String regex)
+ throws MalformedURLException {
+ String value;
+ if (inQuery) {
+ value = url.getQuery();
+ } else {
+ value = url.getRef();
+ }
+ if (value == null) {
+ return url;
+ }
+ String newValue = value.replaceAll(regex, "");
+ String replaced = url.toString().replaceFirst(value, newValue);
+ if (inQuery && replaced.endsWith("&")) {
+ replaced = replaced.substring(0, replaced.length() - 1);
+ }
+ return new URL(replaced);
+ }
+
+ private static URL formatUrlWithSeek(URL url, String position, String parameter, String trailer,
+ boolean inQuery) throws MalformedURLException {
+ String value;
+ String separator;
+ String newValue;
+ if (inQuery) {
+ value = url.getQuery();
+ separator = "?";
+ } else {
+ value = url.getRef();
+ separator = "#";
+ }
+ if (value == null) {
+ newValue = String.format(Locale.getDefault(), "%s%s%s%s%s",
+ url.toString(), separator, parameter, position, trailer);
+ return new URL(newValue);
+ }
+ if (inQuery) {
+ newValue = String.format(Locale.getDefault(), "%s&%s%s%s",
+ value, parameter, position, trailer);
+ } else {
+ newValue = String.format(Locale.getDefault(), "%s%s%s",
+ parameter, position, trailer);
+ }
+ return new URL(url.toString().replaceFirst(value, newValue));
+ }
+}
diff --git a/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisActivity.java b/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisActivity.java
index 7f0b7b2d..345356ef 100644
--- a/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisActivity.java
+++ b/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisActivity.java
@@ -20,9 +20,12 @@
package org.kde.kdeconnect.Plugins.MprisPlugin;
+import android.content.ActivityNotFoundException;
+import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
+import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
@@ -37,15 +40,18 @@ 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;
@@ -77,6 +83,9 @@ public class MprisActivity extends AppCompatActivity {
@BindView(R.id.ff_button)
ImageButton ffButton;
+ @BindView(R.id.open_url_button)
+ ImageButton openUrlButton;
+
@BindView(R.id.time_textview)
TextView timeText;
@@ -288,6 +297,7 @@ public class MprisActivity extends AppCompatActivity {
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);
//Show and hide previous/next buttons simultaneously
if (playerStatus.isGoPreviousAllowed() || playerStatus.isGoNextAllowed()) {
@@ -387,6 +397,23 @@ public class MprisActivity extends AppCompatActivity {
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);
diff --git a/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisPlugin.java b/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisPlugin.java
index fad0f1f6..f6205b8f 100644
--- a/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisPlugin.java
+++ b/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisPlugin.java
@@ -20,6 +20,7 @@
package org.kde.kdeconnect.Plugins.MprisPlugin;
+import android.annotation.NonNull;
import android.app.Activity;
import android.content.Intent;
import android.graphics.Bitmap;
@@ -53,6 +54,7 @@ public class MprisPlugin extends Plugin {
private String artist = "";
private String album = "";
private String albumArtUrl = "";
+ private String url = "";
private int volume = 50;
private long length = -1;
private long lastPosition = 0;
@@ -136,6 +138,11 @@ public class MprisPlugin extends Plugin {
return AlbumArtCache.getAlbumArt(albumArtUrl, MprisPlugin.this, player);
}
+ @NonNull
+ public String getUrl() {
+ return url;
+ }
+
public boolean isSetVolumeAllowed() {
return !isSpotify();
}
@@ -282,6 +289,7 @@ public class MprisPlugin extends Plugin {
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")) {