aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDocile-Alligator <25734209+Docile-Alligator@users.noreply.github.com>2022-09-04 14:57:11 +0000
committerDocile-Alligator <25734209+Docile-Alligator@users.noreply.github.com>2022-09-04 14:57:11 +0000
commit6d224c307d105c9b1d2a46f0f81a11cabc4d32b1 (patch)
treefba26ef32bef23778c8ee850e84fa571ad7b01dd
parent064b2ceedca7f082da94392b8e0c1f1411d1abef (diff)
downloadinfinity-for-reddit-6d224c307d105c9b1d2a46f0f81a11cabc4d32b1.tar
infinity-for-reddit-6d224c307d105c9b1d2a46f0f81a11cabc4d32b1.tar.gz
infinity-for-reddit-6d224c307d105c9b1d2a46f0f81a11cabc4d32b1.tar.bz2
infinity-for-reddit-6d224c307d105c9b1d2a46f0f81a11cabc4d32b1.tar.lz
infinity-for-reddit-6d224c307d105c9b1d2a46f0f81a11cabc4d32b1.tar.xz
infinity-for-reddit-6d224c307d105c9b1d2a46f0f81a11cabc4d32b1.tar.zst
infinity-for-reddit-6d224c307d105c9b1d2a46f0f81a11cabc4d32b1.zip
Fix stupid Redgifs API issue.
Diffstat (limited to '')
-rw-r--r--app/build.gradle4
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/AppModule.java25
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/FetchGfycatOrRedgifsVideoLinks.java40
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/RedgifsAccessTokenAuthenticator.java90
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/activities/ViewVideoActivity.java82
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/adapters/HistoryPostRecyclerViewAdapter.java34
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/adapters/PostDetailRecyclerViewAdapter.java25
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/adapters/PostRecyclerViewAdapter.java32
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/apis/RedgifsAPI.java21
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/customviews/CustomToroContainer.java2
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/customviews/LoopAvailableExoCreator.java6
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/fragments/HistoryPostFragment.java15
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/fragments/PostFragment.java28
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/fragments/ViewPostDetailFragment.java16
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/utils/APIUtils.java37
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/utils/SharedPreferencesUtils.java1
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/AdsExoPlayerViewHelper.java188
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/AdsPlayable.java119
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/BaseMeter.java90
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/CacheManager.java70
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/Config.java195
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/DefaultExoCreator.java193
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ExoCreator.java79
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ExoPlayable.java205
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ExoPlayerDispatcher.java61
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ExoPlayerViewHelper.java171
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/MediaSourceBuilder.java102
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/Playable.java365
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/PlayableImpl.java287
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/PlayerDispatcher.java51
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/PlayerSelector.java151
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ToroExo.java297
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ToroExoPlayer.java93
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ToroPlayer.java170
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ToroUtil.java115
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/annotations/Beta.java30
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/annotations/RemoveIn.java32
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/annotations/Sorted.java36
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/helper/ToroPlayerHelper.java241
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/media/DrmMedia.java38
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/media/PlaybackInfo.java144
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/media/VolumeInfo.java121
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ui/ToroControlView.java278
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/widget/Common.java92
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/widget/Container.java1137
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/widget/PlaybackInfoCache.java402
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/widget/PlayerManager.java146
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/widget/PressablePlayerSelector.java158
-rw-r--r--app/src/main/res/drawable/ic_volume_off_black_24dp.xml25
-rw-r--r--app/src/main/res/drawable/ic_volume_up_black_24dp.xml25
-rw-r--r--app/src/main/res/layout/toro_exo_control_view.xml181
-rw-r--r--app/src/main/res/layout/toro_exo_player_view.xml89
-rw-r--r--app/src/main/res/values/strings.xml19
-rw-r--r--app/src/main/res/values/styles.xml26
54 files changed, 6559 insertions, 121 deletions
diff --git a/app/build.gradle b/app/build.gradle
index 9132c52c..31ad2fc0 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -77,11 +77,11 @@ dependencies {
implementation "com.google.android.exoplayer:exoplayer-dash:$exoplayerVersion"
implementation "com.google.android.exoplayer:exoplayer-hls:$exoplayerVersion"
implementation "com.google.android.exoplayer:exoplayer-ui:$exoplayerVersion"
- def toroVersion = "3.7.0.2010003"
+ /*def toroVersion = "3.7.0.2010003"
implementation "im.ene.toro3:toro:$toroVersion"
implementation("im.ene.toro3:toro-ext-exoplayer:$toroVersion") {
exclude module: 'extension-ima'
- }
+ }*/
/** Third-party **/
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/AppModule.java b/app/src/main/java/ml/docilealligator/infinityforreddit/AppModule.java
index 78e475b8..3f5f51fd 100644
--- a/app/src/main/java/ml/docilealligator/infinityforreddit/AppModule.java
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/AppModule.java
@@ -20,15 +20,15 @@ import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
-import im.ene.toro.exoplayer.Config;
-import im.ene.toro.exoplayer.ExoCreator;
-import im.ene.toro.exoplayer.MediaSourceBuilder;
-import im.ene.toro.exoplayer.ToroExo;
import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper;
import ml.docilealligator.infinityforreddit.customviews.LoopAvailableExoCreator;
import ml.docilealligator.infinityforreddit.utils.APIUtils;
import ml.docilealligator.infinityforreddit.utils.CustomThemeSharedPreferencesUtils;
import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils;
+import ml.docilealligator.infinityforreddit.videoautoplay.Config;
+import ml.docilealligator.infinityforreddit.videoautoplay.ExoCreator;
+import ml.docilealligator.infinityforreddit.videoautoplay.MediaSourceBuilder;
+import ml.docilealligator.infinityforreddit.videoautoplay.ToroExo;
import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
import retrofit2.Retrofit;
@@ -119,9 +119,24 @@ class AppModule {
@Provides
@Named("redgifs")
@Singleton
- Retrofit provideRedgifsRetrofit() {
+ Retrofit provideRedgifsRetrofit(@Named("current_account") SharedPreferences currentAccountSharedPreferences) {
+ OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder();
+ okHttpClientBuilder
+ .addInterceptor(chain -> chain.proceed(
+ chain.request()
+ .newBuilder()
+ .header("User-Agent", APIUtils.getRedgifsUserAgent(mApplication))
+ .build()
+ ))
+ .addInterceptor(new RedgifsAccessTokenAuthenticator(currentAccountSharedPreferences))
+ .connectTimeout(30, TimeUnit.SECONDS)
+ .readTimeout(30, TimeUnit.SECONDS)
+ .writeTimeout(30, TimeUnit.SECONDS)
+ .connectionPool(new ConnectionPool(0, 1, TimeUnit.NANOSECONDS));
+
return new Retrofit.Builder()
.baseUrl(APIUtils.REDGIFS_API_BASE_URI)
+ .client(okHttpClientBuilder.build())
.addConverterFactory(ScalarsConverterFactory.create())
.build();
}
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/FetchGfycatOrRedgifsVideoLinks.java b/app/src/main/java/ml/docilealligator/infinityforreddit/FetchGfycatOrRedgifsVideoLinks.java
index 8040d466..d1bfd941 100644
--- a/app/src/main/java/ml/docilealligator/infinityforreddit/FetchGfycatOrRedgifsVideoLinks.java
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/FetchGfycatOrRedgifsVideoLinks.java
@@ -1,5 +1,7 @@
package ml.docilealligator.infinityforreddit;
+import android.content.Context;
+import android.content.SharedPreferences;
import android.os.Handler;
import org.json.JSONException;
@@ -9,7 +11,10 @@ import java.io.IOException;
import java.util.concurrent.Executor;
import ml.docilealligator.infinityforreddit.apis.GfycatAPI;
+import ml.docilealligator.infinityforreddit.apis.RedgifsAPI;
+import ml.docilealligator.infinityforreddit.utils.APIUtils;
import ml.docilealligator.infinityforreddit.utils.JSONUtils;
+import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils;
import retrofit2.Call;
import retrofit2.Response;
import retrofit2.Retrofit;
@@ -21,18 +26,14 @@ public class FetchGfycatOrRedgifsVideoLinks {
void failed(int errorCode);
}
- public static void fetchGfycatOrRedgifsVideoLinks(Executor executor, Handler handler, Retrofit gfycatRetrofit,
- String gfycatId, boolean isGfycatVideo,
- FetchGfycatOrRedgifsVideoLinksListener fetchGfycatOrRedgifsVideoLinksListener) {
+ public static void fetchGfycatVideoLinks(Executor executor, Handler handler, Retrofit gfycatRetrofit,
+ String gfycatId,
+ FetchGfycatOrRedgifsVideoLinksListener fetchGfycatOrRedgifsVideoLinksListener) {
executor.execute(() -> {
try {
Response<String> response = gfycatRetrofit.create(GfycatAPI.class).getGfycatData(gfycatId).execute();
if (response.isSuccessful()) {
- if (isGfycatVideo) {
- parseGfycatVideoLinks(handler, response.body(), fetchGfycatOrRedgifsVideoLinksListener);
- } else {
- parseRedgifsVideoLinks(handler, response.body(), fetchGfycatOrRedgifsVideoLinksListener);
- }
+ parseGfycatVideoLinks(handler, response.body(), fetchGfycatOrRedgifsVideoLinksListener);
} else {
handler.post(() -> fetchGfycatOrRedgifsVideoLinksListener.failed(response.code()));
}
@@ -41,12 +42,31 @@ public class FetchGfycatOrRedgifsVideoLinks {
handler.post(() -> fetchGfycatOrRedgifsVideoLinksListener.failed(-1));
}
});
+ }
+ public static void fetchRedgifsVideoLinks(Context context, Executor executor, Handler handler, Retrofit redgifsRetrofit,
+ SharedPreferences currentAccountSharedPreferences,
+ String gfycatId,
+ FetchGfycatOrRedgifsVideoLinksListener fetchGfycatOrRedgifsVideoLinksListener) {
+ executor.execute(() -> {
+ try {
+ Response<String> response = redgifsRetrofit.create(RedgifsAPI.class).getRedgifsData(APIUtils.getRedgifsOAuthHeader(currentAccountSharedPreferences.getString(SharedPreferencesUtils.REDGIFS_ACCESS_TOKEN, "")),
+ gfycatId, APIUtils.getRedgifsUserAgent(context)).execute();
+ if (response.isSuccessful()) {
+ parseRedgifsVideoLinks(handler, response.body(), fetchGfycatOrRedgifsVideoLinksListener);
+ } else {
+ handler.post(() -> fetchGfycatOrRedgifsVideoLinksListener.failed(response.code()));
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ handler.post(() -> fetchGfycatOrRedgifsVideoLinksListener.failed(-1));
+ }
+ });
}
public static void fetchGfycatOrRedgifsVideoLinksInRecyclerViewAdapter(Executor executor, Handler handler,
Call<String> gfycatCall,
- String gfycatId, boolean isGfycatVideo,
+ boolean isGfycatVideo,
boolean automaticallyTryRedgifs,
FetchGfycatOrRedgifsVideoLinksListener fetchGfycatOrRedgifsVideoLinksListener) {
executor.execute(() -> {
@@ -61,7 +81,7 @@ public class FetchGfycatOrRedgifsVideoLinks {
} else {
if (response.code() == 404 && isGfycatVideo && automaticallyTryRedgifs) {
fetchGfycatOrRedgifsVideoLinksInRecyclerViewAdapter(executor, handler, gfycatCall.clone(),
- gfycatId, false, false, fetchGfycatOrRedgifsVideoLinksListener);
+ false, false, fetchGfycatOrRedgifsVideoLinksListener);
} else {
handler.post(() -> fetchGfycatOrRedgifsVideoLinksListener.failed(response.code()));
}
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/RedgifsAccessTokenAuthenticator.java b/app/src/main/java/ml/docilealligator/infinityforreddit/RedgifsAccessTokenAuthenticator.java
new file mode 100644
index 00000000..a4feec43
--- /dev/null
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/RedgifsAccessTokenAuthenticator.java
@@ -0,0 +1,90 @@
+package ml.docilealligator.infinityforreddit;
+
+import android.content.SharedPreferences;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import ml.docilealligator.infinityforreddit.apis.RedgifsAPI;
+import ml.docilealligator.infinityforreddit.utils.APIUtils;
+import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils;
+import okhttp3.Headers;
+import okhttp3.Interceptor;
+import okhttp3.Response;
+import retrofit2.Call;
+import retrofit2.Retrofit;
+import retrofit2.converter.scalars.ScalarsConverterFactory;
+
+public class RedgifsAccessTokenAuthenticator implements Interceptor {
+ private SharedPreferences mCurrentAccountSharedPreferences;
+
+ public RedgifsAccessTokenAuthenticator(SharedPreferences currentAccountSharedPreferences) {
+ this.mCurrentAccountSharedPreferences = currentAccountSharedPreferences;
+ }
+
+ private String refreshAccessToken() {
+ Retrofit retrofit = new Retrofit.Builder()
+ .baseUrl(APIUtils.REDGIFS_API_BASE_URI)
+ .addConverterFactory(ScalarsConverterFactory.create())
+ .build();
+ RedgifsAPI api = retrofit.create(RedgifsAPI.class);
+
+ Map<String, String> params = new HashMap<>();
+ params.put(APIUtils.GRANT_TYPE_KEY, APIUtils.GRANT_TYPE_CLIENT_CREDENTIALS);
+ params.put(APIUtils.CLIENT_ID_KEY, APIUtils.REDGIFS_CLIENT_ID);
+ params.put(APIUtils.CLIENT_SECRET_KEY, APIUtils.REDGIFS_CLIENT_SECRET);
+
+ Call<String> accessTokenCall = api.getRedgifsAccessToken(params);
+ try {
+ retrofit2.Response<String> response = accessTokenCall.execute();
+ if (response.isSuccessful() && response.body() != null) {
+ String newAccessToken = new JSONObject(response.body()).getString(APIUtils.ACCESS_TOKEN_KEY);
+ mCurrentAccountSharedPreferences.edit().putString(SharedPreferencesUtils.REDGIFS_ACCESS_TOKEN, newAccessToken).apply();
+
+ return newAccessToken;
+ }
+ return "";
+ } catch (IOException | JSONException e) {
+ e.printStackTrace();
+ }
+
+ return "";
+ }
+
+ @NonNull
+ @Override
+ public Response intercept(@NonNull Chain chain) throws IOException {
+ Response response = chain.proceed(chain.request());
+ if (response.code() == 401 || response.code() == 400) {
+ String accessTokenHeader = response.request().header(APIUtils.AUTHORIZATION_KEY);
+ if (accessTokenHeader == null) {
+ return response;
+ }
+
+ String accessToken = accessTokenHeader.substring(APIUtils.AUTHORIZATION_BASE.length() - 1).trim();
+ synchronized (this) {
+ String accessTokenFromSharedPreferences = mCurrentAccountSharedPreferences.getString(SharedPreferencesUtils.REDGIFS_ACCESS_TOKEN, "");
+ if (accessToken.equals(accessTokenFromSharedPreferences)) {
+ String newAccessToken = refreshAccessToken();
+ if (!newAccessToken.equals("")) {
+ response.close();
+ return chain.proceed(response.request().newBuilder().headers(Headers.of(APIUtils.getRedgifsOAuthHeader(newAccessToken))).build());
+ } else {
+ return response;
+ }
+ } else {
+ response.close();
+ return chain.proceed(response.request().newBuilder().headers(Headers.of(APIUtils.getRedgifsOAuthHeader(accessTokenFromSharedPreferences))).build());
+ }
+ }
+ }
+ return response;
+ }
+}
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/activities/ViewVideoActivity.java b/app/src/main/java/ml/docilealligator/infinityforreddit/activities/ViewVideoActivity.java
index 434390fb..2fab98e7 100644
--- a/app/src/main/java/ml/docilealligator/infinityforreddit/activities/ViewVideoActivity.java
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/activities/ViewVideoActivity.java
@@ -65,7 +65,6 @@ import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory;
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
import com.google.android.exoplayer2.util.MimeTypes;
-import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoListener;
import com.google.android.material.bottomappbar.BottomAppBar;
import com.google.android.material.snackbar.Snackbar;
@@ -104,6 +103,7 @@ import ml.docilealligator.infinityforreddit.post.FetchPost;
import ml.docilealligator.infinityforreddit.post.Post;
import ml.docilealligator.infinityforreddit.services.DownloadMediaService;
import ml.docilealligator.infinityforreddit.services.DownloadRedditVideoService;
+import ml.docilealligator.infinityforreddit.utils.APIUtils;
import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils;
import ml.docilealligator.infinityforreddit.utils.Utils;
import retrofit2.Call;
@@ -221,6 +221,10 @@ public class ViewVideoActivity extends AppCompatActivity implements CustomFontRe
SharedPreferences mSharedPreferences;
@Inject
+ @Named("current_account")
+ SharedPreferences mCurrentAccountSharedPreferences;
+
+ @Inject
CustomThemeWrapper mCustomThemeWrapper;
@Inject
@@ -503,8 +507,7 @@ public class ViewVideoActivity extends AppCompatActivity implements CustomFontRe
loadStreamableVideo(shortCode, savedInstanceState);
} else {
dataSourceFactory = new CacheDataSourceFactory(mSimpleCache,
- new DefaultDataSourceFactory(ViewVideoActivity.this,
- Util.getUserAgent(ViewVideoActivity.this, "Infinity")));
+ new DefaultDataSourceFactory(ViewVideoActivity.this, APIUtils.getRedgifsUserAgent(ViewVideoActivity.this)));
player.prepare(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mVideoUri));
preparePlayer(savedInstanceState);
}
@@ -535,8 +538,7 @@ public class ViewVideoActivity extends AppCompatActivity implements CustomFontRe
}
} else {
dataSourceFactory = new CacheDataSourceFactory(mSimpleCache,
- new DefaultDataSourceFactory(ViewVideoActivity.this,
- Util.getUserAgent(ViewVideoActivity.this, "Infinity")));
+ new DefaultDataSourceFactory(ViewVideoActivity.this, APIUtils.getRedgifsUserAgent(ViewVideoActivity.this)));
player.prepare(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mVideoUri));
preparePlayer(savedInstanceState);
}
@@ -549,7 +551,7 @@ public class ViewVideoActivity extends AppCompatActivity implements CustomFontRe
}
// Produces DataSource instances through which media data is loaded.
dataSourceFactory = new CacheDataSourceFactory(mSimpleCache,
- new DefaultHttpDataSourceFactory(Util.getUserAgent(this, "Infinity")));
+ new DefaultHttpDataSourceFactory(APIUtils.getRedgifsUserAgent(ViewVideoActivity.this)));
// Prepare the player with the source.
player.prepare(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mVideoUri));
preparePlayer(savedInstanceState);
@@ -560,7 +562,7 @@ public class ViewVideoActivity extends AppCompatActivity implements CustomFontRe
videoFileName = subredditName + "-" + id + ".mp4";
// Produces DataSource instances through which media data is loaded.
dataSourceFactory = new CacheDataSourceFactory(mSimpleCache,
- new DefaultHttpDataSourceFactory(Util.getUserAgent(this, "Infinity")));
+ new DefaultHttpDataSourceFactory(APIUtils.getRedgifsUserAgent(ViewVideoActivity.this)));
// Prepare the player with the source.
player.prepare(new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(mVideoUri));
preparePlayer(savedInstanceState);
@@ -678,24 +680,22 @@ public class ViewVideoActivity extends AppCompatActivity implements CustomFontRe
private void loadGfycatOrRedgifsVideo(Retrofit retrofit, String gfycatId, boolean isGfycatVideo,
Bundle savedInstanceState, boolean needErrorHandling) {
progressBar.setVisibility(View.VISIBLE);
- FetchGfycatOrRedgifsVideoLinks.fetchGfycatOrRedgifsVideoLinks(mExecutor, new Handler(), retrofit, gfycatId,
- isGfycatVideo, new FetchGfycatOrRedgifsVideoLinks.FetchGfycatOrRedgifsVideoLinksListener() {
- @Override
- public void success(String webm, String mp4) {
- progressBar.setVisibility(View.GONE);
- mVideoUri = Uri.parse(webm);
- videoDownloadUrl = mp4;
- dataSourceFactory = new CacheDataSourceFactory(mSimpleCache,
- new DefaultDataSourceFactory(ViewVideoActivity.this,
- Util.getUserAgent(ViewVideoActivity.this, "Infinity")));
- preparePlayer(savedInstanceState);
- player.prepare(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mVideoUri));
- }
+ if (isGfycatVideo) {
+ FetchGfycatOrRedgifsVideoLinks.fetchGfycatVideoLinks(mExecutor, new Handler(), retrofit, gfycatId,
+ new FetchGfycatOrRedgifsVideoLinks.FetchGfycatOrRedgifsVideoLinksListener() {
+ @Override
+ public void success(String webm, String mp4) {
+ progressBar.setVisibility(View.GONE);
+ mVideoUri = Uri.parse(webm);
+ videoDownloadUrl = mp4;
+ dataSourceFactory = new CacheDataSourceFactory(mSimpleCache,
+ new DefaultDataSourceFactory(ViewVideoActivity.this, APIUtils.getRedgifsUserAgent(ViewVideoActivity.this)));
+ preparePlayer(savedInstanceState);
+ player.prepare(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mVideoUri));
+ }
- @Override
- public void failed(int errorCode) {
- progressBar.setVisibility(View.GONE);
- if (videoType == VIDEO_TYPE_GFYCAT) {
+ @Override
+ public void failed(int errorCode) {
if (errorCode == 404 && needErrorHandling) {
if (mSharedPreferences.getBoolean(SharedPreferencesUtils.AUTOMATICALLY_TRY_REDGIFS, true)) {
loadGfycatOrRedgifsVideo(redgifsRetrofit, gfycatId, false, savedInstanceState, false);
@@ -704,13 +704,32 @@ public class ViewVideoActivity extends AppCompatActivity implements CustomFontRe
view -> loadGfycatOrRedgifsVideo(redgifsRetrofit, gfycatId, false, savedInstanceState, false)).show();
}
} else {
+ progressBar.setVisibility(View.GONE);
Toast.makeText(ViewVideoActivity.this, R.string.fetch_gfycat_video_failed, Toast.LENGTH_SHORT).show();
}
- } else {
+ }
+ });
+ } else {
+ FetchGfycatOrRedgifsVideoLinks.fetchRedgifsVideoLinks(this, mExecutor, new Handler(), redgifsRetrofit,
+ mCurrentAccountSharedPreferences, gfycatId, new FetchGfycatOrRedgifsVideoLinks.FetchGfycatOrRedgifsVideoLinksListener() {
+ @Override
+ public void success(String webm, String mp4) {
+ progressBar.setVisibility(View.GONE);
+ mVideoUri = Uri.parse(webm);
+ videoDownloadUrl = mp4;
+ dataSourceFactory = new CacheDataSourceFactory(mSimpleCache,
+ new DefaultDataSourceFactory(ViewVideoActivity.this, APIUtils.getRedgifsUserAgent(ViewVideoActivity.this)));
+ preparePlayer(savedInstanceState);
+ player.prepare(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mVideoUri));
+ }
+
+ @Override
+ public void failed(int errorCode) {
+ progressBar.setVisibility(View.GONE);
Toast.makeText(ViewVideoActivity.this, R.string.fetch_redgifs_video_failed, Toast.LENGTH_SHORT).show();
}
- }
- });
+ });
+ }
}
private void loadVReddItVideo(Bundle savedInstanceState) {
@@ -764,7 +783,7 @@ public class ViewVideoActivity extends AppCompatActivity implements CustomFontRe
videoFileName = "imgur-" + FilenameUtils.getName(videoDownloadUrl);
// Produces DataSource instances through which media data is loaded.
dataSourceFactory = new CacheDataSourceFactory(mSimpleCache,
- new DefaultHttpDataSourceFactory(Util.getUserAgent(ViewVideoActivity.this, "Infinity")));
+ new DefaultHttpDataSourceFactory(APIUtils.getRedgifsUserAgent(ViewVideoActivity.this)));
// Prepare the player with the source.
player.prepare(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mVideoUri));
preparePlayer(savedInstanceState);
@@ -779,9 +798,7 @@ public class ViewVideoActivity extends AppCompatActivity implements CustomFontRe
videoFileName = subredditName + "-" + id + ".mp4";
// Produces DataSource instances through which media data is loaded.
dataSourceFactory = new CacheDataSourceFactory(mSimpleCache,
- new DefaultHttpDataSourceFactory(
- Util.getUserAgent(ViewVideoActivity.this,
- "Infinity")));
+ new DefaultHttpDataSourceFactory(APIUtils.getRedgifsUserAgent(ViewVideoActivity.this)));
// Prepare the player with the source.
preparePlayer(savedInstanceState);
player.prepare(new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(mVideoUri));
@@ -823,8 +840,7 @@ public class ViewVideoActivity extends AppCompatActivity implements CustomFontRe
videoDownloadUrl = streamableVideo.mp4 == null ? streamableVideo.mp4Mobile.url : streamableVideo.mp4.url;
mVideoUri = Uri.parse(videoDownloadUrl);
dataSourceFactory = new CacheDataSourceFactory(mSimpleCache,
- new DefaultDataSourceFactory(ViewVideoActivity.this,
- Util.getUserAgent(ViewVideoActivity.this, "Infinity")));
+ new DefaultDataSourceFactory(ViewVideoActivity.this, APIUtils.getRedgifsUserAgent(ViewVideoActivity.this)));
preparePlayer(savedInstanceState);
player.prepare(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mVideoUri));
}
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/adapters/HistoryPostRecyclerViewAdapter.java b/app/src/main/java/ml/docilealligator/infinityforreddit/adapters/HistoryPostRecyclerViewAdapter.java
index 201e7930..2eba5f0c 100644
--- a/app/src/main/java/ml/docilealligator/infinityforreddit/adapters/HistoryPostRecyclerViewAdapter.java
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/adapters/HistoryPostRecyclerViewAdapter.java
@@ -56,14 +56,6 @@ import java.util.concurrent.Executor;
import butterknife.BindView;
import butterknife.ButterKnife;
-import im.ene.toro.CacheManager;
-import im.ene.toro.ToroPlayer;
-import im.ene.toro.ToroUtil;
-import im.ene.toro.exoplayer.ExoCreator;
-import im.ene.toro.exoplayer.ExoPlayerViewHelper;
-import im.ene.toro.exoplayer.Playable;
-import im.ene.toro.media.PlaybackInfo;
-import im.ene.toro.widget.Container;
import jp.wasabeef.glide.transformations.BlurTransformation;
import jp.wasabeef.glide.transformations.RoundedCornersTransformation;
import ml.docilealligator.infinityforreddit.FetchGfycatOrRedgifsVideoLinks;
@@ -83,18 +75,26 @@ import ml.docilealligator.infinityforreddit.activities.ViewSubredditDetailActivi
import ml.docilealligator.infinityforreddit.activities.ViewUserDetailActivity;
import ml.docilealligator.infinityforreddit.activities.ViewVideoActivity;
import ml.docilealligator.infinityforreddit.apis.GfycatAPI;
+import ml.docilealligator.infinityforreddit.apis.RedgifsAPI;
import ml.docilealligator.infinityforreddit.apis.StreamableAPI;
import ml.docilealligator.infinityforreddit.bottomsheetfragments.ShareLinkBottomSheetFragment;
import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper;
import ml.docilealligator.infinityforreddit.customviews.AspectRatioGifImageView;
import ml.docilealligator.infinityforreddit.events.PostUpdateEventToPostDetailFragment;
import ml.docilealligator.infinityforreddit.fragments.HistoryPostFragment;
-import ml.docilealligator.infinityforreddit.fragments.PostFragment;
import ml.docilealligator.infinityforreddit.post.Post;
import ml.docilealligator.infinityforreddit.post.PostPagingSource;
import ml.docilealligator.infinityforreddit.utils.APIUtils;
import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils;
import ml.docilealligator.infinityforreddit.utils.Utils;
+import ml.docilealligator.infinityforreddit.videoautoplay.CacheManager;
+import ml.docilealligator.infinityforreddit.videoautoplay.ExoCreator;
+import ml.docilealligator.infinityforreddit.videoautoplay.ExoPlayerViewHelper;
+import ml.docilealligator.infinityforreddit.videoautoplay.Playable;
+import ml.docilealligator.infinityforreddit.videoautoplay.ToroPlayer;
+import ml.docilealligator.infinityforreddit.videoautoplay.ToroUtil;
+import ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo;
+import ml.docilealligator.infinityforreddit.videoautoplay.widget.Container;
import pl.droidsonroids.gif.GifImageView;
import retrofit2.Call;
import retrofit2.Retrofit;
@@ -124,6 +124,7 @@ public class HistoryPostRecyclerViewAdapter extends PagingDataAdapter<Post, Recy
private BaseActivity mActivity;
private HistoryPostFragment mFragment;
private SharedPreferences mSharedPreferences;
+ private SharedPreferences mCurrentAccountSharedPreferences;
private Executor mExecutor;
private Retrofit mOauthRetrofit;
private Retrofit mGfycatRetrofit;
@@ -209,14 +210,15 @@ public class HistoryPostRecyclerViewAdapter extends PagingDataAdapter<Post, Recy
Retrofit gfycatRetrofit, Retrofit redgifsRetrofit, Retrofit streambleRetrofit,
CustomThemeWrapper customThemeWrapper, Locale locale,
String accessToken, String accountName, int postType, int postLayout, boolean displaySubredditName,
- SharedPreferences sharedPreferences, SharedPreferences nsfwAndSpoilerSharedPreferences,
- SharedPreferences postHistorySharedPreferences,
+ SharedPreferences sharedPreferences, SharedPreferences currentAccountSharedPreferences,
+ SharedPreferences nsfwAndSpoilerSharedPreferences,
ExoCreator exoCreator, Callback callback) {
super(DIFF_CALLBACK);
if (activity != null) {
mActivity = activity;
mFragment = fragment;
mSharedPreferences = sharedPreferences;
+ mCurrentAccountSharedPreferences = currentAccountSharedPreferences;
mExecutor = executor;
mOauthRetrofit = oauthRetrofit;
mGfycatRetrofit = gfycatRetrofit;
@@ -676,9 +678,10 @@ public class HistoryPostRecyclerViewAdapter extends PagingDataAdapter<Post, Recy
if ((post.isGfycat() || post.isRedgifs()) && !post.isLoadGfycatOrStreamableVideoSuccess()) {
((PostVideoAutoplayViewHolder) holder).fetchGfycatOrStreamableVideoCall =
- (post.isGfycat() ? mGfycatRetrofit : mRedgifsRetrofit).create(GfycatAPI.class).getGfycatData(post.getGfycatId());
+ post.isGfycat() ? mGfycatRetrofit.create(GfycatAPI.class).getGfycatData(post.getGfycatId()) :
+ mRedgifsRetrofit.create(RedgifsAPI.class).getRedgifsData(APIUtils.getRedgifsOAuthHeader(mCurrentAccountSharedPreferences.getString(SharedPreferencesUtils.REDGIFS_ACCESS_TOKEN, "")), post.getGfycatId(), APIUtils.getRedgifsUserAgent(mActivity));
FetchGfycatOrRedgifsVideoLinks.fetchGfycatOrRedgifsVideoLinksInRecyclerViewAdapter(mExecutor, new Handler(),
- ((PostVideoAutoplayViewHolder) holder).fetchGfycatOrStreamableVideoCall, post.getGfycatId(),
+ ((PostVideoAutoplayViewHolder) holder).fetchGfycatOrStreamableVideoCall,
post.isGfycat(), mAutomaticallyTryRedgifs,
new FetchGfycatOrRedgifsVideoLinks.FetchGfycatOrRedgifsVideoLinksListener() {
@Override
@@ -837,9 +840,10 @@ public class HistoryPostRecyclerViewAdapter extends PagingDataAdapter<Post, Recy
if ((post.isGfycat() || post.isRedgifs()) && !post.isLoadGfycatOrStreamableVideoSuccess()) {
((PostCard2VideoAutoplayViewHolder) holder).fetchGfycatOrStreamableVideoCall =
- (post.isGfycat() ? mGfycatRetrofit : mRedgifsRetrofit).create(GfycatAPI.class).getGfycatData(post.getGfycatId());
+ post.isGfycat() ? mGfycatRetrofit.create(GfycatAPI.class).getGfycatData(post.getGfycatId()) :
+ mRedgifsRetrofit.create(RedgifsAPI.class).getRedgifsData(APIUtils.getRedgifsOAuthHeader(mCurrentAccountSharedPreferences.getString(SharedPreferencesUtils.REDGIFS_ACCESS_TOKEN, "")), post.getGfycatId(), APIUtils.getRedgifsUserAgent(mActivity));
FetchGfycatOrRedgifsVideoLinks.fetchGfycatOrRedgifsVideoLinksInRecyclerViewAdapter(mExecutor, new Handler(),
- ((PostCard2VideoAutoplayViewHolder) holder).fetchGfycatOrStreamableVideoCall, post.getGfycatId(),
+ ((PostCard2VideoAutoplayViewHolder) holder).fetchGfycatOrStreamableVideoCall,
post.isGfycat(), mAutomaticallyTryRedgifs,
new FetchGfycatOrRedgifsVideoLinks.FetchGfycatOrRedgifsVideoLinksListener() {
@Override
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/adapters/PostDetailRecyclerViewAdapter.java b/app/src/main/java/ml/docilealligator/infinityforreddit/adapters/PostDetailRecyclerViewAdapter.java
index c4a119ee..4db8166f 100644
--- a/app/src/main/java/ml/docilealligator/infinityforreddit/adapters/PostDetailRecyclerViewAdapter.java
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/adapters/PostDetailRecyclerViewAdapter.java
@@ -54,14 +54,6 @@ import java.util.concurrent.Executor;
import butterknife.BindView;
import butterknife.ButterKnife;
-import im.ene.toro.CacheManager;
-import im.ene.toro.ToroPlayer;
-import im.ene.toro.ToroUtil;
-import im.ene.toro.exoplayer.ExoCreator;
-import im.ene.toro.exoplayer.ExoPlayerViewHelper;
-import im.ene.toro.exoplayer.Playable;
-import im.ene.toro.media.PlaybackInfo;
-import im.ene.toro.widget.Container;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonConfiguration;
@@ -100,6 +92,7 @@ import ml.docilealligator.infinityforreddit.activities.ViewSubredditDetailActivi
import ml.docilealligator.infinityforreddit.activities.ViewUserDetailActivity;
import ml.docilealligator.infinityforreddit.activities.ViewVideoActivity;
import ml.docilealligator.infinityforreddit.apis.GfycatAPI;
+import ml.docilealligator.infinityforreddit.apis.RedgifsAPI;
import ml.docilealligator.infinityforreddit.apis.StreamableAPI;
import ml.docilealligator.infinityforreddit.asynctasks.LoadSubredditIcon;
import ml.docilealligator.infinityforreddit.asynctasks.LoadUserData;
@@ -118,6 +111,14 @@ import ml.docilealligator.infinityforreddit.post.PostPagingSource;
import ml.docilealligator.infinityforreddit.utils.APIUtils;
import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils;
import ml.docilealligator.infinityforreddit.utils.Utils;
+import ml.docilealligator.infinityforreddit.videoautoplay.CacheManager;
+import ml.docilealligator.infinityforreddit.videoautoplay.ExoCreator;
+import ml.docilealligator.infinityforreddit.videoautoplay.ExoPlayerViewHelper;
+import ml.docilealligator.infinityforreddit.videoautoplay.Playable;
+import ml.docilealligator.infinityforreddit.videoautoplay.ToroPlayer;
+import ml.docilealligator.infinityforreddit.videoautoplay.ToroUtil;
+import ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo;
+import ml.docilealligator.infinityforreddit.videoautoplay.widget.Container;
import pl.droidsonroids.gif.GifImageView;
import retrofit2.Call;
import retrofit2.Retrofit;
@@ -140,6 +141,7 @@ public class PostDetailRecyclerViewAdapter extends RecyclerView.Adapter<Recycler
private Retrofit mRedgifsRetrofit;
private Retrofit mStreamableRetrofit;
private RedditDataRoomDatabase mRedditDataRoomDatabase;
+ private SharedPreferences mCurrentAccountSharedPreferences;
private RequestManager mGlide;
private SaveMemoryCenterInisdeDownsampleStrategy mSaveMemoryCenterInsideDownSampleStrategy;
private Markwon mPostDetailMarkwon;
@@ -221,6 +223,7 @@ public class PostDetailRecyclerViewAdapter extends RecyclerView.Adapter<Recycler
boolean separatePostAndComments, String accessToken,
String accountName, Post post, Locale locale,
SharedPreferences sharedPreferences,
+ SharedPreferences currentAccountSharedPreferences,
SharedPreferences nsfwAndSpoilerSharedPreferences,
SharedPreferences postDetailsSharedPreferences,
ExoCreator exoCreator,
@@ -236,6 +239,7 @@ public class PostDetailRecyclerViewAdapter extends RecyclerView.Adapter<Recycler
mRedditDataRoomDatabase = redditDataRoomDatabase;
mGlide = glide;
mSaveMemoryCenterInsideDownSampleStrategy = new SaveMemoryCenterInisdeDownsampleStrategy(Integer.parseInt(sharedPreferences.getString(SharedPreferencesUtils.POST_FEED_MAX_RESOLUTION, "5000000")));
+ mCurrentAccountSharedPreferences = currentAccountSharedPreferences;
mSecondaryTextColor = customThemeWrapper.getSecondaryTextColor();
int markdownColor = customThemeWrapper.getPostContentColor();
int postSpoilerBackgroundColor = markdownColor | 0xFF000000;
@@ -678,9 +682,10 @@ public class PostDetailRecyclerViewAdapter extends RecyclerView.Adapter<Recycler
if (mPost.isGfycat() || mPost.isRedgifs() && !mPost.isLoadGfycatOrStreamableVideoSuccess()) {
((PostDetailVideoAutoplayViewHolder) holder).fetchGfycatOrStreamableVideoCall =
- (mPost.isGfycat() ? mGfycatRetrofit : mRedgifsRetrofit).create(GfycatAPI.class).getGfycatData(mPost.getGfycatId());
+ mPost.isGfycat() ? mGfycatRetrofit.create(GfycatAPI.class).getGfycatData(mPost.getGfycatId()) :
+ mRedgifsRetrofit.create(RedgifsAPI.class).getRedgifsData(APIUtils.getRedgifsOAuthHeader(mCurrentAccountSharedPreferences.getString(SharedPreferencesUtils.REDGIFS_ACCESS_TOKEN, "")), mPost.getGfycatId(), APIUtils.getRedgifsUserAgent(mActivity));
FetchGfycatOrRedgifsVideoLinks.fetchGfycatOrRedgifsVideoLinksInRecyclerViewAdapter(mExecutor, new Handler(),
- ((PostDetailVideoAutoplayViewHolder) holder).fetchGfycatOrStreamableVideoCall, mPost.getGfycatId(),
+ ((PostDetailVideoAutoplayViewHolder) holder).fetchGfycatOrStreamableVideoCall,
mPost.isGfycat(), mAutomaticallyTryRedgifs,
new FetchGfycatOrRedgifsVideoLinks.FetchGfycatOrRedgifsVideoLinksListener() {
@Override
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/adapters/PostRecyclerViewAdapter.java b/app/src/main/java/ml/docilealligator/infinityforreddit/adapters/PostRecyclerViewAdapter.java
index a50fd29b..4bf33ddf 100644
--- a/app/src/main/java/ml/docilealligator/infinityforreddit/adapters/PostRecyclerViewAdapter.java
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/adapters/PostRecyclerViewAdapter.java
@@ -56,14 +56,6 @@ import java.util.concurrent.Executor;
import butterknife.BindView;
import butterknife.ButterKnife;
-import im.ene.toro.CacheManager;
-import im.ene.toro.ToroPlayer;
-import im.ene.toro.ToroUtil;
-import im.ene.toro.exoplayer.ExoCreator;
-import im.ene.toro.exoplayer.ExoPlayerViewHelper;
-import im.ene.toro.exoplayer.Playable;
-import im.ene.toro.media.PlaybackInfo;
-import im.ene.toro.widget.Container;
import jp.wasabeef.glide.transformations.BlurTransformation;
import jp.wasabeef.glide.transformations.RoundedCornersTransformation;
import ml.docilealligator.infinityforreddit.FetchGfycatOrRedgifsVideoLinks;
@@ -84,6 +76,7 @@ import ml.docilealligator.infinityforreddit.activities.ViewSubredditDetailActivi
import ml.docilealligator.infinityforreddit.activities.ViewUserDetailActivity;
import ml.docilealligator.infinityforreddit.activities.ViewVideoActivity;
import ml.docilealligator.infinityforreddit.apis.GfycatAPI;
+import ml.docilealligator.infinityforreddit.apis.RedgifsAPI;
import ml.docilealligator.infinityforreddit.apis.StreamableAPI;
import ml.docilealligator.infinityforreddit.bottomsheetfragments.ShareLinkBottomSheetFragment;
import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper;
@@ -95,6 +88,14 @@ import ml.docilealligator.infinityforreddit.post.PostPagingSource;
import ml.docilealligator.infinityforreddit.utils.APIUtils;
import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils;
import ml.docilealligator.infinityforreddit.utils.Utils;
+import ml.docilealligator.infinityforreddit.videoautoplay.CacheManager;
+import ml.docilealligator.infinityforreddit.videoautoplay.ExoCreator;
+import ml.docilealligator.infinityforreddit.videoautoplay.ExoPlayerViewHelper;
+import ml.docilealligator.infinityforreddit.videoautoplay.Playable;
+import ml.docilealligator.infinityforreddit.videoautoplay.ToroPlayer;
+import ml.docilealligator.infinityforreddit.videoautoplay.ToroUtil;
+import ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo;
+import ml.docilealligator.infinityforreddit.videoautoplay.widget.Container;
import pl.droidsonroids.gif.GifImageView;
import retrofit2.Call;
import retrofit2.Retrofit;
@@ -128,6 +129,7 @@ public class PostRecyclerViewAdapter extends PagingDataAdapter<Post, RecyclerVie
private BaseActivity mActivity;
private PostFragment mFragment;
private SharedPreferences mSharedPreferences;
+ private SharedPreferences mCurrentAccountSharedPreferences;
private Executor mExecutor;
private Retrofit mOauthRetrofit;
private Retrofit mGfycatRetrofit;
@@ -220,7 +222,8 @@ public class PostRecyclerViewAdapter extends PagingDataAdapter<Post, RecyclerVie
Retrofit gfycatRetrofit, Retrofit redgifsRetrofit, Retrofit streambleRetrofit,
CustomThemeWrapper customThemeWrapper, Locale locale,
String accessToken, String accountName, int postType, int postLayout, boolean displaySubredditName,
- SharedPreferences sharedPreferences, SharedPreferences nsfwAndSpoilerSharedPreferences,
+ SharedPreferences sharedPreferences, SharedPreferences currentAccountSharedPreferences,
+ SharedPreferences nsfwAndSpoilerSharedPreferences,
SharedPreferences postHistorySharedPreferences,
ExoCreator exoCreator, Callback callback) {
super(DIFF_CALLBACK);
@@ -228,6 +231,7 @@ public class PostRecyclerViewAdapter extends PagingDataAdapter<Post, RecyclerVie
mActivity = activity;
mFragment = fragment;
mSharedPreferences = sharedPreferences;
+ mCurrentAccountSharedPreferences = currentAccountSharedPreferences;
mExecutor = executor;
mOauthRetrofit = oauthRetrofit;
mGfycatRetrofit = gfycatRetrofit;
@@ -705,9 +709,10 @@ public class PostRecyclerViewAdapter extends PagingDataAdapter<Post, RecyclerVie
if ((post.isGfycat() || post.isRedgifs()) && !post.isLoadGfycatOrStreamableVideoSuccess()) {
((PostVideoAutoplayViewHolder) holder).fetchGfycatOrStreamableVideoCall =
- (post.isGfycat() ? mGfycatRetrofit : mRedgifsRetrofit).create(GfycatAPI.class).getGfycatData(post.getGfycatId());
+ post.isGfycat() ? mGfycatRetrofit.create(GfycatAPI.class).getGfycatData(post.getGfycatId()) :
+ mRedgifsRetrofit.create(RedgifsAPI.class).getRedgifsData(APIUtils.getRedgifsOAuthHeader(mCurrentAccountSharedPreferences.getString(SharedPreferencesUtils.REDGIFS_ACCESS_TOKEN, "")), post.getGfycatId(), APIUtils.getRedgifsUserAgent(mActivity));
FetchGfycatOrRedgifsVideoLinks.fetchGfycatOrRedgifsVideoLinksInRecyclerViewAdapter(mExecutor, new Handler(),
- ((PostVideoAutoplayViewHolder) holder).fetchGfycatOrStreamableVideoCall, post.getGfycatId(),
+ ((PostVideoAutoplayViewHolder) holder).fetchGfycatOrStreamableVideoCall,
post.isGfycat(), mAutomaticallyTryRedgifs,
new FetchGfycatOrRedgifsVideoLinks.FetchGfycatOrRedgifsVideoLinksListener() {
@Override
@@ -870,9 +875,10 @@ public class PostRecyclerViewAdapter extends PagingDataAdapter<Post, RecyclerVie
if ((post.isGfycat() || post.isRedgifs()) && !post.isLoadGfycatOrStreamableVideoSuccess()) {
((PostCard2VideoAutoplayViewHolder) holder).fetchGfycatOrStreamableVideoCall =
- (post.isGfycat() ? mGfycatRetrofit : mRedgifsRetrofit).create(GfycatAPI.class).getGfycatData(post.getGfycatId());
+ post.isGfycat() ? mGfycatRetrofit.create(GfycatAPI.class).getGfycatData(post.getGfycatId()) :
+ mRedgifsRetrofit.create(RedgifsAPI.class).getRedgifsData(APIUtils.getRedgifsOAuthHeader(mCurrentAccountSharedPreferences.getString(SharedPreferencesUtils.REDGIFS_ACCESS_TOKEN, "")), post.getGfycatId(), APIUtils.getRedgifsUserAgent(mActivity));
FetchGfycatOrRedgifsVideoLinks.fetchGfycatOrRedgifsVideoLinksInRecyclerViewAdapter(mExecutor, new Handler(),
- ((PostCard2VideoAutoplayViewHolder) holder).fetchGfycatOrStreamableVideoCall, post.getGfycatId(),
+ ((PostCard2VideoAutoplayViewHolder) holder).fetchGfycatOrStreamableVideoCall,
post.isGfycat(), mAutomaticallyTryRedgifs,
new FetchGfycatOrRedgifsVideoLinks.FetchGfycatOrRedgifsVideoLinksListener() {
@Override
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/apis/RedgifsAPI.java b/app/src/main/java/ml/docilealligator/infinityforreddit/apis/RedgifsAPI.java
new file mode 100644
index 00000000..1a4bf809
--- /dev/null
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/apis/RedgifsAPI.java
@@ -0,0 +1,21 @@
+package ml.docilealligator.infinityforreddit.apis;
+
+import java.util.Map;
+
+import retrofit2.Call;
+import retrofit2.http.FieldMap;
+import retrofit2.http.FormUrlEncoded;
+import retrofit2.http.GET;
+import retrofit2.http.HeaderMap;
+import retrofit2.http.POST;
+import retrofit2.http.Path;
+import retrofit2.http.Query;
+
+public interface RedgifsAPI {
+ @GET("/v2/gifs/{id}")
+ Call<String> getRedgifsData(@HeaderMap Map<String, String> headers, @Path("id") String id, @Query("user-agent") String userAgent);
+
+ @FormUrlEncoded
+ @POST("/v2/oauth/client")
+ Call<String> getRedgifsAccessToken(@FieldMap Map<String, String> params);
+}
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/customviews/CustomToroContainer.java b/app/src/main/java/ml/docilealligator/infinityforreddit/customviews/CustomToroContainer.java
index 8d925ae6..74d367b8 100644
--- a/app/src/main/java/ml/docilealligator/infinityforreddit/customviews/CustomToroContainer.java
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/customviews/CustomToroContainer.java
@@ -5,7 +5,7 @@ import android.util.AttributeSet;
import androidx.annotation.Nullable;
-import im.ene.toro.widget.Container;
+import ml.docilealligator.infinityforreddit.videoautoplay.widget.Container;
public class CustomToroContainer extends Container {
private OnWindowFocusChangedListener listener;
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/customviews/LoopAvailableExoCreator.java b/app/src/main/java/ml/docilealligator/infinityforreddit/customviews/LoopAvailableExoCreator.java
index 5ccda4be..01fc9a32 100644
--- a/app/src/main/java/ml/docilealligator/infinityforreddit/customviews/LoopAvailableExoCreator.java
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/customviews/LoopAvailableExoCreator.java
@@ -7,10 +7,10 @@ import androidx.annotation.NonNull;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
-import im.ene.toro.exoplayer.Config;
-import im.ene.toro.exoplayer.DefaultExoCreator;
-import im.ene.toro.exoplayer.ToroExo;
import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils;
+import ml.docilealligator.infinityforreddit.videoautoplay.Config;
+import ml.docilealligator.infinityforreddit.videoautoplay.DefaultExoCreator;
+import ml.docilealligator.infinityforreddit.videoautoplay.ToroExo;
public class LoopAvailableExoCreator extends DefaultExoCreator {
private final SharedPreferences sharedPreferences;
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/HistoryPostFragment.java b/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/HistoryPostFragment.java
index 76f3330c..06ef97e9 100644
--- a/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/HistoryPostFragment.java
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/HistoryPostFragment.java
@@ -1,7 +1,7 @@
package ml.docilealligator.infinityforreddit.fragments;
-import static im.ene.toro.media.PlaybackInfo.INDEX_UNSET;
-import static im.ene.toro.media.PlaybackInfo.TIME_UNSET;
+import static ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo.INDEX_UNSET;
+import static ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo.TIME_UNSET;
import android.content.Context;
import android.content.SharedPreferences;
@@ -63,9 +63,6 @@ import javax.inject.Named;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.Unbinder;
-import im.ene.toro.exoplayer.ExoCreator;
-import im.ene.toro.media.PlaybackInfo;
-import im.ene.toro.media.VolumeInfo;
import ml.docilealligator.infinityforreddit.FetchPostFilterReadPostsAndConcatenatedSubredditNames;
import ml.docilealligator.infinityforreddit.FragmentCommunicator;
import ml.docilealligator.infinityforreddit.Infinity;
@@ -129,6 +126,9 @@ import ml.docilealligator.infinityforreddit.postfilter.PostFilter;
import ml.docilealligator.infinityforreddit.postfilter.PostFilterUsage;
import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils;
import ml.docilealligator.infinityforreddit.utils.Utils;
+import ml.docilealligator.infinityforreddit.videoautoplay.ExoCreator;
+import ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo;
+import ml.docilealligator.infinityforreddit.videoautoplay.media.VolumeInfo;
import retrofit2.Retrofit;
public class HistoryPostFragment extends Fragment implements FragmentCommunicator {
@@ -177,6 +177,9 @@ public class HistoryPostFragment extends Fragment implements FragmentCommunicato
@Named("default")
SharedPreferences mSharedPreferences;
@Inject
+ @Named("current_account")
+ SharedPreferences mCurrentAccountSharedPreferences;
+ @Inject
@Named("post_layout")
SharedPreferences mPostLayoutSharedPreferences;
@Inject
@@ -378,7 +381,7 @@ public class HistoryPostFragment extends Fragment implements FragmentCommunicato
mAdapter = new HistoryPostRecyclerViewAdapter(activity, this, mExecutor, mOauthRetrofit, mGfycatRetrofit,
mRedgifsRetrofit, mStreamableRetrofit, mCustomThemeWrapper, locale,
accessToken, accountName, postType, postLayout, true,
- mSharedPreferences, mNsfwAndSpoilerSharedPreferences, mPostHistorySharedPreferences,
+ mSharedPreferences, mCurrentAccountSharedPreferences, mNsfwAndSpoilerSharedPreferences,
mExoCreator, new HistoryPostRecyclerViewAdapter.Callback() {
@Override
public void typeChipClicked(int filter) {
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/PostFragment.java b/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/PostFragment.java
index 5551d31e..2cd04385 100644
--- a/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/PostFragment.java
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/PostFragment.java
@@ -1,8 +1,7 @@
package ml.docilealligator.infinityforreddit.fragments;
-
-import static im.ene.toro.media.PlaybackInfo.INDEX_UNSET;
-import static im.ene.toro.media.PlaybackInfo.TIME_UNSET;
+import static ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo.INDEX_UNSET;
+import static ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo.TIME_UNSET;
import android.content.Context;
import android.content.Intent;
@@ -67,9 +66,6 @@ import javax.inject.Named;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.Unbinder;
-import im.ene.toro.exoplayer.ExoCreator;
-import im.ene.toro.media.PlaybackInfo;
-import im.ene.toro.media.VolumeInfo;
import ml.docilealligator.infinityforreddit.ActivityToolbarInterface;
import ml.docilealligator.infinityforreddit.FetchPostFilterReadPostsAndConcatenatedSubredditNames;
import ml.docilealligator.infinityforreddit.FragmentCommunicator;
@@ -137,6 +133,9 @@ import ml.docilealligator.infinityforreddit.postfilter.PostFilter;
import ml.docilealligator.infinityforreddit.postfilter.PostFilterUsage;
import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils;
import ml.docilealligator.infinityforreddit.utils.Utils;
+import ml.docilealligator.infinityforreddit.videoautoplay.ExoCreator;
+import ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo;
+import ml.docilealligator.infinityforreddit.videoautoplay.media.VolumeInfo;
import retrofit2.Retrofit;
@@ -195,6 +194,9 @@ public class PostFragment extends Fragment implements FragmentCommunicator {
@Named("default")
SharedPreferences mSharedPreferences;
@Inject
+ @Named("current_account")
+ SharedPreferences mCurrentAccountSharedPreferences;
+ @Inject
@Named("sort_type")
SharedPreferences mSortTypeSharedPreferences;
@Inject
@@ -458,7 +460,7 @@ public class PostFragment extends Fragment implements FragmentCommunicator {
mAdapter = new PostRecyclerViewAdapter(activity, this, mExecutor, mOauthRetrofit, mGfycatRetrofit,
mRedgifsRetrofit, mStreamableRetrofit, mCustomThemeWrapper, locale,
accessToken, accountName, postType, postLayout, true,
- mSharedPreferences, mNsfwAndSpoilerSharedPreferences, mPostHistorySharedPreferences,
+ mSharedPreferences, mCurrentAccountSharedPreferences, mNsfwAndSpoilerSharedPreferences, mPostHistorySharedPreferences,
mExoCreator, new PostRecyclerViewAdapter.Callback() {
@Override
public void typeChipClicked(int filter) {
@@ -535,7 +537,7 @@ public class PostFragment extends Fragment implements FragmentCommunicator {
mAdapter = new PostRecyclerViewAdapter(activity, this, mExecutor, mOauthRetrofit, mGfycatRetrofit,
mRedgifsRetrofit, mStreamableRetrofit, mCustomThemeWrapper, locale,
accessToken, accountName, postType, postLayout, displaySubredditName,
- mSharedPreferences, mNsfwAndSpoilerSharedPreferences, mPostHistorySharedPreferences,
+ mSharedPreferences, mCurrentAccountSharedPreferences, mNsfwAndSpoilerSharedPreferences, mPostHistorySharedPreferences,
mExoCreator, new PostRecyclerViewAdapter.Callback() {
@Override
public void typeChipClicked(int filter) {
@@ -606,7 +608,7 @@ public class PostFragment extends Fragment implements FragmentCommunicator {
mAdapter = new PostRecyclerViewAdapter(activity, this, mExecutor, mOauthRetrofit, mGfycatRetrofit,
mRedgifsRetrofit, mStreamableRetrofit, mCustomThemeWrapper, locale,
accessToken, accountName, postType, postLayout, true,
- mSharedPreferences, mNsfwAndSpoilerSharedPreferences, mPostHistorySharedPreferences,
+ mSharedPreferences, mCurrentAccountSharedPreferences, mNsfwAndSpoilerSharedPreferences, mPostHistorySharedPreferences,
mExoCreator, new PostRecyclerViewAdapter.Callback() {
@Override
public void typeChipClicked(int filter) {
@@ -671,7 +673,7 @@ public class PostFragment extends Fragment implements FragmentCommunicator {
mAdapter = new PostRecyclerViewAdapter(activity, this, mExecutor, mOauthRetrofit, mGfycatRetrofit,
mRedgifsRetrofit, mStreamableRetrofit, mCustomThemeWrapper, locale,
accessToken, accountName, postType, postLayout, true,
- mSharedPreferences, mNsfwAndSpoilerSharedPreferences, mPostHistorySharedPreferences,
+ mSharedPreferences, mCurrentAccountSharedPreferences, mNsfwAndSpoilerSharedPreferences, mPostHistorySharedPreferences,
mExoCreator, new PostRecyclerViewAdapter.Callback() {
@Override
public void typeChipClicked(int filter) {
@@ -732,7 +734,7 @@ public class PostFragment extends Fragment implements FragmentCommunicator {
mAdapter = new PostRecyclerViewAdapter(activity, this, mExecutor, mOauthRetrofit, mGfycatRetrofit,
mRedgifsRetrofit, mStreamableRetrofit, mCustomThemeWrapper, locale,
accessToken, accountName, postType, postLayout, true,
- mSharedPreferences, mNsfwAndSpoilerSharedPreferences, mPostHistorySharedPreferences,
+ mSharedPreferences, mCurrentAccountSharedPreferences, mNsfwAndSpoilerSharedPreferences, mPostHistorySharedPreferences,
mExoCreator, new PostRecyclerViewAdapter.Callback() {
@Override
public void typeChipClicked(int filter) {
@@ -792,7 +794,7 @@ public class PostFragment extends Fragment implements FragmentCommunicator {
mAdapter = new PostRecyclerViewAdapter(activity, this, mExecutor, mOauthRetrofit, mGfycatRetrofit,
mRedgifsRetrofit, mStreamableRetrofit, mCustomThemeWrapper, locale,
accessToken, accountName, postType, postLayout, true,
- mSharedPreferences, mNsfwAndSpoilerSharedPreferences, mPostHistorySharedPreferences,
+ mSharedPreferences, mCurrentAccountSharedPreferences, mNsfwAndSpoilerSharedPreferences, mPostHistorySharedPreferences,
mExoCreator, new PostRecyclerViewAdapter.Callback() {
@Override
public void typeChipClicked(int filter) {
@@ -849,7 +851,7 @@ public class PostFragment extends Fragment implements FragmentCommunicator {
mAdapter = new PostRecyclerViewAdapter(activity, this, mExecutor, mOauthRetrofit, mGfycatRetrofit,
mRedgifsRetrofit, mStreamableRetrofit, mCustomThemeWrapper, locale,
accessToken, accountName, postType, postLayout, true,
- mSharedPreferences, mNsfwAndSpoilerSharedPreferences, mPostHistorySharedPreferences,
+ mSharedPreferences, mCurrentAccountSharedPreferences, mNsfwAndSpoilerSharedPreferences, mPostHistorySharedPreferences,
mExoCreator, new PostRecyclerViewAdapter.Callback() {
@Override
public void typeChipClicked(int filter) {
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/ViewPostDetailFragment.java b/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/ViewPostDetailFragment.java
index 5821b2fd..d1cc9976 100644
--- a/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/ViewPostDetailFragment.java
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/ViewPostDetailFragment.java
@@ -1,9 +1,9 @@
package ml.docilealligator.infinityforreddit.fragments;
-import static im.ene.toro.media.PlaybackInfo.INDEX_UNSET;
-import static im.ene.toro.media.PlaybackInfo.TIME_UNSET;
import static ml.docilealligator.infinityforreddit.activities.CommentActivity.RETURN_EXTRA_COMMENT_DATA_KEY;
import static ml.docilealligator.infinityforreddit.activities.CommentActivity.WRITE_COMMENT_REQUEST_CODE;
+import static ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo.INDEX_UNSET;
+import static ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo.TIME_UNSET;
import android.annotation.SuppressLint;
import android.app.Activity;
@@ -63,9 +63,6 @@ import javax.inject.Named;
import butterknife.BindView;
import butterknife.ButterKnife;
-import im.ene.toro.exoplayer.ExoCreator;
-import im.ene.toro.media.PlaybackInfo;
-import im.ene.toro.media.VolumeInfo;
import ml.docilealligator.infinityforreddit.DeleteThing;
import ml.docilealligator.infinityforreddit.Flair;
import ml.docilealligator.infinityforreddit.FragmentCommunicator;
@@ -113,6 +110,9 @@ import ml.docilealligator.infinityforreddit.subreddit.SubredditData;
import ml.docilealligator.infinityforreddit.utils.APIUtils;
import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils;
import ml.docilealligator.infinityforreddit.utils.Utils;
+import ml.docilealligator.infinityforreddit.videoautoplay.ExoCreator;
+import ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo;
+import ml.docilealligator.infinityforreddit.videoautoplay.media.VolumeInfo;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
@@ -556,7 +556,7 @@ public class ViewPostDetailFragment extends Fragment implements FragmentCommunic
this, mExecutor, mCustomThemeWrapper, mRetrofit, mOauthRetrofit, mGfycatRetrofit,
mRedgifsRetrofit, mStreamableRetrofit, mRedditDataRoomDatabase, mGlide,
mSeparatePostAndComments, mAccessToken, mAccountName, mPost, mLocale,
- mSharedPreferences, mNsfwAndSpoilerSharedPreferences, mPostDetailsSharedPreferences,
+ mSharedPreferences, mCurrentAccountSharedPreferences, mNsfwAndSpoilerSharedPreferences, mPostDetailsSharedPreferences,
mExoCreator, post -> EventBus.getDefault().post(new PostUpdateEventToPostList(mPost, postListPosition)));
mCommentsAdapter = new CommentsRecyclerViewAdapter(activity,
this, mCustomThemeWrapper, mExecutor, mRetrofit, mOauthRetrofit,
@@ -1239,8 +1239,8 @@ public class ViewPostDetailFragment extends Fragment implements FragmentCommunic
mRetrofit, mOauthRetrofit, mGfycatRetrofit, mRedgifsRetrofit,
mStreamableRetrofit, mRedditDataRoomDatabase, mGlide, mSeparatePostAndComments,
mAccessToken, mAccountName, mPost, mLocale, mSharedPreferences,
- mNsfwAndSpoilerSharedPreferences, mPostDetailsSharedPreferences,
- mExoCreator,
+ mCurrentAccountSharedPreferences, mNsfwAndSpoilerSharedPreferences,
+ mPostDetailsSharedPreferences, mExoCreator,
post1 -> EventBus.getDefault().post(new PostUpdateEventToPostList(mPost, postListPosition)));
mCommentsAdapter = new CommentsRecyclerViewAdapter(activity,
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/utils/APIUtils.java b/app/src/main/java/ml/docilealligator/infinityforreddit/utils/APIUtils.java
index 927f564a..248c917b 100644
--- a/app/src/main/java/ml/docilealligator/infinityforreddit/utils/APIUtils.java
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/utils/APIUtils.java
@@ -1,5 +1,6 @@
package ml.docilealligator.infinityforreddit.utils;
+import android.content.Context;
import android.util.Base64;
import java.util.HashMap;
@@ -19,7 +20,7 @@ public class APIUtils {
public static final String API_UPLOAD_MEDIA_URI = "https://reddit-uploaded-media.s3-accelerate.amazonaws.com";
public static final String API_UPLOAD_VIDEO_URI = "https://reddit-uploaded-video.s3-accelerate.amazonaws.com";
public static final String GFYCAT_API_BASE_URI = "https://api.gfycat.com/v1/gfycats/";
- public static final String REDGIFS_API_BASE_URI = "https://api.redgifs.com/v2/gifs/";
+ public static final String REDGIFS_API_BASE_URI = "https://api.redgifs.com";
public static final String IMGUR_API_BASE_URI = "https://api.imgur.com/3/";
public static final String PUSHSHIFT_API_BASE_URI = "https://api.pushshift.io/";
public static final String REVEDDIT_API_BASE_URI = "https://api.reveddit.com/";
@@ -27,8 +28,11 @@ public class APIUtils {
public static final String STREAMABLE_API_BASE_URI = "https://api.streamable.com";
public static final String CLIENT_ID_KEY = "client_id";
+ public static final String CLIENT_SECRET_KEY = "client_secret";
public static final String CLIENT_ID = "NOe2iKrPPzwscA";
public static final String IMGUR_CLIENT_ID = "Client-ID cc671794e0ab397";
+ public static final String REDGIFS_CLIENT_ID = "1828d0bcc93-15ac-bde6-0005-d2ecbe8daab3";
+ public static final String REDGIFS_CLIENT_SECRET = "TJBlw7jRXW65NAGgFBtgZHu97WlzRXHYybK81sZ9dLM=";
public static final String RESPONSE_TYPE_KEY = "response_type";
public static final String RESPONSE_TYPE = "code";
public static final String STATE_KEY = "state";
@@ -48,6 +52,7 @@ public class APIUtils {
public static final String GRANT_TYPE_KEY = "grant_type";
public static final String GRANT_TYPE_REFRESH_TOKEN = "refresh_token";
+ public static final String GRANT_TYPE_CLIENT_CREDENTIALS = "client_credentials";
public static final String REFRESH_TOKEN_KEY = "refresh_token";
public static final String DIR_KEY = "dir";
@@ -111,6 +116,11 @@ public class APIUtils {
public static final String REFERER_KEY = "Referer";
public static final String REVEDDIT_REFERER = "https://www.reveddit.com/";
+ /*public static final String HOST_KEY = "Host";
+ public static final String REDGIFS_HOST = "api.redgifs.com";
+ public static final String CONTENT_TYPE_KEY = "Content-Type";
+ public static final String */
+
public static Map<String, String> getHttpBasicAuthHeader() {
Map<String, String> params = new HashMap<>();
String credentials = String.format("%s:%s", APIUtils.CLIENT_ID, "");
@@ -126,6 +136,12 @@ public class APIUtils {
return params;
}
+ public static Map<String, String> getRedgifsOAuthHeader(String redgifsAccessToken) {
+ Map<String, String> params = new HashMap<>();
+ params.put(APIUtils.AUTHORIZATION_KEY, APIUtils.AUTHORIZATION_BASE + redgifsAccessToken);
+ return params;
+ }
+
public static RequestBody getRequestBody(String s) {
return RequestBody.create(s, MediaType.parse("text/plain"));
}
@@ -137,4 +153,23 @@ public class APIUtils {
params.put(APIUtils.USER_AGENT_KEY, APIUtils.USER_AGENT);
return params;
}
+
+ public static String getRedgifsUserAgent(Context context) {
+ return APIUtils.USER_AGENT;
+ /*String versionName;
+ try {
+ String packageName = context.getPackageName();
+ PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
+ versionName = info.versionName;
+ } catch (Exception e) {
+ versionName = "?";
+ }
+ return "Toro ExoPlayer Extension, v3.7.0.2010003"
+ + "/"
+ + versionName
+ + " (Linux;Android "
+ + Build.VERSION.RELEASE
+ + ") "
+ + ExoPlayerLibraryInfo.VERSION_SLASHY;*/
+ }
}
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/utils/SharedPreferencesUtils.java b/app/src/main/java/ml/docilealligator/infinityforreddit/utils/SharedPreferencesUtils.java
index 3b431a42..29f42e4b 100644
--- a/app/src/main/java/ml/docilealligator/infinityforreddit/utils/SharedPreferencesUtils.java
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/utils/SharedPreferencesUtils.java
@@ -337,6 +337,7 @@ public class SharedPreferencesUtils {
public static final String ACCOUNT_NAME = "account_name";
public static final String ACCESS_TOKEN = "access_token";
public static final String ACCOUNT_IMAGE_URL = "account_image_url";
+ public static final String REDGIFS_ACCESS_TOKEN = "redgifs_access_token";
public static final String NAVIGATION_DRAWER_SHARED_PREFERENCES_FILE = "ml.docilealligator.infinityforreddit.navigation_drawer";
public static final String COLLAPSE_ACCOUNT_SECTION = "collapse_account_section";
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/AdsExoPlayerViewHelper.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/AdsExoPlayerViewHelper.java
new file mode 100644
index 00000000..22a63afc
--- /dev/null
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/AdsExoPlayerViewHelper.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (c) 2018 Nam Nguyen, nam@ene.im
+ *
+ * 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 ml.docilealligator.infinityforreddit.videoautoplay;
+
+import android.content.Context;
+import android.net.Uri;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.google.android.exoplayer2.source.ads.AdsLoader;
+import com.google.android.exoplayer2.ui.PlayerView;
+
+import ml.docilealligator.infinityforreddit.videoautoplay.annotations.Beta;
+
+/**
+ * A {@link ToroPlayerHelper} to integrate ExoPlayer IMA Extension. Work together with {@link
+ * AdsPlayable}.
+ *
+ * @author eneim (2018/08/22).
+ * @since 3.6.0.2802
+ */
+@SuppressWarnings("unused") @Beta //
+public class AdsExoPlayerViewHelper extends ExoPlayerViewHelper {
+
+ static class DefaultAdViewProvider implements AdsLoader.AdViewProvider {
+
+ @NonNull final ViewGroup viewGroup;
+
+ DefaultAdViewProvider(@NonNull ViewGroup viewGroup) {
+ this.viewGroup = viewGroup;
+ }
+
+ @Override public ViewGroup getAdViewGroup() {
+ return this.viewGroup;
+ }
+
+ @Override public View[] getAdOverlayViews() {
+ return new View[0];
+ }
+ }
+
+ private static AdsPlayable createPlayable( ///
+ ToroPlayer player, //
+ ExoCreator creator, //
+ Uri contentUri, //
+ String fileExt, //
+ AdsLoader adsLoader, //
+ AdsLoader.AdViewProvider adViewProvider //
+ ) {
+ return new AdsPlayable(creator, contentUri, fileExt, player, adsLoader, adViewProvider);
+ }
+
+ private static AdsPlayable createPlayable( //
+ ToroPlayer player, //
+ Config config, //
+ Uri contentUri, //
+ String fileExt, //
+ AdsLoader adsLoader, //
+ AdsLoader.AdViewProvider adViewProvider //
+ ) {
+ Context context = player.getPlayerView().getContext();
+ return createPlayable(player, ToroExo.with(context).getCreator(config), contentUri, fileExt,
+ adsLoader, adViewProvider);
+ }
+
+ private static AdsPlayable createPlayable( //
+ ToroPlayer player, //
+ Uri contentUri, //
+ String fileExt, //
+ AdsLoader adsLoader, //
+ AdsLoader.AdViewProvider adViewProvider //
+ ) {
+ Context context = player.getPlayerView().getContext();
+ return createPlayable(player, ToroExo.with(context).getDefaultCreator(), contentUri, fileExt,
+ adsLoader, adViewProvider);
+ }
+
+ // Neither ExoCreator nor Config are provided.
+
+ /**
+ * Create new {@link AdsExoPlayerViewHelper} for a {@link ToroPlayer} and {@link AdsLoader}.
+ *
+ * @param adContainer if {@code null} then overlay of {@link PlayerView} will be used.
+ */
+ @Deprecated
+ public AdsExoPlayerViewHelper( //
+ @NonNull ToroPlayer player, //
+ @NonNull Uri uri, //
+ @Nullable String fileExt, //
+ @NonNull AdsLoader adsLoader, //
+ @Nullable ViewGroup adContainer //
+ ) {
+ super(player,
+ createPlayable(player, uri, fileExt, adsLoader,
+ adContainer != null ? new DefaultAdViewProvider(adContainer) : null));
+ }
+
+ public AdsExoPlayerViewHelper( //
+ @NonNull ToroPlayer player, //
+ @NonNull Uri uri, //
+ @Nullable String fileExt, //
+ @NonNull AdsLoader adsLoader, //
+ @Nullable ViewGroup adContainer, // will be ignored
+ @Nullable AdsLoader.AdViewProvider adViewProvider //
+ ) {
+ super(player, createPlayable(player, uri, fileExt, adsLoader, adViewProvider));
+ }
+
+ // ExoCreator is provided.
+
+ /**
+ * Create new {@link AdsExoPlayerViewHelper} for a {@link ToroPlayer} and {@link AdsLoader}.
+ *
+ * @param adContainer if {@code null} then overlay of {@link PlayerView} will be used.
+ */
+ @Deprecated
+ public AdsExoPlayerViewHelper( //
+ @NonNull ToroPlayer player, //
+ @NonNull Uri uri, //
+ @Nullable String fileExt, //
+ @NonNull AdsLoader adsLoader, //
+ @Nullable ViewGroup adContainer, //
+ @NonNull ExoCreator creator //
+ ) {
+ super(player, createPlayable(player, creator, uri, fileExt, adsLoader,
+ adContainer != null ? new DefaultAdViewProvider(adContainer) : null));
+ }
+
+ public AdsExoPlayerViewHelper( //
+ @NonNull ToroPlayer player, //
+ @NonNull Uri uri, //
+ @Nullable String fileExt, //
+ @NonNull AdsLoader adsLoader, //
+ @Nullable ViewGroup adContainer, // will be ignored
+ @Nullable AdsLoader.AdViewProvider adViewProvider, //
+ @NonNull ExoCreator creator //
+ ) {
+ super(player, createPlayable(player, creator, uri, fileExt, adsLoader, adViewProvider));
+ }
+ // Config is provided.
+
+ /**
+ * Create new {@link AdsExoPlayerViewHelper} for a {@link ToroPlayer} and {@link AdsLoader}.
+ *
+ * @param adContainer if {@code null} then overlay of {@link PlayerView} will be used.
+ */
+ @Deprecated
+ public AdsExoPlayerViewHelper( //
+ @NonNull ToroPlayer player, //
+ @NonNull Uri uri, //
+ @Nullable String fileExt, //
+ @NonNull AdsLoader adsLoader, //
+ @Nullable ViewGroup adContainer, //
+ @NonNull Config config //
+ ) {
+ super(player, createPlayable(player, config, uri, fileExt, adsLoader,
+ adContainer != null ? new DefaultAdViewProvider(adContainer) : null));
+ }
+
+ public AdsExoPlayerViewHelper( //
+ @NonNull ToroPlayer player, //
+ @NonNull Uri uri, //
+ @Nullable String fileExt, //
+ @NonNull AdsLoader adsLoader, //
+ @Nullable ViewGroup adContainer, // will be ignored
+ @Nullable AdsLoader.AdViewProvider adViewProvider, //
+ @NonNull Config config //
+ ) {
+ super(player, createPlayable(player, config, uri, fileExt, adsLoader, adViewProvider));
+ }
+}
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/AdsPlayable.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/AdsPlayable.java
new file mode 100644
index 00000000..666b3815
--- /dev/null
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/AdsPlayable.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (c) 2018 Nam Nguyen, nam@ene.im
+ *
+ * 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 ml.docilealligator.infinityforreddit.videoautoplay;
+
+import android.net.Uri;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.ads.AdsLoader;
+import com.google.android.exoplayer2.source.ads.AdsMediaSource;
+import com.google.android.exoplayer2.ui.PlayerView;
+
+import ml.docilealligator.infinityforreddit.videoautoplay.annotations.Beta;
+
+/**
+ * A {@link Playable} that is able to integrate with {@link AdsLoader}.
+ *
+ * @author eneim (2018/08/22).
+ * @since 3.6.0.2802
+ */
+@Beta //
+public class AdsPlayable extends ExoPlayable {
+
+ static class FactoryImpl implements AdsMediaSource.MediaSourceFactory {
+
+ @NonNull final ExoCreator creator;
+ @NonNull final ToroPlayer player;
+
+ FactoryImpl(@NonNull ExoCreator creator, @NonNull ToroPlayer player) {
+ this.creator = creator;
+ this.player = player;
+ }
+
+ @Override public MediaSource createMediaSource(Uri uri) {
+ return this.creator.createMediaSource(uri, null);
+ }
+
+ @Override public int[] getSupportedTypes() {
+ // IMA does not support Smooth Streaming ads.
+ return new int[] { C.TYPE_DASH, C.TYPE_HLS, C.TYPE_OTHER };
+ }
+ }
+
+ @NonNull private final AdsLoader adsLoader;
+ @NonNull private final FactoryImpl factory;
+ @Nullable private final AdsLoader.AdViewProvider adViewProvider;
+
+ /**
+ * @deprecated Use the constructors that use {@link AdsLoader.AdViewProvider} instead.
+ */
+ @Deprecated
+ public AdsPlayable(ExoCreator creator, Uri uri, String fileExt, ToroPlayer player,
+ @NonNull AdsLoader adsLoader, @Nullable final ViewGroup adsContainer) {
+ super(creator, uri, fileExt);
+ this.adsLoader = adsLoader;
+ this.adViewProvider = adsContainer == null ? null
+ : new AdsExoPlayerViewHelper.DefaultAdViewProvider(adsContainer);
+ this.factory = new FactoryImpl(this.creator, player);
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ public AdsPlayable(ExoCreator creator, Uri uri, String fileExt, ToroPlayer player,
+ @NonNull AdsLoader adsLoader, @Nullable AdsLoader.AdViewProvider adViewProvider) {
+ super(creator, uri, fileExt);
+ this.adsLoader = adsLoader;
+ this.adViewProvider = adViewProvider;
+ this.factory = new FactoryImpl(this.creator, player);
+ }
+
+ @CallSuper
+ @Override public void prepare(boolean prepareSource) {
+ this.mediaSource = createAdsMediaSource(creator, mediaUri, fileExt, //
+ factory.player, adsLoader, adViewProvider, factory);
+ super.prepare(prepareSource);
+ }
+
+ @Override protected void beforePrepareMediaSource() {
+ super.beforePrepareMediaSource();
+ adsLoader.setPlayer(player);
+ }
+
+ @Override public void release() {
+ adsLoader.setPlayer(null);
+ super.release();
+ }
+
+ private static MediaSource createAdsMediaSource(ExoCreator creator, Uri uri, String fileExt,
+ ToroPlayer player, AdsLoader adsLoader, AdsLoader.AdViewProvider adViewProvider,
+ AdsMediaSource.MediaSourceFactory factory) {
+ MediaSource original = creator.createMediaSource(uri, fileExt);
+ View playerView = player.getPlayerView();
+ if (!(playerView instanceof PlayerView)) {
+ throw new IllegalArgumentException("Require PlayerView");
+ }
+
+ return new AdsMediaSource(original, factory, adsLoader,
+ adViewProvider == null ? (PlayerView) playerView : adViewProvider);
+ }
+}
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/BaseMeter.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/BaseMeter.java
new file mode 100644
index 00000000..539919b0
--- /dev/null
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/BaseMeter.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2018 Nam Nguyen, nam@ene.im
+ *
+ * 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 ml.docilealligator.infinityforreddit.videoautoplay;
+
+import android.os.Handler;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.google.android.exoplayer2.upstream.BandwidthMeter;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
+import com.google.android.exoplayer2.upstream.TransferListener;
+
+/**
+ * Abstract the {@link DefaultBandwidthMeter}, provide a wider use.
+ *
+ * @author eneim (2018/01/26).
+ * @since 3.4.0
+ */
+
+@SuppressWarnings("WeakerAccess") //
+public final class BaseMeter<T extends BandwidthMeter> implements BandwidthMeter, TransferListener {
+
+ @NonNull protected final T bandwidthMeter;
+ @NonNull protected final TransferListener transferListener;
+
+ /**
+ * @deprecated use {@link #BaseMeter(BandwidthMeter)} instead.
+ */
+ @SuppressWarnings({ "unused" }) //
+ @Deprecated //
+ public BaseMeter(@NonNull T bandwidthMeter, @NonNull TransferListener transferListener) {
+ this(bandwidthMeter);
+ }
+
+ public BaseMeter(@NonNull T bandwidthMeter) {
+ this.bandwidthMeter = ToroUtil.checkNotNull(bandwidthMeter);
+ this.transferListener = ToroUtil.checkNotNull(this.bandwidthMeter.getTransferListener());
+ }
+
+ @Override public long getBitrateEstimate() {
+ return bandwidthMeter.getBitrateEstimate();
+ }
+
+ @Override @Nullable public TransferListener getTransferListener() {
+ return bandwidthMeter.getTransferListener();
+ }
+
+ @Override public void addEventListener(Handler eventHandler, EventListener eventListener) {
+ bandwidthMeter.addEventListener(eventHandler, eventListener);
+ }
+
+ @Override public void removeEventListener(EventListener eventListener) {
+ bandwidthMeter.removeEventListener(eventListener);
+ }
+
+ @Override
+ public void onTransferInitializing(DataSource source, DataSpec dataSpec, boolean isNetwork) {
+ transferListener.onTransferInitializing(source, dataSpec, isNetwork);
+ }
+
+ @Override public void onTransferStart(DataSource source, DataSpec dataSpec, boolean isNetwork) {
+ transferListener.onTransferStart(source, dataSpec, isNetwork);
+ }
+
+ @Override public void onBytesTransferred(DataSource source, DataSpec dataSpec, boolean isNetwork,
+ int bytesTransferred) {
+ transferListener.onBytesTransferred(source, dataSpec, isNetwork, bytesTransferred);
+ }
+
+ @Override public void onTransferEnd(DataSource source, DataSpec dataSpec, boolean isNetwork) {
+ transferListener.onTransferEnd(source, dataSpec, isNetwork);
+ }
+}
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/CacheManager.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/CacheManager.java
new file mode 100644
index 00000000..04342641
--- /dev/null
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/CacheManager.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2017 Nam Nguyen, nam@ene.im
+ *
+ * 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 ml.docilealligator.infinityforreddit.videoautoplay;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.LinkedHashMap;
+
+/**
+ * {@link CacheManager} is a helper interface used by {@link Container} to manage the
+ * {@link PlaybackInfo} of {@link ToroPlayer}s. For each {@link ToroPlayer},
+ * {@link CacheManager} will ask for a unique key for its {@link PlaybackInfo} cache.
+ * {@link Container} uses a {@link LinkedHashMap} to implement the caching mechanism, so
+ * {@link CacheManager} must provide keys which are uniquely distinguished by
+ * {@link Object#equals(Object)}.
+ *
+ * @author eneim (7/5/17).
+ */
+public interface CacheManager {
+
+ /**
+ * Get the unique key for the {@link ToroPlayer} of a specific order. Note that this key must
+ * also be managed by {@link RecyclerView.Adapter} so that it prevents the uniqueness at data
+ * change events.
+ *
+ * @param order order of the {@link ToroPlayer}.
+ * @return the unique key of the {@link ToroPlayer}.
+ */
+ @Nullable Object getKeyForOrder(int order);
+
+ /**
+ * Get the order of a specific key value. Returning a {@code null} order value here will tell
+ * {@link Container} to ignore this key's cache order.
+ *
+ * @param key the key value to lookup.
+ * @return the order of the {@link ToroPlayer} whose unique key value is key.
+ */
+ @Nullable Integer getOrderForKey(@NonNull Object key);
+
+ /**
+ * A built-in {@link CacheManager} that use the order as the unique key. Note that this is not
+ * data-changes-proof. Which means that after data change events, the map may need to be
+ * updated.
+ */
+ CacheManager DEFAULT = new CacheManager() {
+ @Override public Object getKeyForOrder(int order) {
+ return order;
+ }
+
+ @Override public Integer getOrderForKey(@NonNull Object key) {
+ return key instanceof Integer ? (Integer) key : null;
+ }
+ };
+}
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/Config.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/Config.java
new file mode 100644
index 00000000..7fe095d4
--- /dev/null
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/Config.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (c) 2018 Nam Nguyen, nam@ene.im
+ *
+ * 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 ml.docilealligator.infinityforreddit.videoautoplay;
+
+import static com.google.android.exoplayer2.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF;
+import static ml.docilealligator.infinityforreddit.videoautoplay.ToroUtil.checkNotNull;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.util.ObjectsCompat;
+
+import com.google.android.exoplayer2.DefaultLoadControl;
+import com.google.android.exoplayer2.DefaultRenderersFactory.ExtensionRendererMode;
+import com.google.android.exoplayer2.LoadControl;
+import com.google.android.exoplayer2.SimpleExoPlayer;
+import com.google.android.exoplayer2.drm.DrmSessionManager;
+import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
+import com.google.android.exoplayer2.upstream.cache.Cache;
+
+import ml.docilealligator.infinityforreddit.videoautoplay.annotations.Beta;
+
+/**
+ * Necessary configuration for {@link ExoCreator} to produces {@link SimpleExoPlayer} and
+ * {@link MediaSource}. Instance of this class must be construct using {@link Builder}.
+ *
+ * @author eneim (2018/01/23).
+ * @since 3.4.0
+ */
+
+@SuppressWarnings("SimplifiableIfStatement") //
+public final class Config {
+
+ @Nullable
+ private final Context context;
+
+ // primitive flags
+ @ExtensionRendererMode final int extensionMode;
+
+ // NonNull options
+ @NonNull final BaseMeter meter;
+ @NonNull final LoadControl loadControl;
+ @NonNull final MediaSourceBuilder mediaSourceBuilder;
+
+ // Nullable options
+ @Nullable final DrmSessionManager<FrameworkMediaCrypto> drmSessionManager;
+ @Nullable final Cache cache; // null by default
+ // If null, ExoCreator must come up with a default one.
+ // This is to help customizing the Data source, for example using OkHttp extension.
+ @Nullable final DataSource.Factory dataSourceFactory;
+
+ @SuppressWarnings("WeakerAccess") //
+ Config(@Nullable Context context, int extensionMode, @NonNull BaseMeter meter,
+ @NonNull LoadControl loadControl,
+ @Nullable DataSource.Factory dataSourceFactory,
+ @NonNull MediaSourceBuilder mediaSourceBuilder,
+ @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager, @Nullable Cache cache) {
+ this.context = context != null ? context.getApplicationContext() : null;
+ this.extensionMode = extensionMode;
+ this.meter = meter;
+ this.loadControl = loadControl;
+ this.dataSourceFactory = dataSourceFactory;
+ this.mediaSourceBuilder = mediaSourceBuilder;
+ this.drmSessionManager = drmSessionManager;
+ this.cache = cache;
+ }
+
+ @Override public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Config config = (Config) o;
+
+ if (extensionMode != config.extensionMode) return false;
+ if (!meter.equals(config.meter)) return false;
+ if (!loadControl.equals(config.loadControl)) return false;
+ if (!mediaSourceBuilder.equals(config.mediaSourceBuilder)) return false;
+ if (!ObjectsCompat.equals(drmSessionManager, config.drmSessionManager)) return false;
+ if (!ObjectsCompat.equals(cache, config.cache)) return false;
+ return ObjectsCompat.equals(dataSourceFactory, config.dataSourceFactory);
+ }
+
+ @Override public int hashCode() {
+ int result = extensionMode;
+ result = 31 * result + meter.hashCode();
+ result = 31 * result + loadControl.hashCode();
+ result = 31 * result + mediaSourceBuilder.hashCode();
+ result = 31 * result + (drmSessionManager != null ? drmSessionManager.hashCode() : 0);
+ result = 31 * result + (cache != null ? cache.hashCode() : 0);
+ result = 31 * result + (dataSourceFactory != null ? dataSourceFactory.hashCode() : 0);
+ return result;
+ }
+
+ @SuppressWarnings("unused") public Builder newBuilder() {
+ return new Builder(context).setCache(this.cache)
+ .setDrmSessionManager(this.drmSessionManager)
+ .setExtensionMode(this.extensionMode)
+ .setLoadControl(this.loadControl)
+ .setMediaSourceBuilder(this.mediaSourceBuilder)
+ .setMeter(this.meter);
+ }
+
+ /// Builder
+ @SuppressWarnings({ "unused", "WeakerAccess" }) //
+ public static final class Builder {
+
+ @Nullable // only for backward compatibility
+ final Context context;
+
+ /**
+ * @deprecated Use the constructor with nonnull {@link Context} instead.
+ */
+ @Deprecated
+ public Builder() {
+ this(null);
+ }
+
+ public Builder(@Nullable Context context) {
+ this.context = context != null ? context.getApplicationContext() : null;
+ DefaultBandwidthMeter bandwidthMeter =
+ new DefaultBandwidthMeter.Builder(this.context).build();
+ meter = new BaseMeter<>(bandwidthMeter);
+ }
+
+ @ExtensionRendererMode private int extensionMode = EXTENSION_RENDERER_MODE_OFF;
+ private BaseMeter meter;
+ private LoadControl loadControl = new DefaultLoadControl();
+ private DataSource.Factory dataSourceFactory = null;
+ private MediaSourceBuilder mediaSourceBuilder = MediaSourceBuilder.DEFAULT;
+ private DrmSessionManager<FrameworkMediaCrypto> drmSessionManager = null;
+ private Cache cache = null;
+
+ public Builder setExtensionMode(@ExtensionRendererMode int extensionMode) {
+ this.extensionMode = extensionMode;
+ return this;
+ }
+
+ public Builder setMeter(@NonNull BaseMeter meter) {
+ this.meter = checkNotNull(meter, "Need non-null BaseMeter");
+ return this;
+ }
+
+ public Builder setLoadControl(@NonNull LoadControl loadControl) {
+ this.loadControl = checkNotNull(loadControl, "Need non-null LoadControl");
+ return this;
+ }
+
+ // Option is Nullable, but if user customize this, it must be a Nonnull one.
+ public Builder setDataSourceFactory(@NonNull DataSource.Factory dataSourceFactory) {
+ this.dataSourceFactory = checkNotNull(dataSourceFactory);
+ return this;
+ }
+
+ public Builder setMediaSourceBuilder(@NonNull MediaSourceBuilder mediaSourceBuilder) {
+ this.mediaSourceBuilder =
+ checkNotNull(mediaSourceBuilder, "Need non-null MediaSourceBuilder");
+ return this;
+ }
+
+ @Beta //
+ public Builder setDrmSessionManager(
+ @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {
+ this.drmSessionManager = drmSessionManager;
+ return this;
+ }
+
+ public Builder setCache(@Nullable Cache cache) {
+ this.cache = cache;
+ return this;
+ }
+
+ public Config build() {
+ return new Config(context, extensionMode, meter, loadControl, dataSourceFactory,
+ mediaSourceBuilder, drmSessionManager, cache);
+ }
+ }
+}
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/DefaultExoCreator.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/DefaultExoCreator.java
new file mode 100644
index 00000000..af3bbd84
--- /dev/null
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/DefaultExoCreator.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (c) 2018 Nam Nguyen, nam@ene.im
+ *
+ * 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 ml.docilealligator.infinityforreddit.videoautoplay;
+
+import static ml.docilealligator.infinityforreddit.videoautoplay.ToroExo.with;
+import static ml.docilealligator.infinityforreddit.videoautoplay.ToroUtil.checkNotNull;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.Handler;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.google.android.exoplayer2.DefaultRenderersFactory;
+import com.google.android.exoplayer2.LoadControl;
+import com.google.android.exoplayer2.RenderersFactory;
+import com.google.android.exoplayer2.SimpleExoPlayer;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.MediaSourceEventListener;
+import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
+import com.google.android.exoplayer2.trackselection.TrackSelector;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
+import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
+import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
+import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory;
+import com.google.android.exoplayer2.util.Util;
+
+import java.io.IOException;
+
+/**
+ * Usage: use this as-it or inheritance.
+ *
+ * @author eneim (2018/02/04).
+ * @since 3.4.0
+ */
+
+@SuppressWarnings({ "unused", "WeakerAccess" }) //
+public class DefaultExoCreator implements ExoCreator, MediaSourceEventListener {
+
+ final ToroExo toro; // per application
+ final Config config;
+ private final TrackSelector trackSelector; // 'maybe' stateless
+ private final LoadControl loadControl; // stateless
+ private final MediaSourceBuilder mediaSourceBuilder; // stateless
+ private final RenderersFactory renderersFactory; // stateless
+ private final DataSource.Factory mediaDataSourceFactory; // stateless
+ private final DataSource.Factory manifestDataSourceFactory; // stateless
+
+ public DefaultExoCreator(@NonNull ToroExo toro, @NonNull Config config) {
+ this.toro = checkNotNull(toro);
+ this.config = checkNotNull(config);
+ trackSelector = new DefaultTrackSelector();
+ loadControl = config.loadControl;
+ mediaSourceBuilder = config.mediaSourceBuilder;
+
+ DefaultRenderersFactory tempFactory = new DefaultRenderersFactory(this.toro.context);
+ tempFactory.setExtensionRendererMode(config.extensionMode);
+ renderersFactory = tempFactory;
+
+ DataSource.Factory baseFactory = config.dataSourceFactory;
+ if (baseFactory == null) {
+ baseFactory = new DefaultHttpDataSourceFactory(toro.appName, config.meter);
+ }
+ DataSource.Factory factory = new DefaultDataSourceFactory(this.toro.context, //
+ config.meter, baseFactory);
+ if (config.cache != null) factory = new CacheDataSourceFactory(config.cache, factory);
+ mediaDataSourceFactory = factory;
+ manifestDataSourceFactory = new DefaultDataSourceFactory(this.toro.context, this.toro.appName);
+ }
+
+ public DefaultExoCreator(Context context, Config config) {
+ this(with(context), config);
+ }
+
+ @SuppressWarnings("SimplifiableIfStatement")
+ @Override public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ DefaultExoCreator that = (DefaultExoCreator) o;
+
+ if (!toro.equals(that.toro)) return false;
+ if (!trackSelector.equals(that.trackSelector)) return false;
+ if (!loadControl.equals(that.loadControl)) return false;
+ if (!mediaSourceBuilder.equals(that.mediaSourceBuilder)) return false;
+ if (!renderersFactory.equals(that.renderersFactory)) return false;
+ if (!mediaDataSourceFactory.equals(that.mediaDataSourceFactory)) return false;
+ return manifestDataSourceFactory.equals(that.manifestDataSourceFactory);
+ }
+
+ @Override public int hashCode() {
+ int result = toro.hashCode();
+ result = 31 * result + trackSelector.hashCode();
+ result = 31 * result + loadControl.hashCode();
+ result = 31 * result + mediaSourceBuilder.hashCode();
+ result = 31 * result + renderersFactory.hashCode();
+ result = 31 * result + mediaDataSourceFactory.hashCode();
+ result = 31 * result + manifestDataSourceFactory.hashCode();
+ return result;
+ }
+
+ final TrackSelector getTrackSelector() {
+ return trackSelector;
+ }
+
+ @Nullable @Override public Context getContext() {
+ return toro.context;
+ }
+
+ @NonNull @Override public SimpleExoPlayer createPlayer() {
+ return new ToroExoPlayer(toro.context, renderersFactory, trackSelector, loadControl,
+ new DefaultBandwidthMeter.Builder(toro.context).build(), config.drmSessionManager,
+ Util.getLooper());
+ }
+
+ @NonNull @Override public MediaSource createMediaSource(@NonNull Uri uri, String fileExt) {
+ return mediaSourceBuilder.buildMediaSource(this.toro.context, uri, fileExt, new Handler(),
+ manifestDataSourceFactory, mediaDataSourceFactory, this);
+ }
+
+ @NonNull @Override //
+ public Playable createPlayable(@NonNull Uri uri, String fileExt) {
+ return new PlayableImpl(this, uri, fileExt);
+ }
+
+ /// MediaSourceEventListener
+
+ @Override
+ public void onLoadStarted(int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId,
+ LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {
+ // no-ops
+ }
+
+ @Override
+ public void onLoadCompleted(int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId,
+ LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {
+ // no-ops
+ }
+
+ @Override
+ public void onLoadCanceled(int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId,
+ LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {
+ // no-ops
+ }
+
+ @Override
+ public void onLoadError(int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId,
+ LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData, IOException error,
+ boolean wasCanceled) {
+ // no-ops
+ }
+
+ @Override public void onReadingStarted(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) {
+ // no-ops
+ }
+
+ @Override
+ public void onUpstreamDiscarded(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId,
+ MediaLoadData mediaLoadData) {
+ // no-ops
+ }
+
+ @Override public void onDownstreamFormatChanged(int windowIndex,
+ @Nullable MediaSource.MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) {
+ // no-ops
+ }
+
+ @Override
+ public void onMediaPeriodCreated(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) {
+ // no-ops
+ }
+
+ @Override
+ public void onMediaPeriodReleased(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) {
+ // no-ops
+ }
+}
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ExoCreator.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ExoCreator.java
new file mode 100644
index 00000000..c4319493
--- /dev/null
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ExoCreator.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2018 Nam Nguyen, nam@ene.im
+ *
+ * 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 ml.docilealligator.infinityforreddit.videoautoplay;
+
+import android.content.Context;
+import android.net.Uri;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.google.android.exoplayer2.SimpleExoPlayer;
+import com.google.android.exoplayer2.source.MediaSource;
+
+/**
+ * A simple interface whose implementation helps Client to easily create {@link SimpleExoPlayer}
+ * instance, {@link MediaSource} instance or specifically a {@link Playable} instance.
+ *
+ * Most of the time, Client just needs to request for a {@link Playable} for a specific Uri.
+ *
+ * @author eneim (2018/02/04).
+ * @since 3.4.0
+ */
+
+public interface ExoCreator {
+
+ String TAG = "ToroExo:Creator";
+
+ /**
+ * Return current Application context used in {@link ToroExo}. An {@link ExoCreator} must be used
+ * within Application scope.
+ */
+ @Nullable Context getContext();
+
+ /**
+ * Create a new {@link SimpleExoPlayer} instance. This method should always create new instance of
+ * {@link SimpleExoPlayer}, but client should use {@link ExoCreator} indirectly via
+ * {@link ToroExo}.
+ *
+ * @return a new {@link SimpleExoPlayer} instance.
+ */
+ @NonNull SimpleExoPlayer createPlayer();
+
+ /**
+ * Create a {@link MediaSource} from media {@link Uri}.
+ *
+ * @param uri the media {@link Uri}.
+ * @param fileExt the optional (File) extension of the media Uri.
+ * @return a {@link MediaSource} for media {@link Uri}.
+ */
+ @NonNull MediaSource createMediaSource(@NonNull Uri uri, @Nullable String fileExt);
+
+ // Client just needs the method below to work with Toro, but I prepare both 2 above for custom use-cases.
+
+ /**
+ * Create a {@link Playable} for a media {@link Uri}. Client should always use this method for
+ * quick and simple setup. Only use {@link #createMediaSource(Uri, String)} and/or
+ * {@link #createPlayer()} when necessary.
+ *
+ * @param uri the media {@link Uri}.
+ * @param fileExt the optional (File) extension of the media Uri.
+ * @return the {@link Playable} to manage the media {@link Uri}.
+ */
+ @NonNull
+ Playable createPlayable(@NonNull Uri uri, @Nullable String fileExt);
+}
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ExoPlayable.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ExoPlayable.java
new file mode 100644
index 00000000..a159ea17
--- /dev/null
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ExoPlayable.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright (c) 2018 Nam Nguyen, nam@ene.im
+ *
+ * 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 ml.docilealligator.infinityforreddit.videoautoplay;
+
+import static com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS;
+
+import static ml.docilealligator.infinityforreddit.videoautoplay.ToroExo.toro;
+
+import android.net.Uri;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.SimpleExoPlayer;
+import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer;
+import com.google.android.exoplayer2.mediacodec.MediaCodecUtil;
+import com.google.android.exoplayer2.source.BehindLiveWindowException;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
+import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.trackselection.TrackSelector;
+import com.google.android.exoplayer2.ui.PlayerView;
+
+import ml.docilealligator.infinityforreddit.R;
+
+/**
+ * Making {@link Playable} extensible. This can be used with custom {@link ExoCreator}. Extending
+ * this class must make sure the re-usability of the implementation.
+ *
+ * @author eneim (2018/02/26).
+ * @since 3.4.0
+ */
+
+@SuppressWarnings("WeakerAccess")
+public class ExoPlayable extends PlayableImpl {
+
+ @SuppressWarnings("unused") private static final String TAG = "ToroExo:Playable";
+
+ private EventListener listener;
+
+ // Adapt from ExoPlayer demo.
+ protected boolean inErrorState = false;
+ protected TrackGroupArray lastSeenTrackGroupArray;
+
+ /**
+ * Construct an instance of {@link ExoPlayable} from an {@link ExoCreator} and {@link Uri}. The
+ * {@link ExoCreator} is used to request {@link SimpleExoPlayer} instance, while {@link Uri}
+ * defines the media to play.
+ *
+ * @param creator the {@link ExoCreator} instance.
+ * @param uri the {@link Uri} of the media.
+ * @param fileExt the custom extension of the media Uri.
+ */
+ public ExoPlayable(ExoCreator creator, Uri uri, String fileExt) {
+ super(creator, uri, fileExt);
+ }
+
+ @Override public void prepare(boolean prepareSource) {
+ if (listener == null) {
+ listener = new Listener();
+ super.addEventListener(listener);
+ }
+ super.prepare(prepareSource);
+ this.lastSeenTrackGroupArray = null;
+ this.inErrorState = false;
+ }
+
+ @Override public void setPlayerView(@Nullable PlayerView playerView) {
+ // This will also clear these flags
+ if (playerView != this.playerView) {
+ this.lastSeenTrackGroupArray = null;
+ this.inErrorState = false;
+ }
+ super.setPlayerView(playerView);
+ }
+
+ @Override public void reset() {
+ super.reset();
+ this.lastSeenTrackGroupArray = null;
+ this.inErrorState = false;
+ }
+
+ @Override public void release() {
+ if (listener != null) {
+ super.removeEventListener(listener);
+ listener = null;
+ }
+ super.release();
+ this.lastSeenTrackGroupArray = null;
+ this.inErrorState = false;
+ }
+
+ @SuppressWarnings({ "unused" }) //
+ protected void onErrorMessage(@NonNull String message) {
+ // Sub class can have custom reaction about the error here, including not to show this toast
+ // (by not calling super.onErrorMessage(message)).
+ if (this.errorListeners.size() > 0) {
+ this.errorListeners.onError(new RuntimeException(message));
+ } else if (playerView != null) {
+ Toast.makeText(playerView.getContext(), message, Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ class Listener extends DefaultEventListener {
+
+ @Override
+ public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
+ super.onTracksChanged(trackGroups, trackSelections);
+ if (trackGroups == lastSeenTrackGroupArray) return;
+ lastSeenTrackGroupArray = trackGroups;
+ if (!(creator instanceof DefaultExoCreator)) return;
+ TrackSelector selector = ((DefaultExoCreator) creator).getTrackSelector();
+ if (selector instanceof DefaultTrackSelector) {
+ MappedTrackInfo trackInfo = ((DefaultTrackSelector) selector).getCurrentMappedTrackInfo();
+ if (trackInfo != null) {
+ if (trackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO) == RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
+ onErrorMessage(toro.getString(R.string.error_unsupported_video));
+ }
+
+ if (trackInfo.getTypeSupport(C.TRACK_TYPE_AUDIO) == RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
+ onErrorMessage(toro.getString(R.string.error_unsupported_audio));
+ }
+ }
+ }
+ }
+
+ @Override public void onPlayerError(ExoPlaybackException error) {
+ /// Adapt from ExoPlayer Demo
+ String errorString = null;
+ if (error.type == ExoPlaybackException.TYPE_RENDERER) {
+ Exception cause = error.getRendererException();
+ if (cause instanceof MediaCodecRenderer.DecoderInitializationException) {
+ // Special case for decoder initialization failures.
+ MediaCodecRenderer.DecoderInitializationException decoderInitializationException =
+ (MediaCodecRenderer.DecoderInitializationException) cause;
+ if (decoderInitializationException.decoderName == null) {
+ if (decoderInitializationException.getCause() instanceof MediaCodecUtil.DecoderQueryException) {
+ errorString = toro.getString(R.string.error_querying_decoders);
+ } else if (decoderInitializationException.secureDecoderRequired) {
+ errorString = toro.getString(R.string.error_no_secure_decoder,
+ decoderInitializationException.mimeType);
+ } else {
+ errorString = toro.getString(R.string.error_no_decoder,
+ decoderInitializationException.mimeType);
+ }
+ } else {
+ errorString = toro.getString(R.string.error_instantiating_decoder,
+ decoderInitializationException.decoderName);
+ }
+ }
+ }
+
+ if (errorString != null) onErrorMessage(errorString);
+
+ inErrorState = true;
+ if (isBehindLiveWindow(error)) {
+ ExoPlayable.super.reset();
+ } else {
+ ExoPlayable.super.updatePlaybackInfo();
+ }
+
+ super.onPlayerError(error);
+ }
+
+ @Override public void onPositionDiscontinuity(int reason) {
+ if (inErrorState) {
+ // Adapt from ExoPlayer demo.
+ // "This will only occur if the user has performed a seek whilst in the error state. Update
+ // the resume position so that if the user then retries, playback will resume from the
+ // position to which they seek." - ExoPlayer
+ ExoPlayable.super.updatePlaybackInfo();
+ }
+
+ super.onPositionDiscontinuity(reason);
+ }
+ }
+
+ static boolean isBehindLiveWindow(ExoPlaybackException error) {
+ if (error.type != ExoPlaybackException.TYPE_SOURCE) return false;
+ Throwable cause = error.getSourceException();
+ while (cause != null) {
+ if (cause instanceof BehindLiveWindowException) return true;
+ cause = cause.getCause();
+ }
+ return false;
+ }
+}
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ExoPlayerDispatcher.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ExoPlayerDispatcher.java
new file mode 100644
index 00000000..031c1f3a
--- /dev/null
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ExoPlayerDispatcher.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2018 Nam Nguyen, nam@ene.im
+ *
+ * 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 ml.docilealligator.infinityforreddit.videoautoplay;
+
+import com.google.android.exoplayer2.DefaultControlDispatcher;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.ui.PlayerView;
+
+import ml.docilealligator.infinityforreddit.videoautoplay.annotations.Beta;
+import ml.docilealligator.infinityforreddit.videoautoplay.widget.PressablePlayerSelector;
+
+/**
+ * @author eneim (2018/08/18).
+ * @since 3.6.0.2802
+ *
+ * Work with {@link PressablePlayerSelector} and {@link PlayerView} to handle user's custom playback
+ * interaction. A common use-case is when user clicks the Play button to manually start a playback.
+ * We should respect this by putting the {@link ToroPlayer}'s priority to highest, and request a
+ * refresh for all {@link ToroPlayer}.
+ *
+ * The same behaviour should be handled for the case user clicks the Pause button.
+ *
+ * All behaviour should be cleared once user scroll the selection out of playable region. This is
+ * already handled by {@link PressablePlayerSelector}.
+ */
+@Beta //
+public class ExoPlayerDispatcher extends DefaultControlDispatcher {
+
+ private final PressablePlayerSelector playerSelector;
+ private final ToroPlayer toroPlayer;
+
+ public ExoPlayerDispatcher(PressablePlayerSelector playerSelector, ToroPlayer toroPlayer) {
+ this.playerSelector = playerSelector;
+ this.toroPlayer = toroPlayer;
+ }
+
+ @Override public boolean dispatchSetPlayWhenReady(Player player, boolean playWhenReady) {
+ if (playWhenReady) {
+ // Container will handle the call to play.
+ return playerSelector.toPlay(toroPlayer.getPlayerOrder());
+ } else {
+ player.setPlayWhenReady(false);
+ playerSelector.toPause(toroPlayer.getPlayerOrder());
+ return true;
+ }
+ }
+}
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ExoPlayerViewHelper.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ExoPlayerViewHelper.java
new file mode 100644
index 00000000..3ee611a7
--- /dev/null
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ExoPlayerViewHelper.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (c) 2018 Nam Nguyen, nam@ene.im
+ *
+ * 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 ml.docilealligator.infinityforreddit.videoautoplay;
+
+import static ml.docilealligator.infinityforreddit.videoautoplay.ToroExo.with;
+import static ml.docilealligator.infinityforreddit.videoautoplay.ToroUtil.checkNotNull;
+
+import android.net.Uri;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.ui.PlayerView;
+
+import ml.docilealligator.infinityforreddit.videoautoplay.annotations.RemoveIn;
+import ml.docilealligator.infinityforreddit.videoautoplay.helper.ToroPlayerHelper;
+import ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo;
+import ml.docilealligator.infinityforreddit.videoautoplay.media.VolumeInfo;
+import ml.docilealligator.infinityforreddit.videoautoplay.widget.Container;
+
+/**
+ * An implementation of {@link ToroPlayerHelper} where the actual Player is an {@link ExoPlayer}
+ * implementation. This is a bridge between ExoPlayer's callback and ToroPlayerHelper behaviors.
+ *
+ * @author eneim (2018/01/24).
+ * @since 3.4.0
+ */
+
+public class ExoPlayerViewHelper extends ToroPlayerHelper {
+
+ @NonNull private final ExoPlayable playable;
+ @NonNull private final MyEventListeners listeners;
+ private final boolean lazyPrepare;
+
+ // Container is no longer required for constructing new instance.
+ @SuppressWarnings("unused") @RemoveIn(version = "3.6.0") @Deprecated //
+ public ExoPlayerViewHelper(Container container, @NonNull ToroPlayer player, @NonNull Uri uri) {
+ this(player, uri);
+ }
+
+ public ExoPlayerViewHelper(@NonNull ToroPlayer player, @NonNull Uri uri) {
+ this(player, uri, null);
+ }
+
+ public ExoPlayerViewHelper(@NonNull ToroPlayer player, @NonNull Uri uri,
+ @Nullable String fileExt) {
+ this(player, uri, fileExt, with(player.getPlayerView().getContext()).getDefaultCreator());
+ }
+
+ /** Config instance should be kept as global instance. */
+ public ExoPlayerViewHelper(@NonNull ToroPlayer player, @NonNull Uri uri, @Nullable String fileExt,
+ @NonNull Config config) {
+ this(player, uri, fileExt,
+ with(player.getPlayerView().getContext()).getCreator(checkNotNull(config)));
+ }
+
+ public ExoPlayerViewHelper(@NonNull ToroPlayer player, @NonNull Uri uri, @Nullable String fileExt,
+ @NonNull ExoCreator creator) {
+ this(player, new ExoPlayable(creator, uri, fileExt));
+ }
+
+ public ExoPlayerViewHelper(@NonNull ToroPlayer player, @NonNull ExoPlayable playable) {
+ super(player);
+ //noinspection ConstantConditions
+ if (player.getPlayerView() == null || !(player.getPlayerView() instanceof PlayerView)) {
+ throw new IllegalArgumentException("Require non-null PlayerView");
+ }
+
+ listeners = new MyEventListeners();
+ this.playable = playable;
+ this.lazyPrepare = true;
+ }
+
+ @Override protected void initialize(@NonNull PlaybackInfo playbackInfo) {
+ playable.setPlaybackInfo(playbackInfo);
+ playable.addEventListener(listeners);
+ playable.addErrorListener(super.getErrorListeners());
+ playable.addOnVolumeChangeListener(super.getVolumeChangeListeners());
+ playable.prepare(!lazyPrepare);
+ playable.setPlayerView((PlayerView) player.getPlayerView());
+ }
+
+ @Override public void release() {
+ super.release();
+ playable.setPlayerView(null);
+ playable.removeOnVolumeChangeListener(super.getVolumeChangeListeners());
+ playable.removeErrorListener(super.getErrorListeners());
+ playable.removeEventListener(listeners);
+ playable.release();
+ }
+
+ @Override public void play() {
+ playable.play();
+ }
+
+ @Override public void pause() {
+ playable.pause();
+ }
+
+ @Override public boolean isPlaying() {
+ return playable.isPlaying();
+ }
+
+ @Override public void setVolume(float volume) {
+ playable.setVolume(volume);
+ }
+
+ @Override public float getVolume() {
+ return playable.getVolume();
+ }
+
+ @Override public void setVolumeInfo(@NonNull VolumeInfo volumeInfo) {
+ playable.setVolumeInfo(volumeInfo);
+ }
+
+ @Override @NonNull public VolumeInfo getVolumeInfo() {
+ return playable.getVolumeInfo();
+ }
+
+ @NonNull @Override public PlaybackInfo getLatestPlaybackInfo() {
+ return playable.getPlaybackInfo();
+ }
+
+ @Override public void setPlaybackInfo(@NonNull PlaybackInfo playbackInfo) {
+ this.playable.setPlaybackInfo(playbackInfo);
+ }
+
+ public void addEventListener(@NonNull Playable.EventListener listener) {
+ //noinspection ConstantConditions
+ if (listener != null) this.listeners.add(listener);
+ }
+
+ public void removeEventListener(Playable.EventListener listener) {
+ this.listeners.remove(listener);
+ }
+
+ // A proxy, to also hook into ToroPlayerHelper's state change event.
+ private class MyEventListeners extends Playable.EventListeners {
+
+ MyEventListeners() {
+ }
+
+ @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
+ ExoPlayerViewHelper.super.onPlayerStateUpdated(playWhenReady, playbackState); // important
+ super.onPlayerStateChanged(playWhenReady, playbackState);
+ }
+
+ @Override public void onRenderedFirstFrame() {
+ super.onRenderedFirstFrame();
+ internalListener.onFirstFrameRendered();
+ for (ToroPlayer.EventListener listener : ExoPlayerViewHelper.super.getEventListeners()) {
+ listener.onFirstFrameRendered();
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/MediaSourceBuilder.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/MediaSourceBuilder.java
new file mode 100644
index 00000000..7daacc99
--- /dev/null
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/MediaSourceBuilder.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2018 Nam Nguyen, nam@ene.im
+ *
+ * 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 ml.docilealligator.infinityforreddit.videoautoplay;
+
+import static android.text.TextUtils.isEmpty;
+import static com.google.android.exoplayer2.util.Util.inferContentType;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.Handler;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.C.ContentType;
+import com.google.android.exoplayer2.source.LoopingMediaSource;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.MediaSourceEventListener;
+import com.google.android.exoplayer2.source.ProgressiveMediaSource;
+import com.google.android.exoplayer2.source.dash.DashMediaSource;
+import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
+import com.google.android.exoplayer2.source.hls.HlsMediaSource;
+import com.google.android.exoplayer2.upstream.DataSource;
+
+/**
+ * @author eneim (2018/01/24).
+ * @since 3.4.0
+ */
+
+public interface MediaSourceBuilder {
+
+ @NonNull MediaSource buildMediaSource(@NonNull Context context, @NonNull Uri uri,
+ @Nullable String fileExt, @Nullable Handler handler,
+ @NonNull DataSource.Factory manifestDataSourceFactory,
+ @NonNull DataSource.Factory mediaDataSourceFactory,
+ @Nullable MediaSourceEventListener listener);
+
+ MediaSourceBuilder DEFAULT = new MediaSourceBuilder() {
+ @NonNull @Override
+ public MediaSource buildMediaSource(@NonNull Context context, @NonNull Uri uri,
+ @Nullable String ext, @Nullable Handler handler,
+ @NonNull DataSource.Factory manifestDataSourceFactory,
+ @NonNull DataSource.Factory mediaDataSourceFactory, MediaSourceEventListener listener) {
+ @ContentType int type = isEmpty(ext) ? inferContentType(uri) : inferContentType("." + ext);
+ MediaSource result;
+ switch (type) {
+ /*case C.TYPE_SS:
+ result = new SsMediaSource.Factory( //
+ new DefaultSsChunkSource.Factory(mediaDataSourceFactory), manifestDataSourceFactory)//
+ .createMediaSource(uri);
+ break;*/
+ case C.TYPE_DASH:
+ result = new DashMediaSource.Factory(
+ new DefaultDashChunkSource.Factory(mediaDataSourceFactory), manifestDataSourceFactory)
+ .createMediaSource(uri);
+ break;
+ case C.TYPE_HLS:
+ result = new HlsMediaSource.Factory(mediaDataSourceFactory) //
+ .createMediaSource(uri);
+ break;
+ case C.TYPE_OTHER:
+ result = new ProgressiveMediaSource.Factory(mediaDataSourceFactory) //
+ .createMediaSource(uri);
+ break;
+ default:
+ throw new IllegalStateException("Unsupported type: " + type);
+ }
+
+ result.addEventListener(handler, listener);
+ return result;
+ }
+ };
+
+ MediaSourceBuilder LOOPING = new MediaSourceBuilder() {
+
+ @NonNull @Override
+ public MediaSource buildMediaSource(@NonNull Context context, @NonNull Uri uri,
+ @Nullable String fileExt, @Nullable Handler handler,
+ @NonNull DataSource.Factory manifestDataSourceFactory,
+ @NonNull DataSource.Factory mediaDataSourceFactory,
+ @Nullable MediaSourceEventListener listener) {
+ return new LoopingMediaSource(
+ DEFAULT.buildMediaSource(context, uri, fileExt, handler, manifestDataSourceFactory,
+ mediaDataSourceFactory, listener));
+ }
+ };
+}
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/Playable.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/Playable.java
new file mode 100644
index 00000000..a713b5a7
--- /dev/null
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/Playable.java
@@ -0,0 +1,365 @@
+/*
+ * Copyright (c) 2018 Nam Nguyen, nam@ene.im
+ *
+ * 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 ml.docilealligator.infinityforreddit.videoautoplay;
+
+import androidx.annotation.FloatRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.PlaybackParameters;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.SimpleExoPlayer;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.android.exoplayer2.metadata.MetadataOutput;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.text.TextOutput;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.ui.PlayerView;
+import com.google.android.exoplayer2.video.VideoListener;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+import ml.docilealligator.infinityforreddit.videoautoplay.annotations.RemoveIn;
+import ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo;
+import ml.docilealligator.infinityforreddit.videoautoplay.media.VolumeInfo;
+
+/**
+ * Define an interface to control a playback, specific for {@link SimpleExoPlayer} and {@link PlayerView}.
+ *
+ * This interface is designed to be reused across Config change. Implementation must not hold any
+ * strong reference to Activity, and if it supports any kind of that, make sure to implicitly clean
+ * it up.
+ *
+ * @author eneim
+ * @since 3.4.0
+ */
+
+@SuppressWarnings("unused") //
+public interface Playable {
+
+ /**
+ * Prepare the resource for a {@link SimpleExoPlayer}. This method should:
+ * - Request for new {@link SimpleExoPlayer} instance if there is not a usable one.
+ * - Configure {@link EventListener} for it.
+ * - If there is non-trivial PlaybackInfo, update it to the SimpleExoPlayer.
+ * - If client request to prepare MediaSource, then prepare it.
+ *
+ * This method must be called before {@link #setPlayerView(PlayerView)}.
+ *
+ * @param prepareSource if {@code true}, also prepare the MediaSource when preparing the Player,
+ * if {@code false} just do nothing for the MediaSource.
+ */
+ void prepare(boolean prepareSource);
+
+ /**
+ * Set the {@link PlayerView} for this Playable. It is expected that a playback doesn't require a
+ * UI, so this setup is optional. But it must be called after the SimpleExoPlayer is prepared,
+ * that is after {@link #prepare(boolean)} and before {@link #release()}.
+ *
+ * Changing the PlayerView during playback is expected, though not always recommended, especially
+ * on old Devices with low Android API.
+ *
+ * @param playerView the PlayerView to set to the SimpleExoPlayer.
+ */
+ void setPlayerView(@Nullable PlayerView playerView);
+
+ /**
+ * Get current {@link PlayerView} of this Playable.
+ *
+ * @return current PlayerView instance of this Playable.
+ */
+ @Nullable PlayerView getPlayerView();
+
+ /**
+ * Start the playback. If the {@link MediaSource} is not prepared, then also prepare it.
+ */
+ void play();
+
+ /**
+ * Pause the playback.
+ */
+ void pause();
+
+ /**
+ * Reset all resource, so that the playback can start all over again. This is to cleanup the
+ * playback for reuse. The SimpleExoPlayer instance must be still usable without calling
+ * {@link #prepare(boolean)}.
+ */
+ void reset();
+
+ /**
+ * Release all resource. After this, the SimpleExoPlayer is released to the Player pool and the
+ * Playable must call {@link #prepare(boolean)} again to use it again.
+ */
+ void release();
+
+ /**
+ * Get current {@link PlaybackInfo} of the playback.
+ *
+ * @return current PlaybackInfo of the playback.
+ */
+ @NonNull
+ PlaybackInfo getPlaybackInfo();
+
+ /**
+ * Set the custom {@link PlaybackInfo} for this playback. This could suggest a seek.
+ *
+ * @param playbackInfo the PlaybackInfo to set for this playback.
+ */
+ void setPlaybackInfo(@NonNull PlaybackInfo playbackInfo);
+
+ /**
+ * Add a new {@link EventListener} to this Playable. As calling {@link #prepare(boolean)} also
+ * triggers some internal events, this method should be called before {@link #prepare(boolean)} so
+ * that Client could received them all.
+ *
+ * @param listener the EventListener to add, must be not {@code null}.
+ */
+ void addEventListener(@NonNull EventListener listener);
+
+ /**
+ * Remove an {@link EventListener} from this Playable.
+ *
+ * @param listener the EventListener to be removed. If null, nothing happens.
+ */
+ void removeEventListener(EventListener listener);
+
+ /**
+ * !This must only work if the Player in use is a {@link ToroExoPlayer}.
+ */
+ void addOnVolumeChangeListener(@NonNull ToroPlayer.OnVolumeChangeListener listener);
+
+ void removeOnVolumeChangeListener(@Nullable ToroPlayer.OnVolumeChangeListener listener);
+
+ /**
+ * Check if current Playable is playing or not.
+ *
+ * @return {@code true} if this Playable is playing, {@code false} otherwise.
+ */
+ boolean isPlaying();
+
+ /**
+ * Change the volume of current playback.
+ *
+ * @param volume the volume value to be set. Must be a {@code float} of range from 0 to 1.
+ * @deprecated use {@link #setVolumeInfo(VolumeInfo)} instead.
+ */
+ @RemoveIn(version = "3.6.0") @Deprecated //
+ void setVolume(@FloatRange(from = 0.0, to = 1.0) float volume);
+
+ /**
+ * Obtain current volume value. The returned value is a {@code float} of range from 0 to 1.
+ *
+ * @return current volume value.
+ * @deprecated use {@link #getVolumeInfo()} instead.
+ */
+ @RemoveIn(version = "3.6.0") @Deprecated //
+ @FloatRange(from = 0.0, to = 1.0) float getVolume();
+
+ /**
+ * Update playback's volume.
+ *
+ * @param volumeInfo the {@link VolumeInfo} to update to.
+ * @return {@code true} if current Volume info is updated, {@code false} otherwise.
+ */
+ boolean setVolumeInfo(@NonNull VolumeInfo volumeInfo);
+
+ /**
+ * Get current {@link VolumeInfo}.
+ */
+ @NonNull VolumeInfo getVolumeInfo();
+
+ /**
+ * Same as {@link Player#setPlaybackParameters(PlaybackParameters)}
+ */
+ void setParameters(@Nullable PlaybackParameters parameters);
+
+ /**
+ * Same as {@link Player#getPlaybackParameters()}
+ */
+ @Nullable PlaybackParameters getParameters();
+
+ void addErrorListener(@NonNull ToroPlayer.OnErrorListener listener);
+
+ void removeErrorListener(@Nullable ToroPlayer.OnErrorListener listener);
+
+ // Combine necessary interfaces.
+ interface EventListener extends Player.EventListener, VideoListener, TextOutput, MetadataOutput {
+
+ }
+
+ /** Default empty implementation */
+ class DefaultEventListener implements EventListener {
+
+ @Override public void onTimelineChanged(Timeline timeline, Object manifest, int reason) {
+
+ }
+
+ @Override
+ public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
+
+ }
+
+ @Override public void onLoadingChanged(boolean isLoading) {
+
+ }
+
+ @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
+
+ }
+
+ @Override public void onRepeatModeChanged(int repeatMode) {
+
+ }
+
+ @Override public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
+
+ }
+
+ @Override public void onPlayerError(ExoPlaybackException error) {
+
+ }
+
+ @Override public void onPositionDiscontinuity(int reason) {
+
+ }
+
+ @Override public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
+
+ }
+
+ @Override public void onSeekProcessed() {
+
+ }
+
+ @Override public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
+ float pixelWidthHeightRatio) {
+
+ }
+
+ @Override public void onRenderedFirstFrame() {
+
+ }
+
+ @Override public void onCues(List<Cue> cues) {
+
+ }
+
+ @Override public void onMetadata(Metadata metadata) {
+
+ }
+ }
+
+ /** List of EventListener */
+ class EventListeners extends CopyOnWriteArraySet<EventListener> implements EventListener {
+
+ EventListeners() {
+ }
+
+ @Override public void onVideoSizeChanged(int width, int height, int unAppliedRotationDegrees,
+ float pixelWidthHeightRatio) {
+ for (EventListener eventListener : this) {
+ eventListener.onVideoSizeChanged(width, height, unAppliedRotationDegrees,
+ pixelWidthHeightRatio);
+ }
+ }
+
+ @Override public void onRenderedFirstFrame() {
+ for (EventListener eventListener : this) {
+ eventListener.onRenderedFirstFrame();
+ }
+ }
+
+ @Override public void onTimelineChanged(Timeline timeline, Object manifest, int reason) {
+ for (EventListener eventListener : this) {
+ eventListener.onTimelineChanged(timeline, manifest, reason);
+ }
+ }
+
+ @Override
+ public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
+ for (EventListener eventListener : this) {
+ eventListener.onTracksChanged(trackGroups, trackSelections);
+ }
+ }
+
+ @Override public void onLoadingChanged(boolean isLoading) {
+ for (EventListener eventListener : this) {
+ eventListener.onLoadingChanged(isLoading);
+ }
+ }
+
+ @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
+ for (EventListener eventListener : this) {
+ eventListener.onPlayerStateChanged(playWhenReady, playbackState);
+ }
+ }
+
+ @Override public void onRepeatModeChanged(int repeatMode) {
+ for (EventListener eventListener : this) {
+ eventListener.onRepeatModeChanged(repeatMode);
+ }
+ }
+
+ @Override public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
+ for (EventListener eventListener : this) {
+ eventListener.onShuffleModeEnabledChanged(shuffleModeEnabled);
+ }
+ }
+
+ @Override public void onPlayerError(ExoPlaybackException error) {
+ for (EventListener eventListener : this) {
+ eventListener.onPlayerError(error);
+ }
+ }
+
+ @Override public void onPositionDiscontinuity(int reason) {
+ for (EventListener eventListener : this) {
+ eventListener.onPositionDiscontinuity(reason);
+ }
+ }
+
+ @Override public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
+ for (EventListener eventListener : this) {
+ eventListener.onPlaybackParametersChanged(playbackParameters);
+ }
+ }
+
+ @Override public void onSeekProcessed() {
+ for (EventListener eventListener : this) {
+ eventListener.onSeekProcessed();
+ }
+ }
+
+ @Override public void onCues(List<Cue> cues) {
+ for (EventListener eventListener : this) {
+ eventListener.onCues(cues);
+ }
+ }
+
+ @Override public void onMetadata(Metadata metadata) {
+ for (EventListener eventListener : this) {
+ eventListener.onMetadata(metadata);
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/PlayableImpl.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/PlayableImpl.java
new file mode 100644
index 00000000..d1d47c07
--- /dev/null
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/PlayableImpl.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright (c) 2018 Nam Nguyen, nam@ene.im
+ *
+ * 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 ml.docilealligator.infinityforreddit.videoautoplay;
+
+import static ml.docilealligator.infinityforreddit.videoautoplay.ToroExo.with;
+import static ml.docilealligator.infinityforreddit.videoautoplay.ToroUtil.checkNotNull;
+import static ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo.INDEX_UNSET;
+import static ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo.TIME_UNSET;
+
+import android.net.Uri;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.PlaybackParameters;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.SimpleExoPlayer;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.ui.PlayerView;
+
+import ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo;
+import ml.docilealligator.infinityforreddit.videoautoplay.media.VolumeInfo;
+
+/**
+ * [20180225]
+ *
+ * Default implementation of {@link Playable}.
+ *
+ * Instance of {@link Playable} should be reusable. Retaining instance of Playable across config
+ * change must guarantee that all {@link EventListener} are cleaned up on config change.
+ *
+ * @author eneim (2018/02/25).
+ */
+@SuppressWarnings("WeakerAccess") //
+class PlayableImpl implements Playable {
+
+ private final PlaybackInfo playbackInfo = new PlaybackInfo(); // never expose to outside.
+
+ protected final EventListeners listeners = new EventListeners(); // original listener.
+ protected final ToroPlayer.VolumeChangeListeners volumeChangeListeners = new ToroPlayer.VolumeChangeListeners();
+ protected final ToroPlayer.ErrorListeners errorListeners = new ToroPlayer.ErrorListeners();
+
+ protected final Uri mediaUri; // immutable, parcelable
+ protected final String fileExt;
+ protected final ExoCreator creator; // required, cached
+
+ protected SimpleExoPlayer player; // on-demand, cached
+ protected MediaSource mediaSource; // on-demand, since we do not reuse MediaSource now.
+ protected PlayerView playerView; // on-demand, not always required.
+
+ private boolean sourcePrepared = false;
+ private boolean listenerApplied = false;
+
+ PlayableImpl(ExoCreator creator, Uri uri, String fileExt) {
+ this.creator = creator;
+ this.mediaUri = uri;
+ this.fileExt = fileExt;
+ }
+
+ @CallSuper @Override public void prepare(boolean prepareSource) {
+ if (prepareSource) {
+ ensureMediaSource();
+ ensurePlayerView();
+ }
+ }
+
+ @CallSuper @Override public void setPlayerView(@Nullable PlayerView playerView) {
+ if (this.playerView == playerView) return;
+ if (playerView == null) {
+ this.playerView.setPlayer(null);
+ } else {
+ if (this.player != null) {
+ PlayerView.switchTargetView(this.player, this.playerView, playerView);
+ }
+ }
+
+ this.playerView = playerView;
+ }
+
+ @Override public final PlayerView getPlayerView() {
+ return this.playerView;
+ }
+
+ @CallSuper @Override public void play() {
+ ensureMediaSource();
+ ensurePlayerView();
+ checkNotNull(player, "Playable#play(): Player is null!");
+ player.setPlayWhenReady(true);
+ }
+
+ @CallSuper @Override public void pause() {
+ // Player is not required to be non-null here.
+ if (player != null) player.setPlayWhenReady(false);
+ }
+
+ @CallSuper @Override public void reset() {
+ this.playbackInfo.reset();
+ if (player != null) {
+ // reset volume to default
+ ToroExo.setVolumeInfo(this.player, new VolumeInfo(false, 1.f));
+ player.stop(true);
+ }
+ this.mediaSource = null; // so it will be re-prepared when play() is called.
+ this.sourcePrepared = false;
+ }
+
+ @CallSuper @Override public void release() {
+ this.setPlayerView(null);
+ if (this.player != null) {
+ // reset volume to default
+ ToroExo.setVolumeInfo(this.player, new VolumeInfo(false, 1.f));
+ this.player.stop(true);
+ if (listenerApplied) {
+ player.removeListener(listeners);
+ player.removeVideoListener(listeners);
+ player.removeTextOutput(listeners);
+ player.removeMetadataOutput(listeners);
+ if (this.player instanceof ToroExoPlayer) {
+ ((ToroExoPlayer) this.player).removeOnVolumeChangeListener(this.volumeChangeListeners);
+ }
+ listenerApplied = false;
+ }
+ with(checkNotNull(creator.getContext(), "ExoCreator has no Context")) //
+ .releasePlayer(this.creator, this.player);
+ }
+ this.player = null;
+ this.mediaSource = null;
+ this.sourcePrepared = false;
+ }
+
+ @CallSuper @NonNull @Override public PlaybackInfo getPlaybackInfo() {
+ updatePlaybackInfo();
+ return new PlaybackInfo(playbackInfo.getResumeWindow(), playbackInfo.getResumePosition(),
+ playbackInfo.getVolumeInfo());
+ }
+
+ @CallSuper @Override public void setPlaybackInfo(@NonNull PlaybackInfo playbackInfo) {
+ this.playbackInfo.setResumeWindow(playbackInfo.getResumeWindow());
+ this.playbackInfo.setResumePosition(playbackInfo.getResumePosition());
+ this.setVolumeInfo(playbackInfo.getVolumeInfo());
+
+ if (player != null) {
+ ToroExo.setVolumeInfo(player, this.playbackInfo.getVolumeInfo());
+ boolean haveResumePosition = this.playbackInfo.getResumeWindow() != INDEX_UNSET;
+ if (haveResumePosition) {
+ player.seekTo(this.playbackInfo.getResumeWindow(), this.playbackInfo.getResumePosition());
+ }
+ }
+ }
+
+ @Override public final void addEventListener(@NonNull EventListener listener) {
+ //noinspection ConstantConditions
+ if (listener != null) this.listeners.add(listener);
+ }
+
+ @Override public final void removeEventListener(EventListener listener) {
+ this.listeners.remove(listener);
+ }
+
+ @CallSuper @Override public void setVolume(float volume) {
+ checkNotNull(player, "Playable#setVolume(): Player is null!");
+ playbackInfo.getVolumeInfo().setTo(volume == 0, volume);
+ ToroExo.setVolumeInfo(player, this.playbackInfo.getVolumeInfo());
+ }
+
+ @CallSuper @Override public float getVolume() {
+ return checkNotNull(player, "Playable#getVolume(): Player is null!").getVolume();
+ }
+
+ @Override public boolean setVolumeInfo(@NonNull VolumeInfo volumeInfo) {
+ boolean changed = !this.playbackInfo.getVolumeInfo().equals(checkNotNull(volumeInfo));
+ if (changed) {
+ this.playbackInfo.getVolumeInfo().setTo(volumeInfo.isMute(), volumeInfo.getVolume());
+ if (player != null) ToroExo.setVolumeInfo(player, this.playbackInfo.getVolumeInfo());
+ }
+ return changed;
+ }
+
+ @NonNull @Override public VolumeInfo getVolumeInfo() {
+ return this.playbackInfo.getVolumeInfo();
+ }
+
+ @Override public void setParameters(@Nullable PlaybackParameters parameters) {
+ checkNotNull(player, "Playable#setParameters(PlaybackParameters): Player is null") //
+ .setPlaybackParameters(parameters);
+ }
+
+ @Override public PlaybackParameters getParameters() {
+ return checkNotNull(player, "Playable#getParameters(): Player is null").getPlaybackParameters();
+ }
+
+ @Override
+ public void addOnVolumeChangeListener(@NonNull ToroPlayer.OnVolumeChangeListener listener) {
+ volumeChangeListeners.add(checkNotNull(listener));
+ }
+
+ @Override
+ public void removeOnVolumeChangeListener(@Nullable ToroPlayer.OnVolumeChangeListener listener) {
+ volumeChangeListeners.remove(listener);
+ }
+
+ @Override public boolean isPlaying() {
+ return player != null && player.getPlayWhenReady();
+ }
+
+ @Override public void addErrorListener(@NonNull ToroPlayer.OnErrorListener listener) {
+ this.errorListeners.add(checkNotNull(listener));
+ }
+
+ @Override public void removeErrorListener(@Nullable ToroPlayer.OnErrorListener listener) {
+ this.errorListeners.remove(listener);
+ }
+
+ final void updatePlaybackInfo() {
+ if (player == null || player.getPlaybackState() == Player.STATE_IDLE) return;
+ playbackInfo.setResumeWindow(player.getCurrentWindowIndex());
+ playbackInfo.setResumePosition(player.isCurrentWindowSeekable() ? //
+ Math.max(0, player.getCurrentPosition()) : TIME_UNSET);
+ playbackInfo.setVolumeInfo(ToroExo.getVolumeInfo(player));
+ }
+
+ private void ensurePlayerView() {
+ if (playerView != null && playerView.getPlayer() != player) playerView.setPlayer(player);
+ }
+
+ // TODO [20180822] Double check this.
+ private void ensureMediaSource() {
+ if (mediaSource == null) { // Only actually prepare the source when play() is called.
+ sourcePrepared = false;
+ mediaSource = creator.createMediaSource(mediaUri, fileExt);
+ }
+
+ if (!sourcePrepared) {
+ ensurePlayer(); // sourcePrepared is set to false only when player is null.
+ beforePrepareMediaSource();
+ player.prepare(mediaSource, playbackInfo.getResumeWindow() == C.INDEX_UNSET, false);
+ sourcePrepared = true;
+ }
+ }
+
+ private void ensurePlayer() {
+ if (player == null) {
+ sourcePrepared = false;
+ player = with(checkNotNull(creator.getContext(), "ExoCreator has no Context")) //
+ .requestPlayer(creator);
+ listenerApplied = false;
+ }
+
+ if (!listenerApplied) {
+ if (player instanceof ToroExoPlayer) {
+ ((ToroExoPlayer) player).addOnVolumeChangeListener(volumeChangeListeners);
+ }
+ player.addListener(listeners);
+ player.addVideoListener(listeners);
+ player.addTextOutput(listeners);
+ player.addMetadataOutput(listeners);
+ listenerApplied = true;
+ }
+
+ ToroExo.setVolumeInfo(player, this.playbackInfo.getVolumeInfo());
+ boolean haveResumePosition = playbackInfo.getResumeWindow() != C.INDEX_UNSET;
+ if (haveResumePosition) {
+ player.seekTo(playbackInfo.getResumeWindow(), playbackInfo.getResumePosition());
+ }
+ }
+
+ // Trick to inject to the Player creation event.
+ // Required for AdsLoader to set Player.
+ protected void beforePrepareMediaSource() {
+ }
+}
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/PlayerDispatcher.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/PlayerDispatcher.java
new file mode 100644
index 00000000..4f86ccd9
--- /dev/null
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/PlayerDispatcher.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2018 Nam Nguyen, nam@ene.im
+ *
+ * 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 ml.docilealligator.infinityforreddit.videoautoplay;
+
+/**
+ * This is an addition layer used in PlayerManager. Setting this where
+ * {@link #getDelayToPlay(ToroPlayer)} returns a positive value will result in a delay in playback
+ * play(). While returning {@link #DELAY_NONE} will dispatch the action immediately, and returning
+ * {@link #DELAY_INFINITE} will not dispatch the action.
+ *
+ * @author eneim (2018/02/24).
+ * @since 3.4.0
+ */
+
+public interface PlayerDispatcher {
+
+ int DELAY_INFINITE = -1;
+
+ int DELAY_NONE = 0;
+
+ /**
+ * Return the number of milliseconds that a call to {@link ToroPlayer#play()} should be delayed.
+ * Returning {@link #DELAY_INFINITE} will not start the playback, while returning {@link
+ * #DELAY_NONE} will start it immediately.
+ *
+ * @param player the player that is about to play.
+ * @return number of milliseconds to delay the play, or one of {@link #DELAY_INFINITE} or
+ * {@link #DELAY_NONE}. No other negative number should be used.
+ */
+ int getDelayToPlay(ToroPlayer player);
+
+ PlayerDispatcher DEFAULT = new PlayerDispatcher() {
+ @Override public int getDelayToPlay(ToroPlayer player) {
+ return DELAY_NONE;
+ }
+ };
+}
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/PlayerSelector.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/PlayerSelector.java
new file mode 100644
index 00000000..e42a55a7
--- /dev/null
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/PlayerSelector.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (c) 2017 Nam Nguyen, nam@ene.im
+ *
+ * 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 ml.docilealligator.infinityforreddit.videoautoplay;
+
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static ml.docilealligator.infinityforreddit.videoautoplay.ToroUtil.visibleAreaOffset;
+import static ml.docilealligator.infinityforreddit.videoautoplay.annotations.Sorted.Order.ASCENDING;
+
+import androidx.annotation.NonNull;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.NavigableMap;
+import java.util.TreeMap;
+
+import ml.docilealligator.infinityforreddit.videoautoplay.annotations.Sorted;
+import ml.docilealligator.infinityforreddit.videoautoplay.widget.Container;
+
+/**
+ * @author eneim | 6/2/17.
+ *
+ * PlayerSelector is a convenient class to help selecting the players to start Media
+ * playback.
+ *
+ * On specific event of RecyclerView, such as Child view attached/detached, scroll, the
+ * Collection of players those are available for a playback will change. PlayerSelector is
+ * responded to select a specific number of players from that updated Collection to start a
+ * new playback or pause an old playback if the corresponding Player is not selected
+ * anymore.
+ *
+ * Client should implement a custom PlayerSelecter and set it to the Container for expected
+ * behaviour. By default, Toro comes with linear selection implementation (the Selector
+ * that will iterate over the Collection and select the players from top to bottom until a
+ * certain condition is fullfilled, for example the maximum of player count is reached).
+ *
+ * Custom Selector can have more complicated selecting logics, for example: among 2n + 1
+ * playable widgets, select n players in the middles ...
+ */
+
+@SuppressWarnings("unused") //
+public interface PlayerSelector {
+
+ String TAG = "ToroLib:Selector";
+
+ /**
+ * Select a collection of {@link ToroPlayer}s to start a playback (if there is non-playing) item.
+ * Playing item are also selected.
+ *
+ * @param container current {@link Container} that holds the players.
+ * @param items a mutable collection of candidate {@link ToroPlayer}s, which are the players
+ * those can start a playback. Items are sorted in order obtained from
+ * {@link ToroPlayer#getPlayerOrder()}.
+ * @return the collection of {@link ToroPlayer}s to start a playback. An on-going playback can be
+ * selected, but it will keep playing.
+ */
+ @NonNull Collection<ToroPlayer> select(@NonNull Container container,
+ @Sorted(order = ASCENDING) @NonNull List<ToroPlayer> items);
+
+ /**
+ * The 'reverse' selector of this selector, which can help to select the reversed collection of
+ * that expected by this selector.
+ * For example: this selector will select the first playable {@link ToroPlayer} from top, so the
+ * 'reverse' selector will select the last playable {@link ToroPlayer} from top.
+ *
+ * @return The PlayerSelector that has opposite selecting logic. If there is no special one,
+ * return "this".
+ */
+ @NonNull PlayerSelector reverse();
+
+ PlayerSelector DEFAULT = new PlayerSelector() {
+ @NonNull @Override public Collection<ToroPlayer> select(@NonNull Container container, //
+ @Sorted(order = ASCENDING) @NonNull List<ToroPlayer> items) {
+ int count = items.size();
+ return count > 0 ? singletonList(items.get(0)) : Collections.<ToroPlayer>emptyList();
+ }
+
+ @NonNull @Override public PlayerSelector reverse() {
+ return DEFAULT_REVERSE;
+ }
+ };
+
+ PlayerSelector DEFAULT_REVERSE = new PlayerSelector() {
+ @NonNull @Override public Collection<ToroPlayer> select(@NonNull Container container, //
+ @Sorted(order = ASCENDING) @NonNull List<ToroPlayer> items) {
+ int count = items.size();
+ return count > 0 ? singletonList(items.get(count - 1)) : Collections.<ToroPlayer>emptyList();
+ }
+
+ @NonNull @Override public PlayerSelector reverse() {
+ return DEFAULT;
+ }
+ };
+
+ @SuppressWarnings("unused") PlayerSelector BY_AREA = new PlayerSelector() {
+
+ NavigableMap<Float, ToroPlayer> areas = new TreeMap<>(new Comparator<Float>() {
+ @Override public int compare(Float o1, Float o2) {
+ return Float.compare(o2, o1); // reverse order, from high to low.
+ }
+ });
+
+ @NonNull @Override public Collection<ToroPlayer> select(@NonNull final Container container,
+ @Sorted(order = ASCENDING) @NonNull List<ToroPlayer> items) {
+ areas.clear();
+ int count = items.size();
+ if (count > 0) {
+ for (int i = 0; i < count; i++) {
+ ToroPlayer item = items.get(i);
+ if (!areas.containsValue(item)) areas.put(visibleAreaOffset(item, container), item);
+ }
+
+ count = areas.size();
+ }
+
+ return count > 0 ? singletonList(areas.firstEntry().getValue())
+ : Collections.<ToroPlayer>emptyList();
+ }
+
+ @NonNull @Override public PlayerSelector reverse() {
+ return this;
+ }
+ };
+
+ @SuppressWarnings("unused") PlayerSelector NONE = new PlayerSelector() {
+ @NonNull @Override public Collection<ToroPlayer> select(@NonNull Container container, //
+ @Sorted(order = ASCENDING) @NonNull List<ToroPlayer> items) {
+ return emptyList();
+ }
+
+ @NonNull @Override public PlayerSelector reverse() {
+ return this;
+ }
+ };
+}
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ToroExo.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ToroExo.java
new file mode 100644
index 00000000..147157b6
--- /dev/null
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ToroExo.java
@@ -0,0 +1,297 @@
+/*
+ * Copyright (c) 2018 Nam Nguyen, nam@ene.im
+ *
+ * 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 ml.docilealligator.infinityforreddit.videoautoplay;
+
+import static android.widget.Toast.LENGTH_SHORT;
+import static com.google.android.exoplayer2.drm.UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME;
+import static com.google.android.exoplayer2.util.Util.getDrmUuid;
+import static java.lang.Runtime.getRuntime;
+import static ml.docilealligator.infinityforreddit.videoautoplay.ToroUtil.checkNotNull;
+
+import android.annotation.SuppressLint;
+import android.app.Application;
+import android.content.Context;
+import android.text.TextUtils;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.StringRes;
+import androidx.core.util.Pools;
+
+import com.google.android.exoplayer2.SimpleExoPlayer;
+import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
+import com.google.android.exoplayer2.drm.DrmSessionManager;
+import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
+import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
+import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
+import com.google.android.exoplayer2.drm.UnsupportedDrmException;
+import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
+import com.google.android.exoplayer2.upstream.HttpDataSource;
+import com.google.android.exoplayer2.util.Util;
+
+import java.net.CookieHandler;
+import java.net.CookieManager;
+import java.net.CookiePolicy;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.UUID;
+
+import ml.docilealligator.infinityforreddit.R;
+import ml.docilealligator.infinityforreddit.utils.APIUtils;
+import ml.docilealligator.infinityforreddit.videoautoplay.media.DrmMedia;
+import ml.docilealligator.infinityforreddit.videoautoplay.media.VolumeInfo;
+
+/**
+ * Global helper class to manage {@link ExoCreator} and {@link SimpleExoPlayer} instances.
+ * In this setup, {@link ExoCreator} and SimpleExoPlayer pools are cached. A {@link Config}
+ * is a key for each {@link ExoCreator}.
+ *
+ * A suggested usage is as below:
+ * <pre><code>
+ * ExoCreator creator = ToroExo.with(this).getDefaultCreator();
+ * Playable playable = creator.createPlayable(uri);
+ * playable.prepare();
+ * // next: setup PlayerView and start the playback.
+ * </code></pre>
+ *
+ * @author eneim (2018/01/26).
+ * @since 3.4.0
+ */
+
+public final class ToroExo {
+
+ private static final String TAG = "ToroExo";
+
+ // Magic number: Build.VERSION.SDK_INT / 6 --> API 16 ~ 18 will set pool size to 2, etc.
+ @SuppressWarnings("WeakerAccess") //
+ static final int MAX_POOL_SIZE = Math.max(Util.SDK_INT / 6, getRuntime().availableProcessors());
+ @SuppressLint("StaticFieldLeak") //
+ static volatile ToroExo toro;
+
+ public static ToroExo with(Context context) {
+ if (toro == null) {
+ synchronized (ToroExo.class) {
+ if (toro == null) toro = new ToroExo(context.getApplicationContext());
+ }
+ }
+ return toro;
+ }
+
+ @NonNull final String appName;
+ @NonNull final Context context; // Application context
+ @NonNull private final Map<Config, ExoCreator> creators;
+ @NonNull private final Map<ExoCreator, Pools.Pool<SimpleExoPlayer>> playerPools;
+
+ private Config defaultConfig; // will be created on the first time it is used.
+
+ private ToroExo(@NonNull Context context /* Application context */) {
+ this.context = context;
+ this.appName = getUserAgent();
+ this.playerPools = new HashMap<>();
+ this.creators = new HashMap<>();
+
+ // Adapt from ExoPlayer demo app. Start this on demand.
+ CookieManager cookieManager = new CookieManager();
+ cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
+ if (CookieHandler.getDefault() != cookieManager) {
+ CookieHandler.setDefault(cookieManager);
+ }
+ }
+
+ /**
+ * Utility method to produce {@link ExoCreator} instance from a {@link Config}.
+ */
+ public final ExoCreator getCreator(Config config) {
+ ExoCreator creator = this.creators.get(config);
+ if (creator == null) {
+ creator = new DefaultExoCreator(this, config);
+ this.creators.put(config, creator);
+ }
+
+ return creator;
+ }
+
+ @SuppressWarnings("WeakerAccess") public final Config getDefaultConfig() {
+ if (defaultConfig == null) defaultConfig = new Config.Builder(context).build();
+ return defaultConfig;
+ }
+
+ /**
+ * Get the default {@link ExoCreator}. This ExoCreator is configured by {@link #defaultConfig}.
+ */
+ public final ExoCreator getDefaultCreator() {
+ return getCreator(getDefaultConfig());
+ }
+
+ /**
+ * Request an instance of {@link SimpleExoPlayer}. It can be an existing instance cached by Pool
+ * or new one.
+ *
+ * The creator may or may not be the one created by either {@link #getCreator(Config)} or
+ * {@link #getDefaultCreator()}.
+ *
+ * @param creator the {@link ExoCreator} that is scoped to the {@link SimpleExoPlayer} config.
+ * @return an usable {@link SimpleExoPlayer} instance.
+ */
+ @NonNull //
+ public final SimpleExoPlayer requestPlayer(@NonNull ExoCreator creator) {
+ SimpleExoPlayer player = getPool(checkNotNull(creator)).acquire();
+ if (player == null) player = creator.createPlayer();
+ return player;
+ }
+
+ /**
+ * Release player to Pool attached to the creator.
+ *
+ * @param creator the {@link ExoCreator} that created the player.
+ * @param player the {@link SimpleExoPlayer} to be released back to the Pool
+ * @return true if player is released to relevant Pool, false otherwise.
+ */
+ @SuppressWarnings({ "WeakerAccess", "UnusedReturnValue" }) //
+ public final boolean releasePlayer(@NonNull ExoCreator creator, @NonNull SimpleExoPlayer player) {
+ return getPool(checkNotNull(creator)).release(player);
+ }
+
+ /**
+ * Release and clear all current cached ExoPlayer instances. This should be called when
+ * client Application runs out of memory ({@link Application#onTrimMemory(int)} for example).
+ */
+ public final void cleanUp() {
+ // TODO [2018/03/07] Test this. Ref: https://stackoverflow.com/a/1884916/1553254
+ for (Iterator<Map.Entry<ExoCreator, Pools.Pool<SimpleExoPlayer>>> it =
+ playerPools.entrySet().iterator(); it.hasNext(); ) {
+ Pools.Pool<SimpleExoPlayer> pool = it.next().getValue();
+ SimpleExoPlayer item;
+ while ((item = pool.acquire()) != null) item.release();
+ it.remove();
+ }
+ }
+
+ /// internal APIs
+ private Pools.Pool<SimpleExoPlayer> getPool(ExoCreator creator) {
+ Pools.Pool<SimpleExoPlayer> pool = playerPools.get(creator);
+ if (pool == null) {
+ pool = new Pools.SimplePool<>(MAX_POOL_SIZE);
+ playerPools.put(creator, pool);
+ }
+
+ return pool;
+ }
+
+ /**
+ * Get a possibly-non-localized String from existing resourceId.
+ */
+ /* pkg */ String getString(@StringRes int resId, @Nullable Object... params) {
+ return params == null || params.length < 1 ? //
+ this.context.getString(resId) : this.context.getString(resId, params);
+ }
+
+ /**
+ * Utility method to build a {@link DrmSessionManager} that can be used in {@link Config}
+ *
+ * Usage:
+ * <pre><code>
+ * DrmSessionManager manager = ToroExo.with(context).createDrmSessionManager(mediaDrm);
+ * Config config = new Config.Builder().setDrmSessionManager(manager);
+ * ExoCreator creator = ToroExo.with(context).getCreator(config);
+ * </code></pre>
+ */
+ @SuppressWarnings("unused") @RequiresApi(18) @Nullable //
+ public DrmSessionManager<FrameworkMediaCrypto> createDrmSessionManager(@NonNull DrmMedia drm) {
+ DrmSessionManager<FrameworkMediaCrypto> drmSessionManager = null;
+ int errorStringId = R.string.error_drm_unknown;
+ String subString = null;
+ if (Util.SDK_INT < 18) {
+ errorStringId = R.string.error_drm_not_supported;
+ } else {
+ UUID drmSchemeUuid = getDrmUuid(checkNotNull(drm).getType());
+ if (drmSchemeUuid == null) {
+ errorStringId = R.string.error_drm_unsupported_scheme;
+ } else {
+ HttpDataSource.Factory factory = new DefaultHttpDataSourceFactory(appName);
+ try {
+ drmSessionManager = buildDrmSessionManagerV18(drmSchemeUuid, drm.getLicenseUrl(),
+ drm.getKeyRequestPropertiesArray(), drm.multiSession(), factory);
+ } catch (UnsupportedDrmException e) {
+ e.printStackTrace();
+ errorStringId = e.reason == REASON_UNSUPPORTED_SCHEME ? //
+ R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown;
+ if (e.reason == REASON_UNSUPPORTED_SCHEME) {
+ subString = drm.getType();
+ }
+ }
+ }
+ }
+
+ if (drmSessionManager == null) {
+ String error = TextUtils.isEmpty(subString) ? context.getString(errorStringId)
+ : context.getString(errorStringId) + ": " + subString;
+ Toast.makeText(context, error, LENGTH_SHORT).show();
+ }
+
+ return drmSessionManager;
+ }
+
+ @RequiresApi(18) private static DrmSessionManager<FrameworkMediaCrypto> buildDrmSessionManagerV18(
+ @NonNull UUID uuid, @Nullable String licenseUrl, @Nullable String[] keyRequestPropertiesArray,
+ boolean multiSession, @NonNull HttpDataSource.Factory httpDataSourceFactory)
+ throws UnsupportedDrmException {
+ HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl, httpDataSourceFactory);
+ if (keyRequestPropertiesArray != null) {
+ for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) {
+ drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i],
+ keyRequestPropertiesArray[i + 1]);
+ }
+ }
+ return new DefaultDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid), drmCallback,
+ null, multiSession);
+ }
+
+ // Share the code of setting Volume. For use inside library only.
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) //
+ public static void setVolumeInfo(@NonNull SimpleExoPlayer player,
+ @NonNull VolumeInfo volumeInfo) {
+ if (player instanceof ToroExoPlayer) {
+ ((ToroExoPlayer) player).setVolumeInfo(volumeInfo);
+ } else {
+ if (volumeInfo.isMute()) {
+ player.setVolume(0f);
+ } else {
+ player.setVolume(volumeInfo.getVolume());
+ }
+ }
+ }
+
+ @SuppressWarnings("WeakerAccess") @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) //
+ public static VolumeInfo getVolumeInfo(SimpleExoPlayer player) {
+ if (player instanceof ToroExoPlayer) {
+ return new VolumeInfo(((ToroExoPlayer) player).getVolumeInfo());
+ } else {
+ float volume = player.getVolume();
+ return new VolumeInfo(volume == 0, volume);
+ }
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private static String getUserAgent() {
+ return APIUtils.USER_AGENT;
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ToroExoPlayer.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ToroExoPlayer.java
new file mode 100644
index 00000000..2fe92569
--- /dev/null
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ToroExoPlayer.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) 2018 Nam Nguyen, nam@ene.im
+ *
+ * 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 ml.docilealligator.infinityforreddit.videoautoplay;
+
+import static ml.docilealligator.infinityforreddit.videoautoplay.ToroUtil.checkNotNull;
+
+import android.content.Context;
+import android.os.Looper;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.google.android.exoplayer2.LoadControl;
+import com.google.android.exoplayer2.RenderersFactory;
+import com.google.android.exoplayer2.SimpleExoPlayer;
+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.upstream.BandwidthMeter;
+
+import ml.docilealligator.infinityforreddit.videoautoplay.media.VolumeInfo;
+
+/**
+ * A custom {@link SimpleExoPlayer} that also notify the change of Volume.
+ *
+ * @author eneim (2018/03/27).
+ */
+@SuppressWarnings("WeakerAccess") //
+public class ToroExoPlayer extends SimpleExoPlayer {
+
+ protected ToroExoPlayer(Context context, RenderersFactory renderersFactory,
+ TrackSelector trackSelector, LoadControl loadControl, BandwidthMeter bandwidthMeter,
+ @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager, Looper looper) {
+ super(context, renderersFactory, trackSelector, loadControl, bandwidthMeter, drmSessionManager,
+ looper);
+ }
+
+ private ToroPlayer.VolumeChangeListeners listeners;
+
+ public final void addOnVolumeChangeListener(@NonNull ToroPlayer.OnVolumeChangeListener listener) {
+ if (this.listeners == null) this.listeners = new ToroPlayer.VolumeChangeListeners();
+ this.listeners.add(checkNotNull(listener));
+ }
+
+ public final void removeOnVolumeChangeListener(ToroPlayer.OnVolumeChangeListener listener) {
+ if (this.listeners != null) this.listeners.remove(listener);
+ }
+
+ public final void clearOnVolumeChangeListener() {
+ if (this.listeners != null) this.listeners.clear();
+ }
+
+ @CallSuper @Override public void setVolume(float audioVolume) {
+ this.setVolumeInfo(new VolumeInfo(audioVolume == 0, audioVolume));
+ }
+
+ private final VolumeInfo volumeInfo = new VolumeInfo(false, 1f);
+
+ @SuppressWarnings("UnusedReturnValue")
+ public final boolean setVolumeInfo(@NonNull VolumeInfo volumeInfo) {
+ boolean changed = !this.volumeInfo.equals(volumeInfo);
+ if (changed) {
+ this.volumeInfo.setTo(volumeInfo.isMute(), volumeInfo.getVolume());
+ super.setVolume(volumeInfo.isMute() ? 0 : volumeInfo.getVolume());
+ if (listeners != null) {
+ for (ToroPlayer.OnVolumeChangeListener listener : this.listeners) {
+ listener.onVolumeChanged(volumeInfo);
+ }
+ }
+ }
+
+ return changed;
+ }
+
+ @SuppressWarnings("unused") @NonNull public final VolumeInfo getVolumeInfo() {
+ return volumeInfo;
+ }
+}
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ToroPlayer.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ToroPlayer.java
new file mode 100644
index 00000000..5075b1b7
--- /dev/null
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ToroPlayer.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (c) 2017 Nam Nguyen, nam@ene.im
+ *
+ * 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 ml.docilealligator.infinityforreddit.videoautoplay;
+
+import android.view.View;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+import ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo;
+import ml.docilealligator.infinityforreddit.videoautoplay.media.VolumeInfo;
+import ml.docilealligator.infinityforreddit.videoautoplay.widget.Container;
+
+/**
+ * Definition of a Player used in Toro. Besides common playback command ({@link #play()}, {@link
+ * #pause()}, etc), it provides the library necessary information about the playback and
+ * components.
+ *
+ * @author eneim | 5/31/17.
+ */
+
+public interface ToroPlayer {
+
+ @NonNull View getPlayerView();
+
+ @NonNull
+ PlaybackInfo getCurrentPlaybackInfo();
+
+ /**
+ * Initialize resource for the incoming playback. After this point, {@link ToroPlayer} should be
+ * able to start the playback at anytime in the future (This doesn't mean that any call to {@link
+ * ToroPlayer#play()} will start the playback immediately. It can start buffering enough resource
+ * before any rendering).
+ *
+ * @param container the RecyclerView contains this Player.
+ * @param playbackInfo initialize info for the preparation.
+ */
+ void initialize(@NonNull Container container, @NonNull PlaybackInfo playbackInfo);
+
+ /**
+ * Start playback or resume from a pausing state.
+ */
+ void play();
+
+ /**
+ * Pause current playback.
+ */
+ void pause();
+
+ boolean isPlaying();
+
+ /**
+ * Tear down all the setup. This should release all player instances.
+ */
+ void release();
+
+ boolean wantsToPlay();
+
+ /**
+ * @return prefer playback order in list. Can be customized.
+ */
+ int getPlayerOrder();
+
+ /**
+ * A convenient callback to help {@link ToroPlayer} to listen to different playback states.
+ */
+ interface EventListener {
+
+ void onFirstFrameRendered();
+
+ void onBuffering(); // ExoPlayer state: 2
+
+ void onPlaying(); // ExoPlayer state: 3, play flag: true
+
+ void onPaused(); // ExoPlayer state: 3, play flag: false
+
+ void onCompleted(); // ExoPlayer state: 4
+ }
+
+ interface OnVolumeChangeListener {
+
+ void onVolumeChanged(@NonNull VolumeInfo volumeInfo);
+ }
+
+ interface OnErrorListener {
+
+ void onError(Exception error);
+ }
+
+ class EventListeners extends CopyOnWriteArraySet<EventListener> implements EventListener {
+
+ @Override public void onFirstFrameRendered() {
+ for (EventListener listener : this) {
+ listener.onFirstFrameRendered();
+ }
+ }
+
+ @Override public void onBuffering() {
+ for (EventListener listener : this) {
+ listener.onBuffering();
+ }
+ }
+
+ @Override public void onPlaying() {
+ for (EventListener listener : this) {
+ listener.onPlaying();
+ }
+ }
+
+ @Override public void onPaused() {
+ for (EventListener listener : this) {
+ listener.onPaused();
+ }
+ }
+
+ @Override public void onCompleted() {
+ for (EventListener listener : this) {
+ listener.onCompleted();
+ }
+ }
+ }
+
+ class ErrorListeners extends CopyOnWriteArraySet<OnErrorListener>
+ implements ToroPlayer.OnErrorListener {
+
+ @Override public void onError(Exception error) {
+ for (ToroPlayer.OnErrorListener listener : this) {
+ listener.onError(error);
+ }
+ }
+ }
+
+ class VolumeChangeListeners extends CopyOnWriteArraySet<ToroPlayer.OnVolumeChangeListener>
+ implements ToroPlayer.OnVolumeChangeListener {
+
+ @Override public void onVolumeChanged(@NonNull VolumeInfo volumeInfo) {
+ for (ToroPlayer.OnVolumeChangeListener listener : this) {
+ listener.onVolumeChanged(volumeInfo);
+ }
+ }
+ }
+
+ // Adapt from ExoPlayer.
+ @Retention(RetentionPolicy.SOURCE) //
+ @IntDef({ State.STATE_IDLE, State.STATE_BUFFERING, State.STATE_READY, State.STATE_END }) //
+ @interface State {
+ int STATE_IDLE = 1;
+ int STATE_BUFFERING = 2;
+ int STATE_READY = 3;
+ int STATE_END = 4;
+ }
+}
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ToroUtil.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ToroUtil.java
new file mode 100644
index 00000000..ecfc7d1d
--- /dev/null
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ToroUtil.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (c) 2017 Nam Nguyen, nam@ene.im
+ *
+ * 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 ml.docilealligator.infinityforreddit.videoautoplay;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+
+import androidx.annotation.FloatRange;
+import androidx.annotation.NonNull;
+import androidx.coordinatorlayout.widget.CoordinatorLayout;
+
+import ml.docilealligator.infinityforreddit.videoautoplay.widget.Container;
+
+/**
+ * @author eneim | 5/31/17.
+ */
+
+public final class ToroUtil {
+
+ @SuppressWarnings("unused") private static final String TAG = "ToroLib:Util";
+
+ private ToroUtil() {
+ throw new RuntimeException("Meh!");
+ }
+
+ /**
+ * Get the ratio in range of 0.0 ~ 1.0 the visible area of a {@link ToroPlayer}'s playerView.
+ *
+ * @param player the {@link ToroPlayer} need to investigate.
+ * @param container the {@link ViewParent} that holds the {@link ToroPlayer}. If {@code null}
+ * then this method must returns 0.0f;
+ * @return the value in range of 0.0 ~ 1.0 of the visible area.
+ */
+ @FloatRange(from = 0.0, to = 1.0) //
+ public static float visibleAreaOffset(@NonNull ToroPlayer player, ViewParent container) {
+ if (container == null) return 0.0f;
+
+ View playerView = player.getPlayerView();
+ Rect drawRect = new Rect();
+ playerView.getDrawingRect(drawRect);
+ int drawArea = drawRect.width() * drawRect.height();
+
+ Rect playerRect = new Rect();
+ boolean visible = playerView.getGlobalVisibleRect(playerRect, new Point());
+
+ float offset = 0.f;
+ if (visible && drawArea > 0) {
+ int visibleArea = playerRect.height() * playerRect.width();
+ offset = visibleArea / (float) drawArea;
+ }
+ return offset;
+ }
+
+ /**
+ * Ensures that an object reference passed as a parameter to the calling
+ * method is not null.
+ *
+ * @param reference an object reference
+ * @return the non-null reference that was validated
+ * @throws NullPointerException if {@code reference} is null
+ */
+ public static @NonNull <T> T checkNotNull(final T reference) {
+ if (reference == null) {
+ throw new NullPointerException();
+ }
+ return reference;
+ }
+
+ /**
+ * Ensures that an object reference passed as a parameter to the calling
+ * method is not null.
+ *
+ * @param reference an object reference
+ * @param errorMessage the exception message to use if the check fails; will
+ * be converted to a string using {@link String#valueOf(Object)}
+ * @return the non-null reference that was validated
+ * @throws NullPointerException if {@code reference} is null
+ */
+ public static @NonNull <T> T checkNotNull(final T reference, final Object errorMessage) {
+ if (reference == null) {
+ throw new NullPointerException(String.valueOf(errorMessage));
+ }
+ return reference;
+ }
+
+ @SuppressWarnings("unchecked") //
+ public static void wrapParamBehavior(@NonNull final Container container,
+ final Container.BehaviorCallback callback) {
+ container.setBehaviorCallback(callback);
+ ViewGroup.LayoutParams params = container.getLayoutParams();
+ if (params instanceof CoordinatorLayout.LayoutParams) {
+ CoordinatorLayout.Behavior temp = ((CoordinatorLayout.LayoutParams) params).getBehavior();
+ if (temp != null) {
+ ((CoordinatorLayout.LayoutParams) params).setBehavior(new Container.Behavior(temp));
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/annotations/Beta.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/annotations/Beta.java
new file mode 100644
index 00000000..11f9944b
--- /dev/null
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/annotations/Beta.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2018 Nam Nguyen, nam@ene.im
+ *
+ * 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 ml.docilealligator.infinityforreddit.videoautoplay.annotations;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Indicate that the feature is still in Beta testing and may not ready for production.
+ *
+ * @author eneim (2018/02/27).
+ */
+
+@Retention(RetentionPolicy.SOURCE) //
+public @interface Beta {
+}
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/annotations/RemoveIn.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/annotations/RemoveIn.java
new file mode 100644
index 00000000..504c0ebf
--- /dev/null
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/annotations/RemoveIn.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2018 Nam Nguyen, nam@ene.im
+ *
+ * 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 ml.docilealligator.infinityforreddit.videoautoplay.annotations;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * This annotation is to mark deprecated objects to be removed from library from a certain version,
+ * specific by {@link #version()}. This is to help quickly navigate through them, and to make it clear.
+ *
+ * @author eneim (2018/05/01).
+ */
+@Retention(RetentionPolicy.SOURCE) //
+public @interface RemoveIn {
+
+ String version();
+}
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/annotations/Sorted.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/annotations/Sorted.java
new file mode 100644
index 00000000..91b72c85
--- /dev/null
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/annotations/Sorted.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2018 Nam Nguyen, nam@ene.im
+ *
+ * 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 ml.docilealligator.infinityforreddit.videoautoplay.annotations;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * @author eneim (2018/02/07).
+ *
+ * Annotate that a list of items are sorted in specific {@link Order}.
+ */
+
+@Retention(RetentionPolicy.SOURCE) //
+public @interface Sorted {
+
+ Order order() default Order.ASCENDING;
+
+ enum Order {
+ ASCENDING, DESCENDING, UNSORTED
+ }
+}
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/helper/ToroPlayerHelper.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/helper/ToroPlayerHelper.java
new file mode 100644
index 00000000..7f2aa825
--- /dev/null
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/helper/ToroPlayerHelper.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (c) 2017 Nam Nguyen, nam@ene.im
+ *
+ * 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 ml.docilealligator.infinityforreddit.videoautoplay.helper;
+
+import static ml.docilealligator.infinityforreddit.videoautoplay.ToroUtil.checkNotNull;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.FloatRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+import ml.docilealligator.infinityforreddit.videoautoplay.ToroPlayer;
+import ml.docilealligator.infinityforreddit.videoautoplay.annotations.RemoveIn;
+import ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo;
+import ml.docilealligator.infinityforreddit.videoautoplay.media.VolumeInfo;
+import ml.docilealligator.infinityforreddit.videoautoplay.widget.Container;
+
+/**
+ * General definition of a helper class for a specific {@link ToroPlayer}. This class helps
+ * forwarding the playback state to the {@link ToroPlayer} if there is any {@link EventListener}
+ * registered. It also requests the initialization for the Player.
+ *
+ * From 3.4.0, this class can be reused as much as possible.
+ *
+ * @author eneim | 6/11/17.
+ */
+public abstract class ToroPlayerHelper {
+
+ private final Handler handler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
+ @Override public boolean handleMessage(Message msg) {
+ boolean playWhenReady = (boolean) msg.obj;
+ switch (msg.what) {
+ case ToroPlayer.State.STATE_IDLE:
+ // TODO: deal with idle state, maybe error handling.
+ break;
+ case ToroPlayer.State.STATE_BUFFERING /* Player.STATE_BUFFERING */:
+ internalListener.onBuffering();
+ for (ToroPlayer.EventListener listener : getEventListeners()) {
+ listener.onBuffering();
+ }
+ break;
+ case ToroPlayer.State.STATE_READY /* Player.STATE_READY */:
+ if (playWhenReady) {
+ internalListener.onPlaying();
+ } else {
+ internalListener.onPaused();
+ }
+
+ for (ToroPlayer.EventListener listener : getEventListeners()) {
+ if (playWhenReady) {
+ listener.onPlaying();
+ } else {
+ listener.onPaused();
+ }
+ }
+ break;
+ case ToroPlayer.State.STATE_END /* Player.STATE_ENDED */:
+ internalListener.onCompleted();
+ for (ToroPlayer.EventListener listener : getEventListeners()) {
+ listener.onCompleted();
+ }
+ break;
+ default:
+ break;
+ }
+ return true;
+ }
+ });
+
+ @NonNull protected final ToroPlayer player;
+
+ // This instance should be setup from #initialize and cleared from #release
+ protected Container container;
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) //
+ private ToroPlayer.EventListeners eventListeners;
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) //
+ private ToroPlayer.VolumeChangeListeners volumeChangeListeners;
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) //
+ private ToroPlayer.ErrorListeners errorListeners;
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) //
+ protected final ToroPlayer.EventListener internalListener = new ToroPlayer.EventListener() {
+ @Override public void onFirstFrameRendered() {
+
+ }
+
+ @Override public void onBuffering() {
+ // do nothing
+ }
+
+ @Override public void onPlaying() {
+ player.getPlayerView().setKeepScreenOn(true);
+ }
+
+ @Override public void onPaused() {
+ player.getPlayerView().setKeepScreenOn(false);
+ if (container != null) {
+ container.savePlaybackInfo( //
+ player.getPlayerOrder(), checkNotNull(player.getCurrentPlaybackInfo()));
+ }
+ }
+
+ @Override public void onCompleted() {
+ if (container != null) {
+ // Save PlaybackInfo.SCRAP to mark this player to be re-init.
+ container.savePlaybackInfo(player.getPlayerOrder(), PlaybackInfo.SCRAP);
+ }
+ }
+ };
+
+ public ToroPlayerHelper(@NonNull ToroPlayer player) {
+ this.player = player;
+ }
+
+ public final void addPlayerEventListener(@NonNull ToroPlayer.EventListener listener) {
+ getEventListeners().add(checkNotNull(listener));
+ }
+
+ public final void removePlayerEventListener(ToroPlayer.EventListener listener) {
+ if (eventListeners != null) eventListeners.remove(listener);
+ }
+
+ /**
+ * Initialize the necessary resource for the incoming playback. For example, prepare the
+ * ExoPlayer instance for SimpleExoPlayerView. The initialization is feed by an initial playback
+ * info, telling if the playback should start from a specific position or from beginning.
+ *
+ * Normally this info can be obtained from cache if there is cache manager, or {@link PlaybackInfo#SCRAP}
+ * if there is no such cached information.
+ *
+ * @param playbackInfo the initial playback info.
+ */
+ protected abstract void initialize(@NonNull PlaybackInfo playbackInfo);
+
+ public final void initialize(@NonNull Container container, @NonNull PlaybackInfo playbackInfo) {
+ this.container = container;
+ this.initialize(playbackInfo);
+ }
+
+ public abstract void play();
+
+ public abstract void pause();
+
+ public abstract boolean isPlaying();
+
+ /**
+ * @deprecated use {@link #setVolumeInfo(VolumeInfo)} instead.
+ */
+ @RemoveIn(version = "3.6.0") @Deprecated //
+ public abstract void setVolume(@FloatRange(from = 0.0, to = 1.0) float volume);
+
+ /**
+ * @deprecated use {@link #getVolumeInfo()} instead.
+ */
+ @RemoveIn(version = "3.6.0") @Deprecated //
+ public abstract @FloatRange(from = 0.0, to = 1.0) float getVolume();
+
+ public abstract void setVolumeInfo(@NonNull VolumeInfo volumeInfo);
+
+ @NonNull public abstract VolumeInfo getVolumeInfo();
+
+ /**
+ * Get latest playback info. Either on-going playback info if current player is playing, or latest
+ * playback info available if player is paused.
+ *
+ * @return latest {@link PlaybackInfo} of current Player.
+ */
+ @NonNull public abstract PlaybackInfo getLatestPlaybackInfo();
+
+ public abstract void setPlaybackInfo(@NonNull PlaybackInfo playbackInfo);
+
+ @CallSuper
+ public void addOnVolumeChangeListener(@NonNull ToroPlayer.OnVolumeChangeListener listener) {
+ getVolumeChangeListeners().add(checkNotNull(listener));
+ }
+
+ @CallSuper public void removeOnVolumeChangeListener(ToroPlayer.OnVolumeChangeListener listener) {
+ if (volumeChangeListeners != null) volumeChangeListeners.remove(listener);
+ }
+
+ @CallSuper public void addErrorListener(@NonNull ToroPlayer.OnErrorListener listener) {
+ getErrorListeners().add(checkNotNull(listener));
+ }
+
+ @CallSuper public void removeErrorListener(ToroPlayer.OnErrorListener listener) {
+ if (errorListeners != null) errorListeners.remove(listener);
+ }
+
+ @NonNull protected final ToroPlayer.EventListeners getEventListeners() {
+ if (eventListeners == null) eventListeners = new ToroPlayer.EventListeners();
+ return eventListeners;
+ }
+
+ @NonNull protected final ToroPlayer.VolumeChangeListeners getVolumeChangeListeners() {
+ if (volumeChangeListeners == null) {
+ volumeChangeListeners = new ToroPlayer.VolumeChangeListeners();
+ }
+ return volumeChangeListeners;
+ }
+
+ @NonNull protected final ToroPlayer.ErrorListeners getErrorListeners() {
+ if (errorListeners == null) errorListeners = new ToroPlayer.ErrorListeners();
+ return errorListeners;
+ }
+
+ // Mimic ExoPlayer
+ @CallSuper protected final void onPlayerStateUpdated(boolean playWhenReady,
+ @ToroPlayer.State int playbackState) {
+ handler.obtainMessage(playbackState, playWhenReady).sendToTarget();
+ }
+
+ @CallSuper public void release() {
+ handler.removeCallbacksAndMessages(null);
+ this.container = null;
+ }
+
+ @NonNull @Override public String toString() {
+ return "ToroLib:Helper{" + "player=" + player + ", container=" + container + '}';
+ }
+}
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/media/DrmMedia.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/media/DrmMedia.java
new file mode 100644
index 00000000..f48f3885
--- /dev/null
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/media/DrmMedia.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2017 Nam Nguyen, nam@ene.im
+ *
+ * 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 ml.docilealligator.infinityforreddit.videoautoplay.media;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * @author eneim | 6/5/17.
+ *
+ * A definition of DRM media type.
+ */
+
+public interface DrmMedia {
+
+ // DRM Scheme
+ @NonNull String getType();
+
+ @Nullable String getLicenseUrl();
+
+ @Nullable String[] getKeyRequestPropertiesArray();
+
+ boolean multiSession();
+}
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/media/PlaybackInfo.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/media/PlaybackInfo.java
new file mode 100644
index 00000000..445fe80a
--- /dev/null
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/media/PlaybackInfo.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (c) 2017 Nam Nguyen, nam@ene.im
+ *
+ * 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 ml.docilealligator.infinityforreddit.videoautoplay.media;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+
+/**
+ * @author eneim | 6/6/17.
+ */
+
+public class PlaybackInfo implements Parcelable {
+
+ public static final long TIME_UNSET = Long.MIN_VALUE + 1;
+
+ public static final int INDEX_UNSET = -1;
+
+ private int resumeWindow;
+ private long resumePosition;
+ @NonNull private VolumeInfo volumeInfo;
+
+ public PlaybackInfo(int resumeWindow, long resumePosition) {
+ this.resumeWindow = resumeWindow;
+ this.resumePosition = resumePosition;
+ this.volumeInfo = new VolumeInfo(false, 1.f);
+ }
+
+ public PlaybackInfo(int resumeWindow, long resumePosition, @NonNull VolumeInfo volumeInfo) {
+ this.resumeWindow = resumeWindow;
+ this.resumePosition = resumePosition;
+ this.volumeInfo = volumeInfo;
+ }
+
+ public PlaybackInfo() {
+ this(INDEX_UNSET, TIME_UNSET);
+ }
+
+ public PlaybackInfo(PlaybackInfo other) {
+ this(other.getResumeWindow(), other.getResumePosition(), other.getVolumeInfo());
+ }
+
+ public int getResumeWindow() {
+ return resumeWindow;
+ }
+
+ public void setResumeWindow(int resumeWindow) {
+ this.resumeWindow = resumeWindow;
+ }
+
+ public long getResumePosition() {
+ return resumePosition;
+ }
+
+ public void setResumePosition(long resumePosition) {
+ this.resumePosition = resumePosition;
+ }
+
+ @NonNull public VolumeInfo getVolumeInfo() {
+ return volumeInfo;
+ }
+
+ public void setVolumeInfo(@NonNull VolumeInfo volumeInfo) {
+ this.volumeInfo = volumeInfo;
+ }
+
+ public void reset() {
+ resumeWindow = INDEX_UNSET;
+ resumePosition = TIME_UNSET;
+ volumeInfo = new VolumeInfo(false, 1.f);
+ }
+
+ @Override public String toString() {
+ return this == SCRAP ? "Info:SCRAP" : //
+ "Info{"
+ + "window="
+ + resumeWindow
+ + ", position="
+ + resumePosition
+ + ", volume="
+ + volumeInfo
+ + '}';
+ }
+
+ @Override public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof PlaybackInfo)) return false;
+
+ PlaybackInfo that = (PlaybackInfo) o;
+
+ if (resumeWindow != that.resumeWindow) return false;
+ return resumePosition == that.resumePosition;
+ }
+
+ @Override public int hashCode() {
+ int result = resumeWindow;
+ result = 31 * result + (int) (resumePosition ^ (resumePosition >>> 32));
+ return result;
+ }
+
+ @Override public int describeContents() {
+ return 0;
+ }
+
+ @Override public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(this.resumeWindow);
+ dest.writeLong(this.resumePosition);
+ dest.writeParcelable(this.volumeInfo, flags);
+ }
+
+ protected PlaybackInfo(Parcel in) {
+ this.resumeWindow = in.readInt();
+ this.resumePosition = in.readLong();
+ this.volumeInfo = in.readParcelable(VolumeInfo.class.getClassLoader());
+ }
+
+ public static final Creator<PlaybackInfo> CREATOR = new Creator<PlaybackInfo>() {
+ @Override public PlaybackInfo createFromParcel(Parcel source) {
+ return new PlaybackInfo(source);
+ }
+
+ @Override public PlaybackInfo[] newArray(int size) {
+ return new PlaybackInfo[size];
+ }
+ };
+
+ // A default PlaybackInfo instance, only use this to mark un-initialized players.
+ public static final PlaybackInfo SCRAP = new PlaybackInfo();
+}
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/media/VolumeInfo.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/media/VolumeInfo.java
new file mode 100644
index 00000000..34308820
--- /dev/null
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/media/VolumeInfo.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (c) 2018 Nam Nguyen, nam@ene.im
+ *
+ * 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 ml.docilealligator.infinityforreddit.videoautoplay.media;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import androidx.annotation.FloatRange;
+
+/**
+ * Information about volume of a playback. There are a few state this class could show:
+ *
+ * - An expected volume value.
+ * - State of mute or not.
+ *
+ * When {@link #mute} is {@code true}, {@link #volume} value will be ignored. But when {@link #mute}
+ * is set to {@code false}, actual volume value will be brought to the playback.
+ *
+ * This volume information doesn't relate to system Volume. Which means that even if client set
+ * this to non-mute volume, the device's volume setup wins the actual behavior.
+ *
+ * @author eneim (2018/03/14).
+ */
+public final class VolumeInfo implements Parcelable {
+
+ // Indicate that the playback is in muted state or not.
+ private boolean mute;
+ // The actual Volume value if 'mute' is false.
+ @FloatRange(from = 0, to = 1) private float volume;
+
+ public VolumeInfo(boolean mute, @FloatRange(from = 0, to = 1) float volume) {
+ this.mute = mute;
+ this.volume = volume;
+ }
+
+ public VolumeInfo(VolumeInfo other) {
+ this(other.isMute(), other.getVolume());
+ }
+
+ public boolean isMute() {
+ return mute;
+ }
+
+ public void setMute(boolean mute) {
+ this.mute = mute;
+ }
+
+ @FloatRange(from = 0, to = 1) public float getVolume() {
+ return volume;
+ }
+
+ public void setVolume(@FloatRange(from = 0, to = 1) float volume) {
+ this.volume = volume;
+ }
+
+ public void setTo(boolean mute, @FloatRange(from = 0, to = 1) float volume) {
+ this.mute = mute;
+ this.volume = volume;
+ }
+
+ @Override public int describeContents() {
+ return 0;
+ }
+
+ @Override public void writeToParcel(Parcel dest, int flags) {
+ dest.writeByte(this.mute ? (byte) 1 : (byte) 0);
+ dest.writeFloat(this.volume);
+ }
+
+ protected VolumeInfo(Parcel in) {
+ this.mute = in.readByte() != 0;
+ this.volume = in.readFloat();
+ }
+
+ public static final Creator<VolumeInfo> CREATOR = new ClassLoaderCreator<VolumeInfo>() {
+ @Override public VolumeInfo createFromParcel(Parcel source, ClassLoader loader) {
+ return new VolumeInfo(source);
+ }
+
+ @Override public VolumeInfo createFromParcel(Parcel source) {
+ return new VolumeInfo(source);
+ }
+
+ @Override public VolumeInfo[] newArray(int size) {
+ return new VolumeInfo[size];
+ }
+ };
+
+ @SuppressWarnings("SimplifiableIfStatement") @Override public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ VolumeInfo that = (VolumeInfo) o;
+
+ if (mute != that.mute) return false;
+ return Float.compare(that.volume, volume) == 0;
+ }
+
+ @Override public int hashCode() {
+ int result = (mute ? 1 : 0);
+ result = 31 * result + (volume != +0.0f ? Float.floatToIntBits(volume) : 0);
+ return result;
+ }
+
+ @Override public String toString() {
+ return "Vol{" + "mute=" + mute + ", volume=" + volume + '}';
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ui/ToroControlView.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ui/ToroControlView.java
new file mode 100644
index 00000000..0ab474db
--- /dev/null
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ui/ToroControlView.java
@@ -0,0 +1,278 @@
+/*
+ * Copyright (c) 2018 Nam Nguyen, nam@ene.im
+ *
+ * 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 ml.docilealligator.infinityforreddit.videoautoplay.ui;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.core.view.ViewCompat;
+
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.SimpleExoPlayer;
+import com.google.android.exoplayer2.ui.PlayerControlView;
+import com.google.android.exoplayer2.ui.PlayerView;
+import com.google.android.exoplayer2.ui.TimeBar;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+import ml.docilealligator.infinityforreddit.R;
+import ml.docilealligator.infinityforreddit.videoautoplay.ToroExo;
+import ml.docilealligator.infinityforreddit.videoautoplay.ToroExoPlayer;
+import ml.docilealligator.infinityforreddit.videoautoplay.ToroPlayer;
+import ml.docilealligator.infinityforreddit.videoautoplay.media.VolumeInfo;
+
+/**
+ * An extension of {@link PlayerControlView} that adds Volume control buttons. It works on-par
+ * with {@link PlayerView}. Will be automatically inflated when client uses {@link R.layout.toro_exo_player_view}
+ * for {@link PlayerView} layout.
+ *
+ * @author eneim (2018/08/20).
+ * @since 3.6.0.2802
+ */
+public class ToroControlView extends PlayerControlView {
+
+ @SuppressWarnings("unused") static final String TAG = "ToroExo:Control";
+
+ // Statically obtain from super class.
+ protected static Method hideAfterTimeoutMethod; // from parent ...
+ protected static boolean hideMethodFetched;
+ protected static Field hideActionField;
+ protected static boolean hideActionFetched;
+
+ final ComponentListener componentListener;
+ final View volumeUpButton;
+ final View volumeOffButton;
+ final TimeBar volumeBar;
+ final VolumeInfo volumeInfo = new VolumeInfo(false, 1);
+
+ public ToroControlView(Context context) {
+ this(context, null);
+ }
+
+ public ToroControlView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ToroControlView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ volumeOffButton = findViewById(R.id.exo_volume_off);
+ volumeUpButton = findViewById(R.id.exo_volume_up);
+ volumeBar = findViewById(R.id.volume_bar);
+ componentListener = new ComponentListener();
+ }
+
+ @Override public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ if (volumeUpButton != null) volumeUpButton.setOnClickListener(componentListener);
+ if (volumeOffButton != null) volumeOffButton.setOnClickListener(componentListener);
+ if (volumeBar != null) volumeBar.addListener(componentListener);
+
+ updateVolumeButtons();
+ }
+
+ @Override public void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ if (volumeUpButton != null) volumeUpButton.setOnClickListener(null);
+ if (volumeOffButton != null) volumeOffButton.setOnClickListener(null);
+ if (volumeBar != null) volumeBar.removeListener(componentListener);
+ this.setPlayer(null);
+ }
+
+ @SuppressLint("ClickableViewAccessibility") @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ // After processing all children' touch event, this View will just stop it here.
+ // User can click to PlayerView to show/hide this view, but since this View's height is not
+ // significantly large, clicking to show/hide may disturb other actions like clicking to button,
+ // seeking the bars, etc. This extension will stop the touch event here so that PlayerView has
+ // nothing to do when User touch this View.
+ return true;
+ }
+
+ @Override public void setPlayer(Player player) {
+ Player current = super.getPlayer();
+ if (current == player) return;
+
+ if (current instanceof ToroExoPlayer) {
+ ((ToroExoPlayer) current).removeOnVolumeChangeListener(componentListener);
+ }
+
+ super.setPlayer(player);
+ current = super.getPlayer();
+ @NonNull final VolumeInfo tempVol;
+ if (current instanceof ToroExoPlayer) {
+ tempVol = ((ToroExoPlayer) current).getVolumeInfo();
+ ((ToroExoPlayer) current).addOnVolumeChangeListener(componentListener);
+ } else if (current instanceof SimpleExoPlayer) {
+ float volume = ((SimpleExoPlayer) current).getVolume();
+ tempVol = new VolumeInfo(volume == 0, volume);
+ } else {
+ tempVol = new VolumeInfo(false, 1f);
+ }
+
+ this.volumeInfo.setTo(tempVol.isMute(), tempVol.getVolume());
+ updateVolumeButtons();
+ }
+
+ @Override protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
+ super.onVisibilityChanged(changedView, visibility);
+ if (changedView == this) updateVolumeButtons();
+ }
+
+ @SuppressWarnings("ConstantConditions") //
+ void updateVolumeButtons() {
+ if (!isVisible() || !ViewCompat.isAttachedToWindow(this)) {
+ return;
+ }
+ boolean requestButtonFocus = false;
+ // if muted then show volumeOffButton, or else show volumeUpButton
+ boolean muted = volumeInfo.isMute();
+ if (volumeOffButton != null) {
+ requestButtonFocus |= muted && volumeOffButton.isFocused();
+ volumeOffButton.setVisibility(muted ? View.VISIBLE : View.GONE);
+ }
+ if (volumeUpButton != null) {
+ requestButtonFocus |= !muted && volumeUpButton.isFocused();
+ volumeUpButton.setVisibility(!muted ? View.VISIBLE : View.GONE);
+ }
+
+ if (volumeBar != null) {
+ volumeBar.setDuration(100);
+ volumeBar.setPosition(muted ? 0 : (long) (volumeInfo.getVolume() * 100));
+ }
+
+ if (requestButtonFocus) {
+ requestButtonFocus();
+ }
+
+ // A hack to access PlayerControlView's hideAfterTimeout. Don't want to re-implement it.
+ // Reflection happens once for all instances, so it should not affect the performance.
+ if (!hideMethodFetched) {
+ try {
+ hideAfterTimeoutMethod = PlayerControlView.class.getDeclaredMethod("hideAfterTimeout");
+ hideAfterTimeoutMethod.setAccessible(true);
+ } catch (NoSuchMethodException e) {
+ e.printStackTrace();
+ }
+ hideMethodFetched = true;
+ }
+
+ if (hideAfterTimeoutMethod != null) {
+ try {
+ hideAfterTimeoutMethod.invoke(this);
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ } catch (InvocationTargetException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ private void requestButtonFocus() {
+ boolean muted = volumeInfo.isMute();
+ if (!muted && volumeUpButton != null) {
+ volumeUpButton.requestFocus();
+ } else if (muted && volumeOffButton != null) {
+ volumeOffButton.requestFocus();
+ }
+ }
+
+ void dispatchOnScrubStart() {
+ // Fetch the 'hideAction' Runnable from super class. We need this to synchronize the show/hide
+ // behaviour when user does something.
+ if (!hideActionFetched) {
+ try {
+ hideActionField = PlayerControlView.class.getDeclaredField("hideAction");
+ hideActionField.setAccessible(true);
+ } catch (NoSuchFieldException e) {
+ e.printStackTrace();
+ }
+ hideActionFetched = true;
+ }
+
+ if (hideActionField != null) {
+ try {
+ removeCallbacks((Runnable) hideActionField.get(this));
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ // Scrub Move will always modify actual Volume, there is no 'mute-with-non-zero-volume' state.
+ void dispatchOnScrubMove(long position) {
+ if (position > 100) position = 100;
+ if (position < 0) position = 0;
+
+ float actualVolume = position / (float) 100;
+ this.volumeInfo.setTo(actualVolume == 0, actualVolume);
+ if (getPlayer() instanceof SimpleExoPlayer) {
+ ToroExo.setVolumeInfo((SimpleExoPlayer) getPlayer(), this.volumeInfo);
+ }
+
+ updateVolumeButtons();
+ }
+
+ void dispatchOnScrubStop(long position) {
+ this.dispatchOnScrubMove(position);
+ }
+
+ private class ComponentListener
+ implements OnClickListener, TimeBar.OnScrubListener, ToroPlayer.OnVolumeChangeListener {
+
+ ComponentListener() {
+ }
+
+ @Override public void onClick(View v) {
+ Player player = ToroControlView.super.getPlayer();
+ if (!(player instanceof SimpleExoPlayer)) return;
+ if (v == volumeOffButton) { // click to vol Off --> unmute
+ volumeInfo.setTo(false, volumeInfo.getVolume());
+ } else if (v == volumeUpButton) { // click to vol Up --> mute
+ volumeInfo.setTo(true, volumeInfo.getVolume());
+ }
+ ToroExo.setVolumeInfo((SimpleExoPlayer) player, volumeInfo);
+ updateVolumeButtons();
+ }
+
+ /// TimeBar.OnScrubListener
+
+ @Override public void onScrubStart(TimeBar timeBar, long position) {
+ dispatchOnScrubStart();
+ }
+
+ @Override public void onScrubMove(TimeBar timeBar, long position) {
+ dispatchOnScrubMove(position);
+ }
+
+ @Override public void onScrubStop(TimeBar timeBar, long position, boolean canceled) {
+ dispatchOnScrubStop(position);
+ }
+
+ /// ToroPlayer.OnVolumeChangeListener
+
+ @Override public void onVolumeChanged(@NonNull VolumeInfo volumeInfo) {
+ ToroControlView.this.volumeInfo.setTo(volumeInfo.isMute(), volumeInfo.getVolume());
+ updateVolumeButtons();
+ }
+ }
+}
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/widget/Common.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/widget/Common.java
new file mode 100644
index 00000000..30e86ace
--- /dev/null
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/widget/Common.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (c) 2017 Nam Nguyen, nam@ene.im
+ *
+ * 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 ml.docilealligator.infinityforreddit.videoautoplay.widget;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import ml.docilealligator.infinityforreddit.videoautoplay.ToroPlayer;
+
+/**
+ * @author eneim | 6/2/17.
+ *
+ * A hub for internal convenient methods.
+ */
+
+@SuppressWarnings({ "unused", "WeakerAccess" }) //
+@RestrictTo(RestrictTo.Scope.LIBRARY) //
+final class Common {
+
+ private static final String TAG = "ToroLib:Common";
+ // Keep static values to reduce instance initialization. We don't need to access its value.
+ private static final Rect dummyRect = new Rect();
+ private static final Point dummyPoint = new Point();
+
+ interface Filter<T> {
+
+ boolean accept(T target);
+ }
+
+ static int compare(int x, int y) {
+ //noinspection UseCompareMethod
+ return (x < y) ? -1 : ((x == y) ? 0 : 1);
+ }
+
+ static long max(Long... numbers) {
+ List<Long> list = Arrays.asList(numbers);
+ return Collections.<Long>max(list);
+ }
+
+ static Comparator<ToroPlayer> ORDER_COMPARATOR = new Comparator<ToroPlayer>() {
+ @Override public int compare(ToroPlayer o1, ToroPlayer o2) {
+ return Common.compare(o1.getPlayerOrder(), o2.getPlayerOrder());
+ }
+ };
+
+ static final Comparator<Integer> ORDER_COMPARATOR_INT = new Comparator<Integer>() {
+ @Override public int compare(Integer o1, Integer o2) {
+ return o1.compareTo(o2);
+ }
+ };
+
+ static boolean allowsToPlay(@NonNull ToroPlayer player) {
+ dummyRect.setEmpty();
+ dummyPoint.set(0, 0);
+ boolean valid = player instanceof RecyclerView.ViewHolder; // Should be true
+ if (valid) valid = ((RecyclerView.ViewHolder) player).itemView.getParent() != null;
+ if (valid) valid = player.getPlayerView().getGlobalVisibleRect(dummyRect, dummyPoint);
+ return valid;
+ }
+
+ @Nullable static <T> T findFirst(List<T> source, Filter<T> filter) {
+ for (T t : source) {
+ if (filter.accept(t)) return t;
+ }
+
+ return null;
+ }
+}
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/widget/Container.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/widget/Container.java
new file mode 100644
index 00000000..bf5958cf
--- /dev/null
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/widget/Container.java
@@ -0,0 +1,1137 @@
+/*
+ * Copyright (c) 2017 Nam Nguyen, nam@ene.im
+ *
+ * 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 ml.docilealligator.infinityforreddit.videoautoplay.widget;
+
+import static android.content.Context.POWER_SERVICE;
+import static ml.docilealligator.infinityforreddit.videoautoplay.ToroUtil.checkNotNull;
+import static ml.docilealligator.infinityforreddit.videoautoplay.widget.Common.max;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.PowerManager;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.coordinatorlayout.widget.CoordinatorLayout;
+import androidx.core.view.WindowInsetsCompat;
+import androidx.customview.view.AbsSavedState;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import ml.docilealligator.infinityforreddit.videoautoplay.CacheManager;
+import ml.docilealligator.infinityforreddit.videoautoplay.PlayerDispatcher;
+import ml.docilealligator.infinityforreddit.videoautoplay.PlayerSelector;
+import ml.docilealligator.infinityforreddit.videoautoplay.ToroPlayer;
+import ml.docilealligator.infinityforreddit.videoautoplay.annotations.RemoveIn;
+import ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo;
+
+/**
+ * A custom {@link RecyclerView} that is capable of managing and controlling the {@link ToroPlayer}s'
+ * playback behaviour.
+ *
+ * A client wish to have the auto playback behaviour should replace the normal use of
+ * {@link RecyclerView} with {@link Container}.
+ *
+ * By default, {@link Container} doesn't support playback position saving/restoring. This is
+ * because {@link Container} has no idea about the uniqueness of media content those are being
+ * played. This can be archived by supplying {@link Container} with a valid {@link CacheManager}. A
+ * {@link CacheManager} will help providing the uniqueness of Medias by which it can correctly
+ * save/restore the playback state of a specific media item. Setup this can be done using
+ * {@link Container#setCacheManager(CacheManager)}.
+ *
+ * {@link Container} uses {@link PlayerSelector} to control the {@link ToroPlayer}. A
+ * {@link PlayerSelector} will be asked to select which {@link ToroPlayer} to start playback, and
+ * those are not selected will be paused. By default, it uses {@link PlayerSelector#DEFAULT}.
+ * Custom {@link PlayerSelector} can be set via {@link Container#setPlayerSelector(PlayerSelector)}.
+ *
+ * @author eneim | 5/31/17.
+ */
+
+@SuppressWarnings({ "unused", "ConstantConditions" }) //
+public class Container extends RecyclerView {
+
+ private static final String TAG = "ToroLib:Container";
+
+ static final int SOME_BLINKS = 50; // 3 frames ...
+
+ /* package */ final PlayerManager playerManager;
+ /* package */ final ChildLayoutChangeListener childLayoutChangeListener;
+ /* package */ PlayerDispatcher playerDispatcher = PlayerDispatcher.DEFAULT;
+ /* package */ RecyclerListenerImpl recyclerListener; // null = not attached/detached
+ /* package */ PlayerSelector playerSelector = PlayerSelector.DEFAULT; // null = do nothing
+ /* package */ Handler animatorFinishHandler; // null = not attached/detached
+ /* package */ BehaviorCallback behaviorCallback;
+
+ public Container(Context context) {
+ this(context, null);
+ }
+
+ public Container(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public Container(Context context, @Nullable AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ playerManager = new PlayerManager();
+ childLayoutChangeListener = new ChildLayoutChangeListener(this);
+ requestDisallowInterceptTouchEvent(true);
+ }
+
+ @Override public final void setRecyclerListener(RecyclerListener listener) {
+ if (recyclerListener == null) recyclerListener = new RecyclerListenerImpl(this);
+ recyclerListener.delegate = listener;
+ super.setRecyclerListener(recyclerListener);
+ }
+
+ @CallSuper @Override protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ if (getAdapter() != null) dataObserver.registerAdapter(getAdapter());
+ if (animatorFinishHandler == null) {
+ animatorFinishHandler = new Handler(new AnimatorHelper(this));
+ }
+
+ PowerManager powerManager = (PowerManager) getContext().getSystemService(POWER_SERVICE);
+ if (powerManager != null && powerManager.isScreenOn()) {
+ this.screenState = View.SCREEN_STATE_ON;
+ } else {
+ this.screenState = View.SCREEN_STATE_OFF;
+ }
+
+ /* setRecyclerListener can be called before this, it is considered as user-setup */
+ if (recyclerListener == null) {
+ recyclerListener = new RecyclerListenerImpl(this);
+ recyclerListener.delegate = NULL; // mark as it is set by Toro, not user.
+ super.setRecyclerListener(recyclerListener); // must be a super call
+ }
+
+ playbackInfoCache.onAttach();
+ playerManager.onAttach();
+
+ ViewGroup.LayoutParams params = getLayoutParams();
+ if (params instanceof CoordinatorLayout.LayoutParams) {
+ CoordinatorLayout.Behavior behavior = ((CoordinatorLayout.LayoutParams) params).getBehavior();
+ if (behavior instanceof Behavior) {
+ ((Behavior) behavior).onViewAttached(this);
+ }
+ }
+ }
+
+ @CallSuper @Override protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ ViewGroup.LayoutParams params = getLayoutParams();
+ if (params instanceof CoordinatorLayout.LayoutParams) {
+ CoordinatorLayout.Behavior behavior = ((CoordinatorLayout.LayoutParams) params).getBehavior();
+ if (behavior instanceof Behavior) {
+ ((Behavior) behavior).onViewDetached(this);
+ }
+ }
+
+ if (recyclerListener != null && recyclerListener.delegate == NULL) { // set by Toro, not user.
+ super.setRecyclerListener(null); // must be a super call
+ recyclerListener = null;
+ }
+
+ if (animatorFinishHandler != null) {
+ animatorFinishHandler.removeCallbacksAndMessages(null);
+ animatorFinishHandler = null;
+ }
+
+ List<ToroPlayer> players = playerManager.getPlayers();
+ if (!players.isEmpty()) {
+ for (int size = players.size(), i = size - 1; i >= 0; i--) {
+ ToroPlayer player = players.get(i);
+ if (player.isPlaying()) {
+ this.savePlaybackInfo(player.getPlayerOrder(), player.getCurrentPlaybackInfo());
+ playerManager.pause(player);
+ }
+ playerManager.release(player);
+ }
+ playerManager.clear();
+ }
+ playerManager.onDetach();
+ playbackInfoCache.onDetach();
+ dataObserver.registerAdapter(null);
+ childLayoutChangeListener.containerRef.clear();
+ }
+
+ /**
+ * Filter current managed {@link ToroPlayer}s using {@link Filter}. Result is sorted by Player
+ * order obtained from {@link ToroPlayer#getPlayerOrder()}.
+ *
+ * @param filter the {@link Filter} to a {@link ToroPlayer}.
+ * @return list of players accepted by {@link Filter}. Empty list if there is no available player.
+ */
+ @NonNull public final List<ToroPlayer> filterBy(Filter filter) {
+ List<ToroPlayer> result = new ArrayList<>();
+ for (ToroPlayer player : playerManager.getPlayers()) {
+ if (filter.accept(player)) result.add(player);
+ }
+ Collections.sort(result, Common.ORDER_COMPARATOR);
+ return result;
+ }
+
+ // This method is called when:
+ // [1] A ViewHolder is newly created, bound and then attached to RecyclerView.
+ // [2] A ViewHolder is detached before, but still in bound state, not be recycled,
+ // and now be re-attached to RecyclerView.
+ // In either cases, PlayerManager should not manage the ViewHolder before this point.
+ @CallSuper @Override public void onChildAttachedToWindow(@NonNull final View child) {
+ super.onChildAttachedToWindow(child);
+ child.addOnLayoutChangeListener(childLayoutChangeListener);
+ final ViewHolder holder = getChildViewHolder(child);
+ if (!(holder instanceof ToroPlayer)) return;
+
+ final ToroPlayer player = (ToroPlayer) holder;
+ final View playerView = player.getPlayerView();
+ if (playerView == null) {
+ throw new NullPointerException("Expected non-null playerView, found null for: " + player);
+ }
+
+ playbackInfoCache.onPlayerAttached(player);
+ if (playerManager.manages(player)) {
+ // I don't expect this to be called. If this happens, make sure to note the scenario.
+ Log.w(TAG, "!!Already managed: player = [" + player + "]");
+ // Only if container is in idle state and player is not playing.
+ if (getScrollState() == SCROLL_STATE_IDLE && !player.isPlaying()) {
+ playerManager.play(player, playerDispatcher);
+ }
+ } else {
+ // LeakCanary report a leak of OnGlobalLayoutListener but I cannot figure out why ...
+ child.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
+ @Override public void onGlobalLayout() {
+ child.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ if (Common.allowsToPlay(player)) {
+ if (playerManager.attachPlayer(player)) {
+ dispatchUpdateOnAnimationFinished(false);
+ }
+ }
+ }
+ });
+ }
+ }
+
+ @CallSuper @Override public void onChildDetachedFromWindow(@NonNull View child) {
+ super.onChildDetachedFromWindow(child);
+ child.removeOnLayoutChangeListener(childLayoutChangeListener);
+ ViewHolder holder = getChildViewHolder(child);
+ if (!(holder instanceof ToroPlayer)) return;
+ final ToroPlayer player = (ToroPlayer) holder;
+
+ boolean playerManaged = playerManager.manages(player);
+ if (player.isPlaying()) {
+ if (!playerManaged) {
+ player.pause(); // Unstable state, so forcefully pause this by itself.
+ /* throw new IllegalStateException(
+ "Player is playing while it is not in managed state: " + player); */
+ }
+ this.savePlaybackInfo(player.getPlayerOrder(), player.getCurrentPlaybackInfo());
+ playerManager.pause(player);
+ }
+ if (playerManaged) {
+ playerManager.detachPlayer(player);
+ }
+ playbackInfoCache.onPlayerDetached(player);
+ // RecyclerView#onChildDetachedFromWindow(View) is called after other removal finishes, so
+ // sometime it happens after all Animation, but we also need to update playback here.
+ // If there is no anymore child view, this call will end early.
+ dispatchUpdateOnAnimationFinished(true);
+ // finally release the player
+ // if player manager could not manager player, release by itself.
+ if (!playerManager.release(player)) player.release();
+ }
+
+ @CallSuper @Override public void onScrollStateChanged(int state) {
+ super.onScrollStateChanged(state);
+ // Need to handle the dead playback even when the Container is still scrolling/flinging.
+ List<ToroPlayer> players = playerManager.getPlayers();
+ // 1. Find players those are managed but not qualified to play anymore.
+ for (int i = 0, size = players.size(); i < size; i++) {
+ ToroPlayer player = players.get(i);
+ if (Common.allowsToPlay(player)) continue;
+ if (player.isPlaying()) {
+ this.savePlaybackInfo(player.getPlayerOrder(), player.getCurrentPlaybackInfo());
+ playerManager.pause(player);
+ }
+ if (!playerManager.release(player)) player.release();
+ playerManager.detachPlayer(player);
+ }
+
+ // 2. Refresh the good players list.
+ LayoutManager layout = super.getLayoutManager();
+ // current number of visible 'Virtual Children', or zero if there is no LayoutManager available.
+ int childCount = layout != null ? layout.getChildCount() : 0;
+ if (childCount <= 0 || state != SCROLL_STATE_IDLE) {
+ playerManager.deferPlaybacks();
+ return;
+ }
+
+ for (int i = 0; i < childCount; i++) {
+ View child = layout.getChildAt(i);
+ ViewHolder holder = super.getChildViewHolder(child);
+ if (holder instanceof ToroPlayer) {
+ ToroPlayer player = (ToroPlayer) holder;
+ // Check candidate's condition
+ if (Common.allowsToPlay(player)) {
+ if (!playerManager.manages(player)) {
+ playerManager.attachPlayer(player);
+ }
+ // Don't check the attach result, because the player may be managed already.
+ if (!player.isPlaying()) { // not playing or not ready to play.
+ playerManager.initialize(player, Container.this);
+ }
+ }
+ }
+ }
+
+ final List<ToroPlayer> source = playerManager.getPlayers();
+ int count = source.size();
+ if (count < 1) return; // No available player, return.
+
+ List<ToroPlayer> candidates = new ArrayList<>();
+ for (int i = 0; i < count; i++) {
+ ToroPlayer player = source.get(i);
+ if (player.wantsToPlay()) candidates.add(player);
+ }
+ Collections.sort(candidates, Common.ORDER_COMPARATOR);
+
+ Collection<ToroPlayer> toPlay = playerSelector != null ? playerSelector.select(this, candidates)
+ : Collections.<ToroPlayer>emptyList();
+ for (ToroPlayer player : toPlay) {
+ if (!player.isPlaying()) playerManager.play(player, playerDispatcher);
+ }
+
+ source.removeAll(toPlay);
+ // Now 'source' contains only ones need to be paused.
+ for (ToroPlayer player : source) {
+ if (player.isPlaying()) {
+ this.savePlaybackInfo(player.getPlayerOrder(), player.getCurrentPlaybackInfo());
+ playerManager.pause(player);
+ }
+ }
+ }
+
+ /**
+ * Setup a {@link PlayerSelector}. Set a {@code null} {@link PlayerSelector} will stop all
+ * playback.
+ *
+ * @param playerSelector new {@link PlayerSelector} for this {@link Container}.
+ */
+ public final void setPlayerSelector(@Nullable PlayerSelector playerSelector) {
+ if (this.playerSelector == playerSelector) return;
+ this.playerSelector = playerSelector;
+ // dispatchUpdateOnAnimationFinished(true); // doesn't work well :(
+ // Immediately update.
+ this.onScrollStateChanged(SCROLL_STATE_IDLE);
+ }
+
+ /**
+ * Get current {@link PlayerSelector}. Can be {@code null}.
+ *
+ * @return current {@link #playerSelector}
+ */
+ @Nullable public final PlayerSelector getPlayerSelector() {
+ return playerSelector;
+ }
+
+ public final void setPlayerDispatcher(@NonNull PlayerDispatcher playerDispatcher) {
+ this.playerDispatcher = checkNotNull(playerDispatcher);
+ }
+
+ /** Define the callback that to be used later by {@link Behavior} if setup. */
+ public final void setBehaviorCallback(@Nullable BehaviorCallback behaviorCallback) {
+ this.behaviorCallback = behaviorCallback;
+ }
+
+ ////// Handle update after data change animation
+
+ long getMaxAnimationDuration() {
+ ItemAnimator animator = getItemAnimator();
+ if (animator == null) return SOME_BLINKS;
+ return max(animator.getAddDuration(), animator.getMoveDuration(), animator.getRemoveDuration(),
+ animator.getChangeDuration());
+ }
+
+ void dispatchUpdateOnAnimationFinished(boolean immediate) {
+ if (getScrollState() != SCROLL_STATE_IDLE) return;
+ if (animatorFinishHandler == null) return;
+ final long duration = immediate ? SOME_BLINKS : getMaxAnimationDuration();
+ if (getItemAnimator() != null) {
+ getItemAnimator().isRunning(new ItemAnimator.ItemAnimatorFinishedListener() {
+ @Override public void onAnimationsFinished() {
+ animatorFinishHandler.removeCallbacksAndMessages(null);
+ animatorFinishHandler.sendEmptyMessageDelayed(-1, duration);
+ }
+ });
+ } else {
+ animatorFinishHandler.removeCallbacksAndMessages(null);
+ animatorFinishHandler.sendEmptyMessageDelayed(-1, duration);
+ }
+ }
+
+ ////// Adapter Data Observer setup
+
+ /**
+ * See {@link ToroDataObserver}
+ */
+ private final ToroDataObserver dataObserver = new ToroDataObserver();
+
+ /**
+ * See {@link Adapter#registerAdapterDataObserver(AdapterDataObserver)}
+ * See {@link Adapter#unregisterAdapterDataObserver(AdapterDataObserver)}
+ */
+ @CallSuper @Override public void setAdapter(Adapter adapter) {
+ super.setAdapter(adapter);
+ dataObserver.registerAdapter(adapter);
+ }
+
+ /**
+ * See {@link Container#setAdapter(Adapter)}
+ */
+ @CallSuper @Override public void swapAdapter(Adapter adapter,
+ boolean removeAndRecycleExistingViews) {
+ super.swapAdapter(adapter, removeAndRecycleExistingViews);
+ dataObserver.registerAdapter(adapter);
+ }
+
+ //// PlaybackInfo Cache implementation
+ /* pkg */ final PlaybackInfoCache playbackInfoCache = new PlaybackInfoCache(this);
+ /* pkg */ Initializer playerInitializer = Initializer.DEFAULT;
+ private CacheManager cacheManager = null; // null by default
+
+ public final void setPlayerInitializer(@NonNull Initializer playerInitializer) {
+ this.playerInitializer = playerInitializer;
+ }
+
+ /**
+ * Save {@link PlaybackInfo} for the current {@link ToroPlayer} of a specific order.
+ * If called with {@link PlaybackInfo#SCRAP}, it is a hint that the Player is completed and need
+ * to be re-initialized.
+ *
+ * @param order order of the {@link ToroPlayer}.
+ * @param playbackInfo current {@link PlaybackInfo} of the {@link ToroPlayer}. Null info will be ignored.
+ */
+ public final void savePlaybackInfo(int order, @Nullable PlaybackInfo playbackInfo) {
+ if (playbackInfo != null) playbackInfoCache.savePlaybackInfo(order, playbackInfo);
+ }
+
+ /**
+ * Get the cached {@link PlaybackInfo} at a specific order.
+ *
+ * @param order order of the {@link ToroPlayer} to get the cached {@link PlaybackInfo}.
+ * @return cached {@link PlaybackInfo} if available, a new one if there is no cached one.
+ */
+ @NonNull public final PlaybackInfo getPlaybackInfo(int order) {
+ return playbackInfoCache.getPlaybackInfo(order);
+ }
+
+ /**
+ * Get current list of {@link ToroPlayer}s' orders whose {@link PlaybackInfo} are cached.
+ * Returning an empty list will disable the save/restore of player's position.
+ *
+ * @return list of {@link ToroPlayer}s' orders.
+ * @deprecated Use {@link #getLatestPlaybackInfos()} for the same purpose.
+ */
+ @RemoveIn(version = "3.6.0") @Deprecated //
+ @NonNull public List<Integer> getSavedPlayerOrders() {
+ return new ArrayList<>(playbackInfoCache.coldKeyToOrderMap.keySet());
+ }
+
+ /**
+ * Get a {@link SparseArray} contains cached {@link PlaybackInfo} of {@link ToroPlayer}s managed
+ * by this {@link Container}. If there is non-null {@link CacheManager}, this method should
+ * return the list of all {@link PlaybackInfo} cached by {@link PlaybackInfoCache}, otherwise,
+ * this method returns current {@link PlaybackInfo} of attached {@link ToroPlayer}s only.
+ */
+ @NonNull public SparseArray<PlaybackInfo> getLatestPlaybackInfos() {
+ SparseArray<PlaybackInfo> cache = new SparseArray<>();
+ List<ToroPlayer> activePlayers = this.filterBy(Container.Filter.PLAYING);
+ // This will update hotCache and coldCache if they are available.
+ for (ToroPlayer player : activePlayers) {
+ this.savePlaybackInfo(player.getPlayerOrder(), player.getCurrentPlaybackInfo());
+ }
+
+ if (cacheManager == null) {
+ if (playbackInfoCache.hotCache != null) {
+ for (Map.Entry<Integer, PlaybackInfo> entry : playbackInfoCache.hotCache.entrySet()) {
+ cache.put(entry.getKey(), entry.getValue());
+ }
+ }
+ } else {
+ for (Map.Entry<Integer, Object> entry : playbackInfoCache.coldKeyToOrderMap.entrySet()) {
+ cache.put(entry.getKey(), playbackInfoCache.coldCache.get(entry.getValue()));
+ }
+ }
+
+ return cache;
+ }
+
+ /**
+ * Set a {@link CacheManager} to this {@link Container}. A {@link CacheManager} will
+ * allow this {@link Container} to save/restore {@link PlaybackInfo} on various states or life
+ * cycle events. Setting a {@code null} {@link CacheManager} will remove that ability.
+ * {@link Container} doesn't have a non-null {@link CacheManager} by default.
+ *
+ * Setting this while there is a {@code non-null} {@link CacheManager} available will clear
+ * current {@link PlaybackInfo} cache.
+ *
+ * @param cacheManager The {@link CacheManager} to set to the {@link Container}.
+ */
+ public final void setCacheManager(@Nullable CacheManager cacheManager) {
+ if (this.cacheManager == cacheManager) return;
+ this.playbackInfoCache.clearCache();
+ this.cacheManager = cacheManager;
+ }
+
+ /**
+ * Get current {@link CacheManager} of the {@link Container}.
+ *
+ * @return current {@link CacheManager} of the {@link Container}. Can be {@code null}.
+ */
+ @Nullable public final CacheManager getCacheManager() {
+ return cacheManager;
+ }
+
+ /**
+ * Temporary save current playback infos when the App is stopped but not re-created. (For example:
+ * User press App Stack). If not {@code empty} then user is back from a living-but-stopped state.
+ */
+ final SparseArray<PlaybackInfo> tmpStates = new SparseArray<>();
+
+ /**
+ * In case user press "App Stack" button, this View's window will have visibility change from
+ * {@link #VISIBLE} to {@link #INVISIBLE} to {@link #GONE}. When user is back from that state,
+ * the visibility changes from {@link #GONE} to {@link #INVISIBLE} to {@link #VISIBLE}. A proper
+ * playback needs to handle this case too.
+ */
+ @CallSuper @Override protected void onWindowVisibilityChanged(int visibility) {
+ super.onWindowVisibilityChanged(visibility);
+ if (visibility == View.GONE) {
+ List<ToroPlayer> players = playerManager.getPlayers();
+ // if onSaveInstanceState is called before, source will contain no item, just fine.
+ for (ToroPlayer player : players) {
+ if (player.isPlaying()) {
+ this.savePlaybackInfo(player.getPlayerOrder(), player.getCurrentPlaybackInfo());
+ playerManager.pause(player);
+ }
+ }
+ } else if (visibility == View.VISIBLE) {
+ if (tmpStates.size() > 0) {
+ for (int i = 0; i < tmpStates.size(); i++) {
+ int order = tmpStates.keyAt(i);
+ PlaybackInfo playbackInfo = tmpStates.get(order);
+ this.savePlaybackInfo(order, playbackInfo);
+ }
+ }
+ tmpStates.clear();
+ dispatchUpdateOnAnimationFinished(true);
+ }
+
+ dispatchWindowVisibilityMayChange();
+ }
+
+ private int screenState;
+
+ @Override public void onScreenStateChanged(int screenState) {
+ super.onScreenStateChanged(screenState);
+ this.screenState = screenState;
+ dispatchWindowVisibilityMayChange();
+ }
+
+ @Override public void onWindowFocusChanged(boolean hasWindowFocus) {
+ super.onWindowFocusChanged(hasWindowFocus);
+ dispatchWindowVisibilityMayChange();
+ }
+
+ /**
+ * This method supports the case that by some reasons, Container should changes it behaviour not
+ * caused by any Activity recreation (so {@link #onSaveInstanceState()} and
+ * {@link #onRestoreInstanceState(Parcelable)} could not help).
+ *
+ * This method is called when:
+ * - Screen state changed.
+ * or - Window focus changed.
+ * or - Window visibility changed.
+ *
+ * For each of that event, Screen may be turned off or Window's focus state may change, we need
+ * to decide if Container should keep current playback state or change it.
+ *
+ * <strong>Discussion</strong>: In fact, we expect that: Container will be playing if the
+ * following conditions are all satisfied:
+ * - Current window is visible. (but not necessarily focused).
+ * - Container is visible in Window (partly is fine, we care about the Media player).
+ * - Container is focused in Window. (so we don't screw up other components' focuses).
+ *
+ * In lower API (eg: 16), {@link #getWindowVisibility()} always returns {@link #VISIBLE}, which
+ * cannot tell much. We need to investigate this flag in various APIs in various Scenarios.
+ */
+ private void dispatchWindowVisibilityMayChange() {
+ if (screenState == SCREEN_STATE_OFF) {
+ List<ToroPlayer> players = playerManager.getPlayers();
+ for (ToroPlayer player : players) {
+ if (player.isPlaying()) {
+ this.savePlaybackInfo(player.getPlayerOrder(), player.getCurrentPlaybackInfo());
+ playerManager.pause(player);
+ }
+ }
+ } else if (screenState == SCREEN_STATE_ON
+ // Container is focused in current Window
+ && hasFocus()
+ // In fact, Android 24+ supports multi-window mode in which visible Window may not have focus.
+ // In that case, other triggers are supposed to be called and we are safe here.
+ // Need further investigation if need.
+ && hasWindowFocus()) {
+ // tmpStates may be consumed already, if there is a good reason for that, so not a big deal.
+ if (tmpStates.size() > 0) {
+ for (int i = 0, size = tmpStates.size(); i < size; i++) {
+ int order = tmpStates.keyAt(i);
+ this.savePlaybackInfo(order, tmpStates.get(order));
+ }
+ }
+ tmpStates.clear();
+ dispatchUpdateOnAnimationFinished(true);
+ }
+ }
+
+ @Override protected Parcelable onSaveInstanceState() {
+ Parcelable superState = super.onSaveInstanceState();
+ List<ToroPlayer> source = playerManager.getPlayers();
+ for (ToroPlayer player : source) {
+ if (player.isPlaying()) {
+ this.savePlaybackInfo(player.getPlayerOrder(), player.getCurrentPlaybackInfo());
+ playerManager.pause(player);
+ }
+ }
+
+ final SparseArray<PlaybackInfo> states = playbackInfoCache.saveStates();
+
+ boolean recreating =
+ getContext() instanceof Activity && ((Activity) getContext()).isChangingConfigurations();
+
+ // Release current players on recreation event only.
+ // Note that there are cases where this method is called without the activity destroying/recreating.
+ // For example: in API 26 (my test mostly run on 8.0), when user click to "Current App" button,
+ // current Activity will enter the "Stop" state but not be destroyed/recreated and View hierarchy
+ // state will be saved (this method is called).
+ //
+ // We only need to release current resources when the recreation happens.
+ if (recreating) {
+ for (ToroPlayer player : source) {
+ if (!playerManager.release(player)) player.release();
+ playerManager.detachPlayer(player);
+ }
+ }
+
+ // Client must consider this behavior using CacheManager implement.
+ PlayerViewState playerViewState = new PlayerViewState(superState);
+ playerViewState.statesCache = states;
+
+ // To mark that this method was called. An activity recreation will clear this.
+ if (states != null && states.size() > 0) {
+ for (int i = 0; i < states.size(); i++) {
+ PlaybackInfo value = states.valueAt(i);
+ if (value != null) tmpStates.put(states.keyAt(i), value);
+ }
+ }
+
+ return playerViewState;
+ }
+
+ @Override protected void onRestoreInstanceState(Parcelable state) {
+ if (!(state instanceof PlayerViewState)) {
+ super.onRestoreInstanceState(state);
+ return;
+ }
+
+ PlayerViewState viewState = (PlayerViewState) state;
+ super.onRestoreInstanceState(viewState.getSuperState());
+ SparseArray<?> saveStates = viewState.statesCache;
+ if (saveStates != null) playbackInfoCache.restoreStates(saveStates);
+ }
+
+ /**
+ * Store the array of {@link PlaybackInfo} of recently cached playback. This state will be used
+ * only when {@link #cacheManager} is not {@code null}. Extension of {@link Container} must
+ * also have its own version of {@link SavedState} extends this {@link PlayerViewState}.
+ */
+ public static class PlayerViewState extends AbsSavedState {
+
+ SparseArray<?> statesCache;
+
+ /**
+ * Called by onSaveInstanceState
+ */
+ PlayerViewState(Parcelable superState) {
+ super(superState);
+ }
+
+ /**
+ * Called by CREATOR
+ */
+ PlayerViewState(Parcel in, ClassLoader loader) {
+ super(in, loader);
+ statesCache = in.readSparseArray(loader);
+ }
+
+ PlayerViewState(Parcel in) {
+ super(in);
+ }
+
+ @Override public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ //noinspection unchecked
+ dest.writeSparseArray((SparseArray<Object>) statesCache);
+ }
+
+ public static final Creator<PlayerViewState> CREATOR =
+ new ClassLoaderCreator<PlayerViewState>() { // Added from API 13
+ @Override public PlayerViewState createFromParcel(Parcel in, ClassLoader loader) {
+ return new PlayerViewState(in, loader);
+ }
+
+ @Override public PlayerViewState createFromParcel(Parcel source) {
+ return new PlayerViewState(source);
+ }
+
+ @Override public PlayerViewState[] newArray(int size) {
+ return new PlayerViewState[size];
+ }
+ };
+
+ @NonNull @Override public String toString() {
+ return "Cache{" + "states=" + statesCache + '}';
+ }
+ }
+
+ private final class ToroDataObserver extends AdapterDataObserver {
+
+ private Adapter adapter;
+
+ ToroDataObserver() {
+ }
+
+ final void registerAdapter(Adapter adapter) {
+ if (this.adapter == adapter) return;
+ if (this.adapter != null) {
+ this.adapter.unregisterAdapterDataObserver(this);
+ this.adapter.unregisterAdapterDataObserver(playbackInfoCache);
+ }
+
+ this.adapter = adapter;
+ if (this.adapter != null) {
+ this.adapter.registerAdapterDataObserver(this);
+ this.adapter.registerAdapterDataObserver(playbackInfoCache);
+ }
+ }
+
+ @Override public void onChanged() {
+ dispatchUpdateOnAnimationFinished(true);
+ }
+
+ @Override public void onItemRangeChanged(int positionStart, int itemCount) {
+ dispatchUpdateOnAnimationFinished(false);
+ }
+
+ @Override public void onItemRangeInserted(int positionStart, int itemCount) {
+ dispatchUpdateOnAnimationFinished(false);
+ }
+
+ @Override public void onItemRangeRemoved(int positionStart, int itemCount) {
+ dispatchUpdateOnAnimationFinished(false);
+ }
+
+ @Override public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
+ dispatchUpdateOnAnimationFinished(false);
+ }
+ }
+
+ /**
+ * A {@link Handler.Callback} that will fake a scroll with {@link #SCROLL_STATE_IDLE} to refresh
+ * all the playback. This is relatively expensive.
+ */
+ private static class AnimatorHelper implements Handler.Callback {
+
+ @NonNull private final Container container;
+
+ AnimatorHelper(@NonNull Container container) {
+ this.container = container;
+ }
+
+ @Override public boolean handleMessage(Message msg) {
+ this.container.onScrollStateChanged(SCROLL_STATE_IDLE);
+ return true;
+ }
+ }
+
+ private static class RecyclerListenerImpl implements RecyclerView.RecyclerListener {
+
+ final Container container;
+ RecyclerListener delegate;
+
+ RecyclerListenerImpl(@NonNull Container container) {
+ this.container = container;
+ }
+
+ @Override public void onViewRecycled(@NonNull ViewHolder holder) {
+ if (this.delegate != null) this.delegate.onViewRecycled(holder);
+ if (holder instanceof ToroPlayer) {
+ ToroPlayer player = (ToroPlayer) holder;
+ this.container.playbackInfoCache.onPlayerRecycled(player);
+ this.container.playerManager.recycle(player);
+ }
+ }
+ }
+
+ // This instance is to mark a RecyclerListenerImpl to be set by Toro, not by user.
+ private static final RecyclerListener NULL = new RecyclerListener() {
+ @Override public void onViewRecycled(@NonNull ViewHolder holder) {
+ // No-ops
+ }
+ };
+
+ /**
+ * An utility interface, used by {@link Container} to filter for {@link ToroPlayer}.
+ */
+ public interface Filter {
+
+ /**
+ * Check a {@link ToroPlayer} for a condition.
+ *
+ * @param player the {@link ToroPlayer} to check.
+ * @return {@code true} if this accepts the {@link ToroPlayer}, {@code false} otherwise.
+ */
+ boolean accept(@NonNull ToroPlayer player);
+
+ /**
+ * A built-in {@link Filter} that accepts only {@link ToroPlayer} that is playing.
+ */
+ Filter PLAYING = new Filter() {
+ @Override public boolean accept(@NonNull ToroPlayer player) {
+ return player.isPlaying();
+ }
+ };
+
+ /**
+ * A built-in {@link Filter} that accepts only {@link ToroPlayer} that is managed by Container.
+ * Actually any {@link ToroPlayer} to be filtered is already managed.
+ */
+ Filter MANAGING = new Filter() {
+ @Override public boolean accept(@NonNull ToroPlayer player) {
+ return true;
+ }
+ };
+ }
+
+ /**
+ * This behaviour is to catch the touch/fling/scroll caused by other children of
+ * {@link CoordinatorLayout}. We try to acknowledge user actions by intercepting the call but not
+ * consume the events.
+ *
+ * This class helps solve the issue when Client has a {@link Container} inside a
+ * {@link CoordinatorLayout} together with an {@link AppBarLayout} whose direct child is a
+ * {@link CollapsingToolbarLayout} (which is 'scrollable'). This 'scroll behavior' is not the
+ * same as Container's natural scroll. When user 'scrolls' to collapse or expand the {@link
+ * AppBarLayout}, {@link CoordinatorLayout} will offset the {@link Container} to make room for
+ * {@link AppBarLayout}, in which {@link Container} will not receive any scrolling event update,
+ * but just be shifted along the scrolling axis. This behavior results in a bad case that after
+ * the AppBarLayout collapse its direct {@link CollapsingToolbarLayout}, the Video may be fully
+ * visible, but because the Container has no way to know about that event, there is no playback
+ * update.
+ *
+ * @since 3.4.2
+ */
+ public static class Behavior extends CoordinatorLayout.Behavior<Container>
+ implements Handler.Callback {
+
+ @NonNull final CoordinatorLayout.Behavior<? super Container> delegate;
+ @Nullable BehaviorCallback callback;
+
+ static final int EVENT_IDLE = 1;
+ static final int EVENT_SCROLL = 2;
+ static final int EVENT_TOUCH = 3;
+ static final int EVENT_DELAY = 150;
+
+ final AtomicBoolean scrollConsumed = new AtomicBoolean(false);
+
+ Handler handler;
+
+ void onViewAttached(Container container) {
+ if (handler == null) handler = new Handler(this);
+ this.callback = container.behaviorCallback;
+ }
+
+ void onViewDetached(Container container) {
+ if (handler != null) {
+ handler.removeCallbacksAndMessages(null);
+ handler = null;
+ }
+ this.callback = null;
+ }
+
+ @Override public boolean handleMessage(Message msg) {
+ if (callback == null) return true;
+ switch (msg.what) {
+ case EVENT_SCROLL:
+ case EVENT_TOUCH:
+ scrollConsumed.set(false);
+ handler.removeMessages(EVENT_IDLE);
+ handler.sendEmptyMessageDelayed(EVENT_IDLE, EVENT_DELAY);
+ break;
+ case EVENT_IDLE:
+ // idle --> consume it.
+ if (!scrollConsumed.getAndSet(true)) callback.onFinishInteraction();
+ break;
+ }
+ return true;
+ }
+
+ /* No default constructors. Using this class from xml will result in error. */
+
+ // public Behavior() {
+ // }
+ //
+ // public Behavior(Context context, AttributeSet attrs) {
+ // super(context, attrs);
+ // }
+
+ public Behavior(@NonNull CoordinatorLayout.Behavior<Container> delegate) {
+ this.delegate = checkNotNull(delegate, "Behavior is null.");
+ }
+
+ /// We only need to intercept the following 3 methods:
+
+ @Override public boolean onInterceptTouchEvent(@NonNull CoordinatorLayout parent,
+ @NonNull Container child, @NonNull MotionEvent ev) {
+ if (this.handler != null) {
+ this.handler.removeCallbacksAndMessages(null);
+ this.handler.sendEmptyMessage(EVENT_TOUCH);
+ }
+ return delegate.onInterceptTouchEvent(parent, child, ev);
+ }
+
+ @Override
+ public boolean onTouchEvent(@NonNull CoordinatorLayout parent, @NonNull Container child,
+ @NonNull MotionEvent ev) {
+ if (this.handler != null) {
+ this.handler.removeCallbacksAndMessages(null);
+ this.handler.sendEmptyMessage(EVENT_TOUCH);
+ }
+ return delegate.onTouchEvent(parent, child, ev);
+ }
+
+ @Override
+ public boolean onStartNestedScroll(@NonNull CoordinatorLayout layout, @NonNull Container child,
+ @NonNull View directTargetChild, @NonNull View target, int axes, int type) {
+ if (this.handler != null) {
+ this.handler.removeCallbacksAndMessages(null);
+ this.handler.sendEmptyMessage(EVENT_SCROLL);
+ }
+ return delegate.onStartNestedScroll(layout, child, directTargetChild, target, axes, type);
+ }
+
+ /// Other methods
+
+ @Override public void onAttachedToLayoutParams(@NonNull CoordinatorLayout.LayoutParams params) {
+ if (handler == null) handler = new Handler(this);
+ delegate.onAttachedToLayoutParams(params);
+ }
+
+ @Override public void onDetachedFromLayoutParams() {
+ if (handler != null) {
+ handler.removeCallbacksAndMessages(null);
+ handler = null;
+ }
+ delegate.onDetachedFromLayoutParams();
+ }
+
+ @Override @ColorInt
+ public int getScrimColor(@NonNull CoordinatorLayout parent, @NonNull Container child) {
+ return delegate.getScrimColor(parent, child);
+ }
+
+ @Override
+ public float getScrimOpacity(@NonNull CoordinatorLayout parent, @NonNull Container child) {
+ return delegate.getScrimOpacity(parent, child);
+ }
+
+ @Override public boolean blocksInteractionBelow(@NonNull CoordinatorLayout parent,
+ @NonNull Container child) {
+ return delegate.blocksInteractionBelow(parent, child);
+ }
+
+ @Override
+ public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull Container child,
+ @NonNull View dependency) {
+ return delegate.layoutDependsOn(parent, child, dependency);
+ }
+
+ @Override public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent,
+ @NonNull Container child, @NonNull View dependency) {
+ return delegate.onDependentViewChanged(parent, child, dependency);
+ }
+
+ @Override
+ public void onDependentViewRemoved(@NonNull CoordinatorLayout parent, @NonNull Container child,
+ @NonNull View dependency) {
+ delegate.onDependentViewRemoved(parent, child, dependency);
+ }
+
+ @Override
+ public boolean onMeasureChild(@NonNull CoordinatorLayout parent, @NonNull Container child,
+ int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
+ return delegate.onMeasureChild(parent, child, parentWidthMeasureSpec, widthUsed,
+ parentHeightMeasureSpec, heightUsed);
+ }
+
+ @Override
+ public boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull Container child,
+ int layoutDirection) {
+ return delegate.onLayoutChild(parent, child, layoutDirection);
+ }
+
+ @Override
+ public void onNestedScrollAccepted(@NonNull CoordinatorLayout layout, @NonNull Container child,
+ @NonNull View directTargetChild, @NonNull View target, int axes, int type) {
+ delegate.onNestedScrollAccepted(layout, child, directTargetChild, target, axes, type);
+ }
+
+ @Override
+ public void onStopNestedScroll(@NonNull CoordinatorLayout layout, @NonNull Container child,
+ @NonNull View target, int type) {
+ delegate.onStopNestedScroll(layout, child, target, type);
+ }
+
+ @Override
+ public void onNestedScroll(@NonNull CoordinatorLayout layout, @NonNull Container child,
+ @NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed,
+ int type) {
+ delegate.onNestedScroll(layout, child, target, dxConsumed, dyConsumed, dxUnconsumed,
+ dyUnconsumed, type);
+ }
+
+ @Override
+ public void onNestedPreScroll(@NonNull CoordinatorLayout layout, @NonNull Container child,
+ @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
+ delegate.onNestedPreScroll(layout, child, target, dx, dy, consumed, type);
+ }
+
+ @Override
+ public boolean onNestedFling(@NonNull CoordinatorLayout layout, @NonNull Container child,
+ @NonNull View target, float velocityX, float velocityY, boolean consumed) {
+ return delegate.onNestedFling(layout, child, target, velocityX, velocityY, consumed);
+ }
+
+ @Override
+ public boolean onNestedPreFling(@NonNull CoordinatorLayout layout, @NonNull Container child,
+ @NonNull View target, float velocityX, float velocityY) {
+ return delegate.onNestedPreFling(layout, child, target, velocityX, velocityY);
+ }
+
+ @Override @NonNull
+ public WindowInsetsCompat onApplyWindowInsets(@NonNull CoordinatorLayout layout,
+ @NonNull Container child, @NonNull WindowInsetsCompat insets) {
+ return delegate.onApplyWindowInsets(layout, child, insets);
+ }
+
+ @Override public boolean onRequestChildRectangleOnScreen(@NonNull CoordinatorLayout layout,
+ @NonNull Container child, @NonNull Rect rectangle, boolean immediate) {
+ return delegate.onRequestChildRectangleOnScreen(layout, child, rectangle, immediate);
+ }
+
+ @Override
+ public void onRestoreInstanceState(@NonNull CoordinatorLayout parent, @NonNull Container child,
+ @NonNull Parcelable state) {
+ delegate.onRestoreInstanceState(parent, child, state);
+ }
+
+ @Override public Parcelable onSaveInstanceState(@NonNull CoordinatorLayout parent,
+ @NonNull Container child) {
+ return delegate.onSaveInstanceState(parent, child);
+ }
+
+ @Override
+ public boolean getInsetDodgeRect(@NonNull CoordinatorLayout parent, @NonNull Container child,
+ @NonNull Rect rect) {
+ return delegate.getInsetDodgeRect(parent, child, rect);
+ }
+ }
+
+ /**
+ * Callback for {@link Behavior} to tell the Client that User has finished the interaction for
+ * enough amount of time, so it (the Client) should do something. Normally, we ask Container to
+ * dispatch an 'idle scroll' to refresh the player list.
+ */
+ public interface BehaviorCallback {
+
+ void onFinishInteraction();
+ }
+
+ public interface Initializer {
+
+ @NonNull PlaybackInfo initPlaybackInfo(int order);
+
+ Initializer DEFAULT = new Initializer() {
+ @NonNull @Override public PlaybackInfo initPlaybackInfo(int order) {
+ return new PlaybackInfo();
+ }
+ };
+ }
+
+ static class ChildLayoutChangeListener implements OnLayoutChangeListener {
+
+ final WeakReference<Container> containerRef;
+
+ ChildLayoutChangeListener(Container container) {
+ this.containerRef = new WeakReference<>(container);
+ }
+
+ @Override
+ public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
+ int oldTop, int oldRight, int oldBottom) {
+ Container container = containerRef.get();
+ if (container == null) return;
+ if (layoutDidChange(left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom)) {
+ container.dispatchUpdateOnAnimationFinished(false);
+ }
+ }
+ }
+
+ static boolean layoutDidChange(int left, int top, int right, int bottom, int oldLeft, int oldTop,
+ int oldRight, int oldBottom) {
+ return left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom;
+ }
+}
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/widget/PlaybackInfoCache.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/widget/PlaybackInfoCache.java
new file mode 100644
index 00000000..9934779b
--- /dev/null
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/widget/PlaybackInfoCache.java
@@ -0,0 +1,402 @@
+/*
+ * Copyright (c) 2018 Nam Nguyen, nam@ene.im
+ *
+ * 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 ml.docilealligator.infinityforreddit.videoautoplay.widget;
+
+import static ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo.SCRAP;
+import static ml.docilealligator.infinityforreddit.videoautoplay.widget.Common.ORDER_COMPARATOR_INT;
+
+import android.annotation.SuppressLint;
+import android.util.SparseArray;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+import ml.docilealligator.infinityforreddit.videoautoplay.ToroPlayer;
+import ml.docilealligator.infinityforreddit.videoautoplay.ToroUtil;
+import ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo;
+
+/**
+ * @author eneim (2018/04/24).
+ *
+ * Design Target:
+ *
+ * [1] Manage the {@link PlaybackInfo} of current {@link ToroPlayer}s. Should match 1-1 with the
+ * {@link ToroPlayer}s that {@link PlayerManager} is managing.
+ *
+ * [2] If a non-null {@link CacheManager} provided to the {@link Container}, this class must
+ * properly manage the {@link PlaybackInfo} of detached {@link ToroPlayer} and restore it to
+ * previous state after being re-attached.
+ */
+@SuppressWarnings({ "unused" })
+@SuppressLint("UseSparseArrays") //
+final class PlaybackInfoCache extends RecyclerView.AdapterDataObserver {
+
+ @NonNull private final Container container;
+ // Cold cache represents the map between key obtained from CacheManager and PlaybackInfo. If the
+ // CacheManager is null, this cache will hold nothing.
+ /* pkg */ HashMap<Object, PlaybackInfo> coldCache = new HashMap<>();
+
+ // Hot cache represents the map between Player's order and its PlaybackInfo. A key-value map only
+ // lives within a Player's attached state.
+ // Being a TreeMap because we need to traversal through it in order sometime.
+ /* pkg */ TreeMap<Integer, PlaybackInfo> hotCache; // only cache attached Views.
+
+ // Holds the map between Player's order and its key obtain from CacheManager.
+ /* pkg */ TreeMap<Integer, Object> coldKeyToOrderMap = new TreeMap<>(ORDER_COMPARATOR_INT);
+
+ PlaybackInfoCache(@NonNull Container container) {
+ this.container = container;
+ }
+
+ final void onAttach() {
+ hotCache = new TreeMap<>(ORDER_COMPARATOR_INT);
+ }
+
+ final void onDetach() {
+ if (hotCache != null) {
+ hotCache.clear();
+ hotCache = null;
+ }
+ coldKeyToOrderMap.clear();
+ }
+
+ final void onPlayerAttached(ToroPlayer player) {
+ int playerOrder = player.getPlayerOrder();
+ // [1] Check if there is cold cache for this player
+ Object key = getKey(playerOrder);
+ if (key != null) coldKeyToOrderMap.put(playerOrder, key);
+
+ PlaybackInfo cache = key == null ? null : coldCache.get(key);
+ if (cache == null || cache == SCRAP) {
+ // We init this even if there is no CacheManager available, because this is what User expects.
+ cache = container.playerInitializer.initPlaybackInfo(playerOrder);
+ // Only save to cold cache when there is a valid CacheManager (key is not null).
+ if (key != null) coldCache.put(key, cache);
+ }
+
+ if (hotCache != null) hotCache.put(playerOrder, cache);
+ }
+
+ // Will be called from Container#onChildViewDetachedFromWindow(View)
+ // Therefore, it may not be called on all views. For example: when user close the App, by default
+ // when RecyclerView is detached from Window, it will not call onChildViewDetachedFromWindow for
+ // its children.
+ // This method will:
+ // [1] Take current hot cache entry of the player, and put back to cold cache.
+ // [2] Remove the hot cache entry of the player.
+ final void onPlayerDetached(ToroPlayer player) {
+ int playerOrder = player.getPlayerOrder();
+ if (hotCache != null && hotCache.containsKey(playerOrder)) {
+ PlaybackInfo cache = hotCache.remove(playerOrder);
+ Object key = getKey(playerOrder);
+ if (key != null) coldCache.put(key, cache);
+ }
+ }
+
+ @SuppressWarnings("unused") final void onPlayerRecycled(ToroPlayer player) {
+ // TODO do anything here?
+ }
+
+ /// Adapter change events handling
+
+ @Override public void onChanged() {
+ if (container.getCacheManager() != null) {
+ for (Integer key : coldKeyToOrderMap.keySet()) {
+ Object cacheKey = getKey(key);
+ coldCache.put(cacheKey, SCRAP);
+ coldKeyToOrderMap.put(key, cacheKey);
+ }
+ }
+
+ if (hotCache != null) {
+ for (Integer key : hotCache.keySet()) {
+ hotCache.put(key, SCRAP);
+ }
+ }
+ }
+
+ @Override public void onItemRangeChanged(final int positionStart, final int itemCount) {
+ if (itemCount == 0) return;
+ if (container.getCacheManager() != null) {
+ Set<Integer> changedColdKeys = new TreeSet<>(ORDER_COMPARATOR_INT);
+ for (Integer key : coldKeyToOrderMap.keySet()) {
+ if (key >= positionStart && key < positionStart + itemCount) {
+ changedColdKeys.add(key);
+ }
+ }
+
+ for (Integer key : changedColdKeys) {
+ Object cacheKey = getKey(key);
+ coldCache.put(cacheKey, SCRAP);
+ coldKeyToOrderMap.put(key, cacheKey);
+ }
+ }
+
+ if (hotCache != null) {
+ Set<Integer> changedHotKeys = new TreeSet<>(ORDER_COMPARATOR_INT);
+ for (Integer key : hotCache.keySet()) {
+ if (key >= positionStart && key < positionStart + itemCount) {
+ changedHotKeys.add(key);
+ }
+ }
+
+ for (Integer key : changedHotKeys) {
+ hotCache.put(key, SCRAP);
+ }
+ }
+ }
+
+ @Override public void onItemRangeInserted(final int positionStart, final int itemCount) {
+ if (itemCount == 0) return;
+ PlaybackInfo value;
+ // Cold cache update
+ if (container.getCacheManager() != null) {
+ // [1] Take keys of old one.
+ // 1.1 Extract subset of keys only:
+ Set<Integer> changedColdKeys = new TreeSet<>(ORDER_COMPARATOR_INT);
+ for (Integer key : coldKeyToOrderMap.keySet()) {
+ if (key >= positionStart) {
+ changedColdKeys.add(key);
+ }
+ }
+
+ // 1.2 Extract entries from cold cache to a temp cache.
+ final Map<Object, PlaybackInfo> changeColdEntriesCache = new HashMap<>();
+ for (Integer key : changedColdKeys) {
+ if ((value = coldCache.remove(coldKeyToOrderMap.get(key))) != null) {
+ changeColdEntriesCache.put(key, value);
+ }
+ }
+
+ // 1.2 Update cold Cache with new keys
+ for (Integer key : changedColdKeys) {
+ coldCache.put(getKey(key + itemCount), changeColdEntriesCache.get(key));
+ }
+
+ // 1.3 Update coldKeyToOrderMap;
+ for (Integer key : changedColdKeys) {
+ coldKeyToOrderMap.put(key, getKey(key));
+ }
+ }
+
+ // [1] Remove cache if there is any appearance
+ if (hotCache != null) {
+ // [2] Shift cache by specific number
+ Map<Integer, PlaybackInfo> changedHotEntriesCache = new HashMap<>();
+ Set<Integer> changedHotKeys = new TreeSet<>(ORDER_COMPARATOR_INT);
+ for (Integer key : hotCache.keySet()) {
+ if (key >= positionStart) {
+ changedHotKeys.add(key);
+ }
+ }
+
+ for (Integer key : changedHotKeys) {
+ if ((value = hotCache.remove(key)) != null) {
+ changedHotEntriesCache.put(key, value);
+ }
+ }
+
+ for (Integer key : changedHotKeys) {
+ hotCache.put(key + itemCount, changedHotEntriesCache.get(key));
+ }
+ }
+ }
+
+ @Override public void onItemRangeRemoved(final int positionStart, final int itemCount) {
+ if (itemCount == 0) return;
+ PlaybackInfo value;
+ // Cold cache update
+ if (container.getCacheManager() != null) {
+ // [1] Take keys of old one.
+ // 1.1 Extract subset of keys only:
+ Set<Integer> changedColdKeys = new TreeSet<>(ORDER_COMPARATOR_INT);
+ for (Integer key : coldKeyToOrderMap.keySet()) {
+ if (key >= positionStart + itemCount) changedColdKeys.add(key);
+ }
+ // 1.2 Extract entries from cold cache to a temp cache.
+ final Map<Object, PlaybackInfo> changeColdEntriesCache = new HashMap<>();
+ for (Integer key : changedColdKeys) {
+ if ((value = coldCache.remove(coldKeyToOrderMap.get(key))) != null) {
+ changeColdEntriesCache.put(key, value);
+ }
+ }
+
+ // 1.2 Update cold Cache with new keys
+ for (Integer key : changedColdKeys) {
+ coldCache.put(getKey(key - itemCount), changeColdEntriesCache.get(key));
+ }
+
+ // 1.3 Update coldKeyToOrderMap;
+ for (Integer key : changedColdKeys) {
+ coldKeyToOrderMap.put(key, getKey(key));
+ }
+ }
+
+ // [1] Remove cache if there is any appearance
+ if (hotCache != null) {
+ for (int i = 0; i < itemCount; i++) {
+ hotCache.remove(positionStart + i);
+ }
+
+ // [2] Shift cache by specific number
+ Map<Integer, PlaybackInfo> changedHotEntriesCache = new HashMap<>();
+ Set<Integer> changedHotKeys = new TreeSet<>(ORDER_COMPARATOR_INT);
+ for (Integer key : hotCache.keySet()) {
+ if (key >= positionStart + itemCount) changedHotKeys.add(key);
+ }
+
+ for (Integer key : changedHotKeys) {
+ if ((value = hotCache.remove(key)) != null) {
+ changedHotEntriesCache.put(key, value);
+ }
+ }
+
+ for (Integer key : changedHotKeys) {
+ hotCache.put(key - itemCount, changedHotEntriesCache.get(key));
+ }
+ }
+ }
+
+ // Dude I wanna test this thing >.<
+ @Override public void onItemRangeMoved(final int fromPos, final int toPos, int itemCount) {
+ if (fromPos == toPos) return;
+
+ final int low = fromPos < toPos ? fromPos : toPos;
+ final int high = fromPos + toPos - low;
+ final int shift = fromPos < toPos ? -1 : 1; // how item will be shifted due to the move
+ PlaybackInfo value;
+ // [1] Migrate cold cache.
+ if (container.getCacheManager() != null) {
+ // 1.1 Extract subset of keys only:
+ Set<Integer> changedColdKeys = new TreeSet<>(ORDER_COMPARATOR_INT);
+ for (Integer key : coldKeyToOrderMap.keySet()) {
+ if (key >= low && key <= high) changedColdKeys.add(key);
+ }
+ // 1.2 Extract entries from cold cache to a temp cache.
+ final Map<Object, PlaybackInfo> changeColdEntries = new HashMap<>();
+ for (Integer key : changedColdKeys) {
+ if ((value = coldCache.remove(coldKeyToOrderMap.get(key))) != null) {
+ changeColdEntries.put(key, value);
+ }
+ }
+
+ // 1.2 Update cold Cache with new keys
+ for (Integer key : changedColdKeys) {
+ if (key == low) {
+ coldCache.put(getKey(high), changeColdEntries.get(key));
+ } else {
+ coldCache.put(getKey(key + shift), changeColdEntries.get(key));
+ }
+ }
+
+ // 1.3 Update coldKeyToOrderMap;
+ for (Integer key : changedColdKeys) {
+ coldKeyToOrderMap.put(key, getKey(key));
+ }
+ }
+
+ // [2] Migrate hot cache.
+ if (hotCache != null) {
+ Set<Integer> changedHotKeys = new TreeSet<>(ORDER_COMPARATOR_INT);
+ for (Integer key : hotCache.keySet()) {
+ if (key >= low && key <= high) changedHotKeys.add(key);
+ }
+
+ Map<Integer, PlaybackInfo> changedHotEntriesCache = new HashMap<>();
+ for (Integer key : changedHotKeys) {
+ if ((value = hotCache.remove(key)) != null) changedHotEntriesCache.put(key, value);
+ }
+
+ for (Integer key : changedHotKeys) {
+ if (key == low) {
+ hotCache.put(high, changedHotEntriesCache.get(key));
+ } else {
+ hotCache.put(key + shift, changedHotEntriesCache.get(key));
+ }
+ }
+ }
+ }
+
+ @Nullable private Object getKey(int position) {
+ return position == RecyclerView.NO_POSITION ? null : container.getCacheManager() == null ? null
+ : container.getCacheManager().getKeyForOrder(position);
+ }
+
+ //@Nullable private Integer getOrder(Object key) {
+ // return container.getCacheManager() == null ? null
+ // : container.getCacheManager().getOrderForKey(key);
+ //}
+
+ @NonNull final PlaybackInfo getPlaybackInfo(int position) {
+ PlaybackInfo info = hotCache != null ? hotCache.get(position) : null;
+ if (info == SCRAP) { // has hot cache, but was SCRAP.
+ info = container.playerInitializer.initPlaybackInfo(position);
+ }
+
+ Object key = getKey(position);
+ info = info != null ? info : (key != null ? coldCache.get(key) : null);
+ if (info == null) info = container.playerInitializer.initPlaybackInfo(position);
+ return info;
+ }
+
+ // Call by Container#savePlaybackInfo and that method is called right before any pausing.
+ final void savePlaybackInfo(int position, @NonNull PlaybackInfo playbackInfo) {
+ ToroUtil.checkNotNull(playbackInfo);
+ if (hotCache != null) hotCache.put(position, playbackInfo);
+ Object key = getKey(position);
+ if (key != null) coldCache.put(key, playbackInfo);
+ }
+
+ @NonNull SparseArray<PlaybackInfo> saveStates() {
+ SparseArray<PlaybackInfo> states = new SparseArray<>();
+ if (container.getCacheManager() != null) {
+ for (Map.Entry<Integer, Object> entry : coldKeyToOrderMap.entrySet()) {
+ states.put(entry.getKey(), coldCache.get(entry.getValue()));
+ }
+ } else if (hotCache != null) {
+ for (Map.Entry<Integer, PlaybackInfo> entry : hotCache.entrySet()) {
+ states.put(entry.getKey(), entry.getValue());
+ }
+ }
+ return states;
+ }
+
+ void restoreStates(@Nullable SparseArray<?> savedStates) {
+ int cacheSize;
+ if (savedStates != null && (cacheSize = savedStates.size()) > 0) {
+ for (int i = 0; i < cacheSize; i++) {
+ int order = savedStates.keyAt(i);
+ Object key = getKey(order);
+ coldKeyToOrderMap.put(order, key);
+ PlaybackInfo playbackInfo = (PlaybackInfo) savedStates.get(order);
+ if (playbackInfo != null) this.savePlaybackInfo(order, playbackInfo);
+ }
+ }
+ }
+
+ final void clearCache() {
+ coldCache.clear();
+ if (hotCache != null) hotCache.clear();
+ }
+}
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/widget/PlayerManager.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/widget/PlayerManager.java
new file mode 100644
index 00000000..b083a363
--- /dev/null
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/widget/PlayerManager.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (c) 2017 Nam Nguyen, nam@ene.im
+ *
+ * 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 ml.docilealligator.infinityforreddit.videoautoplay.widget;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+
+import androidx.annotation.NonNull;
+import androidx.collection.ArraySet;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import ml.docilealligator.infinityforreddit.videoautoplay.PlayerDispatcher;
+import ml.docilealligator.infinityforreddit.videoautoplay.ToroPlayer;
+
+/**
+ * Manage the collection of {@link ToroPlayer}s for a specific {@link Container}.
+ *
+ * Task: collect all Players in which "{@link Common#allowsToPlay(ToroPlayer)}" returns true, then
+ * initialize them.
+ *
+ * @author eneim | 5/31/17.
+ */
+@SuppressWarnings({ "unused", "UnusedReturnValue", "StatementWithEmptyBody" }) //
+final class PlayerManager implements Handler.Callback {
+
+ private static final String TAG = "ToroLib:Manager";
+ private Handler handler;
+
+ // Make sure each ToroPlayer will present only once in this Manager.
+ private final Set<ToroPlayer> players = new ArraySet<>();
+
+ boolean attachPlayer(@NonNull ToroPlayer player) {
+ return players.add(player);
+ }
+
+ boolean detachPlayer(@NonNull ToroPlayer player) {
+ if (handler != null) handler.removeCallbacksAndMessages(player);
+ return players.remove(player);
+ }
+
+ boolean manages(@NonNull ToroPlayer player) {
+ return players.contains(player);
+ }
+
+ /**
+ * Return a "Copy" of the collection of players this manager is managing.
+ *
+ * @return a non null collection of Players those a managed.
+ */
+ @NonNull List<ToroPlayer> getPlayers() {
+ return new ArrayList<>(this.players);
+ }
+
+ void initialize(@NonNull ToroPlayer player, Container container) {
+ player.initialize(container, container.getPlaybackInfo(player.getPlayerOrder()));
+ }
+
+ // 2018.07.02 Directly pass PlayerDispatcher so that we can easily expand the ability in the future.
+ void play(@NonNull ToroPlayer player, PlayerDispatcher dispatcher) {
+ this.play(player, dispatcher.getDelayToPlay(player));
+ }
+
+ private void play(@NonNull ToroPlayer player, int delay) {
+ if (delay < PlayerDispatcher.DELAY_INFINITE) throw new IllegalArgumentException("Too negative");
+ if (handler == null) return; // equals to that this is not attached yet.
+ handler.removeMessages(MSG_PLAY, player); // remove undone msg for this player
+ if (delay == PlayerDispatcher.DELAY_INFINITE) {
+ // do nothing
+ } else if (delay == PlayerDispatcher.DELAY_NONE) {
+ player.play();
+ } else {
+ handler.sendMessageDelayed(handler.obtainMessage(MSG_PLAY, player), delay);
+ }
+ }
+
+ void pause(@NonNull ToroPlayer player) {
+ // remove all msg sent for the player
+ if (handler != null) handler.removeCallbacksAndMessages(player);
+ player.pause();
+ }
+
+ // return false if this manager could not release the player.
+ // normally when this manager doesn't manage the player.
+ boolean release(@NonNull ToroPlayer player) {
+ if (handler != null) handler.removeCallbacksAndMessages(null);
+ if (manages(player)) {
+ player.release();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ void recycle(ToroPlayer player) {
+ if (handler != null) handler.removeCallbacksAndMessages(player);
+ }
+
+ void clear() {
+ if (handler != null) handler.removeCallbacksAndMessages(null);
+ this.players.clear();
+ }
+
+ void deferPlaybacks() {
+ if (handler != null) handler.removeMessages(MSG_PLAY);
+ }
+
+ void onAttach() {
+ // do nothing
+ if (handler == null) handler = new Handler(Looper.getMainLooper(), this);
+ }
+
+ void onDetach() {
+ if (handler != null) {
+ handler.removeCallbacksAndMessages(null);
+ handler = null;
+ }
+ }
+
+ @SuppressWarnings("WeakerAccess") static final int MSG_PLAY = 100;
+
+ @Override public boolean handleMessage(Message msg) {
+ if (msg.what == MSG_PLAY && msg.obj instanceof ToroPlayer) {
+ ToroPlayer player = (ToroPlayer) msg.obj;
+ player.play();
+ }
+ return true;
+ }
+}
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/widget/PressablePlayerSelector.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/widget/PressablePlayerSelector.java
new file mode 100644
index 00000000..b72d9cd3
--- /dev/null
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/widget/PressablePlayerSelector.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (c) 2018 Nam Nguyen, nam@ene.im
+ *
+ * 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 ml.docilealligator.infinityforreddit.videoautoplay.widget;
+
+import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
+import static java.util.Collections.singletonList;
+
+import static ml.docilealligator.infinityforreddit.videoautoplay.widget.Common.allowsToPlay;
+import static ml.docilealligator.infinityforreddit.videoautoplay.widget.Common.findFirst;
+
+import android.view.View;
+import android.view.View.OnLongClickListener;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import ml.docilealligator.infinityforreddit.videoautoplay.PlayerSelector;
+import ml.docilealligator.infinityforreddit.videoautoplay.ToroPlayer;
+import ml.docilealligator.infinityforreddit.videoautoplay.ToroUtil;
+import ml.docilealligator.infinityforreddit.videoautoplay.annotations.Beta;
+
+/**
+ * @author eneim (2018/08/17).
+ *
+ * A 'Press to Play' {@link PlayerSelector}.
+ *
+ * This is a {@link OnLongClickListener} that co-operates with {@link Container} to selectively
+ * trigger the playback. The common usecase is to allow user to long click on a {@link ToroPlayer}
+ * to trigger its playback. In that case, we should set that {@link ToroPlayer} to highest priority
+ * among the candidates, and also to clear its priority when user scroll it out of the playable region.
+ *
+ * This class also have a {@link #toPause} field to handle the case where User want to specifically
+ * pause a {@link ToroPlayer}. This selection will also be cleared with the same rules of toPlay.
+ * @since 3.6.0.2802
+ */
+@SuppressWarnings("WeakerAccess") @Beta //
+public class PressablePlayerSelector implements PlayerSelector, OnLongClickListener {
+
+ protected final WeakReference<Container> weakContainer;
+ protected final PlayerSelector delegate;
+ protected final AtomicInteger toPlay = new AtomicInteger(NO_POSITION);
+ protected final AtomicInteger toPause = new AtomicInteger(NO_POSITION);
+
+ final Common.Filter<ToroPlayer> filterToPlay = new Common.Filter<ToroPlayer>() {
+ @Override public boolean accept(ToroPlayer target) {
+ return target.getPlayerOrder() == toPlay.get();
+ }
+ };
+
+ final Common.Filter<ToroPlayer> filterToPause = new Common.Filter<ToroPlayer>() {
+ @Override public boolean accept(ToroPlayer target) {
+ return target.getPlayerOrder() == toPause.get();
+ }
+ };
+
+ public PressablePlayerSelector(Container container) {
+ this(container, DEFAULT);
+ }
+
+ public PressablePlayerSelector(Container container, PlayerSelector delegate) {
+ this(new WeakReference<>(ToroUtil.checkNotNull(container)), ToroUtil.checkNotNull(delegate));
+ }
+
+ PressablePlayerSelector(WeakReference<Container> container, PlayerSelector delegate) {
+ this.weakContainer = container;
+ this.delegate = ToroUtil.checkNotNull(delegate);
+ }
+
+ @Override public boolean onLongClick(View v) {
+ Container container = weakContainer.get();
+ if (container == null) return false; // fail fast
+
+ toPause.set(NO_POSITION); // long click will always mean to 'press to play'.
+
+ RecyclerView.ViewHolder viewHolder = container.findContainingViewHolder(v);
+ boolean handled = viewHolder instanceof ToroPlayer;
+ if (handled) handled = allowsToPlay((ToroPlayer) viewHolder);
+
+ int position = handled ? viewHolder.getAdapterPosition() : NO_POSITION;
+ if (handled) handled = position != toPlay.getAndSet(position);
+
+ if (handled) container.onScrollStateChanged(RecyclerView.SCROLL_STATE_IDLE);
+ return handled;
+ }
+
+ @Override @NonNull public Collection<ToroPlayer> select(@NonNull Container container,
+ @NonNull List<ToroPlayer> items) {
+ // Make sure client doesn't use this instance to wrong Container.
+ if (container != this.weakContainer.get()) return new ArrayList<>();
+
+ // If there is a request to pause, we need to prioritize that first.
+ ToroPlayer toPauseCandidate = null;
+ if (toPause.get() >= 0) {
+ toPauseCandidate = findFirst(items, filterToPause);
+ if (toPauseCandidate == null) {
+ // the order to pause doesn't present in candidate, we clear the selection.
+ toPause.set(NO_POSITION); // remove the paused one.
+ }
+ }
+
+ if (toPlay.get() >= 0) {
+ ToroPlayer toPlayCandidate = findFirst(items, filterToPlay);
+ if (toPlayCandidate != null) {
+ if (allowsToPlay(toPlayCandidate)) {
+ return singletonList(toPlayCandidate);
+ }
+ }
+ }
+ // In the list of candidates, selected item no longer presents or is not allowed to play,
+ // we should reset the selection.
+ toPlay.set(NO_POSITION);
+ // Wrap by an ArrayList to make it modifiable.
+ Collection<ToroPlayer> selected = new ArrayList<>(delegate.select(container, items));
+ if (toPauseCandidate != null) selected.remove(toPauseCandidate);
+ return selected;
+ }
+
+ @Override @NonNull public PlayerSelector reverse() {
+ return new PressablePlayerSelector(this.weakContainer, delegate.reverse());
+ }
+
+ public boolean toPlay(int position) {
+ if (toPause.get() == position) toPause.set(NO_POSITION);
+ Container container = weakContainer.get();
+ if (container == null) return false;
+ if (position != toPlay.getAndSet(position)) {
+ container.onScrollStateChanged(RecyclerView.SCROLL_STATE_IDLE);
+ return true;
+ }
+
+ return false;
+ }
+
+ public void toPause(int position) {
+ toPlay.set(NO_POSITION);
+ toPause.set(position);
+ }
+}
diff --git a/app/src/main/res/drawable/ic_volume_off_black_24dp.xml b/app/src/main/res/drawable/ic_volume_off_black_24dp.xml
new file mode 100644
index 00000000..628a8aef
--- /dev/null
+++ b/app/src/main/res/drawable/ic_volume_off_black_24dp.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (c) 2018 Nam Nguyen, nam@ene.im
+ ~
+ ~ 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="32dp"
+ android:viewportHeight="24.0"
+ android:viewportWidth="24.0"
+ android:width="32dp">
+ <path
+ android:fillColor="#FFFFFF"
+ android:pathData="M16.5,12c0,-1.77 -1.02,-3.29 -2.5,-4.03v2.21l2.45,2.45c0.03,-0.2 0.05,-0.41 0.05,-0.63zM19,12c0,0.94 -0.2,1.82 -0.54,2.64l1.51,1.51C20.63,14.91 21,13.5 21,12c0,-4.28 -2.99,-7.86 -7,-8.77v2.06c2.89,0.86 5,3.54 5,6.71zM4.27,3L3,4.27 7.73,9L3,9v6h4l5,5v-6.73l4.25,4.25c-0.67,0.52 -1.42,0.93 -2.25,1.18v2.06c1.38,-0.31 2.63,-0.95 3.69,-1.81L19.73,21 21,19.73l-9,-9L4.27,3zM12,4L9.91,6.09 12,8.18L12,4z"/>
+</vector>
diff --git a/app/src/main/res/drawable/ic_volume_up_black_24dp.xml b/app/src/main/res/drawable/ic_volume_up_black_24dp.xml
new file mode 100644
index 00000000..7c11fa9e
--- /dev/null
+++ b/app/src/main/res/drawable/ic_volume_up_black_24dp.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (c) 2018 Nam Nguyen, nam@ene.im
+ ~
+ ~ 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="32dp"
+ android:height="32dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FFFFFF"
+ android:pathData="M3,9v6h4l5,5L12,4L7,9L3,9zM16.5,12c0,-1.77 -1.02,-3.29 -2.5,-4.03v8.05c1.48,-0.73 2.5,-2.25 2.5,-4.02zM14,3.23v2.06c2.89,0.86 5,3.54 5,6.71s-2.11,5.85 -5,6.71v2.06c4.01,-0.91 7,-4.49 7,-8.77s-2.99,-7.86 -7,-8.77z"/>
+</vector>
diff --git a/app/src/main/res/layout/toro_exo_control_view.xml b/app/src/main/res/layout/toro_exo_control_view.xml
new file mode 100644
index 00000000..481230bf
--- /dev/null
+++ b/app/src/main/res/layout/toro_exo_control_view.xml
@@ -0,0 +1,181 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (c) 2018 Nam Nguyen, nam@ene.im
+ ~
+ ~ 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.
+ -->
+
+<androidx.appcompat.widget.LinearLayoutCompat
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom"
+ android:layout_marginTop="4dp"
+ android:background="#CC000000"
+ android:gravity="center_vertical"
+ android:layoutDirection="ltr"
+ android:orientation="horizontal"
+ android:theme="@style/Theme.AppCompat"
+ tools:ignore="UnusedAttribute"
+ >
+
+ <!--<androidx.appcompat.widget.AppCompatImageButton
+ android:id="@id/exo_play"
+ tools:ignore="ContentDescription"
+ style="@style/ToroMediaButton.Play"
+ />
+
+ <androidx.appcompat.widget.AppCompatImageButton
+ android:id="@id/exo_pause"
+ tools:ignore="ContentDescription"
+ tools:visibility="gone"
+ style="@style/ToroMediaButton.Pause"
+ />
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@id/exo_position"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:includeFontPadding="false"
+ android:paddingLeft="4dp"
+ android:paddingRight="4dp"
+ android:textColor="#FFBEBEBE"
+ android:textSize="14sp"
+ android:textStyle="bold"
+ tools:text="1:25"
+ />
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:includeFontPadding="false"
+ android:text="/"
+ android:textColor="#FFBEBEBE"
+ android:textSize="14sp"
+ android:textStyle="bold"
+ tools:ignore="HardcodedText"
+ />
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@id/exo_duration"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:includeFontPadding="false"
+ android:paddingLeft="4dp"
+ android:paddingRight="4dp"
+ android:textColor="#FFBEBEBE"
+ android:textSize="14sp"
+ android:textStyle="bold"
+ tools:text="5:25"
+ />
+
+ <com.google.android.exoplayer2.ui.DefaultTimeBar
+ android:id="@id/exo_progress"
+ android:layout_width="0dp"
+ android:layout_height="24dp"
+ android:layout_weight="1"
+ />
+
+ <androidx.appcompat.widget.AppCompatImageButton
+ android:id="@+id/exo_volume_up"
+ tools:ignore="ContentDescription"
+ style="@style/ToroMediaButton.VolumeUp"
+ />
+
+ <androidx.appcompat.widget.AppCompatImageButton
+ android:id="@+id/exo_volume_off"
+ tools:ignore="ContentDescription"
+ tools:visibility="gone"
+ style="@style/ToroMediaButton.VolumeOff"
+ />-->
+
+ <androidx.appcompat.widget.AppCompatImageButton
+ android:id="@id/exo_play"
+ tools:ignore="ContentDescription"
+ style="@style/ToroMediaButton.Play"
+ />
+
+ <androidx.appcompat.widget.AppCompatImageButton
+ android:id="@id/exo_pause"
+ tools:ignore="ContentDescription"
+ tools:visibility="gone"
+ style="@style/ToroMediaButton.Pause"
+ />
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@id/exo_position"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:includeFontPadding="false"
+ android:paddingLeft="4dp"
+ android:paddingRight="4dp"
+ android:textColor="#FFBEBEBE"
+ android:textSize="14sp"
+ android:textStyle="bold"
+ tools:text="1:25"
+ />
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:includeFontPadding="false"
+ android:text="/"
+ android:textColor="#FFBEBEBE"
+ android:textSize="14sp"
+ android:textStyle="bold"
+ tools:ignore="HardcodedText"
+ />
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@id/exo_duration"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:includeFontPadding="false"
+ android:paddingLeft="4dp"
+ android:paddingRight="4dp"
+ android:textColor="#FFBEBEBE"
+ android:textSize="14sp"
+ android:textStyle="bold"
+ tools:text="5:25"
+ />
+
+ <com.google.android.exoplayer2.ui.DefaultTimeBar
+ android:id="@id/exo_progress"
+ android:layout_width="0dp"
+ android:layout_height="24dp"
+ android:layout_weight="1"
+ />
+
+ <androidx.appcompat.widget.AppCompatImageButton
+ android:id="@+id/exo_volume_up"
+ tools:ignore="ContentDescription"
+ style="@style/ToroMediaButton.VolumeUp"
+ />
+
+ <androidx.appcompat.widget.AppCompatImageButton
+ android:id="@+id/exo_volume_off"
+ tools:ignore="ContentDescription"
+ tools:visibility="gone"
+ style="@style/ToroMediaButton.VolumeOff"
+ />
+
+ <com.google.android.exoplayer2.ui.DefaultTimeBar
+ android:id="@+id/volume_bar"
+ android:layout_width="80dp"
+ android:layout_height="24dp"
+ android:layout_marginEnd="8dp"
+ android:layout_marginRight="8dp"
+ />
+
+</androidx.appcompat.widget.LinearLayoutCompat> \ No newline at end of file
diff --git a/app/src/main/res/layout/toro_exo_player_view.xml b/app/src/main/res/layout/toro_exo_player_view.xml
new file mode 100644
index 00000000..b318b954
--- /dev/null
+++ b/app/src/main/res/layout/toro_exo_player_view.xml
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (c) 2018 Nam Nguyen, nam@ene.im
+ ~
+ ~ 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.
+ -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ >
+
+ <com.google.android.exoplayer2.ui.AspectRatioFrameLayout
+ android:id="@id/exo_content_frame"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ >
+
+ <!-- Video surface will be inserted as the first child of the content frame. -->
+
+ <View
+ android:id="@id/exo_shutter"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/black"
+ />
+
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@id/exo_artwork"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scaleType="fitXY"
+ />
+
+ <com.google.android.exoplayer2.ui.SubtitleView
+ android:id="@id/exo_subtitles"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ />
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@id/exo_error_message"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:background="@color/exo_error_message_background_color"
+ android:gravity="center"
+ android:padding="16dp"
+ />
+
+ </com.google.android.exoplayer2.ui.AspectRatioFrameLayout>
+
+ <FrameLayout
+ android:id="@id/exo_ad_overlay"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ />
+
+ <FrameLayout
+ android:id="@id/exo_overlay"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ />
+
+ <ProgressBar
+ android:id="@id/exo_buffering"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:indeterminate="true"
+ />
+
+ <ml.docilealligator.infinityforreddit.videoautoplay.ui.ToroControlView
+ android:id="@id/exo_controller"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom"
+ app:controller_layout_id="@layout/toro_exo_control_view" />
+
+</merge> \ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index ee96d717..29108bee 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,4 +1,4 @@
-<resources>
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="application_name" translatable="false">Infinity</string>
<string name="login_activity_label">Login</string>
<string name="search_activity_label" translatable="false"> </string>
@@ -1295,4 +1295,21 @@
<string name="handle_link">Handle Link</string>
<string name="invalid_link">Invalid link</string>
+ <string name="unexpected_intent_action">Unexpected intent action: <xliff:g id="action">%1$s</xliff:g></string>
+ <string name="enable_random_adaptation">Enable random adaptation</string>
+ <string name="error_drm_not_supported">Protected content not supported on API levels below 18</string>
+ <string name="error_drm_unsupported_scheme">This device does not support the required DRM scheme</string>
+ <string name="error_drm_unknown">An unknown DRM error occurred</string>
+ <string name="error_no_decoder">This device does not provide a decoder for <xliff:g id="mime_type">%1$s</xliff:g></string>
+ <string name="error_no_secure_decoder">This device does not provide a secure decoder for <xliff:g id="mime_type">%1$s</xliff:g></string>
+ <string name="error_querying_decoders">Unable to query device decoders</string>
+ <string name="error_instantiating_decoder">Unable to instantiate decoder <xliff:g id="decoder_name">%1$s</xliff:g></string>
+ <string name="error_unsupported_video">Media includes video tracks, but none are playable by this device</string>
+ <string name="error_unsupported_audio">Media includes audio tracks, but none are playable by this device</string>
+ <string name="storage_permission_denied">Permission to access storage was denied</string>
+ <string name="sample_list_load_error">One or more sample lists failed to load</string>
+
+ <string name="exo_controls_volume_up_description">Volume Up</string>
+ <string name="exo_controls_volume_off_description">Volume Off</string>
+
</resources>
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 40a2c351..35c75d46 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -517,4 +517,30 @@
<item name="switchStyle">@style/Widget.App.Switch.Dark</item>
</style>
+ <style name="ToroMediaButton">
+ <item name="android:background">@null</item>
+ <item name="android:layout_width">65dp</item>
+ <item name="android:layout_height">48dp</item>
+ </style>
+
+ <style name="ToroMediaButton.Play">
+ <item name="android:src">@drawable/exo_controls_play</item>
+ <item name="android:contentDescription">@string/exo_controls_play_description</item>
+ </style>
+
+ <style name="ToroMediaButton.Pause">
+ <item name="android:src">@drawable/exo_controls_pause</item>
+ <item name="android:contentDescription">@string/exo_controls_pause_description</item>
+ </style>
+
+ <style name="ToroMediaButton.VolumeUp">
+ <item name="android:src">@drawable/ic_volume_up_black_24dp</item>
+ <item name="android:contentDescription">@string/exo_controls_volume_up_description</item>
+ </style>
+
+ <style name="ToroMediaButton.VolumeOff">
+ <item name="android:src">@drawable/ic_volume_off_black_24dp</item>
+ <item name="android:contentDescription">@string/exo_controls_volume_off_description</item>
+ </style>
+
</resources>