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")) {