Fix stupid Redgifs API issue.

This commit is contained in:
Docile-Alligator 2022-09-05 00:57:11 +10:00
parent 064b2ceedc
commit 6d224c307d
54 changed files with 6559 additions and 121 deletions

View File

@ -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 **/

View File

@ -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();
}

View File

@ -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()));
}

View File

@ -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;
}
}

View File

@ -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;
@ -220,6 +220,10 @@ public class ViewVideoActivity extends AppCompatActivity implements CustomFontRe
@Named("default")
SharedPreferences mSharedPreferences;
@Inject
@Named("current_account")
SharedPreferences mCurrentAccountSharedPreferences;
@Inject
CustomThemeWrapper mCustomThemeWrapper;
@ -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));
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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);
}

View File

@ -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;

View File

@ -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;

View File

@ -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) {

View File

@ -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) {

View File

@ -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,

View File

@ -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;*/
}
}

View File

@ -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";

View File

@ -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));
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
};
}

View File

@ -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);
}
}
}

View File

@ -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
}
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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();
}
}
}
}

View File

@ -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));
}
};
}

View File

@ -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);
}
}
}
}

View File

@ -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() {
}
}

View File

@ -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;
}
};
}

View File

@ -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;
}
};
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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));
}
}
}
}

View File

@ -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 {
}

View File

@ -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();
}

View File

@ -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
}
}

View File

@ -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 + '}';
}
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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 + '}';
}
}

View File

@ -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();
}
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>