2
0
mirror of https://github.com/KDE/kdeconnect-android synced 2025-08-31 14:15:14 +00:00

Merge branch 'strace/kdeconnect-android-seamless_playback'

This commit is contained in:
Albert Vaca Cintora
2020-03-21 00:50:25 +01:00
6 changed files with 200 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
android:viewportWidth="32"
android:viewportHeight="32"
android:width="32dp"
android:height="32dp">
<path
android:pathData="M8.990234 5c-0.5522847 0 -1 0.4477153 -1 1 0 0.5522847 0.4477153 1 1 1 0.5522847 0 1 -0.4477153 1 -1 0 -0.5522847 -0.4477153 -1 -1 -1zm2 4L11 29 16.841796 21.769531 26 21Z"
android:fillColor="#FF000000" />
</vector>

View File

@@ -101,6 +101,14 @@
android:src="@drawable/ic_next_black"
android:theme="@style/DisableableButton" />
<ImageButton
android:id="@+id/open_url_button"
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:layout_weight="0.25"
android:contentDescription="@string/mpris_open_url"
android:src="@drawable/ic_arrow_black"
android:theme="@style/DisableableButton" />
</LinearLayout>
<LinearLayout

View File

@@ -356,4 +356,7 @@
<string name="location_permission_needed_title">Permission required</string>
<string name="location_permission_needed_desc">Android requires the Location permission to identify your WiFi network</string>
<string name="clipboard_android_x_incompat">Android 10 has removed clipboard access to all apps. This plugin will be disabled.</string>
<string name="mpris_open_url">Open URL</string>
<string name="cant_open_url">Can\'t open URL</string>
<string name="cant_format_seek_uri">Can\'t format URL for seek</string>
</resources>

View File

@@ -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));
}
}

View File

@@ -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);

View File

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