Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ea2b81b28 | ||
|
|
c568862ee1 | ||
|
|
42b767f619 | ||
|
|
ebf7fc4244 | ||
|
|
102642d58f | ||
|
|
5115e1181e | ||
|
|
f70ac357b6 | ||
|
|
7136341c3c | ||
|
|
9b49af4c42 | ||
|
|
fa59595dd5 | ||
|
|
7c3f495036 | ||
|
|
d05ef3d810 | ||
|
|
f5c875959a | ||
|
|
b45700739a | ||
|
|
a0430a13d1 | ||
|
|
9dfe855940 | ||
|
|
d09c45f74f | ||
|
|
16d6b6b2ed | ||
|
|
75e8938db3 | ||
|
|
dff36669bf | ||
|
|
1953508209 |
@@ -20,7 +20,9 @@
|
||||
<!-- Generic permissions -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.ACCESS_WIFI_STATE"
|
||||
tools:ignore="LeanbackUsesWifi" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
|
||||
<!-- Device feature requirements -->
|
||||
@@ -38,15 +40,16 @@
|
||||
<application
|
||||
android:name=".JellyfinApplication"
|
||||
android:allowBackup="true"
|
||||
android:appCategory="video"
|
||||
android:banner="@drawable/app_banner"
|
||||
android:dataExtractionRules="@xml/backup_rules"
|
||||
android:fullBackupContent="@xml/backup_content"
|
||||
android:icon="@drawable/app_icon"
|
||||
android:label="@string/app_name"
|
||||
android:largeHeap="true"
|
||||
android:roundIcon="@drawable/app_icon_round"
|
||||
android:theme="@style/Theme.Jellyfin"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:ignore="GoogleAppIndexingWarning">
|
||||
android:usesCleartextTraffic="true">
|
||||
|
||||
<service
|
||||
android:name=".auth.AuthenticatorService"
|
||||
|
||||
@@ -44,10 +44,12 @@ class ExpandableTextView(context: Context, attrs: AttributeSet?) : AppCompatText
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTextChanged(text: CharSequence?, start: Int, lengthBefore: Int, lengthAfter: Int) {
|
||||
super.onTextChanged(text, start, lengthBefore, lengthAfter)
|
||||
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
||||
super.onLayout(changed, left, top, right, bottom)
|
||||
|
||||
isFocusable = !text.isNullOrBlank()
|
||||
isClickable = !text.isNullOrBlank()
|
||||
// Only make focusable when text is ellipsized.
|
||||
val isEllipsized = !text.isNullOrBlank() && lineCount > 0 && layout.getEllipsisCount(lineCount - 1) > 0
|
||||
isFocusable = isEllipsized
|
||||
isClickable = isEllipsized
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1258,7 +1258,8 @@ public class CustomPlaybackOverlayFragment extends Fragment implements LiveTvGui
|
||||
}
|
||||
|
||||
public void setCurrentTime(long time) {
|
||||
leanbackOverlayFragment.updateCurrentPosition();
|
||||
if (leanbackOverlayFragment != null)
|
||||
leanbackOverlayFragment.updateCurrentPosition();
|
||||
}
|
||||
|
||||
public void setSecondaryTime(long time) {
|
||||
|
||||
@@ -29,7 +29,6 @@ import org.jellyfin.androidtv.ui.livetv.TvManager;
|
||||
import org.jellyfin.androidtv.util.DeviceUtils;
|
||||
import org.jellyfin.androidtv.util.TimeUtils;
|
||||
import org.jellyfin.androidtv.util.Utils;
|
||||
import org.jellyfin.androidtv.util.apiclient.PlaybackHelper;
|
||||
import org.jellyfin.androidtv.util.apiclient.ReportingHelper;
|
||||
import org.jellyfin.androidtv.util.apiclient.StreamHelper;
|
||||
import org.jellyfin.androidtv.util.profile.BaseProfile;
|
||||
@@ -88,7 +87,7 @@ public class PlaybackController {
|
||||
private int mDefaultSubIndex = -1;
|
||||
private int mDefaultAudioIndex = -1;
|
||||
private boolean burningSubs = false;
|
||||
private double mRequestedPlaybackSpeed = -1.0;
|
||||
private float mRequestedPlaybackSpeed = -1.0f;
|
||||
|
||||
private PlayMethod mPlaybackMethod = PlayMethod.Transcode;
|
||||
|
||||
@@ -163,7 +162,17 @@ public class PlaybackController {
|
||||
mPlaybackMethod = value;
|
||||
}
|
||||
|
||||
public void setPlaybackSpeed(@NonNull Double speed) {
|
||||
public float getPlaybackSpeed() {
|
||||
if (hasInitializedVideoManager()) {
|
||||
// Actually poll the video manager, since exoplayer can revert back
|
||||
// to 1x if it can't go faster, so we should check directly
|
||||
return mVideoManager.getPlaybackSpeed();
|
||||
} else {
|
||||
return mRequestedPlaybackSpeed;
|
||||
}
|
||||
}
|
||||
|
||||
public void setPlaybackSpeed(@NonNull float speed) {
|
||||
mRequestedPlaybackSpeed = speed;
|
||||
if (hasInitializedVideoManager()) {
|
||||
mVideoManager.setPlaybackSpeed(speed);
|
||||
@@ -205,7 +214,7 @@ public class PlaybackController {
|
||||
}
|
||||
|
||||
public int getSubtitleStreamIndex() {
|
||||
return (mCurrentOptions != null && mCurrentOptions.getSubtitleStreamIndex() != null) ? mCurrentOptions.getSubtitleStreamIndex() : mDefaultSubIndex;
|
||||
return (mCurrentOptions != null && mCurrentOptions.getSubtitleStreamIndex() != null) ? mCurrentOptions.getSubtitleStreamIndex() : -1;
|
||||
}
|
||||
|
||||
public List<SubtitleStreamInfo> getSubtitleStreams() {
|
||||
@@ -281,22 +290,28 @@ public class PlaybackController {
|
||||
|
||||
@TargetApi(23)
|
||||
private void getDisplayModes() {
|
||||
if (mFragment == null)
|
||||
return;
|
||||
Display display = mFragment.requireActivity().getWindowManager().getDefaultDisplay();
|
||||
mDisplayModes = display.getSupportedModes();
|
||||
Timber.i("** Available display refresh rates:");
|
||||
for (Display.Mode mDisplayMode : mDisplayModes) {
|
||||
Timber.i("%f", mDisplayMode.getRefreshRate());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@TargetApi(23)
|
||||
private Display.Mode findBestDisplayMode(MediaStream videoStream) {
|
||||
if (mDisplayModes == null || videoStream.getRealFrameRate() == null) return null;
|
||||
if (mFragment == null || mDisplayModes == null || videoStream.getRealFrameRate() == null) return null;
|
||||
|
||||
|
||||
int curWeight = 0;
|
||||
Display.Mode bestMode = null;
|
||||
int sourceRate = Math.round(videoStream.getRealFrameRate() * 100);
|
||||
|
||||
Display.Mode defaultMode = mFragment.requireActivity().getWindowManager().getDefaultDisplay().getMode();
|
||||
|
||||
Timber.d("trying to find display mode for video: %sx%s @%sfps", videoStream.getWidth(), videoStream.getHeight(), videoStream.getRealFrameRate());
|
||||
for (Display.Mode mode : mDisplayModes) {
|
||||
// Skip unwanted display modes
|
||||
if (mode.getPhysicalWidth() < 1280 || mode.getPhysicalHeight() < 720) // Skip non-HD
|
||||
@@ -309,8 +324,18 @@ public class PlaybackController {
|
||||
if (rate != sourceRate && rate != sourceRate * 2 && rate != Math.round(sourceRate * 2.5)) // Skip inappropriate rates
|
||||
continue;
|
||||
|
||||
int weight = Math.round(rate * 10000) + (9999 - (mode.getPhysicalWidth() - videoStream.getWidth()));
|
||||
Timber.d("qualifying display mode: %sx%s @%sfps", mode.getPhysicalWidth(), mode.getPhysicalHeight(), mode.getRefreshRate());
|
||||
|
||||
int resolutionDifference = 0;
|
||||
if (!(mode.getPhysicalWidth() == defaultMode.getPhysicalWidth() && mode.getPhysicalHeight() == defaultMode.getPhysicalHeight()))
|
||||
resolutionDifference = mode.getPhysicalWidth() - videoStream.getWidth();
|
||||
int refreshRateDifference = rate - sourceRate;
|
||||
|
||||
// use 100,000 to account for refresh rates 120Hz+ (at 120Hz rate == 12,000)
|
||||
int weight = 100000 - refreshRateDifference + 100000 - resolutionDifference;
|
||||
|
||||
if (weight > curWeight) {
|
||||
Timber.d("preferring mode: %sx%s @%sfps", mode.getPhysicalWidth(), mode.getPhysicalHeight(), mode.getRefreshRate());
|
||||
curWeight = weight;
|
||||
bestMode = mode;
|
||||
}
|
||||
@@ -342,8 +367,6 @@ public class PlaybackController {
|
||||
} else {
|
||||
Timber.i("*** Unable to find display mode for refresh rate: %s", videoStream.getRealFrameRate());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// central place to update mCurrentPosition
|
||||
@@ -381,8 +404,8 @@ public class PlaybackController {
|
||||
play(position, null);
|
||||
}
|
||||
|
||||
private void play(long position, @Nullable Integer transcodedSubtitle) {
|
||||
Timber.d("Play called from state: %s with pos: %d and sub index: %d", mPlaybackState, position, transcodedSubtitle);
|
||||
private void play(long position, @Nullable Integer forcedSubtitleIndex) {
|
||||
Timber.d("Play called from state: %s with pos: %d and sub index: %d", mPlaybackState, position, forcedSubtitleIndex);
|
||||
|
||||
if (position < 0) {
|
||||
Timber.i("Negative start requested - adjusting to zero");
|
||||
@@ -491,8 +514,8 @@ public class PlaybackController {
|
||||
vlcOptions.setEnableDirectStream(false);
|
||||
vlcOptions.setEnableDirectPlay(false);
|
||||
}
|
||||
vlcOptions.setSubtitleStreamIndex(transcodedSubtitle);
|
||||
vlcOptions.setMediaSourceId(transcodedSubtitle != null ? getCurrentMediaSource().getId() : null);
|
||||
vlcOptions.setSubtitleStreamIndex(forcedSubtitleIndex);
|
||||
vlcOptions.setMediaSourceId(forcedSubtitleIndex != null ? getCurrentMediaSource().getId() : null);
|
||||
DeviceProfile vlcProfile = new LibVlcProfile(isLiveTv);
|
||||
vlcOptions.setProfile(vlcProfile);
|
||||
|
||||
@@ -503,8 +526,8 @@ public class PlaybackController {
|
||||
if (exoErrorEncountered || (isLiveTv && !directStreamLiveTv))
|
||||
internalOptions.setEnableDirectStream(false);
|
||||
internalOptions.setMaxAudioChannels(Utils.downMixAudio() ? 2 : null); //have to downmix at server
|
||||
internalOptions.setSubtitleStreamIndex(transcodedSubtitle);
|
||||
internalOptions.setMediaSourceId(transcodedSubtitle != null ? getCurrentMediaSource().getId() : null);
|
||||
internalOptions.setSubtitleStreamIndex(forcedSubtitleIndex);
|
||||
internalOptions.setMediaSourceId(forcedSubtitleIndex != null ? getCurrentMediaSource().getId() : null);
|
||||
DeviceProfile internalProfile = new BaseProfile();
|
||||
if (DeviceUtils.is60() || userPreferences.getValue().get(UserPreferences.Companion.getAc3Enabled())) {
|
||||
boolean hlsSupported = false;
|
||||
@@ -729,6 +752,8 @@ public class PlaybackController {
|
||||
mCurrentOptions.setSubtitleStreamIndex(mDefaultSubIndex);
|
||||
Timber.d("stream started with burnt in subs");
|
||||
burningSubs = true;
|
||||
} else {
|
||||
mCurrentOptions.setSubtitleStreamIndex(null);
|
||||
}
|
||||
|
||||
Long mbPos = position * 10000;
|
||||
@@ -753,7 +778,7 @@ public class PlaybackController {
|
||||
|
||||
// set playback speed to user selection, or 1 if we're watching live-tv
|
||||
if (mVideoManager != null)
|
||||
mVideoManager.setPlaybackSpeed(isLiveTv() ? 1.0 : mRequestedPlaybackSpeed);
|
||||
mVideoManager.setPlaybackSpeed(isLiveTv() ? 1.0f : mRequestedPlaybackSpeed);
|
||||
|
||||
if (mFragment != null) mFragment.updateDisplay();
|
||||
|
||||
@@ -882,20 +907,23 @@ public class PlaybackController {
|
||||
public void switchSubtitleStream(int index) {
|
||||
if (!hasInitializedVideoManager())
|
||||
return;
|
||||
|
||||
// get current timestamp first
|
||||
refreshCurrentPosition();
|
||||
Timber.d("Setting subtitle index to: %d", index);
|
||||
mCurrentOptions.setSubtitleStreamIndex(index);
|
||||
mDefaultSubIndex = index;
|
||||
|
||||
// clear subtitles first
|
||||
if (mFragment != null) mFragment.addManualSubtitles(null);
|
||||
mVideoManager.disableSubs();
|
||||
// clear the default in case there's an error loading the subtitles
|
||||
mDefaultSubIndex = -1;
|
||||
|
||||
// handle setting subtitles as disabled
|
||||
// restart playback if turning off burnt-in subtitles
|
||||
if (index < 0) {
|
||||
mCurrentOptions.setSubtitleStreamIndex(-1);
|
||||
if (burningSubs) {
|
||||
stop();
|
||||
play(mCurrentPosition, -1);
|
||||
} else {
|
||||
if (mFragment != null) mFragment.addManualSubtitles(null);
|
||||
mVideoManager.disableSubs();
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -906,77 +934,77 @@ public class PlaybackController {
|
||||
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.subtitle_error));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// handle according to delivery method
|
||||
SubtitleStreamInfo streamInfo = getSubtitleStreamInfo(index);
|
||||
if (streamInfo == null) {
|
||||
if (mFragment != null)
|
||||
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_unable_load_subs));
|
||||
} else {
|
||||
switch (streamInfo.getDeliveryMethod()) {
|
||||
return;
|
||||
}
|
||||
|
||||
case Encode:
|
||||
// Gonna need to burn in so start a transcode with the sub index
|
||||
stop();
|
||||
if (!mVideoManager.isNativeMode()) {
|
||||
// handle switching on or between burnt-in subtitles, or switching to non-burnt subtitles
|
||||
// if switching from burnt-in subtitles to another type, playback still needs to be restarted
|
||||
if (burningSubs || streamInfo.getDeliveryMethod() == SubtitleDeliveryMethod.Encode) {
|
||||
stop();
|
||||
if (mFragment != null && streamInfo.getDeliveryMethod() == SubtitleDeliveryMethod.Encode)
|
||||
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_burn_sub_warning));
|
||||
play(mCurrentPosition, index);
|
||||
return;
|
||||
}
|
||||
|
||||
// when burnt-in subtitles are selected, mCurrentOptions SubtitleStreamIndex is set in startItem() as soon as playback starts
|
||||
// otherwise mCurrentOptions SubtitleStreamIndex is kept null until now so we knew subtitles needed to be enabled but weren't already
|
||||
|
||||
switch (streamInfo.getDeliveryMethod()) {
|
||||
case Embed:
|
||||
if (!mVideoManager.isNativeMode()) {
|
||||
if (!mVideoManager.setSubtitleTrack(index, getCurrentlyPlayingItem().getMediaStreams())) {
|
||||
// error selecting internal subs
|
||||
if (mFragment != null)
|
||||
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_burn_sub_warning));
|
||||
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_unable_load_subs));
|
||||
} else {
|
||||
mCurrentOptions.setSubtitleStreamIndex(index);
|
||||
mDefaultSubIndex = index;
|
||||
}
|
||||
play(mCurrentPosition, index);
|
||||
break;
|
||||
case Embed:
|
||||
if (!mVideoManager.isNativeMode()) {
|
||||
if (mFragment != null)
|
||||
mFragment.addManualSubtitles(null); // in case these were on
|
||||
if (!mVideoManager.setSubtitleTrack(index, getCurrentlyPlayingItem().getMediaStreams())) {
|
||||
// error selecting internal subs
|
||||
if (mFragment != null)
|
||||
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_unable_load_subs));
|
||||
}
|
||||
break;
|
||||
}
|
||||
// not using vlc - fall through to external handling
|
||||
case External:
|
||||
if (mFragment != null) mFragment.addManualSubtitles(null);
|
||||
mVideoManager.disableSubs();
|
||||
if (mFragment != null) mFragment.showSubLoadingMsg(true);
|
||||
stream.setDeliveryMethod(SubtitleDeliveryMethod.External);
|
||||
stream.setDeliveryUrl(String.format("%1$s/Videos/%2$s/%3$s/Subtitles/%4$s/0/Stream.JSON", apiClient.getValue().getApiUrl(), mCurrentStreamInfo.getItemId(), mCurrentStreamInfo.getMediaSourceId(), String.valueOf(stream.getIndex())));
|
||||
apiClient.getValue().getSubtitles(stream.getDeliveryUrl(), new Response<SubtitleTrackInfo>() {
|
||||
}
|
||||
// not using vlc - fall through to external handling
|
||||
case External:
|
||||
if (mFragment != null) mFragment.showSubLoadingMsg(true);
|
||||
stream.setDeliveryMethod(SubtitleDeliveryMethod.External);
|
||||
stream.setDeliveryUrl(String.format("%1$s/Videos/%2$s/%3$s/Subtitles/%4$s/0/Stream.JSON", apiClient.getValue().getApiUrl(), mCurrentStreamInfo.getItemId(), mCurrentStreamInfo.getMediaSourceId(), String.valueOf(stream.getIndex())));
|
||||
apiClient.getValue().getSubtitles(stream.getDeliveryUrl(), new Response<SubtitleTrackInfo>() {
|
||||
|
||||
@Override
|
||||
public void onResponse(final SubtitleTrackInfo info) {
|
||||
@Override
|
||||
public void onResponse(final SubtitleTrackInfo info) {
|
||||
|
||||
if (info != null) {
|
||||
Timber.d("Adding json subtitle track to player");
|
||||
if (mFragment != null) mFragment.addManualSubtitles(info);
|
||||
} else {
|
||||
Timber.e("Empty subtitle result");
|
||||
if (mFragment != null) {
|
||||
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_unable_load_subs));
|
||||
mFragment.showSubLoadingMsg(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception ex) {
|
||||
Timber.e(ex, "Error downloading subtitles");
|
||||
if (info != null) {
|
||||
Timber.d("Adding json subtitle track to player");
|
||||
if (mFragment != null) mFragment.addManualSubtitles(info);
|
||||
mCurrentOptions.setSubtitleStreamIndex(index);
|
||||
mDefaultSubIndex = index;
|
||||
} else {
|
||||
Timber.e("Empty subtitle result");
|
||||
if (mFragment != null) {
|
||||
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_unable_load_subs));
|
||||
mFragment.showSubLoadingMsg(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
break;
|
||||
case Hls:
|
||||
break;
|
||||
}
|
||||
@Override
|
||||
public void onError(Exception ex) {
|
||||
Timber.e(ex, "Error downloading subtitles");
|
||||
if (mFragment != null) {
|
||||
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_unable_load_subs));
|
||||
mFragment.showSubLoadingMsg(false);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
break;
|
||||
case Hls:
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void pause() {
|
||||
@@ -1396,7 +1424,6 @@ public class PlaybackController {
|
||||
if (mPlaybackState == PlaybackState.PAUSED) {
|
||||
mPlaybackState = PlaybackState.PLAYING;
|
||||
} else {
|
||||
|
||||
// select or disable subtitles
|
||||
Integer currentSubtitleIndex = mCurrentOptions.getSubtitleStreamIndex();
|
||||
if (mDefaultSubIndex >= 0 && currentSubtitleIndex != null && currentSubtitleIndex == mDefaultSubIndex) {
|
||||
|
||||
@@ -32,6 +32,8 @@ public class PlaybackManager {
|
||||
}
|
||||
|
||||
public ArrayList<MediaStream> getInPlaybackSelectableAudioStreams(StreamInfo info) {
|
||||
if (info == null)
|
||||
return null;
|
||||
return info.GetSelectableAudioStreams();
|
||||
}
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
|
||||
private int mVideoVisibleWidth;
|
||||
private int mSarNum;
|
||||
private int mSarDen;
|
||||
private Integer exoplayerAudioIndex = null;
|
||||
|
||||
private long mForcedTime = -1;
|
||||
private long mLastTime = -1;
|
||||
@@ -144,6 +145,12 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
|
||||
public void onTimelineChanged(Timeline timeline, int reason) {
|
||||
Timber.d("Caught player timeline change - reason: %s", reason == Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED ? "PLAYLIST_CHANGED" : "SOURCE_UPDATE");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTracksInfoChanged(TracksInfo tracksInfo) {
|
||||
Timber.d("Tracks info changed");
|
||||
exoplayerAudioIndex = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -290,6 +297,7 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
|
||||
mActivity.finish();
|
||||
return;
|
||||
}
|
||||
exoplayerAudioIndex = null;
|
||||
mExoPlayer.setPlayWhenReady(true);
|
||||
normalWidth = mExoPlayerView.getLayoutParams().width;
|
||||
normalHeight = mExoPlayerView.getLayoutParams().height;
|
||||
@@ -331,6 +339,7 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
|
||||
mExoPlayer.setTrackSelectionParameters(mExoPlayer.getTrackSelectionParameters()
|
||||
.buildUpon()
|
||||
.setTrackSelectionOverrides(overrides).build());
|
||||
exoplayerAudioIndex = null;
|
||||
} else if (mVlcPlayer != null) {
|
||||
mVlcPlayer.stop();
|
||||
}
|
||||
@@ -451,6 +460,9 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
|
||||
if (streamType != MediaStreamType.Subtitle && streamType != MediaStreamType.Audio)
|
||||
return -1;
|
||||
|
||||
if (exoplayerAudioIndex != null && streamType == MediaStreamType.Audio)
|
||||
return exoplayerAudioIndex;
|
||||
|
||||
int chosenTrackType = streamType == MediaStreamType.Subtitle ? C.TRACK_TYPE_TEXT : C.TRACK_TYPE_AUDIO;
|
||||
|
||||
TracksInfo exoTracks = mExoPlayer.getCurrentTracksInfo();
|
||||
@@ -471,8 +483,12 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
|
||||
Timber.d("failed to parse track ID [%s]", trackFormat.id);
|
||||
return -1;
|
||||
}
|
||||
if (id >= 0 && id < allStreams.size())
|
||||
if (id >= 0 && id < allStreams.size()) {
|
||||
Timber.d("re-retrieved exoplayer track index %s", id);
|
||||
if (streamType == MediaStreamType.Audio)
|
||||
exoplayerAudioIndex = id;
|
||||
return id;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
@@ -642,7 +658,17 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
|
||||
return ndx;
|
||||
}
|
||||
|
||||
public void setPlaybackSpeed(@NonNull Double speed) {
|
||||
public float getPlaybackSpeed(){
|
||||
if (!isInitialized()) {
|
||||
return 1.0f;
|
||||
} else if (isNativeMode()){
|
||||
return mExoPlayer.getPlaybackParameters().speed;
|
||||
} else {
|
||||
return mVlcPlayer.getRate();
|
||||
}
|
||||
}
|
||||
|
||||
public void setPlaybackSpeed(@NonNull float speed) {
|
||||
if (speed < 0.25) {
|
||||
Timber.w("Invalid playback speed requested: %f", speed);
|
||||
return;
|
||||
@@ -650,9 +676,9 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
|
||||
Timber.d("Setting playback speed: %f", speed);
|
||||
|
||||
if (nativeMode) {
|
||||
mExoPlayer.setPlaybackParameters(new PlaybackParameters(speed.floatValue()));
|
||||
mExoPlayer.setPlaybackParameters(new PlaybackParameters(speed));
|
||||
} else {
|
||||
mVlcPlayer.setRate(speed.floatValue());
|
||||
mVlcPlayer.setRate(speed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -720,7 +746,7 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
|
||||
// Create a new media player
|
||||
ArrayList<String> options = new ArrayList<>(20);
|
||||
options.add("--network-caching=" + buffer);
|
||||
options.add("--no-audio-time-stretch");
|
||||
options.add("--audio-time-stretch");
|
||||
options.add("--avcodec-skiploopfilter");
|
||||
options.add("1");
|
||||
options.add("--avcodec-skip-frame");
|
||||
|
||||
@@ -3,16 +3,16 @@ package org.jellyfin.androidtv.ui.playback
|
||||
class VideoSpeedController(
|
||||
private val parentController: PlaybackController
|
||||
) {
|
||||
enum class SpeedSteps(val speed: Double) {
|
||||
enum class SpeedSteps(val speed: Float) {
|
||||
// Use named parameter so detekt knows these aren't magic values
|
||||
SPEED_0_25(speed = 0.25),
|
||||
SPEED_0_50(speed = 0.5),
|
||||
SPEED_0_75(speed = 0.75),
|
||||
SPEED_1_00(speed = 1.0),
|
||||
SPEED_1_25(speed = 1.25),
|
||||
SPEED_1_50(speed = 1.50),
|
||||
SPEED_1_75(speed = 1.75),
|
||||
SPEED_2_00(speed = 2.0),
|
||||
SPEED_0_25(speed = 0.25f),
|
||||
SPEED_0_50(speed = 0.5f),
|
||||
SPEED_0_75(speed = 0.75f),
|
||||
SPEED_1_00(speed = 1.0f),
|
||||
SPEED_1_25(speed = 1.25f),
|
||||
SPEED_1_50(speed = 1.50f),
|
||||
SPEED_1_75(speed = 1.75f),
|
||||
SPEED_2_00(speed = 2.0f),
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.jellyfin.androidtv.ui.playback.overlay;
|
||||
|
||||
import static java.lang.Math.round;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.text.format.DateFormat;
|
||||
@@ -78,9 +80,8 @@ public class CustomPlaybackTransportControlGlue extends PlaybackTransportControl
|
||||
this.playbackController = playbackController;
|
||||
|
||||
mRefreshEndTime = () -> {
|
||||
setEndTime();
|
||||
if (!isPlaying()) {
|
||||
setEndTime();
|
||||
|
||||
mHandler.postDelayed(mRefreshEndTime, 30000);
|
||||
}
|
||||
};
|
||||
@@ -211,8 +212,10 @@ public class CustomPlaybackTransportControlGlue extends PlaybackTransportControl
|
||||
}
|
||||
|
||||
void addMediaActions() {
|
||||
primaryActionsAdapter.clear();
|
||||
secondaryActionsAdapter.clear();
|
||||
if (primaryActionsAdapter.size() > 0)
|
||||
primaryActionsAdapter.clear();
|
||||
if (secondaryActionsAdapter.size() > 0)
|
||||
secondaryActionsAdapter.clear();
|
||||
|
||||
// Primary Items
|
||||
primaryActionsAdapter.add(playPauseAction);
|
||||
@@ -296,6 +299,10 @@ public class CustomPlaybackTransportControlGlue extends PlaybackTransportControl
|
||||
} else if (action == playbackSpeedAction) {
|
||||
getPlayerAdapter().getLeanbackOverlayFragment().setFading(false);
|
||||
playbackSpeedAction.handleClickAction(playbackController, getPlayerAdapter().getLeanbackOverlayFragment(), getContext(), view);
|
||||
// Post a callback to calculate the new time, since Exoplayer updates this in an async fashion.
|
||||
// This is a hack, we should instead have onPlaybackParametersChanged call out to this
|
||||
// class to notify rather than poll. But communication is unidirectional at the moment:
|
||||
mHandler.postDelayed(mRefreshEndTime, 5000); // 5 seconds
|
||||
} else if (action == adjustAudioDelayAction) {
|
||||
getPlayerAdapter().getLeanbackOverlayFragment().setFading(false);
|
||||
adjustAudioDelayAction.handleClickAction(playbackController, getPlayerAdapter().getLeanbackOverlayFragment(), getContext(), view);
|
||||
@@ -323,9 +330,11 @@ public class CustomPlaybackTransportControlGlue extends PlaybackTransportControl
|
||||
if (mEndsText == null || getPlayerAdapter().getDuration() < 1)
|
||||
return;
|
||||
long msLeft = getPlayerAdapter().getDuration() - getPlayerAdapter().getCurrentPosition();
|
||||
Calendar ends = Calendar.getInstance();
|
||||
ends.setTimeInMillis(ends.getTimeInMillis() + msLeft);
|
||||
mEndsText.setText(getContext().getString(R.string.lbl_playback_control_ends, DateFormat.getTimeFormat(getContext()).format(ends.getTime())));
|
||||
long realTimeLeft = round(msLeft / playbackController.getPlaybackSpeed());
|
||||
|
||||
Calendar endTime = Calendar.getInstance();
|
||||
endTime.setTimeInMillis(endTime.getTimeInMillis() + realTimeLeft);
|
||||
mEndsText.setText(getContext().getString(R.string.lbl_playback_control_ends, DateFormat.getTimeFormat(getContext()).format(endTime.getTime())));
|
||||
}
|
||||
|
||||
private void notifyActionChanged(Action action) {
|
||||
@@ -378,8 +387,10 @@ public class CustomPlaybackTransportControlGlue extends PlaybackTransportControl
|
||||
}
|
||||
|
||||
void invalidatePlaybackControls() {
|
||||
primaryActionsAdapter.clear();
|
||||
secondaryActionsAdapter.clear();
|
||||
if (primaryActionsAdapter.size() > 0)
|
||||
primaryActionsAdapter.clear();
|
||||
if (secondaryActionsAdapter.size() > 0)
|
||||
secondaryActionsAdapter.clear();
|
||||
addMediaActions();
|
||||
}
|
||||
|
||||
|
||||
@@ -158,6 +158,10 @@ public class VideoPlayerAdapter extends PlayerAdapter {
|
||||
return playbackController.getCurrentlyPlayingItem();
|
||||
}
|
||||
|
||||
public double getPlaybackSpeed() {
|
||||
return playbackController.getPlaybackSpeed();
|
||||
}
|
||||
|
||||
boolean hasChapters() {
|
||||
BaseItemDto item = getCurrentlyPlayingItem();
|
||||
List<ChapterInfoDto> chapters = item.getChapters();
|
||||
|
||||
@@ -27,6 +27,10 @@ public class SelectAudioAction extends CustomAction {
|
||||
public void handleClickAction(PlaybackController playbackController, LeanbackOverlayFragment leanbackOverlayFragment, Context context, View view) {
|
||||
|
||||
List<MediaStream> audioTracks = KoinJavaComponent.<PlaybackManager>get(PlaybackManager.class).getInPlaybackSelectableAudioStreams(playbackController.getCurrentStreamInfo());
|
||||
|
||||
if (audioTracks == null)
|
||||
return;
|
||||
|
||||
int currentAudioIndex = playbackController.getAudioStreamIndex();
|
||||
|
||||
PopupMenu audioMenu = new PopupMenu(context, view, Gravity.END);
|
||||
|
||||
@@ -11,8 +11,9 @@ fun OptionsScreen.aboutCategory() = category {
|
||||
setTitle(R.string.pref_about_title)
|
||||
|
||||
link {
|
||||
// Hardcoded strings for troubleshooting purposes
|
||||
title = "Jellyfin app version"
|
||||
content = BuildConfig.VERSION_NAME
|
||||
content = "jellyfin-androidtv ${BuildConfig.VERSION_NAME} ${BuildConfig.BUILD_TYPE}"
|
||||
icon = R.drawable.ic_jellyfin
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ import java.util.HashMap;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class CardPresenter extends Presenter {
|
||||
private static final double ASPECT_RATIO_BANNER = 434 / 79;
|
||||
private static final double ASPECT_RATIO_BANNER = 1000.0 / 185.0;
|
||||
|
||||
private int mStaticHeight = 300;
|
||||
private ImageType mImageType = ImageType.DEFAULT;
|
||||
|
||||
@@ -160,7 +160,7 @@ fun LegacyBaseItemDto.asSdk(): ModernBaseItemDto = ModernBaseItemDto(
|
||||
parentArtItemId = this.parentArtItemId,
|
||||
parentArtImageTag = this.parentArtImageTag,
|
||||
seriesThumbImageTag = this.seriesThumbImageTag,
|
||||
imageBlurHashes = this.imageBlurHashes.asSdk(),
|
||||
imageBlurHashes = this.imageBlurHashes?.asSdk(),
|
||||
seriesStudio = this.seriesStudio,
|
||||
parentThumbItemId = this.parentThumbItemId,
|
||||
parentThumbImageTag = this.parentThumbImageTag,
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
android:layout_marginBottom="48dp"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="28sp"
|
||||
android:gravity="center"
|
||||
app:strokeWidth="5.0"
|
||||
tools:text="Subtitles" />
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
<string name="lbl_filters">Filters</string>
|
||||
<string name="msg_subtitles_loading">Subtitles loading…</string>
|
||||
<string name="msg_unable_load_subs">Unable to load subtitle</string>
|
||||
<string name="msg_burn_sub_warning">Burned in subs can take a few minutes. Please be patient…</string>
|
||||
<string name="msg_burn_sub_warning">Burning in subtitles. This may take a few seconds…</string>
|
||||
<string name="lbl_recording">Recording</string>
|
||||
<string name="lbl_no_recordings">No Recordings Available</string>
|
||||
<string name="lbl_live_tv_guide">Live TV Guide</string>
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<full-backup-content>
|
||||
<!-- Stored servers & users (without access tokens) -->
|
||||
<include
|
||||
domain="file"
|
||||
path="authentication_store.json" />
|
||||
|
||||
<!-- UserPreferences -->
|
||||
<include
|
||||
domain="sharedpref"
|
||||
path="org.jellyfin.androidtv_preferences" />
|
||||
<include
|
||||
domain="sharedpref"
|
||||
path="org.jellyfin.androidtv.debug_preferences" />
|
||||
<!-- SystemPreferences -->
|
||||
<include
|
||||
domain="sharedpref"
|
||||
path="systemprefs" />
|
||||
<!-- AuthenticationPreferences -->
|
||||
<include
|
||||
domain="sharedpref"
|
||||
path="AuthenticationPreferences" />
|
||||
</full-backup-content>
|
||||
|
||||
47
app/src/main/res/xml/backup_rules.xml
Normal file
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<!-- Stored servers & users (without access tokens) -->
|
||||
<include
|
||||
domain="file"
|
||||
path="authentication_store.json" />
|
||||
|
||||
<!-- UserPreferences -->
|
||||
<include
|
||||
domain="sharedpref"
|
||||
path="org.jellyfin.androidtv_preferences" />
|
||||
<include
|
||||
domain="sharedpref"
|
||||
path="org.jellyfin.androidtv.debug_preferences" />
|
||||
<!-- SystemPreferences -->
|
||||
<include
|
||||
domain="sharedpref"
|
||||
path="systemprefs" />
|
||||
<!-- AuthenticationPreferences -->
|
||||
<include
|
||||
domain="sharedpref"
|
||||
path="AuthenticationPreferences" />
|
||||
</cloud-backup>
|
||||
<device-transfer>
|
||||
<!-- Stored servers & users (without access tokens) -->
|
||||
<include
|
||||
domain="file"
|
||||
path="authentication_store.json" />
|
||||
|
||||
<!-- UserPreferences -->
|
||||
<include
|
||||
domain="sharedpref"
|
||||
path="org.jellyfin.androidtv_preferences" />
|
||||
<include
|
||||
domain="sharedpref"
|
||||
path="org.jellyfin.androidtv.debug_preferences" />
|
||||
<!-- SystemPreferences -->
|
||||
<include
|
||||
domain="sharedpref"
|
||||
path="systemprefs" />
|
||||
<!-- AuthenticationPreferences -->
|
||||
<include
|
||||
domain="sharedpref"
|
||||
path="AuthenticationPreferences" />
|
||||
</device-transfer>
|
||||
</data-extraction-rules>
|
||||
@@ -1,7 +1,7 @@
|
||||
package org.jellyfin.androidtv.ui.playback
|
||||
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.doubles.plusOrMinus
|
||||
import io.kotest.matchers.floats.plusOrMinus
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.mockk.every
|
||||
import io.mockk.justRun
|
||||
@@ -17,19 +17,19 @@ class VideoSpeedControllerTests : FunSpec({
|
||||
|
||||
test("VideoSpeedController.SpeedSteps uses intervals of 0.25") {
|
||||
VideoSpeedController.SpeedSteps.values().forEachIndexed { i, v ->
|
||||
v.speed.shouldBe((i + 1) * 0.25 plusOrMinus 0.0001)
|
||||
v.speed.shouldBe((i + 1) * 0.25f plusOrMinus 1.0E-4F)
|
||||
}
|
||||
}
|
||||
|
||||
test("VideoSpeedController speed is 1 by default") {
|
||||
val mockController = mockk<PlaybackController>(relaxed = true)
|
||||
val slot = slot<Double>()
|
||||
val slot = slot<Float>()
|
||||
justRun { mockController.setPlaybackSpeed(capture(slot)) }
|
||||
|
||||
VideoSpeedController(mockController)
|
||||
|
||||
verify { mockController.setPlaybackSpeed(any()) }
|
||||
slot.captured shouldBe (1.0 plusOrMinus 0.0001)
|
||||
slot.captured shouldBe (1.0f plusOrMinus 1.0E-4F)
|
||||
}
|
||||
|
||||
test("VideoSpeedController.currentSpeed getter returns set value") {
|
||||
@@ -44,7 +44,7 @@ class VideoSpeedControllerTests : FunSpec({
|
||||
|
||||
test("VideoSpeedController.currentSpeed updates the speed in the controller") {
|
||||
val mockController = mockk<PlaybackController>(relaxed = true)
|
||||
val slot = slot<Double>()
|
||||
val slot = slot<Float>()
|
||||
justRun { mockController.setPlaybackSpeed(capture(slot)) }
|
||||
|
||||
val controller = VideoSpeedController(mockController)
|
||||
@@ -52,7 +52,7 @@ class VideoSpeedControllerTests : FunSpec({
|
||||
controller.currentSpeed = expected
|
||||
|
||||
verify { mockController.setPlaybackSpeed(any()) }
|
||||
slot.captured shouldBe (expected.speed plusOrMinus 0.0001)
|
||||
slot.captured shouldBe (expected.speed plusOrMinus 1.0E-4F)
|
||||
}
|
||||
|
||||
test("VideoSpeedController.resetSpeedToDefault() works correctly") {
|
||||
@@ -62,26 +62,26 @@ class VideoSpeedControllerTests : FunSpec({
|
||||
videoController.currentSpeed = VideoSpeedController.SpeedSteps.SPEED_2_00
|
||||
videoController.resetSpeedToDefault()
|
||||
|
||||
val slot = slot<Double>()
|
||||
val slot = slot<Float>()
|
||||
justRun { playbackController.setPlaybackSpeed(capture(slot)) }
|
||||
VideoSpeedController(playbackController)
|
||||
|
||||
verify { playbackController.setPlaybackSpeed(any()) }
|
||||
slot.captured shouldBe (VideoSpeedController.SpeedSteps.SPEED_1_00.speed plusOrMinus 0.0001)
|
||||
slot.captured shouldBe (VideoSpeedController.SpeedSteps.SPEED_1_00.speed plusOrMinus 1.0E-4F)
|
||||
}
|
||||
|
||||
test("VideoSpeedController remembers previous instance speed value") {
|
||||
var lastSetSpeed = 1.0
|
||||
var lastSetSpeed = 1.0f
|
||||
|
||||
VideoSpeedController.SpeedSteps.values().forEach { newSpeed ->
|
||||
val mockController = mockk<PlaybackController>(relaxed = true)
|
||||
val slot = slot<Double>()
|
||||
val slot = slot<Float>()
|
||||
justRun { mockController.setPlaybackSpeed(capture(slot)) }
|
||||
|
||||
val controller = VideoSpeedController(mockController)
|
||||
|
||||
verify { mockController.setPlaybackSpeed(any()) }
|
||||
slot.captured shouldBe (lastSetSpeed plusOrMinus 0.0001)
|
||||
slot.captured shouldBe (lastSetSpeed plusOrMinus 1.0E-4F)
|
||||
|
||||
controller.currentSpeed = newSpeed
|
||||
lastSetSpeed = newSpeed.speed
|
||||
@@ -101,13 +101,13 @@ class VideoSpeedControllerTests : FunSpec({
|
||||
}
|
||||
val speedController = VideoSpeedController(mockController)
|
||||
|
||||
verify { mockController.setPlaybackSpeed(1.0) }
|
||||
verify { mockController.setPlaybackSpeed(1.0f) }
|
||||
speedController.currentSpeed shouldBe VideoSpeedController.SpeedSteps.SPEED_1_00
|
||||
|
||||
// Try to set it back to other values should be ignored
|
||||
speedController.currentSpeed = VideoSpeedController.SpeedSteps.SPEED_2_00
|
||||
|
||||
verify { mockController.setPlaybackSpeed(1.0) }
|
||||
verify { mockController.setPlaybackSpeed(1.0f) }
|
||||
speedController.currentSpeed shouldBe VideoSpeedController.SpeedSteps.SPEED_1_00
|
||||
}
|
||||
|
||||
@@ -117,12 +117,12 @@ class VideoSpeedControllerTests : FunSpec({
|
||||
}
|
||||
val speedController = VideoSpeedController(mockController)
|
||||
|
||||
verify { mockController.setPlaybackSpeed(1.0) }
|
||||
verify { mockController.setPlaybackSpeed(1.0f) }
|
||||
speedController.currentSpeed shouldBe VideoSpeedController.SpeedSteps.SPEED_1_00
|
||||
|
||||
speedController.currentSpeed = VideoSpeedController.SpeedSteps.SPEED_2_00
|
||||
|
||||
verify { mockController.setPlaybackSpeed(2.0) }
|
||||
verify { mockController.setPlaybackSpeed(2.0f) }
|
||||
speedController.currentSpeed shouldBe VideoSpeedController.SpeedSteps.SPEED_2_00
|
||||
}
|
||||
})
|
||||
|
||||
3
fastlane/metadata/android/en-US/changelogs/default.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
Thank you for using Jellyfin on Android TV! Using the latest released Jellyfin Server is always recommended.
|
||||
|
||||
For more information, please see our blog at jellyfin.org, or read the full changelog on GitHub.
|
||||
14
fastlane/metadata/android/en-US/full_description.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
Your media, on your terms.
|
||||
|
||||
The Jellyfin project is an open source, free software media server. No fees, no tracking, no hidden agenda. Get our free server to collect all your audio, video, photos, and more in one place.
|
||||
|
||||
To use the app, you must have a Jellyfin server set up and running. Find out more at <a href="https://jellyfin.org/" target="_blank">jellyfin.org</a>.
|
||||
|
||||
With a Jellyfin server, you can:
|
||||
|
||||
* Watch Live TV and recorded shows from your Jellyfin server (additional hardware/services required)
|
||||
* Stream to a Chromecast device on your network
|
||||
* Stream your media to your Android device
|
||||
* View your collection in an easy to use interface
|
||||
|
||||
This is the official Jellyfin companion app for Android TV. Thank you for using Jellyfin!
|
||||
BIN
fastlane/metadata/android/en-US/images/featureGraphic.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
fastlane/metadata/android/en-US/images/icon.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/1.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/2.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/3.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
fastlane/metadata/android/en-US/images/tvBanner.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
fastlane/metadata/android/en-US/images/tvScreenshots/1.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
fastlane/metadata/android/en-US/images/tvScreenshots/2.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
fastlane/metadata/android/en-US/images/tvScreenshots/3.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
1
fastlane/metadata/android/en-US/short_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
Television client for Jellyfin, the free software media system
|
||||
1
fastlane/metadata/android/en-US/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
Jellyfin for Android TV
|
||||
4
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=e5444a57cda4a95f90b0c9446a9e1b47d3d7f69057765bfb54bd4f482542d548
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-bin.zip
|
||||
distributionSha256Sum=29e49b10984e585d8118b7d0bc452f944e386458df27371b49b4ac1dec4b7fda
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||