Add helper method to convert platform session token to Media3 token
This avoids that apps have to depend on the legacy compat support library when they want to make this conversion. Also add a version to both helper methods that takes a Looper to give apps the option to use an existing Looper, which should be much faster than spinning up a new thread for every method call. Issue: androidx/media#171 PiperOrigin-RevId: 490441913
This commit is contained in:
parent
1803d1cdb8
commit
03f0b53cf8
@ -14,6 +14,9 @@ Release notes
|
|||||||
([#10604](https://github.com/google/ExoPlayer/issues/10604)).
|
([#10604](https://github.com/google/ExoPlayer/issues/10604)).
|
||||||
* Add `ExoPlayer.Builder.setPlaybackLooper` that sets a pre-existing
|
* Add `ExoPlayer.Builder.setPlaybackLooper` that sets a pre-existing
|
||||||
playback thread for a new ExoPlayer instance.
|
playback thread for a new ExoPlayer instance.
|
||||||
|
* Session:
|
||||||
|
* Add helper method to convert platform session token to Media3
|
||||||
|
`SessionToken` ([#171](https://github.com/androidx/media/issues/171)).
|
||||||
* Remove deprecated symbols:
|
* Remove deprecated symbols:
|
||||||
* Remove `DefaultAudioSink` constructors, use `DefaultAudioSink.Builder`
|
* Remove `DefaultAudioSink` constructors, use `DefaultAudioSink.Builder`
|
||||||
instead.
|
instead.
|
||||||
|
@ -807,11 +807,10 @@ public class MediaSession {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the {@link MediaSessionCompat.Token} of the {@link MediaSessionCompat} created
|
* Returns the {@link MediaSessionCompat.Token} of the {@link MediaSessionCompat} created
|
||||||
* internally by this session. You may cast the {@link Object} to {@link
|
* internally by this session.
|
||||||
* MediaSessionCompat.Token}.
|
|
||||||
*/
|
*/
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
public Object getSessionCompatToken() {
|
public MediaSessionCompat.Token getSessionCompatToken() {
|
||||||
return impl.getSessionCompat().getSessionToken();
|
return impl.getSessionCompat().getSessionToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -501,8 +501,7 @@ public class MediaStyleNotificationHelper {
|
|||||||
if (actionsToShowInCompact != null) {
|
if (actionsToShowInCompact != null) {
|
||||||
setShowActionsInCompactView(style, actionsToShowInCompact);
|
setShowActionsInCompactView(style, actionsToShowInCompact);
|
||||||
}
|
}
|
||||||
MediaSessionCompat.Token legacyToken =
|
MediaSessionCompat.Token legacyToken = session.getSessionCompatToken();
|
||||||
(MediaSessionCompat.Token) session.getSessionCompatToken();
|
|
||||||
style.setMediaSession((android.media.session.MediaSession.Token) legacyToken.getToken());
|
style.setMediaSession((android.media.session.MediaSession.Token) legacyToken.getToken());
|
||||||
return style;
|
return style;
|
||||||
}
|
}
|
||||||
|
@ -28,12 +28,14 @@ import android.content.pm.ServiceInfo;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.HandlerThread;
|
import android.os.HandlerThread;
|
||||||
|
import android.os.Looper;
|
||||||
import android.os.ResultReceiver;
|
import android.os.ResultReceiver;
|
||||||
import android.support.v4.media.session.MediaControllerCompat;
|
import android.support.v4.media.session.MediaControllerCompat;
|
||||||
import android.support.v4.media.session.MediaSessionCompat;
|
import android.support.v4.media.session.MediaSessionCompat;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import androidx.annotation.IntDef;
|
import androidx.annotation.IntDef;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.RequiresApi;
|
||||||
import androidx.media.MediaBrowserServiceCompat;
|
import androidx.media.MediaBrowserServiceCompat;
|
||||||
import androidx.media3.common.Bundleable;
|
import androidx.media3.common.Bundleable;
|
||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
@ -258,37 +260,86 @@ public final class SessionToken implements Bundleable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a token from {@link MediaSessionCompat.Token}.
|
* Creates a token from a {@link android.media.session.MediaSession.Token}.
|
||||||
*
|
*
|
||||||
* @return a {@link ListenableFuture} of {@link SessionToken}
|
* @param context A {@link Context}.
|
||||||
|
* @param token The {@link android.media.session.MediaSession.Token}.
|
||||||
|
* @return A {@link ListenableFuture} for the {@link SessionToken}.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("UnnecessarilyFullyQualified") // Avoiding clash with Media3 MediaSession.
|
||||||
|
@UnstableApi
|
||||||
|
@RequiresApi(21)
|
||||||
|
public static ListenableFuture<SessionToken> createSessionToken(
|
||||||
|
Context context, android.media.session.MediaSession.Token token) {
|
||||||
|
return createSessionToken(context, MediaSessionCompat.Token.fromToken(token));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a token from a {@link android.media.session.MediaSession.Token}.
|
||||||
|
*
|
||||||
|
* @param context A {@link Context}.
|
||||||
|
* @param token The {@link android.media.session.MediaSession.Token}.
|
||||||
|
* @param completionLooper The {@link Looper} on which the returned {@link ListenableFuture}
|
||||||
|
* completes. This {@link Looper} can't be used to call {@code future.get()} on the returned
|
||||||
|
* {@link ListenableFuture}.
|
||||||
|
* @return A {@link ListenableFuture} for the {@link SessionToken}.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("UnnecessarilyFullyQualified") // Avoiding clash with Media3 MediaSession.
|
||||||
|
@UnstableApi
|
||||||
|
@RequiresApi(21)
|
||||||
|
public static ListenableFuture<SessionToken> createSessionToken(
|
||||||
|
Context context, android.media.session.MediaSession.Token token, Looper completionLooper) {
|
||||||
|
return createSessionToken(context, MediaSessionCompat.Token.fromToken(token), completionLooper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a token from a {@link MediaSessionCompat.Token}.
|
||||||
|
*
|
||||||
|
* @param context A {@link Context}.
|
||||||
|
* @param compatToken The {@link MediaSessionCompat.Token}.
|
||||||
|
* @return A {@link ListenableFuture} for the {@link SessionToken}.
|
||||||
*/
|
*/
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
public static ListenableFuture<SessionToken> createSessionToken(
|
public static ListenableFuture<SessionToken> createSessionToken(
|
||||||
Context context, Object compatToken) {
|
Context context, MediaSessionCompat.Token compatToken) {
|
||||||
checkNotNull(context, "context must not be null");
|
|
||||||
checkNotNull(compatToken, "compatToken must not be null");
|
|
||||||
checkArgument(compatToken instanceof MediaSessionCompat.Token);
|
|
||||||
|
|
||||||
HandlerThread thread = new HandlerThread("SessionTokenThread");
|
HandlerThread thread = new HandlerThread("SessionTokenThread");
|
||||||
thread.start();
|
thread.start();
|
||||||
|
ListenableFuture<SessionToken> tokenFuture =
|
||||||
|
createSessionToken(context, compatToken, thread.getLooper());
|
||||||
|
tokenFuture.addListener(thread::quit, MoreExecutors.directExecutor());
|
||||||
|
return tokenFuture;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a token from a {@link MediaSessionCompat.Token}.
|
||||||
|
*
|
||||||
|
* @param context A {@link Context}.
|
||||||
|
* @param compatToken The {@link MediaSessionCompat.Token}.
|
||||||
|
* @param completionLooper The {@link Looper} on which the returned {@link ListenableFuture}
|
||||||
|
* completes. This {@link Looper} can't be used to call {@code future.get()} on the returned
|
||||||
|
* {@link ListenableFuture}.
|
||||||
|
* @return A {@link ListenableFuture} for the {@link SessionToken}.
|
||||||
|
*/
|
||||||
|
@UnstableApi
|
||||||
|
public static ListenableFuture<SessionToken> createSessionToken(
|
||||||
|
Context context, MediaSessionCompat.Token compatToken, Looper completionLooper) {
|
||||||
|
checkNotNull(context, "context must not be null");
|
||||||
|
checkNotNull(compatToken, "compatToken must not be null");
|
||||||
|
|
||||||
SettableFuture<SessionToken> future = SettableFuture.create();
|
SettableFuture<SessionToken> future = SettableFuture.create();
|
||||||
// Try retrieving media3 token by connecting to the session.
|
// Try retrieving media3 token by connecting to the session.
|
||||||
MediaControllerCompat controller =
|
MediaControllerCompat controller = new MediaControllerCompat(context, compatToken);
|
||||||
createMediaControllerCompat(context, (MediaSessionCompat.Token) compatToken);
|
|
||||||
String packageName = controller.getPackageName();
|
String packageName = controller.getPackageName();
|
||||||
Handler handler = new Handler(thread.getLooper());
|
Handler handler = new Handler(completionLooper);
|
||||||
Runnable createFallbackLegacyToken =
|
Runnable createFallbackLegacyToken =
|
||||||
() -> {
|
() -> {
|
||||||
int uid = getUid(context.getPackageManager(), packageName);
|
int uid = getUid(context.getPackageManager(), packageName);
|
||||||
SessionToken resultToken =
|
SessionToken resultToken =
|
||||||
new SessionToken(
|
new SessionToken(compatToken, packageName, uid, controller.getSessionInfo());
|
||||||
(MediaSessionCompat.Token) compatToken,
|
|
||||||
packageName,
|
|
||||||
uid,
|
|
||||||
controller.getSessionInfo());
|
|
||||||
future.set(resultToken);
|
future.set(resultToken);
|
||||||
};
|
};
|
||||||
|
// Post creating a fallback token if the command receives no result after a timeout.
|
||||||
|
handler.postDelayed(createFallbackLegacyToken, WAIT_TIME_MS_FOR_SESSION3_TOKEN);
|
||||||
controller.sendCommand(
|
controller.sendCommand(
|
||||||
MediaConstants.SESSION_COMMAND_REQUEST_SESSION3_TOKEN,
|
MediaConstants.SESSION_COMMAND_REQUEST_SESSION3_TOKEN,
|
||||||
/* params= */ null,
|
/* params= */ null,
|
||||||
@ -306,17 +357,13 @@ public final class SessionToken implements Bundleable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Post creating a fallback token if the command receives no result after a timeout.
|
|
||||||
handler.postDelayed(createFallbackLegacyToken, WAIT_TIME_MS_FOR_SESSION3_TOKEN);
|
|
||||||
future.addListener(() -> thread.quit(), MoreExecutors.directExecutor());
|
|
||||||
return future;
|
return future;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a {@link ImmutableSet} of {@link SessionToken} for media session services; {@link
|
* Returns an {@link ImmutableSet} of {@linkplain SessionToken session tokens} for media session
|
||||||
* MediaSessionService}, {@link MediaLibraryService}, and {@link MediaBrowserServiceCompat}
|
* services; {@link MediaSessionService}, {@link MediaLibraryService}, and {@link
|
||||||
* regardless of their activeness. This set represents media apps that publish {@link
|
* MediaBrowserServiceCompat} regardless of their activeness.
|
||||||
* MediaSession}.
|
|
||||||
*
|
*
|
||||||
* <p>The app targeting API level 30 or higher must include a {@code <queries>} element in their
|
* <p>The app targeting API level 30 or higher must include a {@code <queries>} element in their
|
||||||
* manifest to get service tokens of other apps. See the following example and <a
|
* manifest to get service tokens of other apps. See the following example and <a
|
||||||
@ -334,6 +381,8 @@ public final class SessionToken implements Bundleable {
|
|||||||
* </intent>
|
* </intent>
|
||||||
* }</pre>
|
* }</pre>
|
||||||
*/
|
*/
|
||||||
|
// We ask the app to declare the <queries> tags, so it's expected that they are missing.
|
||||||
|
@SuppressWarnings("QueryPermissionsNeeded")
|
||||||
public static ImmutableSet<SessionToken> getAllServiceTokens(Context context) {
|
public static ImmutableSet<SessionToken> getAllServiceTokens(Context context) {
|
||||||
PackageManager pm = context.getPackageManager();
|
PackageManager pm = context.getPackageManager();
|
||||||
List<ResolveInfo> services = new ArrayList<>();
|
List<ResolveInfo> services = new ArrayList<>();
|
||||||
@ -370,6 +419,8 @@ public final class SessionToken implements Bundleable {
|
|||||||
return sessionServiceTokens.build();
|
return sessionServiceTokens.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We ask the app to declare the <queries> tags, so it's expected that they are missing.
|
||||||
|
@SuppressWarnings("QueryPermissionsNeeded")
|
||||||
private static boolean isInterfaceDeclared(
|
private static boolean isInterfaceDeclared(
|
||||||
PackageManager manager, String serviceInterface, ComponentName serviceComponent) {
|
PackageManager manager, String serviceInterface, ComponentName serviceComponent) {
|
||||||
Intent serviceIntent = new Intent(serviceInterface);
|
Intent serviceIntent = new Intent(serviceInterface);
|
||||||
@ -402,11 +453,6 @@ public final class SessionToken implements Bundleable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MediaControllerCompat createMediaControllerCompat(
|
|
||||||
Context context, MediaSessionCompat.Token sessionToken) {
|
|
||||||
return new MediaControllerCompat(context, sessionToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* package */ interface SessionTokenImpl extends Bundleable {
|
/* package */ interface SessionTokenImpl extends Bundleable {
|
||||||
|
|
||||||
boolean isLegacySession();
|
boolean isLegacySession();
|
||||||
|
@ -26,7 +26,6 @@ import android.media.session.MediaSession;
|
|||||||
import android.media.session.PlaybackState;
|
import android.media.session.PlaybackState;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.HandlerThread;
|
import android.os.HandlerThread;
|
||||||
import android.support.v4.media.session.MediaSessionCompat;
|
|
||||||
import androidx.media3.common.Player;
|
import androidx.media3.common.Player;
|
||||||
import androidx.media3.common.Player.State;
|
import androidx.media3.common.Player.State;
|
||||||
import androidx.media3.common.util.Util;
|
import androidx.media3.common.util.Util;
|
||||||
@ -94,8 +93,7 @@ public class MediaControllerWithFrameworkMediaSessionTest {
|
|||||||
@Test
|
@Test
|
||||||
public void createController() throws Exception {
|
public void createController() throws Exception {
|
||||||
SessionToken token =
|
SessionToken token =
|
||||||
SessionToken.createSessionToken(
|
SessionToken.createSessionToken(context, fwkSession.getSessionToken())
|
||||||
context, MediaSessionCompat.Token.fromToken(fwkSession.getSessionToken()))
|
|
||||||
.get(TIMEOUT_MS, MILLISECONDS);
|
.get(TIMEOUT_MS, MILLISECONDS);
|
||||||
MediaController controller =
|
MediaController controller =
|
||||||
new MediaController.Builder(context, token)
|
new MediaController.Builder(context, token)
|
||||||
@ -111,8 +109,7 @@ public class MediaControllerWithFrameworkMediaSessionTest {
|
|||||||
AtomicInteger playbackStateRef = new AtomicInteger();
|
AtomicInteger playbackStateRef = new AtomicInteger();
|
||||||
AtomicBoolean playWhenReadyRef = new AtomicBoolean();
|
AtomicBoolean playWhenReadyRef = new AtomicBoolean();
|
||||||
SessionToken token =
|
SessionToken token =
|
||||||
SessionToken.createSessionToken(
|
SessionToken.createSessionToken(context, fwkSession.getSessionToken())
|
||||||
context, MediaSessionCompat.Token.fromToken(fwkSession.getSessionToken()))
|
|
||||||
.get(TIMEOUT_MS, MILLISECONDS);
|
.get(TIMEOUT_MS, MILLISECONDS);
|
||||||
MediaController controller =
|
MediaController controller =
|
||||||
new MediaController.Builder(context, token)
|
new MediaController.Builder(context, token)
|
||||||
|
@ -20,6 +20,7 @@ import static androidx.media3.test.session.common.CommonConstants.MOCK_MEDIA3_SE
|
|||||||
import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME;
|
import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME;
|
||||||
import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS;
|
import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS;
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static org.junit.Assume.assumeTrue;
|
||||||
|
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
@ -27,6 +28,7 @@ import android.os.Bundle;
|
|||||||
import android.os.Process;
|
import android.os.Process;
|
||||||
import android.support.v4.media.session.MediaSessionCompat;
|
import android.support.v4.media.session.MediaSessionCompat;
|
||||||
import androidx.media3.common.MediaLibraryInfo;
|
import androidx.media3.common.MediaLibraryInfo;
|
||||||
|
import androidx.media3.common.util.Util;
|
||||||
import androidx.media3.test.session.common.HandlerThreadTestRule;
|
import androidx.media3.test.session.common.HandlerThreadTestRule;
|
||||||
import androidx.media3.test.session.common.MainLooperTestRule;
|
import androidx.media3.test.session.common.MainLooperTestRule;
|
||||||
import androidx.media3.test.session.common.TestUtils;
|
import androidx.media3.test.session.common.TestUtils;
|
||||||
@ -68,6 +70,7 @@ public class SessionTokenTest {
|
|||||||
context,
|
context,
|
||||||
new ComponentName(
|
new ComponentName(
|
||||||
context.getPackageName(), MockMediaSessionService.class.getCanonicalName()));
|
context.getPackageName(), MockMediaSessionService.class.getCanonicalName()));
|
||||||
|
|
||||||
assertThat(token.getPackageName()).isEqualTo(context.getPackageName());
|
assertThat(token.getPackageName()).isEqualTo(context.getPackageName());
|
||||||
assertThat(token.getUid()).isEqualTo(Process.myUid());
|
assertThat(token.getUid()).isEqualTo(Process.myUid());
|
||||||
assertThat(token.getType()).isEqualTo(SessionToken.TYPE_SESSION_SERVICE);
|
assertThat(token.getType()).isEqualTo(SessionToken.TYPE_SESSION_SERVICE);
|
||||||
@ -80,6 +83,7 @@ public class SessionTokenTest {
|
|||||||
ComponentName testComponentName =
|
ComponentName testComponentName =
|
||||||
new ComponentName(
|
new ComponentName(
|
||||||
context.getPackageName(), MockMediaLibraryService.class.getCanonicalName());
|
context.getPackageName(), MockMediaLibraryService.class.getCanonicalName());
|
||||||
|
|
||||||
SessionToken token = new SessionToken(context, testComponentName);
|
SessionToken token = new SessionToken(context, testComponentName);
|
||||||
|
|
||||||
assertThat(token.getPackageName()).isEqualTo(context.getPackageName());
|
assertThat(token.getPackageName()).isEqualTo(context.getPackageName());
|
||||||
@ -110,15 +114,36 @@ public class SessionTokenTest {
|
|||||||
assertThat(token.getServiceName()).isEmpty();
|
assertThat(token.getServiceName()).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void createSessionToken_withPlatformTokenFromMedia1Session_returnsTokenForLegacySession()
|
||||||
|
throws Exception {
|
||||||
|
assumeTrue(Util.SDK_INT >= 21);
|
||||||
|
|
||||||
|
MediaSessionCompat sessionCompat =
|
||||||
|
sessionTestRule.ensureReleaseAfterTest(
|
||||||
|
new MediaSessionCompat(context, "createSessionToken_withLegacyToken"));
|
||||||
|
|
||||||
|
SessionToken token =
|
||||||
|
SessionToken.createSessionToken(
|
||||||
|
context,
|
||||||
|
(android.media.session.MediaSession.Token)
|
||||||
|
sessionCompat.getSessionToken().getToken())
|
||||||
|
.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||||
|
|
||||||
|
assertThat(token.isLegacySession()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void createSessionToken_withCompatTokenFromMedia1Session_returnsTokenForLegacySession()
|
public void createSessionToken_withCompatTokenFromMedia1Session_returnsTokenForLegacySession()
|
||||||
throws Exception {
|
throws Exception {
|
||||||
MediaSessionCompat sessionCompat =
|
MediaSessionCompat sessionCompat =
|
||||||
sessionTestRule.ensureReleaseAfterTest(
|
sessionTestRule.ensureReleaseAfterTest(
|
||||||
new MediaSessionCompat(context, "createSessionToken_withLegacyToken"));
|
new MediaSessionCompat(context, "createSessionToken_withLegacyToken"));
|
||||||
|
|
||||||
SessionToken token =
|
SessionToken token =
|
||||||
SessionToken.createSessionToken(context, sessionCompat.getSessionToken())
|
SessionToken.createSessionToken(context, sessionCompat.getSessionToken())
|
||||||
.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||||
|
|
||||||
assertThat(token.isLegacySession()).isTrue();
|
assertThat(token.isLegacySession()).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,6 +175,7 @@ public class SessionTokenTest {
|
|||||||
ComponentName mockBrowserServiceCompatName =
|
ComponentName mockBrowserServiceCompatName =
|
||||||
new ComponentName(
|
new ComponentName(
|
||||||
SUPPORT_APP_PACKAGE_NAME, MockMediaBrowserServiceCompat.class.getCanonicalName());
|
SUPPORT_APP_PACKAGE_NAME, MockMediaBrowserServiceCompat.class.getCanonicalName());
|
||||||
|
|
||||||
Set<SessionToken> serviceTokens =
|
Set<SessionToken> serviceTokens =
|
||||||
SessionToken.getAllServiceTokens(ApplicationProvider.getApplicationContext());
|
SessionToken.getAllServiceTokens(ApplicationProvider.getApplicationContext());
|
||||||
for (SessionToken token : serviceTokens) {
|
for (SessionToken token : serviceTokens) {
|
||||||
@ -162,6 +188,7 @@ public class SessionTokenTest {
|
|||||||
hasMockLibraryService2 = true;
|
hasMockLibraryService2 = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assertThat(hasMockBrowserServiceCompat).isTrue();
|
assertThat(hasMockBrowserServiceCompat).isTrue();
|
||||||
assertThat(hasMockSessionService2).isTrue();
|
assertThat(hasMockSessionService2).isTrue();
|
||||||
assertThat(hasMockLibraryService2).isTrue();
|
assertThat(hasMockLibraryService2).isTrue();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user