mirror of
https://github.com/KDE/kdeconnect-android
synced 2025-09-03 07:35:08 +00:00
Compare commits
19 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
64fd08f3ac | ||
|
e67e43efa1 | ||
|
fda08929af | ||
|
2cb025e368 | ||
|
cf808c03ba | ||
|
2b52cd1547 | ||
|
3d4bf643d4 | ||
|
00b6677aa4 | ||
|
bf0cab9ef2 | ||
|
1c3e6f84a7 | ||
|
c8dbbb1fe8 | ||
|
a17b75264d | ||
|
7276e60aa4 | ||
|
7877d2803c | ||
|
b1c4b6e1e9 | ||
|
eb801fa535 | ||
|
ac4aaf1b39 | ||
|
9840a39992 | ||
|
e73c18d2e3 |
@@ -1,8 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="org.kde.kdeconnect_tp"
|
package="org.kde.kdeconnect_tp"
|
||||||
android:versionCode="1800"
|
android:versionCode="1820"
|
||||||
android:versionName="1.8.0">
|
android:versionName="1.8.2">
|
||||||
|
|
||||||
<supports-screens
|
<supports-screens
|
||||||
android:anyDensity="true"
|
android:anyDensity="true"
|
||||||
@@ -146,7 +146,8 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="org.kde.kdeconnect.Plugins.MprisPlugin.MprisActivity"
|
android:name="org.kde.kdeconnect.Plugins.MprisPlugin.MprisActivity"
|
||||||
android:label="@string/remote_control"
|
android:label="@string/remote_control"
|
||||||
android:parentActivityName="org.kde.kdeconnect.UserInterface.MainActivity">
|
android:parentActivityName="org.kde.kdeconnect.UserInterface.MainActivity"
|
||||||
|
android:launchMode="singleTop">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="org.kde.kdeconnect.UserInterface.MainActivity" />
|
android:value="org.kde.kdeconnect.UserInterface.MainActivity" />
|
||||||
|
10
build.gradle
10
build.gradle
@@ -1,10 +1,7 @@
|
|||||||
buildscript {
|
buildscript {
|
||||||
repositories {
|
repositories {
|
||||||
jcenter()
|
jcenter()
|
||||||
maven {
|
google()
|
||||||
url 'https://maven.google.com/'
|
|
||||||
name 'Google'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.0.1'
|
classpath 'com.android.tools.build:gradle:3.0.1'
|
||||||
@@ -73,10 +70,7 @@ dependencies {
|
|||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
jcenter()
|
jcenter()
|
||||||
maven {
|
google()
|
||||||
url 'https://maven.google.com/'
|
|
||||||
name 'Google'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
implementation 'com.android.support:support-v4:25.4.0'
|
implementation 'com.android.support:support-v4:25.4.0'
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="24dp"
|
android:width="192dp"
|
||||||
android:height="24dp"
|
android:height="192dp"
|
||||||
android:viewportWidth="24.0"
|
android:viewportWidth="24.0"
|
||||||
android:viewportHeight="24.0">
|
android:viewportHeight="24.0">
|
||||||
<path
|
<path
|
||||||
|
@@ -11,6 +11,7 @@
|
|||||||
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:layout_gravity="center"
|
||||||
|
android:visibility="gone"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
|
@@ -82,8 +82,6 @@
|
|||||||
<string name="incoming_file_text">%1s</string>
|
<string name="incoming_file_text">%1s</string>
|
||||||
<string name="outgoing_file_title">Шаљем фајл на %1s</string>
|
<string name="outgoing_file_title">Шаљем фајл на %1s</string>
|
||||||
<string name="outgoing_files_title">Шаљем фајлове на %1s</string>
|
<string name="outgoing_files_title">Шаљем фајлове на %1s</string>
|
||||||
<string name="outgoing_file_text">%1s</string>
|
|
||||||
<string name="outgoing_files_text">Послато %1$d од %2$d фајлова</string>
|
|
||||||
<string name="received_file_title">Примљен фајл са %1s</string>
|
<string name="received_file_title">Примљен фајл са %1s</string>
|
||||||
<string name="received_file_fail_title">Неуспео пријем фајла са %1s</string>
|
<string name="received_file_fail_title">Неуспео пријем фајла са %1s</string>
|
||||||
<string name="received_file_text">Тапните да отворите „%1s“</string>
|
<string name="received_file_text">Тапните да отворите „%1s“</string>
|
||||||
|
@@ -15,7 +15,7 @@
|
|||||||
* GNU General Public License for more details.
|
* GNU General Public License for more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.kde.kdeconnect.Backends.LanBackend;
|
package org.kde.kdeconnect.Backends.LanBackend;
|
||||||
@@ -27,6 +27,7 @@ import android.preference.PreferenceManager;
|
|||||||
import android.util.Base64;
|
import android.util.Base64;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.kde.kdeconnect.Backends.BaseLink;
|
||||||
import org.kde.kdeconnect.Backends.BaseLinkProvider;
|
import org.kde.kdeconnect.Backends.BaseLinkProvider;
|
||||||
import org.kde.kdeconnect.BackgroundService;
|
import org.kde.kdeconnect.BackgroundService;
|
||||||
import org.kde.kdeconnect.Device;
|
import org.kde.kdeconnect.Device;
|
||||||
@@ -59,6 +60,13 @@ import javax.net.ssl.HandshakeCompletedEvent;
|
|||||||
import javax.net.ssl.HandshakeCompletedListener;
|
import javax.net.ssl.HandshakeCompletedListener;
|
||||||
import javax.net.ssl.SSLSocket;
|
import javax.net.ssl.SSLSocket;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This BaseLinkProvider creates {@link LanLink}s to other devices on the same
|
||||||
|
* WiFi network. The first packet sent over a socket must be an
|
||||||
|
* {@link NetworkPacket#createIdentityPacket(Context)}.
|
||||||
|
*
|
||||||
|
* @see #identityPacketReceived(NetworkPacket, Socket, LanLink.ConnectionStarted)
|
||||||
|
*/
|
||||||
public class LanLinkProvider extends BaseLinkProvider implements LanLink.LinkDisconnectedCallback {
|
public class LanLinkProvider extends BaseLinkProvider implements LanLink.LinkDisconnectedCallback {
|
||||||
|
|
||||||
public static final int MIN_VERSION_WITH_SSL_SUPPORT = 6;
|
public static final int MIN_VERSION_WITH_SSL_SUPPORT = 6;
|
||||||
@@ -157,14 +165,14 @@ public class LanLinkProvider extends BaseLinkProvider implements LanLink.LinkDis
|
|||||||
Log.e("KDE/LanLinkProvider", "Cannot connect to " + address);
|
Log.e("KDE/LanLinkProvider", "Cannot connect to " + address);
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
if (!reverseConnectionBlackList.contains(address)) {
|
if (!reverseConnectionBlackList.contains(address)) {
|
||||||
Log.w("KDE/LanLinkProvider","Blacklisting "+address);
|
Log.w("KDE/LanLinkProvider", "Blacklisting " + address);
|
||||||
reverseConnectionBlackList.add(address);
|
reverseConnectionBlackList.add(address);
|
||||||
new Timer().schedule(new TimerTask() {
|
new Timer().schedule(new TimerTask() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
reverseConnectionBlackList.remove(address);
|
reverseConnectionBlackList.remove(address);
|
||||||
}
|
}
|
||||||
}, 5*1000);
|
}, 5 * 1000);
|
||||||
|
|
||||||
// Try to cause a reverse connection
|
// Try to cause a reverse connection
|
||||||
onNetworkChange();
|
onNetworkChange();
|
||||||
@@ -180,6 +188,18 @@ public class LanLinkProvider extends BaseLinkProvider implements LanLink.LinkDis
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a new 'identity' packet is received. Those are passed here by
|
||||||
|
* {@link #tcpPacketReceived(Socket)} and {@link #udpPacketReceived(DatagramPacket)}.
|
||||||
|
* <p>
|
||||||
|
* If the remote device should be connected, this calls {@link #addLink}.
|
||||||
|
* Otherwise, if there was an Exception, we unpair from that device.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param identityPacket identity of a remote device
|
||||||
|
* @param socket a new Socket, which should be used to receive packets from the remote device
|
||||||
|
* @param connectionStarted which side started this connection
|
||||||
|
*/
|
||||||
private void identityPacketReceived(final NetworkPacket identityPacket, final Socket socket, final LanLink.ConnectionStarted connectionStarted) {
|
private void identityPacketReceived(final NetworkPacket identityPacket, final Socket socket, final LanLink.ConnectionStarted connectionStarted) {
|
||||||
|
|
||||||
String myId = DeviceHelper.getDeviceId(context);
|
String myId = DeviceHelper.getDeviceId(context);
|
||||||
@@ -213,20 +233,20 @@ public class LanLinkProvider extends BaseLinkProvider implements LanLink.LinkDis
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.i("KDE/LanLinkProvider","Starting SSL handshake with " + identityPacket.getString("deviceName") + " trusted:"+isDeviceTrusted);
|
Log.i("KDE/LanLinkProvider", "Starting SSL handshake with " + identityPacket.getString("deviceName") + " trusted:" + isDeviceTrusted);
|
||||||
|
|
||||||
final SSLSocket sslsocket = SslHelper.convertToSslSocket(context, socket, deviceId, isDeviceTrusted, clientMode);
|
final SSLSocket sslsocket = SslHelper.convertToSslSocket(context, socket, deviceId, isDeviceTrusted, clientMode);
|
||||||
sslsocket.addHandshakeCompletedListener(new HandshakeCompletedListener() {
|
sslsocket.addHandshakeCompletedListener(new HandshakeCompletedListener() {
|
||||||
@Override
|
@Override
|
||||||
public void handshakeCompleted(HandshakeCompletedEvent event) {
|
public void handshakeCompleted(HandshakeCompletedEvent event) {
|
||||||
String mode = clientMode? "client" : "server";
|
String mode = clientMode ? "client" : "server";
|
||||||
try {
|
try {
|
||||||
Certificate certificate = event.getPeerCertificates()[0];
|
Certificate certificate = event.getPeerCertificates()[0];
|
||||||
identityPacket.set("certificate", Base64.encodeToString(certificate.getEncoded(), 0));
|
identityPacket.set("certificate", Base64.encodeToString(certificate.getEncoded(), 0));
|
||||||
Log.i("KDE/LanLinkProvider","Handshake as " + mode + " successful with " + identityPacket.getString("deviceName") + " secured with " + event.getCipherSuite());
|
Log.i("KDE/LanLinkProvider", "Handshake as " + mode + " successful with " + identityPacket.getString("deviceName") + " secured with " + event.getCipherSuite());
|
||||||
addLink(identityPacket, sslsocket, connectionStarted);
|
addLink(identityPacket, sslsocket, connectionStarted);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.e("KDE/LanLinkProvider","Handshake as " + mode + " failed with " + identityPacket.getString("deviceName"));
|
Log.e("KDE/LanLinkProvider", "Handshake as " + mode + " failed with " + identityPacket.getString("deviceName"));
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
BackgroundService.RunCommand(context, new BackgroundService.InstanceCallback() {
|
BackgroundService.RunCommand(context, new BackgroundService.InstanceCallback() {
|
||||||
@Override
|
@Override
|
||||||
@@ -246,7 +266,7 @@ public class LanLinkProvider extends BaseLinkProvider implements LanLink.LinkDis
|
|||||||
try {
|
try {
|
||||||
sslsocket.startHandshake();
|
sslsocket.startHandshake();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.e("KDE/LanLinkProvider","Handshake failed with " + identityPacket.getString("deviceName"));
|
Log.e("KDE/LanLinkProvider", "Handshake failed with " + identityPacket.getString("deviceName"));
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
|
|
||||||
//String[] ciphers = sslsocket.getSupportedCipherSuites();
|
//String[] ciphers = sslsocket.getSupportedCipherSuites();
|
||||||
@@ -265,6 +285,19 @@ public class LanLinkProvider extends BaseLinkProvider implements LanLink.LinkDis
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add or update a link in the {@link #visibleComputers} map. This method is synchronized, which ensures that only one
|
||||||
|
* link is operated on at a time.
|
||||||
|
* <p>
|
||||||
|
* Without synchronization, the call to {@link SslHelper#parseCertificate(byte[])} in
|
||||||
|
* {@link Device#addLink(NetworkPacket, BaseLink)} crashes on some devices running Oreo 8.1 (SDK level 27).
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param identityPacket representation of remote device
|
||||||
|
* @param socket a new Socket, which should be used to receive packets from the remote device
|
||||||
|
* @param connectionOrigin which side started this connection
|
||||||
|
* @throws IOException if an exception is thrown by {@link LanLink#reset(Socket, LanLink.ConnectionStarted)}
|
||||||
|
*/
|
||||||
private synchronized void addLink(final NetworkPacket identityPacket, Socket socket, LanLink.ConnectionStarted connectionOrigin) throws IOException {
|
private synchronized void addLink(final NetworkPacket identityPacket, Socket socket, LanLink.ConnectionStarted connectionOrigin) throws IOException {
|
||||||
|
|
||||||
String deviceId = identityPacket.getString("deviceId");
|
String deviceId = identityPacket.getString("deviceId");
|
||||||
@@ -313,7 +346,7 @@ public class LanLinkProvider extends BaseLinkProvider implements LanLink.LinkDis
|
|||||||
Log.e("LanLinkProvider", "UdpReceive exception");
|
Log.e("LanLinkProvider", "UdpReceive exception");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.w("UdpListener","Stopping UDP listener");
|
Log.w("UdpListener", "Stopping UDP listener");
|
||||||
}
|
}
|
||||||
}).start();
|
}).start();
|
||||||
return server;
|
return server;
|
||||||
@@ -347,13 +380,13 @@ public class LanLinkProvider extends BaseLinkProvider implements LanLink.LinkDis
|
|||||||
|
|
||||||
static ServerSocket openServerSocketOnFreePort(int minPort) throws IOException {
|
static ServerSocket openServerSocketOnFreePort(int minPort) throws IOException {
|
||||||
int tcpPort = minPort;
|
int tcpPort = minPort;
|
||||||
while(tcpPort < MAX_PORT) {
|
while (tcpPort < MAX_PORT) {
|
||||||
try {
|
try {
|
||||||
ServerSocket candidateServer = new ServerSocket();
|
ServerSocket candidateServer = new ServerSocket();
|
||||||
candidateServer.bind(new InetSocketAddress(tcpPort));
|
candidateServer.bind(new InetSocketAddress(tcpPort));
|
||||||
Log.i("KDE/LanLink", "Using port "+tcpPort);
|
Log.i("KDE/LanLink", "Using port " + tcpPort);
|
||||||
return candidateServer;
|
return candidateServer;
|
||||||
} catch(IOException e) {
|
} catch (IOException e) {
|
||||||
tcpPort++;
|
tcpPort++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -390,7 +423,7 @@ public class LanLinkProvider extends BaseLinkProvider implements LanLink.LinkDis
|
|||||||
bytes = identity.serialize().getBytes(StringsHelper.UTF8);
|
bytes = identity.serialize().getBytes(StringsHelper.UTF8);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
Log.e("KDE/LanLinkProvider","Failed to create DatagramSocket");
|
Log.e("KDE/LanLinkProvider", "Failed to create DatagramSocket");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bytes != null) {
|
if (bytes != null) {
|
||||||
@@ -415,6 +448,7 @@ public class LanLinkProvider extends BaseLinkProvider implements LanLink.LinkDis
|
|||||||
}
|
}
|
||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStart() {
|
public void onStart() {
|
||||||
//Log.i("KDE/LanLinkProvider", "onStart");
|
//Log.i("KDE/LanLinkProvider", "onStart");
|
||||||
@@ -431,7 +465,7 @@ public class LanLinkProvider extends BaseLinkProvider implements LanLink.LinkDis
|
|||||||
// and newer android versions. Although devices with android version less than ICS cannot connect to other devices who also have android version less
|
// and newer android versions. Although devices with android version less than ICS cannot connect to other devices who also have android version less
|
||||||
// than ICS because server is disabled on both
|
// than ICS because server is disabled on both
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
|
||||||
Log.w("KDE/LanLinkProvider","Not starting a TCP server because it's not supported on Android < 14. Operating only as client.");
|
Log.w("KDE/LanLinkProvider", "Not starting a TCP server because it's not supported on Android < 14. Operating only as client.");
|
||||||
} else {
|
} else {
|
||||||
setupTcpListener();
|
setupTcpListener();
|
||||||
}
|
}
|
||||||
@@ -451,17 +485,17 @@ public class LanLinkProvider extends BaseLinkProvider implements LanLink.LinkDis
|
|||||||
listening = false;
|
listening = false;
|
||||||
try {
|
try {
|
||||||
tcpServer.close();
|
tcpServer.close();
|
||||||
} catch (Exception e){
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
udpServer.close();
|
udpServer.close();
|
||||||
} catch (Exception e){
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
udpServerOldPort.close();
|
udpServerOldPort.close();
|
||||||
} catch (Exception e){
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -66,6 +66,10 @@ public final class AlbumArtCache {
|
|||||||
* A list of urls yet to be fetched.
|
* A list of urls yet to be fetched.
|
||||||
*/
|
*/
|
||||||
private static final ArrayList<URL> fetchUrlList = new ArrayList<>();
|
private static final ArrayList<URL> fetchUrlList = new ArrayList<>();
|
||||||
|
/**
|
||||||
|
* A list of urls currently being fetched
|
||||||
|
*/
|
||||||
|
private static final ArrayList<URL> isFetchingList = new ArrayList<>();
|
||||||
/**
|
/**
|
||||||
* A integer indicating how many fetches are in progress.
|
* A integer indicating how many fetches are in progress.
|
||||||
*/
|
*/
|
||||||
@@ -123,7 +127,7 @@ public final class AlbumArtCache {
|
|||||||
* @param albumUrl The album art url
|
* @param albumUrl The album art url
|
||||||
* @return A bitmap for the album art. Can be null if not (yet) found
|
* @return A bitmap for the album art. Can be null if not (yet) found
|
||||||
*/
|
*/
|
||||||
public static Bitmap getAlbumArt(String albumUrl) {
|
public static Bitmap getAlbumArt(String albumUrl, MprisPlugin plugin, String player) {
|
||||||
//If the url is invalid, return "no album art"
|
//If the url is invalid, return "no album art"
|
||||||
if (albumUrl == null || albumUrl.isEmpty()) {
|
if (albumUrl == null || albumUrl.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
@@ -138,8 +142,8 @@ public final class AlbumArtCache {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
//We currently only support http(s) urls
|
//We currently only support http(s) and file urls
|
||||||
if (!url.getProtocol().equals("http") && !url.getProtocol().equals("https")) {
|
if (!url.getProtocol().equals("http") && !url.getProtocol().equals("https") && !url.getProtocol().equals("file")) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,11 +167,7 @@ public final class AlbumArtCache {
|
|||||||
try {
|
try {
|
||||||
DiskLruCache.Snapshot item = diskCache.get(urlToDiskCacheKey(albumUrl));
|
DiskLruCache.Snapshot item = diskCache.get(urlToDiskCacheKey(albumUrl));
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
|
Bitmap result = BitmapFactory.decodeStream(item.getInputStream(0));
|
||||||
decodeOptions.inScaled = false;
|
|
||||||
decodeOptions.inDensity = 1;
|
|
||||||
decodeOptions.inTargetDensity = 1;
|
|
||||||
Bitmap result = BitmapFactory.decodeStream(item.getInputStream(0), null, decodeOptions);
|
|
||||||
item.close();
|
item.close();
|
||||||
MemoryCacheItem memItem = new MemoryCacheItem();
|
MemoryCacheItem memItem = new MemoryCacheItem();
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
@@ -189,7 +189,20 @@ public final class AlbumArtCache {
|
|||||||
|
|
||||||
/* If not found, we have not tried fetching it (recently), or a fetch is in-progress.
|
/* If not found, we have not tried fetching it (recently), or a fetch is in-progress.
|
||||||
Either way, just add it to the fetch queue and starting fetching it if no fetch is running. */
|
Either way, just add it to the fetch queue and starting fetching it if no fetch is running. */
|
||||||
fetchUrl(url);
|
if ("file".equals(url.getProtocol())) {
|
||||||
|
//Special-case file, since we need to fetch it from the remote
|
||||||
|
if (isFetchingList.contains(url)) return null;
|
||||||
|
|
||||||
|
if (!plugin.askTransferAlbumArt(albumUrl, player)) {
|
||||||
|
//It doesn't support transferring the art, so mark it as failed in the memory cache
|
||||||
|
MemoryCacheItem cacheItem = new MemoryCacheItem();
|
||||||
|
cacheItem.failedFetch = true;
|
||||||
|
cacheItem.albumArt = null;
|
||||||
|
memoryCache.put(url.toString(), cacheItem);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fetchUrl(url);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,7 +219,7 @@ public final class AlbumArtCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Only fetch an URL if we're not fetching it already
|
//Only fetch an URL if we're not fetching it already
|
||||||
if (fetchUrlList.contains(url)) {
|
if (fetchUrlList.contains(url) || isFetchingList.contains(url)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,8 +336,8 @@ public final class AlbumArtCache {
|
|||||||
memoryCache.put(url.toString(), cacheItem);
|
memoryCache.put(url.toString(), cacheItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Remove the url from the to-fetch list
|
//Remove the url from the fetching list
|
||||||
fetchUrlList.remove(url);
|
isFetchingList.remove(url);
|
||||||
//Fetch the next url (if any)
|
//Fetch the next url (if any)
|
||||||
--numFetching;
|
--numFetching;
|
||||||
initiateFetch();
|
initiateFetch();
|
||||||
@@ -338,10 +351,19 @@ public final class AlbumArtCache {
|
|||||||
if (numFetching >= 2) return;
|
if (numFetching >= 2) return;
|
||||||
if (fetchUrlList.isEmpty()) return;
|
if (fetchUrlList.isEmpty()) return;
|
||||||
|
|
||||||
++numFetching;
|
|
||||||
|
|
||||||
//Fetch the last-requested url first, it will probably be needed first
|
//Fetch the last-requested url first, it will probably be needed first
|
||||||
URL url = fetchUrlList.get(fetchUrlList.size() - 1);
|
URL url = fetchUrlList.get(fetchUrlList.size() - 1);
|
||||||
|
//Remove the url from the to-fetch list
|
||||||
|
fetchUrlList.remove(url);
|
||||||
|
|
||||||
|
if ("file".equals(url.getProtocol())) {
|
||||||
|
throw new AssertionError("Not file urls should be possible here!");
|
||||||
|
}
|
||||||
|
|
||||||
|
//Download the album art ourselves
|
||||||
|
++numFetching;
|
||||||
|
//Add the url to the currently-fetching list
|
||||||
|
isFetchingList.add(url);
|
||||||
try {
|
try {
|
||||||
DiskLruCache.Editor cacheItem = diskCache.edit(urlToDiskCacheKey(url.toString()));
|
DiskLruCache.Editor cacheItem = diskCache.edit(urlToDiskCacheKey(url.toString()));
|
||||||
if (cacheItem == null) {
|
if (cacheItem == null) {
|
||||||
@@ -392,6 +414,9 @@ public final class AlbumArtCache {
|
|||||||
//We need the disk cache for this
|
//We need the disk cache for this
|
||||||
if (diskCache == null) {
|
if (diskCache == null) {
|
||||||
Log.e("KDE/Mpris/AlbumArtCache", "The disk cache is not intialized!");
|
Log.e("KDE/Mpris/AlbumArtCache", "The disk cache is not intialized!");
|
||||||
|
try {
|
||||||
|
payload.close();
|
||||||
|
} catch (IOException ignored) {}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -400,20 +425,46 @@ public final class AlbumArtCache {
|
|||||||
url = new URL(albumUrl);
|
url = new URL(albumUrl);
|
||||||
} catch (MalformedURLException e) {
|
} catch (MalformedURLException e) {
|
||||||
//Shouldn't happen (checked on receival of the url), but just to be sure
|
//Shouldn't happen (checked on receival of the url), but just to be sure
|
||||||
|
try {
|
||||||
|
payload.close();
|
||||||
|
} catch (IOException ignored) {}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!"file".equals(url.getProtocol())) {
|
if (!"file".equals(url.getProtocol())) {
|
||||||
//Shouldn't happen (otherwise we wouldn't have asked for the payload), but just to be sure
|
//Shouldn't happen (otherwise we wouldn't have asked for the payload), but just to be sure
|
||||||
|
try {
|
||||||
|
payload.close();
|
||||||
|
} catch (IOException ignored) {}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Only fetch the URL if we're not fetching it already
|
//Only fetch the URL if we're not fetching it already
|
||||||
if (fetchUrlList.contains(url)) {
|
if (isFetchingList.contains(url)) {
|
||||||
|
try {
|
||||||
|
payload.close();
|
||||||
|
} catch (IOException ignored) {}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchUrlList.add(url);
|
//Check if we already have this art
|
||||||
|
try {
|
||||||
|
if (memoryCache.get(albumUrl) != null || diskCache.get(urlToDiskCacheKey(albumUrl)) != null) {
|
||||||
|
try {
|
||||||
|
payload.close();
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e("KDE/Mpris/AlbumArtCache", "Disk cache problem!", e);
|
||||||
|
try {
|
||||||
|
payload.close();
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Add it to the currently-fetching list
|
||||||
|
isFetchingList.add(url);
|
||||||
++numFetching;
|
++numFetching;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -422,6 +473,9 @@ public final class AlbumArtCache {
|
|||||||
Log.e("KDE/Mpris/AlbumArtCache",
|
Log.e("KDE/Mpris/AlbumArtCache",
|
||||||
"Two disk cache edits happened at the same time, should be impossible!");
|
"Two disk cache edits happened at the same time, should be impossible!");
|
||||||
--numFetching;
|
--numFetching;
|
||||||
|
try {
|
||||||
|
payload.close();
|
||||||
|
} catch (IOException ignored) {}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -139,6 +139,10 @@ public class MprisActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
targetPlayer = mpris.getPlayerStatus(player);
|
targetPlayer = mpris.getPlayerStatus(player);
|
||||||
updatePlayerStatus(mpris);
|
updatePlayerStatus(mpris);
|
||||||
|
|
||||||
|
if (targetPlayer.isPlaying()) {
|
||||||
|
MprisMediaSession.getInstance().playerSelected(targetPlayer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -246,8 +250,17 @@ public class MprisActivity extends AppCompatActivity {
|
|||||||
findViewById(R.id.volume_layout).setVisibility(playerStatus.isSetVolumeAllowed() ? View.VISIBLE : View.INVISIBLE);
|
findViewById(R.id.volume_layout).setVisibility(playerStatus.isSetVolumeAllowed() ? View.VISIBLE : View.INVISIBLE);
|
||||||
findViewById(R.id.rew_button).setVisibility(playerStatus.isSeekAllowed() ? View.VISIBLE : View.GONE);
|
findViewById(R.id.rew_button).setVisibility(playerStatus.isSeekAllowed() ? View.VISIBLE : View.GONE);
|
||||||
findViewById(R.id.ff_button).setVisibility(playerStatus.isSeekAllowed() ? View.VISIBLE : View.GONE);
|
findViewById(R.id.ff_button).setVisibility(playerStatus.isSeekAllowed() ? View.VISIBLE : View.GONE);
|
||||||
findViewById(R.id.next_button).setVisibility(playerStatus.isGoNextAllowed() ? View.VISIBLE : View.GONE);
|
|
||||||
findViewById(R.id.prev_button).setVisibility(playerStatus.isGoPreviousAllowed() ? View.VISIBLE : View.GONE);
|
//Show and hide previous/next buttons simultaneously
|
||||||
|
if (playerStatus.isGoPreviousAllowed() || playerStatus.isGoNextAllowed()) {
|
||||||
|
findViewById(R.id.prev_button).setVisibility(View.VISIBLE);
|
||||||
|
findViewById(R.id.prev_button).setEnabled(playerStatus.isGoPreviousAllowed());
|
||||||
|
findViewById(R.id.next_button).setVisibility(View.VISIBLE);
|
||||||
|
findViewById(R.id.next_button).setEnabled(playerStatus.isGoNextAllowed());
|
||||||
|
} else {
|
||||||
|
findViewById(R.id.prev_button).setVisibility(View.GONE);
|
||||||
|
findViewById(R.id.next_button).setVisibility(View.GONE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -25,6 +25,7 @@ import android.app.PendingIntent;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.os.Message;
|
import android.os.Message;
|
||||||
@@ -253,6 +254,11 @@ public class MprisMediaSession implements SharedPreferences.OnSharedPreferenceCh
|
|||||||
metadata.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, notificationPlayer.getLength());
|
metadata.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, notificationPlayer.getLength());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Bitmap albumArt = notificationPlayer.getAlbumArt();
|
||||||
|
if (albumArt != null) {
|
||||||
|
metadata.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, albumArt);
|
||||||
|
}
|
||||||
|
|
||||||
mediaSession.setMetadata(metadata.build());
|
mediaSession.setMetadata(metadata.build());
|
||||||
PlaybackStateCompat.Builder playbackState = new PlaybackStateCompat.Builder();
|
PlaybackStateCompat.Builder playbackState = new PlaybackStateCompat.Builder();
|
||||||
|
|
||||||
@@ -326,6 +332,10 @@ public class MprisMediaSession implements SharedPreferences.OnSharedPreferenceCh
|
|||||||
notification.setContentText(notificationPlayer.getPlayer());
|
notification.setContentText(notificationPlayer.getPlayer());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (albumArt != null) {
|
||||||
|
notification.setLargeIcon(albumArt);
|
||||||
|
}
|
||||||
|
|
||||||
if (!notificationPlayer.isPlaying()) {
|
if (!notificationPlayer.isPlaying()) {
|
||||||
Intent iCloseNotification = new Intent(service, MprisMediaNotificationReceiver.class);
|
Intent iCloseNotification = new Intent(service, MprisMediaNotificationReceiver.class);
|
||||||
iCloseNotification.setAction(MprisMediaNotificationReceiver.ACTION_CLOSE_NOTIFICATION);
|
iCloseNotification.setAction(MprisMediaNotificationReceiver.ACTION_CLOSE_NOTIFICATION);
|
||||||
@@ -405,4 +415,9 @@ public class MprisMediaSession implements SharedPreferences.OnSharedPreferenceCh
|
|||||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||||
updateMediaNotification();
|
updateMediaNotification();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void playerSelected(MprisPlugin.MprisPlayer player) {
|
||||||
|
notificationPlayer = player;
|
||||||
|
updateMediaNotification();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -130,7 +130,7 @@ public class MprisPlugin extends Plugin {
|
|||||||
* @return The album art, or null if not available
|
* @return The album art, or null if not available
|
||||||
*/
|
*/
|
||||||
public Bitmap getAlbumArt() {
|
public Bitmap getAlbumArt() {
|
||||||
return AlbumArtCache.getAlbumArt(albumArtUrl);
|
return AlbumArtCache.getAlbumArt(albumArtUrl, MprisPlugin.this, player);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isSetVolumeAllowed() {
|
public boolean isSetVolumeAllowed() {
|
||||||
@@ -205,6 +205,7 @@ public class MprisPlugin extends Plugin {
|
|||||||
public final static String PACKET_TYPE_MPRIS_REQUEST = "kdeconnect.mpris.request";
|
public final static String PACKET_TYPE_MPRIS_REQUEST = "kdeconnect.mpris.request";
|
||||||
|
|
||||||
private HashMap<String, MprisPlayer> players = new HashMap<>();
|
private HashMap<String, MprisPlayer> players = new HashMap<>();
|
||||||
|
private boolean supportAlbumArtPayload = false;
|
||||||
private HashMap<String, Handler> playerStatusUpdated = new HashMap<>();
|
private HashMap<String, Handler> playerStatusUpdated = new HashMap<>();
|
||||||
|
|
||||||
private HashMap<String, Handler> playerListUpdated = new HashMap<>();
|
private HashMap<String, Handler> playerListUpdated = new HashMap<>();
|
||||||
@@ -231,7 +232,6 @@ public class MprisPlugin extends Plugin {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onCreate() {
|
public boolean onCreate() {
|
||||||
requestPlayerList();
|
|
||||||
MprisMediaSession.getInstance().onCreate(context.getApplicationContext(), this, device.getDeviceId());
|
MprisMediaSession.getInstance().onCreate(context.getApplicationContext(), this, device.getDeviceId());
|
||||||
|
|
||||||
//Always request the player list so the data is up-to-date
|
//Always request the player list so the data is up-to-date
|
||||||
@@ -266,6 +266,11 @@ public class MprisPlugin extends Plugin {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onPacketReceived(NetworkPacket np) {
|
public boolean onPacketReceived(NetworkPacket np) {
|
||||||
|
if (np.getBoolean("transferringAlbumArt", false)) {
|
||||||
|
AlbumArtCache.payloadToDiskCache(np.getString("albumArtUrl"), np.getPayload());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (np.has("player")) {
|
if (np.has("player")) {
|
||||||
MprisPlayer playerStatus = players.get(np.getString("player"));
|
MprisPlayer playerStatus = players.get(np.getString("player"));
|
||||||
if (playerStatus != null) {
|
if (playerStatus != null) {
|
||||||
@@ -306,6 +311,9 @@ public class MprisPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Remember if the connected device support album art payloads
|
||||||
|
supportAlbumArtPayload = np.getBoolean("supportAlbumArtPayload", supportAlbumArtPayload);
|
||||||
|
|
||||||
List<String> newPlayerList = np.getStringList("playerList");
|
List<String> newPlayerList = np.getStringList("playerList");
|
||||||
if (newPlayerList != null) {
|
if (newPlayerList != null) {
|
||||||
boolean equals = true;
|
boolean equals = true;
|
||||||
@@ -463,4 +471,22 @@ public class MprisPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -26,6 +26,8 @@ import android.app.PendingIntent;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.BitmapFactory;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
@@ -38,6 +40,7 @@ import org.kde.kdeconnect.Helpers.NotificationHelper;
|
|||||||
import org.kde.kdeconnect_tp.R;
|
import org.kde.kdeconnect_tp.R;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
|
||||||
public class ShareNotification {
|
public class ShareNotification {
|
||||||
|
|
||||||
@@ -99,6 +102,18 @@ public class ShareNotification {
|
|||||||
* - Proxy to real files (in case of the default download folder)
|
* - Proxy to real files (in case of the default download folder)
|
||||||
* - Proxy to the underlying content uri (in case of a custom download folder)
|
* - Proxy to the underlying content uri (in case of a custom download folder)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
//If it's an image, try to show it in the notification
|
||||||
|
if (mimeType.startsWith("image/")) {
|
||||||
|
try {
|
||||||
|
Bitmap image = BitmapFactory.decodeStream(device.getContext().getContentResolver().openInputStream(destinationUri));
|
||||||
|
if (image != null) {
|
||||||
|
builder.setLargeIcon(image);
|
||||||
|
builder.setStyle(new NotificationCompat.BigPictureStyle()
|
||||||
|
.bigPicture(image));
|
||||||
|
}
|
||||||
|
} catch (FileNotFoundException ignored) {}
|
||||||
|
}
|
||||||
if (!"file".equals(destinationUri.getScheme())) {
|
if (!"file".equals(destinationUri.getScheme())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@@ -323,6 +323,7 @@ public class DeviceFragment extends Fragment {
|
|||||||
|
|
||||||
if (device.isPairRequestedByPeer()) {
|
if (device.isPairRequestedByPeer()) {
|
||||||
((TextView) rootView.findViewById(R.id.pair_message)).setText(R.string.pair_requested);
|
((TextView) rootView.findViewById(R.id.pair_message)).setText(R.string.pair_requested);
|
||||||
|
rootView.findViewById(R.id.pairing_buttons).setVisibility(View.VISIBLE);
|
||||||
rootView.findViewById(R.id.pair_progress).setVisibility(View.GONE);
|
rootView.findViewById(R.id.pair_progress).setVisibility(View.GONE);
|
||||||
rootView.findViewById(R.id.pair_button).setVisibility(View.GONE);
|
rootView.findViewById(R.id.pair_button).setVisibility(View.GONE);
|
||||||
rootView.findViewById(R.id.pair_request).setVisibility(View.VISIBLE);
|
rootView.findViewById(R.id.pair_request).setVisibility(View.VISIBLE);
|
||||||
|
@@ -159,16 +159,6 @@ public class PairingFragment extends Fragment implements PairingDeviceItem.Callb
|
|||||||
SectionItem section;
|
SectionItem section;
|
||||||
Resources res = getResources();
|
Resources res = getResources();
|
||||||
|
|
||||||
section = new SectionItem(res.getString(R.string.category_not_paired_devices));
|
|
||||||
section.isSectionEmpty = true;
|
|
||||||
items.add(section);
|
|
||||||
for (Device device : devices) {
|
|
||||||
if (device.isReachable() && !device.isPaired()) {
|
|
||||||
items.add(new PairingDeviceItem(device, PairingFragment.this));
|
|
||||||
section.isSectionEmpty = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
section = new SectionItem(res.getString(R.string.category_connected_devices));
|
section = new SectionItem(res.getString(R.string.category_connected_devices));
|
||||||
section.isSectionEmpty = true;
|
section.isSectionEmpty = true;
|
||||||
items.add(section);
|
items.add(section);
|
||||||
@@ -182,6 +172,16 @@ public class PairingFragment extends Fragment implements PairingDeviceItem.Callb
|
|||||||
items.remove(items.size() - 1); //Remove connected devices section if empty
|
items.remove(items.size() - 1); //Remove connected devices section if empty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
section = new SectionItem(res.getString(R.string.category_not_paired_devices));
|
||||||
|
section.isSectionEmpty = true;
|
||||||
|
items.add(section);
|
||||||
|
for (Device device : devices) {
|
||||||
|
if (device.isReachable() && !device.isPaired()) {
|
||||||
|
items.add(new PairingDeviceItem(device, PairingFragment.this));
|
||||||
|
section.isSectionEmpty = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
section = new SectionItem(res.getString(R.string.category_remembered_devices));
|
section = new SectionItem(res.getString(R.string.category_remembered_devices));
|
||||||
section.isSectionEmpty = true;
|
section.isSectionEmpty = true;
|
||||||
items.add(section);
|
items.add(section);
|
||||||
|
Reference in New Issue
Block a user