mirror of
https://codeberg.org/Bazsalanszky/Infinity-For-Lemmy.git
synced 2025-01-28 18:44:44 +01:00
Fix stupid Redgifs API issue.
This commit is contained in:
parent
064b2ceedc
commit
6d224c307d
@ -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 **/
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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()));
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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;*/
|
||||
}
|
||||
}
|
||||
|
@ -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";
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
};
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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() {
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {
|
||||
}
|
@ -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();
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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 + '}';
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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();
|
||||
}
|
@ -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 + '}';
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
25
app/src/main/res/drawable/ic_volume_off_black_24dp.xml
Normal file
25
app/src/main/res/drawable/ic_volume_off_black_24dp.xml
Normal 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>
|
25
app/src/main/res/drawable/ic_volume_up_black_24dp.xml
Normal file
25
app/src/main/res/drawable/ic_volume_up_black_24dp.xml
Normal 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>
|
181
app/src/main/res/layout/toro_exo_control_view.xml
Normal file
181
app/src/main/res/layout/toro_exo_control_view.xml
Normal 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>
|
89
app/src/main/res/layout/toro_exo_player_view.xml
Normal file
89
app/src/main/res/layout/toro_exo_player_view.xml
Normal 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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user