diff --git a/RELEASENOTES.md b/RELEASENOTES.md
index 3ae194c223..87adaa8840 100644
--- a/RELEASENOTES.md
+++ b/RELEASENOTES.md
@@ -33,6 +33,8 @@
([#4385](https://github.com/google/ExoPlayer/issues/4385)).
* Expose all internal ID3 data stored in MP4 udta boxes, and switch from using
CommentFrame to InternalFrame for frames with gapless metadata in MP4.
+* Allow setting the `Looper`, which is used to access the player, in
+ `ExoPlayerFactory` ([#4278](https://github.com/google/ExoPlayer/issues/4278)).
### 2.8.2 ###
diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java
index 4bafaa4326..e8758cd05b 100644
--- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java
+++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java
@@ -19,7 +19,6 @@ import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
-import android.os.Looper;
import android.os.ResultReceiver;
import android.os.SystemClock;
import android.support.annotation.NonNull;
@@ -39,6 +38,7 @@ import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.util.ErrorMessageProvider;
import com.google.android.exoplayer2.util.RepeatModeUtil;
+import com.google.android.exoplayer2.util.Util;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -323,7 +323,6 @@ public final class MediaSessionConnector {
public final MediaSessionCompat mediaSession;
private final MediaControllerCompat mediaController;
- private final Handler handler;
private final boolean doMaintainMetadata;
private final ExoPlayerEventListener exoPlayerEventListener;
private final MediaSessionCallback mediaSessionCallback;
@@ -341,10 +340,9 @@ public final class MediaSessionConnector {
private RatingCallback ratingCallback;
/**
- * Creates an instance. Must be called on the same thread that is used to construct the player
- * instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}.
- *
- * Equivalent to {@code MediaSessionConnector(mediaSession, new DefaultPlaybackController())}.
+ * Creates an instance.
+ *
+ *
Equivalent to {@code MediaSessionConnector(mediaSession, new DefaultPlaybackController())}.
*
* @param mediaSession The {@link MediaSessionCompat} to connect to.
*/
@@ -353,8 +351,7 @@ public final class MediaSessionConnector {
}
/**
- * Creates an instance. Must be called on the same thread that is used to construct the player
- * instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}.
+ * Creates an instance.
*
*
Equivalent to {@code MediaSessionConnector(mediaSession, playbackController, true, null)}.
*
@@ -367,8 +364,7 @@ public final class MediaSessionConnector {
}
/**
- * Creates an instance. Must be called on the same thread that is used to construct the player
- * instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}.
+ * Creates an instance.
*
* @param mediaSession The {@link MediaSessionCompat} to connect to.
* @param playbackController A {@link PlaybackController} for handling playback actions, or {@code
@@ -388,8 +384,6 @@ public final class MediaSessionConnector {
this.playbackController = playbackController != null ? playbackController
: new DefaultPlaybackController();
this.metadataExtrasPrefix = metadataExtrasPrefix != null ? metadataExtrasPrefix : "";
- this.handler = new Handler(Looper.myLooper() != null ? Looper.myLooper()
- : Looper.getMainLooper());
this.doMaintainMetadata = doMaintainMetadata;
mediaSession.setFlags(BASE_MEDIA_SESSION_FLAGS);
mediaController = mediaSession.getController();
@@ -401,7 +395,8 @@ public final class MediaSessionConnector {
}
/**
- * Sets the player to be connected to the media session.
+ * Sets the player to be connected to the media session. Must be called on the same thread that is
+ * used to access the player.
*
*
The order in which any {@link CustomActionProvider}s are passed determines the order of the
* actions published with the playback state of the session.
@@ -428,6 +423,7 @@ public final class MediaSessionConnector {
this.customActionProviders = (player != null && customActionProviders != null)
? customActionProviders : new CustomActionProvider[0];
if (player != null) {
+ Handler handler = new Handler(Util.getLooper());
mediaSession.setCallback(mediaSessionCallback, handler);
player.addListener(exoPlayerEventListener);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java
index b97790d5fb..ce43772d8d 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java
@@ -89,12 +89,13 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
* model">
*
*
- * - ExoPlayer instances must be accessed from a single application thread. This must be the
- * thread the player is created on if that thread has a {@link Looper}, or the application's
- * main thread otherwise.
- *
- Registered listeners are called on the thread the player is created on if that thread has a
- * {@link Looper}, or the application's main thread otherwise. Note that this means registered
- * listeners are called on the same thread which must be used to access the player.
+ *
- ExoPlayer instances must be accessed from the thread associated with {@link
+ * #getApplicationLooper()}. This Looper can be specified when creating the player, or this is
+ * the Looper of the thread the player is created on, or the Looper of the application's main
+ * thread if the player is created on a thread without Looper.
+ *
- Registered listeners are called on the thread thread associated with {@link
+ * #getApplicationLooper()}. Note that this means registered listeners are called on the same
+ * thread which must be used to access the player.
*
- An internal playback thread is responsible for playback. Injected player components such as
* Renderers, MediaSources, TrackSelectors and LoadControls are called by the player on this
* thread.
@@ -178,13 +179,15 @@ public interface ExoPlayer extends Player {
@Deprecated
@RepeatMode int REPEAT_MODE_ALL = Player.REPEAT_MODE_ALL;
- /**
- * Gets the {@link Looper} associated with the playback thread.
- *
- * @return The {@link Looper} associated with the playback thread.
- */
+ /** Returns the {@link Looper} associated with the playback thread. */
Looper getPlaybackLooper();
+ /**
+ * Returns the {@link Looper} associated with the application thread that's used to access the
+ * player and on which player events are received.
+ */
+ Looper getApplicationLooper();
+
/**
* Prepares the player to play the provided {@link MediaSource}. Equivalent to
* {@code prepare(mediaSource, true, true)}.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java
index 8095ed9c64..e8bd8b34f6 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java
@@ -16,12 +16,14 @@
package com.google.android.exoplayer2;
import android.content.Context;
+import android.os.Looper;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.analytics.AnalyticsCollector;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.util.Clock;
+import com.google.android.exoplayer2.util.Util;
/**
* A factory for {@link ExoPlayer} instances.
@@ -156,7 +158,11 @@ public final class ExoPlayerFactory {
public static SimpleExoPlayer newSimpleInstance(RenderersFactory renderersFactory,
TrackSelector trackSelector, LoadControl loadControl) {
return new SimpleExoPlayer(
- renderersFactory, trackSelector, loadControl, /* drmSessionManager= */ null);
+ renderersFactory,
+ trackSelector,
+ loadControl,
+ /* drmSessionManager= */ null,
+ Util.getLooper());
}
/**
@@ -173,7 +179,8 @@ public final class ExoPlayerFactory {
TrackSelector trackSelector,
LoadControl loadControl,
@Nullable DrmSessionManager drmSessionManager) {
- return new SimpleExoPlayer(renderersFactory, trackSelector, loadControl, drmSessionManager);
+ return new SimpleExoPlayer(
+ renderersFactory, trackSelector, loadControl, drmSessionManager, Util.getLooper());
}
/**
@@ -194,7 +201,62 @@ public final class ExoPlayerFactory {
@Nullable DrmSessionManager drmSessionManager,
AnalyticsCollector.Factory analyticsCollectorFactory) {
return new SimpleExoPlayer(
- renderersFactory, trackSelector, loadControl, drmSessionManager, analyticsCollectorFactory);
+ renderersFactory,
+ trackSelector,
+ loadControl,
+ drmSessionManager,
+ analyticsCollectorFactory,
+ Util.getLooper());
+ }
+
+ /**
+ * Creates a {@link SimpleExoPlayer} instance.
+ *
+ * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
+ * @param trackSelector The {@link TrackSelector} that will be used by the instance.
+ * @param loadControl The {@link LoadControl} that will be used by the instance.
+ * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
+ * will not be used for DRM protected playbacks.
+ * @param looper The {@link Looper} which must be used for all calls to the player and which is
+ * used to call listeners on.
+ */
+ public static SimpleExoPlayer newSimpleInstance(
+ RenderersFactory renderersFactory,
+ TrackSelector trackSelector,
+ LoadControl loadControl,
+ @Nullable DrmSessionManager drmSessionManager,
+ Looper looper) {
+ return new SimpleExoPlayer(
+ renderersFactory, trackSelector, loadControl, drmSessionManager, looper);
+ }
+
+ /**
+ * Creates a {@link SimpleExoPlayer} instance.
+ *
+ * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
+ * @param trackSelector The {@link TrackSelector} that will be used by the instance.
+ * @param loadControl The {@link LoadControl} that will be used by the instance.
+ * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
+ * will not be used for DRM protected playbacks.
+ * @param analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that
+ * will collect and forward all player events.
+ * @param looper The {@link Looper} which must be used for all calls to the player and which is
+ * used to call listeners on.
+ */
+ public static SimpleExoPlayer newSimpleInstance(
+ RenderersFactory renderersFactory,
+ TrackSelector trackSelector,
+ LoadControl loadControl,
+ @Nullable DrmSessionManager drmSessionManager,
+ AnalyticsCollector.Factory analyticsCollectorFactory,
+ Looper looper) {
+ return new SimpleExoPlayer(
+ renderersFactory,
+ trackSelector,
+ loadControl,
+ drmSessionManager,
+ analyticsCollectorFactory,
+ looper);
}
/**
@@ -216,7 +278,20 @@ public final class ExoPlayerFactory {
*/
public static ExoPlayer newInstance(Renderer[] renderers, TrackSelector trackSelector,
LoadControl loadControl) {
- return new ExoPlayerImpl(renderers, trackSelector, loadControl, Clock.DEFAULT);
+ return newInstance(renderers, trackSelector, loadControl, Util.getLooper());
}
+ /**
+ * Creates an {@link ExoPlayer} instance.
+ *
+ * @param renderers The {@link Renderer}s that will be used by the instance.
+ * @param trackSelector The {@link TrackSelector} that will be used by the instance.
+ * @param loadControl The {@link LoadControl} that will be used by the instance.
+ * @param looper The {@link Looper} which must be used for all calls to the player and which is
+ * used to call listeners on.
+ */
+ public static ExoPlayer newInstance(
+ Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl, Looper looper) {
+ return new ExoPlayerImpl(renderers, trackSelector, loadControl, Clock.DEFAULT, looper);
+ }
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
index 390f8b59a5..e803fb30ee 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
@@ -81,10 +81,16 @@ import java.util.concurrent.CopyOnWriteArraySet;
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
* @param loadControl The {@link LoadControl} that will be used by the instance.
* @param clock The {@link Clock} that will be used by the instance.
+ * @param looper The {@link Looper} which must be used for all calls to the player and which is
+ * used to call listeners on.
*/
@SuppressLint("HandlerLeak")
public ExoPlayerImpl(
- Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl, Clock clock) {
+ Renderer[] renderers,
+ TrackSelector trackSelector,
+ LoadControl loadControl,
+ Clock clock,
+ Looper looper) {
Log.i(TAG, "Init " + Integer.toHexString(System.identityHashCode(this)) + " ["
+ ExoPlayerLibraryInfo.VERSION_SLASHY + "] [" + Util.DEVICE_DEBUG_INFO + "]");
Assertions.checkState(renderers.length > 0);
@@ -102,13 +108,13 @@ import java.util.concurrent.CopyOnWriteArraySet;
window = new Timeline.Window();
period = new Timeline.Period();
playbackParameters = PlaybackParameters.DEFAULT;
- Looper eventLooper = Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper();
- eventHandler = new Handler(eventLooper) {
- @Override
- public void handleMessage(Message msg) {
- ExoPlayerImpl.this.handleEvent(msg);
- }
- };
+ eventHandler =
+ new Handler(looper) {
+ @Override
+ public void handleMessage(Message msg) {
+ ExoPlayerImpl.this.handleEvent(msg);
+ }
+ };
playbackInfo =
new PlaybackInfo(
Timeline.EMPTY,
@@ -146,6 +152,11 @@ import java.util.concurrent.CopyOnWriteArraySet;
return internalPlayer.getPlaybackLooper();
}
+ @Override
+ public Looper getApplicationLooper() {
+ return eventHandler.getLooper();
+ }
+
@Override
public void addListener(Player.EventListener listener) {
listeners.add(listener);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java
index 7fa13338ad..0fa7279079 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java
@@ -98,23 +98,40 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
private List currentCues;
/**
- * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
- * @param trackSelector The {@link TrackSelector} that will be used by the instance.
- * @param loadControl The {@link LoadControl} that will be used by the instance.
- * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
- * will not be used for DRM protected playbacks.
+ * @deprecated Use {@link #SimpleExoPlayer(RenderersFactory, TrackSelector, LoadControl,
+ * DrmSessionManager, Looper)}.
*/
+ @Deprecated
protected SimpleExoPlayer(
RenderersFactory renderersFactory,
TrackSelector trackSelector,
LoadControl loadControl,
@Nullable DrmSessionManager drmSessionManager) {
+ this(renderersFactory, trackSelector, loadControl, drmSessionManager, Util.getLooper());
+ }
+
+ /**
+ * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
+ * @param trackSelector The {@link TrackSelector} that will be used by the instance.
+ * @param loadControl The {@link LoadControl} that will be used by the instance.
+ * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
+ * will not be used for DRM protected playbacks.
+ * @param looper The {@link Looper} which must be used for all calls to the player and which is
+ * used to call listeners on.
+ */
+ protected SimpleExoPlayer(
+ RenderersFactory renderersFactory,
+ TrackSelector trackSelector,
+ LoadControl loadControl,
+ @Nullable DrmSessionManager drmSessionManager,
+ Looper looper) {
this(
renderersFactory,
trackSelector,
loadControl,
drmSessionManager,
- new AnalyticsCollector.Factory());
+ new AnalyticsCollector.Factory(),
+ looper);
}
/**
@@ -125,20 +142,24 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
* will not be used for DRM protected playbacks.
* @param analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that
* will collect and forward all player events.
+ * @param looper The {@link Looper} which must be used for all calls to the player and which is
+ * used to call listeners on.
*/
protected SimpleExoPlayer(
RenderersFactory renderersFactory,
TrackSelector trackSelector,
LoadControl loadControl,
@Nullable DrmSessionManager drmSessionManager,
- AnalyticsCollector.Factory analyticsCollectorFactory) {
+ AnalyticsCollector.Factory analyticsCollectorFactory,
+ Looper looper) {
this(
renderersFactory,
trackSelector,
loadControl,
drmSessionManager,
analyticsCollectorFactory,
- Clock.DEFAULT);
+ Clock.DEFAULT,
+ looper);
}
/**
@@ -151,6 +172,8 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
* will collect and forward all player events.
* @param clock The {@link Clock} that will be used by the instance. Should always be {@link
* Clock#DEFAULT}, unless the player is being used from a test.
+ * @param looper The {@link Looper} which must be used for all calls to the player and which is
+ * used to call listeners on.
*/
protected SimpleExoPlayer(
RenderersFactory renderersFactory,
@@ -158,15 +181,15 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
LoadControl loadControl,
@Nullable DrmSessionManager drmSessionManager,
AnalyticsCollector.Factory analyticsCollectorFactory,
- Clock clock) {
+ Clock clock,
+ Looper looper) {
componentListener = new ComponentListener();
videoListeners = new CopyOnWriteArraySet<>();
textOutputs = new CopyOnWriteArraySet<>();
metadataOutputs = new CopyOnWriteArraySet<>();
videoDebugListeners = new CopyOnWriteArraySet<>();
audioDebugListeners = new CopyOnWriteArraySet<>();
- Looper eventLooper = Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper();
- eventHandler = new Handler(eventLooper);
+ eventHandler = new Handler(looper);
renderers =
renderersFactory.createRenderers(
eventHandler,
@@ -184,7 +207,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
currentCues = Collections.emptyList();
// Build the player and associated objects.
- player = createExoPlayerImpl(renderers, trackSelector, loadControl, clock);
+ player = createExoPlayerImpl(renderers, trackSelector, loadControl, clock, looper);
analyticsCollector = analyticsCollectorFactory.createAnalyticsCollector(player, clock);
addListener(analyticsCollector);
videoDebugListeners.add(analyticsCollector);
@@ -671,6 +694,11 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
return player.getPlaybackLooper();
}
+ @Override
+ public Looper getApplicationLooper() {
+ return player.getApplicationLooper();
+ }
+
@Override
public void addListener(Player.EventListener listener) {
player.addListener(listener);
@@ -954,11 +982,17 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
* @param loadControl The {@link LoadControl} that will be used by the instance.
* @param clock The {@link Clock} that will be used by this instance.
+ * @param looper The {@link Looper} which must be used for all calls to the player and which is
+ * used to call listeners on.
* @return A new {@link ExoPlayer} instance.
*/
protected ExoPlayer createExoPlayerImpl(
- Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl, Clock clock) {
- return new ExoPlayerImpl(renderers, trackSelector, loadControl, clock);
+ Renderer[] renderers,
+ TrackSelector trackSelector,
+ LoadControl loadControl,
+ Clock clock,
+ Looper looper) {
+ return new ExoPlayerImpl(renderers, trackSelector, loadControl, clock, looper);
}
private void removeSurfaceCallbacks() {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java
index 52e44c3a4b..680eea6a3e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java
@@ -16,7 +16,6 @@
package com.google.android.exoplayer2.source;
import android.os.Handler;
-import android.os.Looper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
@@ -61,13 +60,14 @@ public class ConcatenatingMediaSource extends CompositeMediaSource mediaSourceHolders;
private final MediaSourceHolder query;
private final Map mediaSourceByMediaPeriod;
- private final List pendingOnCompletionActions;
+ private final List pendingOnCompletionActions;
private final boolean isAtomic;
private final boolean useLazyPreparation;
private final Timeline.Window window;
private final Timeline.Period period;
private @Nullable ExoPlayer player;
+ private @Nullable Handler playerApplicationHandler;
private boolean listenerNotificationScheduled;
private ShuffleOrder shuffleOrder;
private int windowCount;
@@ -351,11 +351,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource addMessage = (MessageData) message;
@@ -493,15 +495,16 @@ public class ConcatenatingMediaSource extends CompositeMediaSource actionsOnCompletion = ((List) message);
+ List actionsOnCompletion = ((List) message);
+ Handler handler = Assertions.checkNotNull(playerApplicationHandler);
for (int i = 0; i < actionsOnCompletion.size(); i++) {
- actionsOnCompletion.get(i).dispatchEvent();
+ handler.post(actionsOnCompletion.get(i));
}
break;
default:
@@ -509,7 +512,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource actionsOnCompletion =
+ List actionsOnCompletion =
pendingOnCompletionActions.isEmpty()
- ? Collections.emptyList()
+ ? Collections.emptyList()
: new ArrayList<>(pendingOnCompletionActions);
pendingOnCompletionActions.clear();
refreshSourceInfo(
@@ -698,34 +701,16 @@ public class ConcatenatingMediaSource extends CompositeMediaSource {
public final int index;
public final T customData;
- public final @Nullable EventDispatcher actionOnCompletion;
+ public final @Nullable Runnable actionOnCompletion;
public MessageData(int index, T customData, @Nullable Runnable actionOnCompletion) {
this.index = index;
- this.actionOnCompletion =
- actionOnCompletion != null ? new EventDispatcher(actionOnCompletion) : null;
+ this.actionOnCompletion = actionOnCompletion;
this.customData = customData;
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java
index 0e12964dfb..7a38350563 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java
@@ -252,11 +252,14 @@ public final class Util {
* assumption that the Handler won't be used to send messages until the callback is fully
* initialized.
*
+ *
If the current thread doesn't have a {@link Looper}, the application's main thread {@link
+ * Looper} is used.
+ *
* @param callback A {@link Handler.Callback}. May be a partially initialized class.
* @return A {@link Handler} with the specified callback on the current {@link Looper} thread.
*/
public static Handler createHandler(Handler.@UnknownInitialization Callback callback) {
- return createHandler(Looper.myLooper(), callback);
+ return createHandler(getLooper(), callback);
}
/**
@@ -275,6 +278,15 @@ public final class Util {
return new Handler(looper, callback);
}
+ /**
+ * Returns the {@link Looper} associated with the current thread, or the {@link Looper} of the
+ * application's main thread if the current thread doesn't have a {@link Looper}.
+ */
+ public static Looper getLooper() {
+ Looper myLooper = Looper.myLooper();
+ return myLooper != null ? myLooper : Looper.getMainLooper();
+ }
+
/**
* Instantiates a new single threaded executor whose thread has the specified name.
*
diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java
index cf7470b80a..b47ab16db6 100644
--- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java
+++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java
@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.testutil;
import static com.google.common.truth.Truth.assertThat;
import android.os.HandlerThread;
+import android.os.Looper;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.DefaultLoadControl;
import com.google.android.exoplayer2.ExoPlaybackException;
@@ -663,7 +664,8 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener
loadControl,
/* drmSessionManager= */ null,
new AnalyticsCollector.Factory(),
- clock);
+ clock,
+ Looper.myLooper());
}
}
}
diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java
index ee3a3a2d32..5062b5c005 100644
--- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java
+++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java
@@ -442,6 +442,11 @@ public class MediaSourceTestRunner {
this.handler = new Handler(looper, this);
}
+ @Override
+ public Looper getApplicationLooper() {
+ return handler.getLooper();
+ }
+
@Override
public PlayerMessage createMessage(PlayerMessage.Target target) {
return new PlayerMessage(
diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java
index b8ad949f4a..214c2b6a5e 100644
--- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java
+++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java
@@ -49,6 +49,11 @@ public abstract class StubExoPlayer implements ExoPlayer {
throw new UnsupportedOperationException();
}
+ @Override
+ public Looper getApplicationLooper() {
+ throw new UnsupportedOperationException();
+ }
+
@Override
public void addListener(Player.EventListener listener) {
throw new UnsupportedOperationException();