mirror of
https://gitlab.com/ultrasonic/ultrasonic.git
synced 2025-06-20 17:03:57 +03:00
Update media3 to v1.0.0-beta03
This commit is contained in:
parent
7d91eb204f
commit
e18abbc856
@ -57,15 +57,15 @@ data class Track(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun compareTo(other: Track): Int {
|
fun compareTo(other: Track): Int {
|
||||||
when {
|
return when {
|
||||||
this.closeness == other.closeness -> {
|
this.closeness == other.closeness -> {
|
||||||
return 0
|
0
|
||||||
}
|
}
|
||||||
this.closeness > other.closeness -> {
|
this.closeness > other.closeness -> {
|
||||||
return -1
|
-1
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
return 1
|
1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,7 @@ class VersionAwareJacksonConverterFactory(
|
|||||||
type: Type,
|
type: Type,
|
||||||
annotations: Array<Annotation>,
|
annotations: Array<Annotation>,
|
||||||
retrofit: Retrofit
|
retrofit: Retrofit
|
||||||
): Converter<ResponseBody, *>? {
|
): Converter<ResponseBody, *> {
|
||||||
val javaType: JavaType = mapper!!.typeFactory.constructType(type)
|
val javaType: JavaType = mapper!!.typeFactory.constructType(type)
|
||||||
val reader: ObjectReader? = mapper!!.readerFor(javaType)
|
val reader: ObjectReader? = mapper!!.readerFor(javaType)
|
||||||
return VersionAwareResponseBodyConverter<Any>(notifier, reader!!)
|
return VersionAwareResponseBodyConverter<Any>(notifier, reader!!)
|
||||||
|
@ -23,8 +23,8 @@ open class SubsonicResponse(
|
|||||||
OK("ok"), ERROR("failed");
|
OK("ok"), ERROR("failed");
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getStatusFromJson(jsonValue: String) = values()
|
fun getStatusFromJson(jsonValue: String) =
|
||||||
.filter { it.jsonValue == jsonValue }.firstOrNull()
|
values().firstOrNull { it.jsonValue == jsonValue }
|
||||||
?: throw IllegalArgumentException("Unknown status value: $jsonValue")
|
?: throw IllegalArgumentException("Unknown status value: $jsonValue")
|
||||||
|
|
||||||
class StatusJsonDeserializer : JsonDeserializer<Status>() {
|
class StatusJsonDeserializer : JsonDeserializer<Status>() {
|
||||||
|
@ -9,7 +9,7 @@ ktlint = "0.43.2"
|
|||||||
ktlintGradle = "11.0.0"
|
ktlintGradle = "11.0.0"
|
||||||
detekt = "1.22.0"
|
detekt = "1.22.0"
|
||||||
preferences = "1.2.0"
|
preferences = "1.2.0"
|
||||||
media3 = "1.0.0-beta02"
|
media3 = "1.0.0-beta03"
|
||||||
|
|
||||||
androidSupport = "1.5.0"
|
androidSupport = "1.5.0"
|
||||||
materialDesign = "1.6.1"
|
materialDesign = "1.6.1"
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
-keep class kotlin.Metadata { *; }
|
-keep class kotlin.Metadata { *; }
|
||||||
-keep class kotlin.reflect.** { *; }
|
-keep class kotlin.reflect.** { *; }
|
||||||
|
|
||||||
-keepclassmembers public class com.company[obfuscated].domain.api.models.** {
|
-keepclassmembers public class org.ultrasonic.domain.api.models.** {
|
||||||
public synthetic <methods>;
|
public synthetic <methods>;
|
||||||
}
|
}
|
||||||
|
|
||||||
-keep class org.jetbrains.kotlin.** { *; }
|
-keep class org.jetbrains.kotlin.** { *; }
|
||||||
-keep class org.jetbrains.annotations.** { *; }
|
-keep class org.jetbrains.annotations.** { *; }
|
||||||
-keepclassmembers class ** {
|
|
||||||
@org.jetbrains.annotations.ReadOnly public *;
|
|
||||||
}
|
|
||||||
|
@ -0,0 +1,639 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License
|
||||||
|
*/
|
||||||
|
package org.moire.ultrasonic.service;
|
||||||
|
|
||||||
|
import static androidx.media3.common.C.INDEX_UNSET;
|
||||||
|
import static androidx.media3.common.Player.COMMAND_INVALID;
|
||||||
|
import static androidx.media3.common.Player.COMMAND_PLAY_PAUSE;
|
||||||
|
import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT;
|
||||||
|
import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM;
|
||||||
|
import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS;
|
||||||
|
import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM;
|
||||||
|
import static androidx.media3.common.Player.COMMAND_STOP;
|
||||||
|
import static androidx.media3.common.Player.STATE_ENDED;
|
||||||
|
import static androidx.media3.common.util.Assertions.checkState;
|
||||||
|
import static androidx.media3.common.util.Assertions.checkStateNotNull;
|
||||||
|
|
||||||
|
import android.app.Notification;
|
||||||
|
import android.app.NotificationChannel;
|
||||||
|
import android.app.NotificationManager;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import androidx.annotation.DoNotInline;
|
||||||
|
import androidx.annotation.DrawableRes;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.RequiresApi;
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
|
import androidx.core.app.NotificationCompat;
|
||||||
|
import androidx.core.graphics.drawable.IconCompat;
|
||||||
|
import androidx.media3.common.C;
|
||||||
|
import androidx.media3.common.MediaMetadata;
|
||||||
|
import androidx.media3.common.Player;
|
||||||
|
import androidx.media3.common.util.Log;
|
||||||
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
import androidx.media3.common.util.Util;
|
||||||
|
import androidx.media3.session.CommandButton;
|
||||||
|
import androidx.media3.session.MediaController;
|
||||||
|
import androidx.media3.session.MediaNotification;
|
||||||
|
import androidx.media3.session.MediaSession;
|
||||||
|
import androidx.media3.session.MediaStyleNotificationHelper;
|
||||||
|
import androidx.media3.session.SessionCommand;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.common.util.concurrent.FutureCallback;
|
||||||
|
import com.google.common.util.concurrent.Futures;
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
|
import org.moire.ultrasonic.R;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default {@link MediaNotification.Provider}.
|
||||||
|
*
|
||||||
|
* <h2>Actions</h2>
|
||||||
|
*
|
||||||
|
* The following actions are included in the provided notifications:
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link MediaController#COMMAND_PLAY_PAUSE} to start or pause playback.
|
||||||
|
* <li>{@link MediaController#COMMAND_SEEK_TO_PREVIOUS} to seek to the previous item.
|
||||||
|
* <li>{@link MediaController#COMMAND_SEEK_TO_NEXT} to seek to the next item.
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Custom commands</h2>
|
||||||
|
*
|
||||||
|
* Custom actions are sent to the session under the hood. You can receive them by overriding the
|
||||||
|
* session callback method {@link MediaSession.Callback#onCustomCommand(MediaSession,
|
||||||
|
* MediaSession.ControllerInfo, SessionCommand, Bundle)}. This is useful because starting with
|
||||||
|
* Android 13, the System UI notification sends commands directly to the session. So handling the
|
||||||
|
* custom commands on the session level allows you to handle them at the same callback for all API
|
||||||
|
* levels.
|
||||||
|
*
|
||||||
|
* <h2>Drawables</h2>
|
||||||
|
*
|
||||||
|
* The drawables used can be overridden by drawables with the same names defined the application.
|
||||||
|
* The drawables are:
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li><b>{@code media3_notification_play}</b> - The play icon.
|
||||||
|
* <li><b>{@code media3_notification_pause}</b> - The pause icon.
|
||||||
|
* <li><b>{@code media3_notification_seek_to_previous}</b> - The previous icon.
|
||||||
|
* <li><b>{@code media3_notification_seek_to_next}</b> - The next icon.
|
||||||
|
* <li><b>{@code media3_notification_small_icon}</b> - The {@link
|
||||||
|
* NotificationCompat.Builder#setSmallIcon(int) small icon}. A different icon can be set with
|
||||||
|
* {@link #setSmallIcon(int)}.
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>String resources</h2>
|
||||||
|
*
|
||||||
|
* String resources used can be overridden by resources with the same names defined the application.
|
||||||
|
* These are:
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li><b>{@code media3_controls_play_description}</b> - The description of the play icon.
|
||||||
|
* <li><b>{@code media3_controls_pause_description}</b> - The description of the pause icon.
|
||||||
|
* <li><b>{@code media3_controls_seek_to_previous_description}</b> - The description of the
|
||||||
|
* previous icon.
|
||||||
|
* <li><b>{@code media3_controls_seek_to_next_description}</b> - The description of the next icon.
|
||||||
|
* <li><b>{@code default_notification_channel_name}</b> The name of the {@link
|
||||||
|
* NotificationChannel} on which created notifications are posted. A different string resource
|
||||||
|
* can be set when constructing the provider with {@link
|
||||||
|
* DefaultMediaNotificationProvider2.Builder#setChannelName(int)}.
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
@UnstableApi
|
||||||
|
public class DefaultMediaNotificationProvider2 implements MediaNotification.Provider {
|
||||||
|
|
||||||
|
/** A builder for {@link DefaultMediaNotificationProvider2} instances. */
|
||||||
|
public static final class Builder {
|
||||||
|
private final Context context;
|
||||||
|
private NotificationIdProvider notificationIdProvider;
|
||||||
|
private String channelId;
|
||||||
|
@StringRes private int channelNameResourceId;
|
||||||
|
private boolean built;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a builder.
|
||||||
|
*
|
||||||
|
* @param context Any {@link Context}.
|
||||||
|
*/
|
||||||
|
public Builder(Context context) {
|
||||||
|
this.context = context;
|
||||||
|
notificationIdProvider = session -> DEFAULT_NOTIFICATION_ID;
|
||||||
|
channelId = DEFAULT_CHANNEL_ID;
|
||||||
|
channelNameResourceId = DEFAULT_CHANNEL_NAME_RESOURCE_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link MediaNotification#notificationId} used for the created notifications. By
|
||||||
|
* default, this is set to {@link #DEFAULT_NOTIFICATION_ID}.
|
||||||
|
*
|
||||||
|
* <p>Overwrites anything set in {@link #setNotificationIdProvider(NotificationIdProvider)}.
|
||||||
|
*
|
||||||
|
* @param notificationId The notification ID.
|
||||||
|
* @return This builder.
|
||||||
|
*/
|
||||||
|
@CanIgnoreReturnValue
|
||||||
|
public Builder setNotificationId(int notificationId) {
|
||||||
|
this.notificationIdProvider = session -> notificationId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the provider for the {@link MediaNotification#notificationId} used for the created
|
||||||
|
* notifications. By default, this is set to a provider that always returns {@link
|
||||||
|
* #DEFAULT_NOTIFICATION_ID}.
|
||||||
|
*
|
||||||
|
* <p>Overwrites anything set in {@link #setNotificationId(int)}.
|
||||||
|
*
|
||||||
|
* @param notificationIdProvider The notification ID provider.
|
||||||
|
* @return This builder.
|
||||||
|
*/
|
||||||
|
@CanIgnoreReturnValue
|
||||||
|
public Builder setNotificationIdProvider(NotificationIdProvider notificationIdProvider) {
|
||||||
|
this.notificationIdProvider = notificationIdProvider;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the ID of the {@link NotificationChannel} on which created notifications are posted on.
|
||||||
|
* By default, this is set to {@link #DEFAULT_CHANNEL_ID}.
|
||||||
|
*
|
||||||
|
* @param channelId The channel ID.
|
||||||
|
* @return This builder.
|
||||||
|
*/
|
||||||
|
@CanIgnoreReturnValue
|
||||||
|
public Builder setChannelId(String channelId) {
|
||||||
|
this.channelId = channelId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the name of the {@link NotificationChannel} on which created notifications are posted
|
||||||
|
* on. By default, this is set to {@link #DEFAULT_CHANNEL_NAME_RESOURCE_ID}.
|
||||||
|
*
|
||||||
|
* @param channelNameResourceId The string resource ID with the channel name.
|
||||||
|
* @return This builder.
|
||||||
|
*/
|
||||||
|
@CanIgnoreReturnValue
|
||||||
|
public Builder setChannelName(@StringRes int channelNameResourceId) {
|
||||||
|
this.channelNameResourceId = channelNameResourceId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the {@link DefaultMediaNotificationProvider2}. The method can be called at most once.
|
||||||
|
*/
|
||||||
|
public DefaultMediaNotificationProvider2 build() {
|
||||||
|
checkState(!built);
|
||||||
|
DefaultMediaNotificationProvider2 provider = new DefaultMediaNotificationProvider2(this);
|
||||||
|
built = true;
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides notification IDs for posting media notifications for given media sessions.
|
||||||
|
*
|
||||||
|
* @see Builder#setNotificationIdProvider(NotificationIdProvider)
|
||||||
|
*/
|
||||||
|
public interface NotificationIdProvider {
|
||||||
|
/** Returns the notification ID for the media notification of the given session. */
|
||||||
|
int getNotificationId(MediaSession mediaSession);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An extras key that can be used to define the index of a {@link CommandButton} in {@linkplain
|
||||||
|
* Notification.MediaStyle#setShowActionsInCompactView(int...) compact view}.
|
||||||
|
*/
|
||||||
|
public static final String COMMAND_KEY_COMPACT_VIEW_INDEX =
|
||||||
|
"androidx.media3.session.command.COMPACT_VIEW_INDEX";
|
||||||
|
|
||||||
|
/** The default ID used for the {@link MediaNotification#notificationId}. */
|
||||||
|
public static final int DEFAULT_NOTIFICATION_ID = 1001;
|
||||||
|
/**
|
||||||
|
* The default ID used for the {@link NotificationChannel} on which created notifications are
|
||||||
|
* posted on.
|
||||||
|
*/
|
||||||
|
public static final String DEFAULT_CHANNEL_ID = "default_channel_id";
|
||||||
|
/**
|
||||||
|
* The default name used for the {@link NotificationChannel} on which created notifications are
|
||||||
|
* posted on.
|
||||||
|
*/
|
||||||
|
@StringRes
|
||||||
|
public static final int DEFAULT_CHANNEL_NAME_RESOURCE_ID =
|
||||||
|
R.string.default_notification_channel_name;
|
||||||
|
|
||||||
|
private static final String TAG = "NotificationProvider";
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
private final NotificationIdProvider notificationIdProvider;
|
||||||
|
private final String channelId;
|
||||||
|
@StringRes private final int channelNameResourceId;
|
||||||
|
private final NotificationManager notificationManager;
|
||||||
|
// Cache the last bitmap load request to avoid reloading the bitmap again, particularly useful
|
||||||
|
// when showing a notification for the same item (e.g. when switching from playing to paused).
|
||||||
|
private final Handler mainHandler;
|
||||||
|
|
||||||
|
private @MonotonicNonNull OnBitmapLoadedFutureCallback pendingOnBitmapLoadedFutureCallback;
|
||||||
|
@DrawableRes private int smallIconResourceId;
|
||||||
|
|
||||||
|
public DefaultMediaNotificationProvider2(Builder builder) {
|
||||||
|
this.context = builder.context;
|
||||||
|
this.notificationIdProvider = builder.notificationIdProvider;
|
||||||
|
this.channelId = builder.channelId;
|
||||||
|
this.channelNameResourceId = builder.channelNameResourceId;
|
||||||
|
notificationManager =
|
||||||
|
checkStateNotNull(
|
||||||
|
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE));
|
||||||
|
mainHandler = new Handler(Looper.getMainLooper());
|
||||||
|
smallIconResourceId = R.drawable.media3_notification_small_icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MediaNotification.Provider implementation
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final MediaNotification createNotification(
|
||||||
|
MediaSession mediaSession,
|
||||||
|
ImmutableList<CommandButton> customLayout,
|
||||||
|
MediaNotification.ActionFactory actionFactory,
|
||||||
|
Callback onNotificationChangedCallback) {
|
||||||
|
ensureNotificationChannel();
|
||||||
|
|
||||||
|
Player player = mediaSession.getPlayer();
|
||||||
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId);
|
||||||
|
int notificationId = notificationIdProvider.getNotificationId(mediaSession);
|
||||||
|
|
||||||
|
MediaStyleNotificationHelper.MediaStyle mediaStyle = new MediaStyleNotificationHelper.MediaStyle(mediaSession);
|
||||||
|
int[] compactViewIndices =
|
||||||
|
addNotificationActions(
|
||||||
|
mediaSession,
|
||||||
|
getMediaButtons(
|
||||||
|
player.getAvailableCommands(),
|
||||||
|
customLayout,
|
||||||
|
/* showPauseButton= */ player.getPlayWhenReady()
|
||||||
|
&& player.getPlaybackState() != STATE_ENDED),
|
||||||
|
builder,
|
||||||
|
actionFactory);
|
||||||
|
mediaStyle.setShowActionsInCompactView(compactViewIndices);
|
||||||
|
|
||||||
|
// Set metadata info in the notification.
|
||||||
|
MediaMetadata metadata = player.getMediaMetadata();
|
||||||
|
builder
|
||||||
|
.setContentTitle(getNotificationContentTitle(metadata))
|
||||||
|
.setContentText(getNotificationContentText(metadata));
|
||||||
|
@Nullable
|
||||||
|
ListenableFuture<Bitmap> bitmapFuture =
|
||||||
|
mediaSession.getBitmapLoader().loadBitmapFromMetadata(metadata);
|
||||||
|
if (bitmapFuture != null) {
|
||||||
|
if (pendingOnBitmapLoadedFutureCallback != null) {
|
||||||
|
pendingOnBitmapLoadedFutureCallback.discardIfPending();
|
||||||
|
}
|
||||||
|
if (bitmapFuture.isDone()) {
|
||||||
|
try {
|
||||||
|
builder.setLargeIcon(Futures.getDone(bitmapFuture));
|
||||||
|
} catch (ExecutionException e) {
|
||||||
|
Log.w(TAG, "Failed to load bitmap", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pendingOnBitmapLoadedFutureCallback =
|
||||||
|
new OnBitmapLoadedFutureCallback(
|
||||||
|
notificationId, builder, onNotificationChangedCallback);
|
||||||
|
Futures.addCallback(
|
||||||
|
bitmapFuture,
|
||||||
|
pendingOnBitmapLoadedFutureCallback,
|
||||||
|
// This callback must be executed on the next looper iteration, after this method has
|
||||||
|
// returned a media notification.
|
||||||
|
mainHandler::post);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (player.isCommandAvailable(COMMAND_STOP) || Util.SDK_INT < 21) {
|
||||||
|
// We must include a cancel intent for pre-L devices.
|
||||||
|
mediaStyle.setCancelButtonIntent(
|
||||||
|
actionFactory.createMediaActionPendingIntent(mediaSession, COMMAND_STOP));
|
||||||
|
}
|
||||||
|
|
||||||
|
long playbackStartTimeMs = getPlaybackStartTimeEpochMs(player);
|
||||||
|
boolean displayElapsedTimeWithChronometer = playbackStartTimeMs != C.TIME_UNSET;
|
||||||
|
builder
|
||||||
|
.setWhen(playbackStartTimeMs)
|
||||||
|
.setShowWhen(displayElapsedTimeWithChronometer)
|
||||||
|
.setUsesChronometer(displayElapsedTimeWithChronometer);
|
||||||
|
|
||||||
|
Notification notification =
|
||||||
|
builder
|
||||||
|
.setContentIntent(mediaSession.getSessionActivity())
|
||||||
|
.setDeleteIntent(
|
||||||
|
actionFactory.createMediaActionPendingIntent(mediaSession, COMMAND_STOP))
|
||||||
|
.setOnlyAlertOnce(true)
|
||||||
|
.setSmallIcon(smallIconResourceId)
|
||||||
|
.setStyle(mediaStyle)
|
||||||
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||||
|
.setOngoing(false)
|
||||||
|
.build();
|
||||||
|
return new MediaNotification(notificationId, notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final boolean handleCustomCommand(MediaSession session, String action, Bundle extras) {
|
||||||
|
// Make the custom action being delegated to the session as a custom session command.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other methods
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the small icon of the notification which is also shown in the system status bar.
|
||||||
|
*
|
||||||
|
* @see NotificationCompat.Builder#setSmallIcon(int)
|
||||||
|
* @param smallIconResourceId The resource id of the small icon.
|
||||||
|
*/
|
||||||
|
public final void setSmallIcon(@DrawableRes int smallIconResourceId) {
|
||||||
|
this.smallIconResourceId = smallIconResourceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the ordered list of {@linkplain CommandButton command buttons} to be used to build the
|
||||||
|
* notification.
|
||||||
|
*
|
||||||
|
* <p>This method is called each time a new notification is built.
|
||||||
|
*
|
||||||
|
* <p>Override this method to customize the buttons on the notification. Commands of the buttons
|
||||||
|
* returned by this method must be contained in {@link MediaController#getAvailableCommands()}.
|
||||||
|
*
|
||||||
|
* <p>By default, the notification shows {@link Player#COMMAND_PLAY_PAUSE} in {@linkplain
|
||||||
|
* Notification.MediaStyle#setShowActionsInCompactView(int...) compact view}. This can be
|
||||||
|
* customized by defining the index of the command in compact view of up to 3 commands in their
|
||||||
|
* extras with key {@link DefaultMediaNotificationProvider2#COMMAND_KEY_COMPACT_VIEW_INDEX}.
|
||||||
|
*
|
||||||
|
* <p>To make the custom layout and commands work, you need to {@linkplain
|
||||||
|
* MediaSession#setCustomLayout(List) set the custom layout of commands} and add the custom
|
||||||
|
* commands to the available commands when a controller {@linkplain
|
||||||
|
* MediaSession.Callback#onConnect(MediaSession, MediaSession.ControllerInfo) connects to the
|
||||||
|
* session}. Controllers that connect after you called {@link MediaSession#setCustomLayout(List)}
|
||||||
|
* need the custom command set in {@link MediaSession.Callback#onPostConnect(MediaSession,
|
||||||
|
* MediaSession.ControllerInfo)} also.
|
||||||
|
*
|
||||||
|
* @param playerCommands The available player commands.
|
||||||
|
* @param customLayout The {@linkplain MediaSession#setCustomLayout(List) custom layout of
|
||||||
|
* commands}.
|
||||||
|
* @param showPauseButton Whether the notification should show a pause button (e.g., because the
|
||||||
|
* player is currently playing content), otherwise show a play button to start playback.
|
||||||
|
* @return The ordered list of command buttons to be placed on the notification.
|
||||||
|
*/
|
||||||
|
protected List<CommandButton> getMediaButtons(
|
||||||
|
Player.Commands playerCommands, List<CommandButton> customLayout, boolean showPauseButton) {
|
||||||
|
// Skip to previous action.
|
||||||
|
List<CommandButton> commandButtons = new ArrayList<>();
|
||||||
|
if (playerCommands.containsAny(COMMAND_SEEK_TO_PREVIOUS, COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)) {
|
||||||
|
Bundle commandButtonExtras = new Bundle();
|
||||||
|
commandButtonExtras.putInt(COMMAND_KEY_COMPACT_VIEW_INDEX, INDEX_UNSET);
|
||||||
|
commandButtons.add(
|
||||||
|
new CommandButton.Builder()
|
||||||
|
.setPlayerCommand(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
|
||||||
|
.setIconResId(R.drawable.media3_notification_seek_to_previous)
|
||||||
|
.setDisplayName(
|
||||||
|
context.getString(R.string.media3_controls_seek_to_previous_description))
|
||||||
|
.setExtras(commandButtonExtras)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
if (playerCommands.contains(COMMAND_PLAY_PAUSE)) {
|
||||||
|
Bundle commandButtonExtras = new Bundle();
|
||||||
|
commandButtonExtras.putInt(COMMAND_KEY_COMPACT_VIEW_INDEX, INDEX_UNSET);
|
||||||
|
commandButtons.add(
|
||||||
|
new CommandButton.Builder()
|
||||||
|
.setPlayerCommand(COMMAND_PLAY_PAUSE)
|
||||||
|
.setIconResId(
|
||||||
|
showPauseButton
|
||||||
|
? R.drawable.media3_notification_pause
|
||||||
|
: R.drawable.media3_notification_play)
|
||||||
|
.setExtras(commandButtonExtras)
|
||||||
|
.setDisplayName(
|
||||||
|
showPauseButton
|
||||||
|
? context.getString(R.string.media3_controls_pause_description)
|
||||||
|
: context.getString(R.string.media3_controls_play_description))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
// Skip to next action.
|
||||||
|
if (playerCommands.containsAny(COMMAND_SEEK_TO_NEXT, COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)) {
|
||||||
|
Bundle commandButtonExtras = new Bundle();
|
||||||
|
commandButtonExtras.putInt(COMMAND_KEY_COMPACT_VIEW_INDEX, INDEX_UNSET);
|
||||||
|
commandButtons.add(
|
||||||
|
new CommandButton.Builder()
|
||||||
|
.setPlayerCommand(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
|
||||||
|
.setIconResId(R.drawable.media3_notification_seek_to_next)
|
||||||
|
.setExtras(commandButtonExtras)
|
||||||
|
.setDisplayName(context.getString(R.string.media3_controls_seek_to_next_description))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
for (int i = 0; i < customLayout.size(); i++) {
|
||||||
|
CommandButton button = customLayout.get(i);
|
||||||
|
if (button.sessionCommand != null
|
||||||
|
&& button.sessionCommand.commandCode == SessionCommand.COMMAND_CODE_CUSTOM) {
|
||||||
|
commandButtons.add(button);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return commandButtons;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the media buttons to the notification builder for the given action factory.
|
||||||
|
*
|
||||||
|
* <p>The list of {@code mediaButtons} is the list resulting from {@link #getMediaButtons(
|
||||||
|
* Player.Commands, List, boolean)}.
|
||||||
|
*
|
||||||
|
* <p>Override this method to customize how the media buttons {@linkplain
|
||||||
|
* NotificationCompat.Builder#addAction(NotificationCompat.Action) are added} to the notification
|
||||||
|
* and define which actions are shown in compact view by returning the indices of the buttons to
|
||||||
|
* be shown in compact view.
|
||||||
|
*
|
||||||
|
* <p>By default, {@link Player#COMMAND_PLAY_PAUSE} is shown in compact view, unless some of the
|
||||||
|
* buttons are marked with {@link DefaultMediaNotificationProvider2#COMMAND_KEY_COMPACT_VIEW_INDEX}
|
||||||
|
* to declare the index in compact view of the given command button in the button extras.
|
||||||
|
*
|
||||||
|
* @param mediaSession The media session to which the actions will be sent.
|
||||||
|
* @param mediaButtons The command buttons to be included in the notification.
|
||||||
|
* @param builder The builder to add the actions to.
|
||||||
|
* @param actionFactory The actions factory to be used to build notifications.
|
||||||
|
* @return The indices of the buttons to be {@linkplain
|
||||||
|
* Notification.MediaStyle#setShowActionsInCompactView(int...) used in compact view of the
|
||||||
|
* notification}.
|
||||||
|
*/
|
||||||
|
protected int[] addNotificationActions(
|
||||||
|
MediaSession mediaSession,
|
||||||
|
List<CommandButton> mediaButtons,
|
||||||
|
NotificationCompat.Builder builder,
|
||||||
|
MediaNotification.ActionFactory actionFactory) {
|
||||||
|
int[] compactViewIndices = new int[3];
|
||||||
|
Arrays.fill(compactViewIndices, INDEX_UNSET);
|
||||||
|
int compactViewCommandCount = 0;
|
||||||
|
for (int i = 0; i < mediaButtons.size(); i++) {
|
||||||
|
CommandButton commandButton = mediaButtons.get(i);
|
||||||
|
if (commandButton.sessionCommand != null) {
|
||||||
|
builder.addAction(
|
||||||
|
actionFactory.createCustomActionFromCustomCommandButton(mediaSession, commandButton));
|
||||||
|
} else {
|
||||||
|
checkState(commandButton.playerCommand != COMMAND_INVALID);
|
||||||
|
builder.addAction(
|
||||||
|
actionFactory.createMediaAction(
|
||||||
|
mediaSession,
|
||||||
|
IconCompat.createWithResource(context, commandButton.iconResId),
|
||||||
|
commandButton.displayName,
|
||||||
|
commandButton.playerCommand));
|
||||||
|
}
|
||||||
|
if (compactViewCommandCount == 3) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int compactViewIndex =
|
||||||
|
commandButton.extras.getInt(
|
||||||
|
COMMAND_KEY_COMPACT_VIEW_INDEX, /* defaultValue= */ INDEX_UNSET);
|
||||||
|
if (compactViewIndex >= 0 && compactViewIndex < compactViewIndices.length) {
|
||||||
|
compactViewCommandCount++;
|
||||||
|
compactViewIndices[compactViewIndex] = i;
|
||||||
|
} else if (commandButton.playerCommand == COMMAND_PLAY_PAUSE
|
||||||
|
&& compactViewCommandCount == 0) {
|
||||||
|
// If there is no custom configuration we use the play/pause action in compact view.
|
||||||
|
compactViewIndices[0] = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int i = 0; i < compactViewIndices.length; i++) {
|
||||||
|
if (compactViewIndices[i] == INDEX_UNSET) {
|
||||||
|
compactViewIndices = Arrays.copyOf(compactViewIndices, i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return compactViewIndices;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the content title to be used to build the notification.
|
||||||
|
*
|
||||||
|
* <p>This method is called each time a new notification is built.
|
||||||
|
*
|
||||||
|
* <p>Override this method to customize which field of {@link MediaMetadata} is used for content
|
||||||
|
* title of the notification.
|
||||||
|
*
|
||||||
|
* <p>By default, the notification shows {@link MediaMetadata#title} as content title.
|
||||||
|
*
|
||||||
|
* @param metadata The media metadata from which content title is fetched.
|
||||||
|
* @return Notification content title.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
protected CharSequence getNotificationContentTitle(MediaMetadata metadata) {
|
||||||
|
return metadata.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the content text to be used to build the notification.
|
||||||
|
*
|
||||||
|
* <p>This method is called each time a new notification is built.
|
||||||
|
*
|
||||||
|
* <p>Override this method to customize which field of {@link MediaMetadata} is used for content
|
||||||
|
* text of the notification.
|
||||||
|
*
|
||||||
|
* <p>By default, the notification shows {@link MediaMetadata#artist} as content text.
|
||||||
|
*
|
||||||
|
* @param metadata The media metadata from which content text is fetched.
|
||||||
|
* @return Notification content text.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
protected CharSequence getNotificationContentText(MediaMetadata metadata) {
|
||||||
|
return metadata.artist;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureNotificationChannel() {
|
||||||
|
if (Util.SDK_INT < 26 || notificationManager.getNotificationChannel(channelId) != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Api26.createNotificationChannel(
|
||||||
|
notificationManager, channelId, context.getString(channelNameResourceId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long getPlaybackStartTimeEpochMs(Player player) {
|
||||||
|
// Changing "showWhen" causes notification flicker if SDK_INT < 21.
|
||||||
|
if (Util.SDK_INT >= 21
|
||||||
|
&& player.isPlaying()
|
||||||
|
&& !player.isPlayingAd()
|
||||||
|
&& !player.isCurrentMediaItemDynamic()
|
||||||
|
&& player.getPlaybackParameters().speed == 1f) {
|
||||||
|
return System.currentTimeMillis() - player.getContentPosition();
|
||||||
|
} else {
|
||||||
|
return C.TIME_UNSET;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class OnBitmapLoadedFutureCallback implements FutureCallback<Bitmap> {
|
||||||
|
private final int notificationId;
|
||||||
|
private final NotificationCompat.Builder builder;
|
||||||
|
private final Callback onNotificationChangedCallback;
|
||||||
|
|
||||||
|
private boolean discarded;
|
||||||
|
|
||||||
|
public OnBitmapLoadedFutureCallback(
|
||||||
|
int notificationId,
|
||||||
|
NotificationCompat.Builder builder,
|
||||||
|
Callback onNotificationChangedCallback) {
|
||||||
|
this.notificationId = notificationId;
|
||||||
|
this.builder = builder;
|
||||||
|
this.onNotificationChangedCallback = onNotificationChangedCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void discardIfPending() {
|
||||||
|
discarded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSuccess(Bitmap result) {
|
||||||
|
if (!discarded) {
|
||||||
|
builder.setLargeIcon(result);
|
||||||
|
onNotificationChangedCallback.onNotificationChanged(
|
||||||
|
new MediaNotification(notificationId, builder.build()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(Throwable t) {
|
||||||
|
if (!discarded) {
|
||||||
|
Log.d(TAG, "Failed to load bitmap", t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(26)
|
||||||
|
private static class Api26 {
|
||||||
|
@DoNotInline
|
||||||
|
public static void createNotificationChannel(
|
||||||
|
NotificationManager notificationManager, String channelId, String channelName) {
|
||||||
|
NotificationChannel channel =
|
||||||
|
new NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_LOW);
|
||||||
|
if (Util.SDK_INT <= 27) {
|
||||||
|
// API 28+ will automatically hide the app icon 'badge' for notifications using
|
||||||
|
// Notification.MediaStyle, but we have to manually hide it for APIs 26 (when badges were
|
||||||
|
// added) and 27.
|
||||||
|
channel.setShowBadge(false);
|
||||||
|
}
|
||||||
|
notificationManager.createNotificationChannel(channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -92,10 +92,10 @@ class ArtistRowBinder(
|
|||||||
val previousItem = adapter.items[index - 1]
|
val previousItem = adapter.items[index - 1]
|
||||||
val previousSectionKey: String
|
val previousSectionKey: String
|
||||||
|
|
||||||
if (previousItem is ArtistOrIndex) {
|
previousSectionKey = if (previousItem is ArtistOrIndex) {
|
||||||
previousSectionKey = getSectionFromName(previousItem.name ?: " ")
|
getSectionFromName(previousItem.name ?: " ")
|
||||||
} else {
|
} else {
|
||||||
previousSectionKey = " "
|
" "
|
||||||
}
|
}
|
||||||
|
|
||||||
val currentSectionKey = getSectionFromName(item.name ?: "")
|
val currentSectionKey = getSectionFromName(item.name ?: "")
|
||||||
|
@ -77,7 +77,7 @@ class ServerSettingsModel(
|
|||||||
*/
|
*/
|
||||||
fun moveItemDown(index: Int) {
|
fun moveItemDown(index: Int) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
if (index < repository.getMaxIndex() ?: 0) {
|
if (index < (repository.getMaxIndex() ?: 0)) {
|
||||||
val itemToBeMoved = repository.findByIndex(index)
|
val itemToBeMoved = repository.findByIndex(index)
|
||||||
val nextItem = repository.findByIndex(index + 1)
|
val nextItem = repository.findByIndex(index + 1)
|
||||||
|
|
||||||
|
@ -71,10 +71,10 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
|||||||
val service = MusicServiceFactory.getMusicService()
|
val service = MusicServiceFactory.getMusicService()
|
||||||
val musicDirectory: MusicDirectory
|
val musicDirectory: MusicDirectory
|
||||||
|
|
||||||
if (Settings.shouldUseId3Tags) {
|
musicDirectory = if (Settings.shouldUseId3Tags) {
|
||||||
musicDirectory = Util.getSongsFromSearchResult(service.getStarred2())
|
Util.getSongsFromSearchResult(service.getStarred2())
|
||||||
} else {
|
} else {
|
||||||
musicDirectory = Util.getSongsFromSearchResult(service.getStarred())
|
Util.getSongsFromSearchResult(service.getStarred())
|
||||||
}
|
}
|
||||||
|
|
||||||
updateList(musicDirectory)
|
updateList(musicDirectory)
|
||||||
@ -171,7 +171,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
|||||||
@Synchronized
|
@Synchronized
|
||||||
fun calculateButtonState(
|
fun calculateButtonState(
|
||||||
selection: List<Track>,
|
selection: List<Track>,
|
||||||
onComplete: (TrackCollectionModel.Companion.ButtonStates) -> Unit
|
onComplete: (ButtonStates) -> Unit
|
||||||
) {
|
) {
|
||||||
val enabled = selection.isNotEmpty()
|
val enabled = selection.isNotEmpty()
|
||||||
var unpinEnabled = false
|
var unpinEnabled = false
|
||||||
@ -200,7 +200,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
|||||||
val pinEnabled = selection.size > pinnedCount
|
val pinEnabled = selection.size > pinnedCount
|
||||||
|
|
||||||
onComplete(
|
onComplete(
|
||||||
TrackCollectionModel.Companion.ButtonStates(
|
ButtonStates(
|
||||||
all = enabled,
|
all = enabled,
|
||||||
pin = pinEnabled,
|
pin = pinEnabled,
|
||||||
unpin = unpinEnabled,
|
unpin = unpinEnabled,
|
||||||
|
@ -1,32 +1,29 @@
|
|||||||
/*
|
/*
|
||||||
* MediaNotificationProvider.kt
|
* CustomNotificationProvider.kt
|
||||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
*
|
*
|
||||||
* Distributed under terms of the GNU GPLv3 license.
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.moire.ultrasonic.playback
|
package org.moire.ultrasonic.playback
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.media3.common.HeartRating
|
import androidx.media3.common.HeartRating
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.session.CommandButton
|
import androidx.media3.session.CommandButton
|
||||||
import androidx.media3.session.DefaultMediaNotificationProvider
|
|
||||||
import androidx.media3.session.MediaNotification
|
import androidx.media3.session.MediaNotification
|
||||||
import androidx.media3.session.MediaSession
|
import androidx.media3.session.MediaSession
|
||||||
import androidx.media3.session.SessionCommand
|
import androidx.media3.session.SessionCommand
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.imageloader.ArtworkBitmapLoader
|
import org.moire.ultrasonic.service.DefaultMediaNotificationProvider2
|
||||||
import org.moire.ultrasonic.service.MediaPlayerController
|
import org.moire.ultrasonic.service.MediaPlayerController
|
||||||
import org.moire.ultrasonic.util.toTrack
|
import org.moire.ultrasonic.util.toTrack
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
@UnstableApi
|
||||||
class MediaNotificationProvider(context: Context) :
|
class CustomNotificationProvider(ctx: Context?) : DefaultMediaNotificationProvider2(Builder(ctx)), KoinComponent {
|
||||||
DefaultMediaNotificationProvider(context, ArtworkBitmapLoader()), KoinComponent {
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* It is currently not possible to edit a MediaItem after creation so the isRated value
|
* It is currently not possible to edit a MediaItem after creation so the isRated value
|
||||||
@ -88,7 +85,7 @@ class MediaNotificationProvider(context: Context) :
|
|||||||
val commands = super.getMediaButtons(playerCommands, customLayout, playWhenReady)
|
val commands = super.getMediaButtons(playerCommands, customLayout, playWhenReady)
|
||||||
|
|
||||||
commands.forEachIndexed { index, command ->
|
commands.forEachIndexed { index, command ->
|
||||||
command.extras.putInt(COMMAND_KEY_COMPACT_VIEW_INDEX, index)
|
command.extras.putInt(androidx.media3.session.DefaultMediaNotificationProvider.COMMAND_KEY_COMPACT_VIEW_INDEX, index)
|
||||||
}
|
}
|
||||||
|
|
||||||
return commands
|
return commands
|
@ -34,6 +34,7 @@ import org.moire.ultrasonic.app.UApp
|
|||||||
import org.moire.ultrasonic.audiofx.EqualizerController
|
import org.moire.ultrasonic.audiofx.EqualizerController
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||||
import org.moire.ultrasonic.domain.Track
|
import org.moire.ultrasonic.domain.Track
|
||||||
|
import org.moire.ultrasonic.imageloader.ArtworkBitmapLoader
|
||||||
import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider
|
import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider
|
||||||
import org.moire.ultrasonic.service.DownloadService
|
import org.moire.ultrasonic.service.DownloadService
|
||||||
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
||||||
@ -112,7 +113,7 @@ class PlaybackService :
|
|||||||
private fun initializeSessionAndPlayer() {
|
private fun initializeSessionAndPlayer() {
|
||||||
if (isStarted) return
|
if (isStarted) return
|
||||||
|
|
||||||
setMediaNotificationProvider(MediaNotificationProvider(UApp.applicationContext()))
|
setMediaNotificationProvider(CustomNotificationProvider(UApp.applicationContext()))
|
||||||
|
|
||||||
// Create a new plain OkHttpClient
|
// Create a new plain OkHttpClient
|
||||||
val builder = OkHttpClient.Builder()
|
val builder = OkHttpClient.Builder()
|
||||||
@ -156,6 +157,7 @@ class PlaybackService :
|
|||||||
// This will need to use the AutoCalls
|
// This will need to use the AutoCalls
|
||||||
mediaLibrarySession = MediaLibrarySession.Builder(this, player, librarySessionCallback)
|
mediaLibrarySession = MediaLibrarySession.Builder(this, player, librarySessionCallback)
|
||||||
.setSessionActivity(getPendingIntentForContent())
|
.setSessionActivity(getPendingIntentForContent())
|
||||||
|
.setBitmapLoader(ArtworkBitmapLoader())
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
// Set a listener to update the API client when the active server has changed
|
// Set a listener to update the API client when the active server has changed
|
||||||
|
@ -39,6 +39,7 @@ import androidx.media3.common.Timeline
|
|||||||
import androidx.media3.common.TrackSelectionParameters
|
import androidx.media3.common.TrackSelectionParameters
|
||||||
import androidx.media3.common.VideoSize
|
import androidx.media3.common.VideoSize
|
||||||
import androidx.media3.common.text.CueGroup
|
import androidx.media3.common.text.CueGroup
|
||||||
|
import androidx.media3.common.util.Size
|
||||||
import androidx.media3.session.MediaSession
|
import androidx.media3.session.MediaSession
|
||||||
import com.google.common.collect.ImmutableList
|
import com.google.common.collect.ImmutableList
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
@ -56,7 +57,7 @@ import org.moire.ultrasonic.api.subsonic.SubsonicRESTException
|
|||||||
import org.moire.ultrasonic.app.UApp.Companion.applicationContext
|
import org.moire.ultrasonic.app.UApp.Companion.applicationContext
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
||||||
import org.moire.ultrasonic.domain.JukeboxStatus
|
import org.moire.ultrasonic.domain.JukeboxStatus
|
||||||
import org.moire.ultrasonic.playback.MediaNotificationProvider
|
import org.moire.ultrasonic.playback.CustomNotificationProvider
|
||||||
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
||||||
import org.moire.ultrasonic.util.Util.getPendingIntentToShowPlayer
|
import org.moire.ultrasonic.util.Util.getPendingIntentToShowPlayer
|
||||||
import org.moire.ultrasonic.util.Util.sleepQuietly
|
import org.moire.ultrasonic.util.Util.sleepQuietly
|
||||||
@ -92,7 +93,7 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
|||||||
private var listeners: MutableList<Player.Listener> = mutableListOf()
|
private var listeners: MutableList<Player.Listener> = mutableListOf()
|
||||||
private val playlist: MutableList<MediaItem> = mutableListOf()
|
private val playlist: MutableList<MediaItem> = mutableListOf()
|
||||||
private var currentIndex: Int = 0
|
private var currentIndex: Int = 0
|
||||||
private val notificationProvider = MediaNotificationProvider(applicationContext())
|
private val notificationProvider = CustomNotificationProvider(applicationContext())
|
||||||
private lateinit var mediaSession: MediaSession
|
private lateinit var mediaSession: MediaSession
|
||||||
private lateinit var notificationManagerCompat: NotificationManagerCompat
|
private lateinit var notificationManagerCompat: NotificationManagerCompat
|
||||||
|
|
||||||
@ -767,7 +768,7 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getCurrentCues(): CueGroup {
|
override fun getCurrentCues(): CueGroup {
|
||||||
return CueGroup.EMPTY
|
return CueGroup.EMPTY_TIME_ZERO
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAudioAttributes(): AudioAttributes {
|
override fun getAudioAttributes(): AudioAttributes {
|
||||||
@ -778,6 +779,10 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
|||||||
return VideoSize(0, 0)
|
return VideoSize(0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getSurfaceSize(): Size {
|
||||||
|
return Size(0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getContentBufferedPosition(): Long {
|
override fun getContentBufferedPosition(): Long {
|
||||||
return bufferedPosition
|
return bufferedPosition
|
||||||
}
|
}
|
||||||
|
@ -129,7 +129,7 @@ class PlaylistTimeline @JvmOverloads constructor(
|
|||||||
init {
|
init {
|
||||||
Assertions.checkState(mediaItems.size == shuffledIndices.size)
|
Assertions.checkState(mediaItems.size == shuffledIndices.size)
|
||||||
this.mediaItems = ImmutableList.copyOf(mediaItems)
|
this.mediaItems = ImmutableList.copyOf(mediaItems)
|
||||||
this.shuffledIndices = Arrays.copyOf(shuffledIndices, shuffledIndices.size)
|
this.shuffledIndices = shuffledIndices.copyOf(shuffledIndices.size)
|
||||||
indicesInShuffled = IntArray(shuffledIndices.size)
|
indicesInShuffled = IntArray(shuffledIndices.size)
|
||||||
for (i in shuffledIndices.indices) {
|
for (i in shuffledIndices.indices) {
|
||||||
indicesInShuffled[shuffledIndices[i]] = i
|
indicesInShuffled[shuffledIndices[i]] = i
|
||||||
|
@ -289,5 +289,5 @@ object Settings {
|
|||||||
private val appContext: Context
|
private val appContext: Context
|
||||||
get() = UApp.applicationContext()
|
get() = UApp.applicationContext()
|
||||||
|
|
||||||
val COLON_PATTERN = Pattern.compile(":")
|
val COLON_PATTERN: Pattern = Pattern.compile(":")
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
a:layout_width="match_parent"
|
a:layout_width="match_parent"
|
||||||
a:layout_height="match_parent"
|
a:layout_height="match_parent"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources>
|
||||||
|
|
||||||
<string name="background_task.loading">載入中…</string>
|
<string name="background_task.loading">載入中…</string>
|
||||||
<string name="button_bar.bookmarks">書籤</string>
|
<string name="button_bar.bookmarks">書籤</string>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources>
|
||||||
|
|
||||||
<style name="CleanSearchStyle" parent="Widget.AppCompat.SearchView">
|
<style name="CleanSearchStyle" parent="Widget.AppCompat.SearchView">
|
||||||
<item name="queryBackground">@null</item>
|
<item name="queryBackground">@null</item>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user