diff --git a/app/build.gradle b/app/build.gradle index 421e7d3b95..06f3a6b5c8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,7 +13,8 @@ android { defaultConfig { applicationId "InfinityLoop1309.NewPipeEnhanced" - minSdk 21 + // media3 1.9+ requires API 23 (media3-session). Bumped from 21 -> drops Android 5.0/5.1. + minSdk 23 //noinspection ExpiredTargetSdkVersion targetSdk 33 versionCode 1100 @@ -137,7 +138,7 @@ ext { androidxRoomVersion = '2.4.2' androidxWorkVersion = '2.10.2' - exoPlayerVersion = '2.18.7' + media3Version = '1.10.1' googleAutoServiceVersion = '1.0.1' groupieVersion = '2.10.0' markwonVersion = '4.6.2' @@ -271,8 +272,14 @@ dependencies { implementation "com.squareup.okhttp3:okhttp:3.12.13" // Media player - implementation "com.google.android.exoplayer:exoplayer:${exoPlayerVersion}" - implementation "com.google.android.exoplayer:extension-mediasession:${exoPlayerVersion}" + implementation "androidx.media3:media3-exoplayer:${media3Version}" + implementation "androidx.media3:media3-exoplayer-dash:${media3Version}" + implementation "androidx.media3:media3-exoplayer-hls:${media3Version}" + implementation "androidx.media3:media3-exoplayer-smoothstreaming:${media3Version}" + implementation "androidx.media3:media3-datasource:${media3Version}" + implementation "androidx.media3:media3-ui:${media3Version}" + implementation "androidx.media3:media3-session:${media3Version}" + implementation "androidx.media3:media3-common:${media3Version}" // Metadata generator for service descriptors compileOnly "com.google.auto.service:auto-service-annotations:${googleAutoServiceVersion}" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5152dd485d..a9e04a151f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -43,6 +43,7 @@ android:banner="@mipmap/tv_banner" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" + android:largeHeap="true" android:logo="@mipmap/ic_launcher" android:theme="@style/OpeningTheme" android:resizeableActivity="true" diff --git a/app/src/main/assets/sabr_potoken_poc.js b/app/src/main/assets/sabr_potoken_poc.js new file mode 100644 index 0000000000..88b5ffa82d --- /dev/null +++ b/app/src/main/assets/sabr_potoken_poc.js @@ -0,0 +1,301 @@ +/* + * SABR PO token POC — WebView pipeline (att mode). + * + * Ported from the local research mint (mint-po-token-browser.mjs), adapted to run inside an Android + * WebView instead of puppeteer. It must be injected AFTER https://www.youtube.com/ has finished + * loading (same-origin is required: att/get and GenerateIT are youtube.com endpoints, and the + * BotGuard interpreter is embedded in the challenge). + * + * Flow: read browser context -> att/get challenge -> run BotGuard VM -> snapshot -> GenerateIT + * integrity token -> mint a videoId-bound PO token -> hand the result back through the + * `SabrPocBridge` JavascriptInterface. + * + * INTERNAL / LOCAL POC ONLY. The minted PO token is session-bound; keep it out of any public log. + * API_KEY / REQUEST_KEY are the well-known public ecosystem constants, not secrets. + */ +(function () { + 'use strict'; + + // YouTube ships a Trusted Types CSP (require-trusted-types-for 'script') that blocks + // new Function()/eval of the BotGuard interpreter. Installing an identity "default" policy makes + // Chromium route those sinks through it, restoring dynamic evaluation. If the CSP forbids + // creating the policy, loadBotGuard() will surface the eval error instead. + try { + if (window.trustedTypes && window.trustedTypes.createPolicy + && !window.trustedTypes.defaultPolicy) { + window.trustedTypes.createPolicy('default', { + createHTML: function (value) { return value; }, + createScript: function (value) { return value; }, + createScriptURL: function (value) { return value; } + }); + } + } catch (ttError) { + // ignore; surfaced later as an eval failure + } + + var API_KEY = 'AIzaSyDyT5W0Jh49F30Pqqtyfdf7pDLFKLJoAnw'; + var REQUEST_KEY = 'O43z0dpjhgX20SCx4KAo'; + + function report(result) { + try { + // eslint-disable-next-line no-undef + SabrPocBridge.onResult(JSON.stringify(result)); + } catch (e) { + // Bridge not present (e.g. plain browser run): fall back to console. + try { + console.log('[sabr-poc] ' + JSON.stringify(result)); + } catch (ignored) { + // nothing else we can do + } + } + } + + function step(message) { + try { console.log('[sabr-poc] ' + message); } catch (e) { /* ignore */ } + } + + function readVisitorData() { + var cfg = window.ytcfg; + var fromCfg = cfg && typeof cfg.get === 'function' ? cfg.get('VISITOR_DATA') : null; + if (fromCfg) { + return fromCfg; + } + var html = document.documentElement.innerHTML; + var marker = '"VISITOR_DATA":"'; + var start = html.indexOf(marker); + if (start < 0) { + throw new Error('Could not find visitor data'); + } + var from = start + marker.length; + var end = html.indexOf('"', from); + if (end < 0) { + throw new Error('Could not find visitor data end'); + } + return html.slice(from, end); + } + + function readClientVersion() { + var cfg = window.ytcfg; + var fromCfg = cfg && typeof cfg.get === 'function' + ? cfg.get('INNERTUBE_CLIENT_VERSION') : null; + return fromCfg || '2.20260114.01.00'; + } + + function normalizeTrustedUrl(value) { + if (!value) { + throw new Error('Missing interpreter url'); + } + return value.indexOf('//') === 0 ? 'https:' + value : value; + } + + function fetchChallenge(ctx) { + var context = { + client: { + clientName: 'WEB', + clientVersion: ctx.clientVersion, + hl: 'en', + gl: 'US', + utcOffsetMinutes: 0, + visitorData: ctx.visitorData + } + }; + return fetch('https://www.youtube.com/youtubei/v1/att/get?prettyPrint=false&alt=json', { + method: 'POST', + headers: { + 'Accept': '*/*', + 'Content-Type': 'application/json', + 'X-Goog-Visitor-Id': ctx.visitorData, + 'X-Youtube-Client-Version': ctx.clientVersion, + 'X-Youtube-Client-Name': '1' + }, + body: JSON.stringify({ + engagementType: 'ENGAGEMENT_TYPE_UNBOUND', + context: context + }) + }).then(function (response) { + return response.json().then(function (data) { + if (!response.ok || !data.bgChallenge) { + throw new Error('att/get failed status=' + response.status); + } + return data.bgChallenge; + }); + }); + } + + function resolveInterpreter(challenge, userAgent) { + var embedded = challenge.interpreterJavascript + && challenge.interpreterJavascript.privateDoNotAccessOrElseSafeScriptWrappedValue; + if (embedded) { + return Promise.resolve(embedded); + } + var url = normalizeTrustedUrl( + (challenge.interpreterJavascript + && challenge.interpreterJavascript + .privateDoNotAccessOrElseTrustedResourceUrlWrappedValue) + || (challenge.interpreterUrl + && challenge.interpreterUrl + .privateDoNotAccessOrElseTrustedResourceUrlWrappedValue)); + return fetch(url, { headers: { 'User-Agent': userAgent } }).then(function (response) { + return response.text().then(function (js) { + if (!response.ok || !js) { + throw new Error('interpreter fetch failed status=' + response.status); + } + return js; + }); + }); + } + + function loadBotGuard(interpreterJavascript, program, globalName) { + return new Promise(function (resolve, reject) { + try { + new Function(interpreterJavascript)(); + } catch (e) { + reject(new Error('interpreter eval failed: ' + e.message)); + return; + } + var vm = window[globalName]; + if (!vm || typeof vm.a !== 'function') { + reject(new Error('BotGuard VM missing init function')); + return; + } + var timeout = setTimeout(function () { + reject(new Error('BotGuard init timeout')); + }, 10000); + try { + vm.a(program, function (asyncSnapshotFunction) { + clearTimeout(timeout); + resolve({ asyncSnapshotFunction: asyncSnapshotFunction }); + }, true, undefined, function () { }, [[], []]); + } catch (e) { + clearTimeout(timeout); + reject(new Error('BotGuard init threw: ' + e.message)); + } + }); + } + + function snapshot(functions, webPoSignalOutput) { + return new Promise(function (resolve, reject) { + var timeout = setTimeout(function () { + reject(new Error('BotGuard snapshot timeout')); + }, 10000); + functions.asyncSnapshotFunction(function (response) { + clearTimeout(timeout); + resolve(response); + }, [undefined, undefined, webPoSignalOutput, undefined]); + }); + } + + function fetchIntegrityToken(botGuardResponse, userAgent) { + return fetch('https://www.youtube.com/api/jnn/v1/GenerateIT', { + method: 'POST', + headers: { + 'content-type': 'application/json+protobuf', + 'x-goog-api-key': API_KEY, + 'x-user-agent': 'grpc-web-javascript/0.1', + 'User-Agent': userAgent + }, + body: JSON.stringify([REQUEST_KEY, botGuardResponse]) + }).then(function (response) { + return response.json().then(function (data) { + var integrityToken = data[0]; + if (typeof integrityToken !== 'string') { + throw new Error('GenerateIT failed status=' + response.status); + } + return integrityToken; + }); + }); + } + + function base64ToU8(value) { + var normalized = value.replace(/-/g, '+').replace(/_/g, '/'); + var padded = normalized + '==='.slice((normalized.length + 3) % 4); + var binary = atob(padded); + var bytes = new Uint8Array(binary.length); + for (var i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + return bytes; + } + + function u8ToBase64Url(value) { + var binary = ''; + for (var i = 0; i < value.length; i++) { + binary += String.fromCharCode(value[i]); + } + return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); + } + + function mint(webPoSignalOutput, integrityToken, identifier) { + var getMinter = webPoSignalOutput[0]; + if (typeof getMinter !== 'function') { + return Promise.reject(new Error('Missing PO minter factory')); + } + return Promise.resolve(getMinter(base64ToU8(integrityToken))).then(function (mintCallback) { + if (typeof mintCallback !== 'function') { + throw new Error('Missing PO mint callback'); + } + return Promise.resolve(mintCallback(new TextEncoder().encode(identifier))) + .then(u8ToBase64Url); + }); + } + + function run() { + step('run start, readyState=' + document.readyState + ' origin=' + location.origin); + var videoId = window.__SABR_POC_VIDEO_ID || 'aqz-KE-bpKQ'; + var ctx = { + visitorData: readVisitorData(), + clientVersion: readClientVersion(), + userAgent: navigator.userAgent + }; + step('context ok visitorLen=' + ctx.visitorData.length + ' clientVersion=' + ctx.clientVersion); + var webPoSignalOutput = []; + var integrityTokenLength = -1; + step('fetching att/get challenge...'); + return fetchChallenge(ctx).then(function (challenge) { + step('challenge ok embedded=' + + !!(challenge.interpreterJavascript + && challenge.interpreterJavascript.privateDoNotAccessOrElseSafeScriptWrappedValue)); + return resolveInterpreter(challenge, ctx.userAgent).then(function (interpreterJs) { + step('interpreter resolved len=' + (interpreterJs ? interpreterJs.length : -1)); + return loadBotGuard(interpreterJs, challenge.program, challenge.globalName); + }); + }).then(function (functions) { + step('botguard loaded, taking snapshot...'); + return snapshot(functions, webPoSignalOutput); + }).then(function (botGuardResponse) { + step('snapshot ok, calling GenerateIT...'); + return fetchIntegrityToken(botGuardResponse, ctx.userAgent); + }).then(function (integrityToken) { + step('integrity token len=' + integrityToken.length + ', minting...'); + integrityTokenLength = integrityToken.length; + return mint(webPoSignalOutput, integrityToken, videoId); + }).then(function (poToken) { + report({ + ok: true, + videoId: videoId, + clientVersion: ctx.clientVersion, + visitorDataLength: ctx.visitorData.length, + integrityTokenLength: integrityTokenLength, + poTokenLength: poToken.length, + poToken: poToken, + userAgent: ctx.userAgent + }); + }); + } + + function reportError(e) { + report({ + ok: false, + error: (e && e.message) ? e.message : String(e), + errorName: e && e.name ? e.name : '', + stack: e && e.stack ? String(e.stack).slice(0, 400) : '', + userAgent: navigator.userAgent + }); + } + + try { + run().catch(reportError); + } catch (e) { + reportError(e); + } +})(); diff --git a/app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt b/app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt index aa849655a9..a34ae5ed22 100644 --- a/app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt +++ b/app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt @@ -2,7 +2,7 @@ package org.schabi.newpipe.error import android.os.Parcelable import androidx.annotation.StringRes -import com.google.android.exoplayer2.ExoPlaybackException +import androidx.media3.exoplayer.ExoPlaybackException import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize import org.schabi.newpipe.R diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 63835ed6e7..c86d04ecf0 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -46,8 +46,8 @@ import androidx.fragment.app.FragmentManager; import androidx.preference.PreferenceManager; -import com.google.android.exoplayer2.PlaybackException; -import com.google.android.exoplayer2.PlaybackParameters; +import androidx.media3.common.PlaybackException; +import androidx.media3.common.PlaybackParameters; import com.google.android.material.appbar.AppBarLayout; import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.tabs.TabLayout; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailPlayerCrasher.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailPlayerCrasher.java index ae704e88c9..af6c41c2be 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailPlayerCrasher.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailPlayerCrasher.java @@ -13,9 +13,9 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.PlaybackException; +import androidx.media3.common.C; +import androidx.media3.exoplayer.ExoPlaybackException; +import androidx.media3.common.PlaybackException; import org.schabi.newpipe.R; import org.schabi.newpipe.databinding.ListRadioIconItemBinding; @@ -29,9 +29,9 @@ import java.util.Map; import java.util.function.Supplier; -import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW; -import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_DECODING_FAILED; -import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_UNSPECIFIED; +import static androidx.media3.common.PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW; +import static androidx.media3.common.PlaybackException.ERROR_CODE_DECODING_FAILED; +import static androidx.media3.common.PlaybackException.ERROR_CODE_UNSPECIFIED; /** * Outsourced logic for crashing the player in the {@link VideoDetailFragment}. diff --git a/app/src/main/java/org/schabi/newpipe/player/NotificationUtil.java b/app/src/main/java/org/schabi/newpipe/player/NotificationUtil.java index 205b786408..c06540db9d 100644 --- a/app/src/main/java/org/schabi/newpipe/player/NotificationUtil.java +++ b/app/src/main/java/org/schabi/newpipe/player/NotificationUtil.java @@ -26,8 +26,8 @@ import java.util.List; import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; -import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL; -import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE; +import static androidx.media3.common.Player.REPEAT_MODE_ALL; +import static androidx.media3.common.Player.REPEAT_MODE_ONE; import static org.schabi.newpipe.player.PlayerService.ACTION_CLOSE; import static org.schabi.newpipe.player.PlayerService.ACTION_FAST_FORWARD; import static org.schabi.newpipe.player.PlayerService.ACTION_FAST_REWIND; @@ -269,19 +269,19 @@ private NotificationCompat.Action getAction( case NotificationConstants.SMART_REWIND_PREVIOUS: if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) { - return getAction(player, R.drawable.exo_notification_previous, + return getAction(player, R.drawable.exo_icon_previous, R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS); } else { - return getAction(player, R.drawable.exo_controls_rewind, + return getAction(player, R.drawable.exo_icon_rewind, R.string.exo_controls_rewind_description, ACTION_FAST_REWIND); } case NotificationConstants.SMART_FORWARD_NEXT: if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) { - return getAction(player, R.drawable.exo_notification_next, + return getAction(player, R.drawable.exo_icon_next, R.string.exo_controls_next_description, ACTION_PLAY_NEXT); } else { - return getAction(player, R.drawable.exo_controls_fastforward, + return getAction(player, R.drawable.exo_icon_fastforward, R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD); } @@ -303,10 +303,10 @@ private NotificationCompat.Action getAction( || player.getCurrentState() == Player.STATE_PREFLIGHT || player.getCurrentState() == Player.STATE_BLOCKED || player.getCurrentState() == Player.STATE_BUFFERING) { - return getAction(player, R.drawable.exo_notification_pause, + return getAction(player, R.drawable.exo_icon_pause, R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE); } else { - return getAction(player, R.drawable.exo_notification_play, + return getAction(player, R.drawable.exo_icon_play, R.string.exo_controls_play_description, ACTION_PLAY_PAUSE); } @@ -324,10 +324,10 @@ private NotificationCompat.Action getAction( case NotificationConstants.SHUFFLE: if (player.getPlayQueue() != null && player.getPlayQueue().isShuffled()) { - return getAction(player, R.drawable.exo_controls_shuffle_on, + return getAction(player, R.drawable.exo_icon_shuffle_on, R.string.exo_controls_shuffle_on_description, ACTION_SHUFFLE); } else { - return getAction(player, R.drawable.exo_controls_shuffle_off, + return getAction(player, R.drawable.exo_icon_shuffle_off, R.string.exo_controls_shuffle_off_description, ACTION_SHUFFLE); } diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java b/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java index b9fba40c65..ea0c233108 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java @@ -21,7 +21,7 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import com.google.android.exoplayer2.PlaybackParameters; +import androidx.media3.common.PlaybackParameters; import org.schabi.newpipe.R; import org.schabi.newpipe.databinding.ActivityPlayerQueueControlBinding; @@ -553,17 +553,17 @@ private void onStateChanged(final int state) { private void onPlayModeChanged(final int repeatMode, final boolean shuffled) { switch (repeatMode) { - case com.google.android.exoplayer2.Player.REPEAT_MODE_OFF: + case androidx.media3.common.Player.REPEAT_MODE_OFF: queueControlBinding.controlRepeat - .setImageResource(R.drawable.exo_controls_repeat_off); + .setImageResource(R.drawable.exo_icon_repeat_off); break; - case com.google.android.exoplayer2.Player.REPEAT_MODE_ONE: + case androidx.media3.common.Player.REPEAT_MODE_ONE: queueControlBinding.controlRepeat - .setImageResource(R.drawable.exo_controls_repeat_one); + .setImageResource(R.drawable.exo_icon_repeat_one); break; - case com.google.android.exoplayer2.Player.REPEAT_MODE_ALL: + case androidx.media3.common.Player.REPEAT_MODE_ALL: queueControlBinding.controlRepeat - .setImageResource(R.drawable.exo_controls_repeat_all); + .setImageResource(R.drawable.exo_icon_repeat_all); break; } diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index 23e803fee1..bfd5b53b02 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -1,18 +1,18 @@ package org.schabi.newpipe.player; -import static com.google.android.exoplayer2.PlaybackException.*; -import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_AUTO_TRANSITION; -import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_INTERNAL; -import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_REMOVE; -import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK; -import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT; -import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SKIP; -import static com.google.android.exoplayer2.Player.DiscontinuityReason; -import static com.google.android.exoplayer2.Player.Listener; -import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL; -import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF; -import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE; -import static com.google.android.exoplayer2.Player.RepeatMode; +import static androidx.media3.common.PlaybackException.*; +import static androidx.media3.common.Player.DISCONTINUITY_REASON_AUTO_TRANSITION; +import static androidx.media3.common.Player.DISCONTINUITY_REASON_INTERNAL; +import static androidx.media3.common.Player.DISCONTINUITY_REASON_REMOVE; +import static androidx.media3.common.Player.DISCONTINUITY_REASON_SEEK; +import static androidx.media3.common.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT; +import static androidx.media3.common.Player.DISCONTINUITY_REASON_SKIP; +import static androidx.media3.common.Player.DiscontinuityReason; +import static androidx.media3.common.Player.Listener; +import static androidx.media3.common.Player.REPEAT_MODE_ALL; +import static androidx.media3.common.Player.REPEAT_MODE_OFF; +import static androidx.media3.common.Player.REPEAT_MODE_ONE; +import static androidx.media3.common.Player.RepeatMode; import static org.schabi.newpipe.QueueItemMenuUtil.openPopupMenu; import static org.schabi.newpipe.extractor.ServiceList.YouTube; import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; @@ -78,22 +78,23 @@ import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.RecyclerView; -import com.google.android.exoplayer2.*; -import com.google.android.exoplayer2.Player.PositionInfo; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.Tracks; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.TrackGroup; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.text.CueGroup; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.MappingTrackSelector; -import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; -import com.google.android.exoplayer2.ui.CaptionStyleCompat; -import com.google.android.exoplayer2.ui.SubtitleView; -import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; -import com.google.android.exoplayer2.util.Util; -import com.google.android.exoplayer2.video.VideoSize; +import androidx.media3.common.*; +import androidx.media3.exoplayer.*; +import androidx.media3.common.Player.PositionInfo; +import androidx.media3.common.Timeline; +import androidx.media3.common.Tracks; +import androidx.media3.exoplayer.source.MediaSource; +import androidx.media3.common.TrackGroup; +import androidx.media3.exoplayer.source.TrackGroupArray; +import androidx.media3.common.text.CueGroup; +import androidx.media3.exoplayer.trackselection.DefaultTrackSelector; +import androidx.media3.exoplayer.trackselection.MappingTrackSelector; +import androidx.media3.ui.AspectRatioFrameLayout; +import androidx.media3.ui.CaptionStyleCompat; +import androidx.media3.ui.SubtitleView; +import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter; +import androidx.media3.common.util.Util; +import androidx.media3.common.VideoSize; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.squareup.picasso.Picasso; import com.squareup.picasso.Target; @@ -130,6 +131,7 @@ import org.schabi.newpipe.player.helper.MediaSessionManager; import org.schabi.newpipe.player.helper.PlayerDataSource; import org.schabi.newpipe.player.helper.PlayerHelper; +import org.schabi.newpipe.player.datasource.SabrSessionStore; import org.schabi.newpipe.player.listeners.view.PlaybackSpeedClickListener; import org.schabi.newpipe.player.listeners.view.QualityClickListener; import org.schabi.newpipe.player.mediaitem.MediaItemTag; @@ -794,7 +796,7 @@ public void handleIntent(@NonNull final Intent intent) { // Player can have state = IDLE when playback is stopped or failed // and we should retry in this case if (simpleExoPlayer.getPlaybackState() - == com.google.android.exoplayer2.Player.STATE_IDLE) { + == androidx.media3.common.Player.STATE_IDLE) { simpleExoPlayer.prepare(); } if (shouldSeek()) { @@ -811,7 +813,7 @@ public void handleIntent(@NonNull final Intent intent) { // Player can have state = IDLE when playback is stopped or failed // and we should retry in this case if (simpleExoPlayer.getPlaybackState() - == com.google.android.exoplayer2.Player.STATE_IDLE) { + == androidx.media3.common.Player.STATE_IDLE) { simpleExoPlayer.prepare(); } simpleExoPlayer.setPlayWhenReady(playWhenReady); @@ -1780,6 +1782,10 @@ private void onUpdateProgress(final int currentProgress, return; } + // Feed the real play head to any live SABR session (no-op otherwise). + getCurrentStreamInfo().ifPresent(info -> + SabrSessionStore.updatePlayerTime(info.getId(), currentProgress)); + if (duration != binding.playbackSeekBar.getMax()) { setVideoDurationToControls(duration); } @@ -2265,7 +2271,7 @@ public void onPlayWhenReadyChanged(final boolean playWhenReady, final int reason + "reason = [" + reason + "]"); } final int playbackState = exoPlayerIsNull() - ? com.google.android.exoplayer2.Player.STATE_IDLE + ? androidx.media3.common.Player.STATE_IDLE : simpleExoPlayer.getPlaybackState(); updatePlaybackState(playWhenReady, playbackState); } @@ -2294,22 +2300,22 @@ private void updatePlaybackState(final boolean playWhenReady, final int playback } switch (playbackState) { - case com.google.android.exoplayer2.Player.STATE_IDLE: // 1 + case androidx.media3.common.Player.STATE_IDLE: // 1 isPrepared = false; break; - case com.google.android.exoplayer2.Player.STATE_BUFFERING: // 2 + case androidx.media3.common.Player.STATE_BUFFERING: // 2 if (isPrepared) { changeState(STATE_BUFFERING); } break; - case com.google.android.exoplayer2.Player.STATE_READY: //3 + case androidx.media3.common.Player.STATE_READY: //3 if (!isPrepared) { isPrepared = true; onPrepared(playWhenReady); } changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); break; - case com.google.android.exoplayer2.Player.STATE_ENDED: // 4 + case androidx.media3.common.Player.STATE_ENDED: // 4 changeState(STATE_COMPLETED); saveStreamProgressStateCompleted(); isPrepared = false; @@ -2826,13 +2832,13 @@ private void setRepeatModeButton(final AppCompatImageButton imageButton, @RepeatMode final int repeatMode) { switch (repeatMode) { case REPEAT_MODE_OFF: - imageButton.setImageResource(R.drawable.exo_controls_repeat_off); + imageButton.setImageResource(R.drawable.exo_icon_repeat_off); break; case REPEAT_MODE_ONE: - imageButton.setImageResource(R.drawable.exo_controls_repeat_one); + imageButton.setImageResource(R.drawable.exo_icon_repeat_one); break; case REPEAT_MODE_ALL: - imageButton.setImageResource(R.drawable.exo_controls_repeat_all); + imageButton.setImageResource(R.drawable.exo_icon_repeat_all); break; } } @@ -2920,13 +2926,13 @@ public void onScreenRotationButtonClicked() { * This is done because not all source resolution errors are {@link PlaybackException}, which * are also captured by {@link ExoPlayer} and stops the playback.
* - * @param player The {@link com.google.android.exoplayer2.Player} whose state changed. - * @param events The {@link com.google.android.exoplayer2.Player.Events} that has triggered + * @param player The {@link androidx.media3.common.Player} whose state changed. + * @param events The {@link androidx.media3.common.Player.Events} that has triggered * the player state changes. **/ @Override - public void onEvents(@NonNull final com.google.android.exoplayer2.Player player, - @NonNull final com.google.android.exoplayer2.Player.Events events) { + public void onEvents(@NonNull final androidx.media3.common.Player player, + @NonNull final androidx.media3.common.Player.Events events) { Listener.super.onEvents(player, events); MediaItemTag.from(player.getCurrentMediaItem()).ifPresent(tag -> { if (tag == currentMetadata) { @@ -3053,7 +3059,7 @@ public void onPrepare() { //region Errors /** - * Process exceptions produced by {@link com.google.android.exoplayer2.ExoPlayer ExoPlayer}. + * Process exceptions produced by {@link androidx.media3.exoplayer.ExoPlayer ExoPlayer}. *There are multiple types of errors:
*The segment is identified by the {@link DataSpec} uri: {@code sabrseg://
v1: uses the best audio/video formats from the player response and a fixed en/US locale.
+ */ +public final class SabrSessionStore { + + private static final MapValidated end to end (emulator + Pixel 8 / GrapheneOS Vanadium): the WebView produces a token + * GenerateIT accepts and that flips SABR protection status 2 -> 1.
+ * + *The provider blocks the calling (loading) thread on a latch while the WebView, driven on the + * main thread, runs the pipeline. Tokens are cached per videoId (~6h, well under the measured ~7-8h + * lifetime).
+ */ +public final class WebViewPoTokenProvider implements SabrPoTokenProvider { + + private static final String TAG = "WebViewPoToken"; + private static final String ASSET = "sabr_potoken_poc.js"; + private static final String DESKTOP_UA = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " + + "(KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36"; + private static final long TOKEN_TTL_MS = 6L * 60L * 60L * 1000L; // 6 hours + // The WebView BotGuard mint can occasionally run long; a single 45s shot timing out returned a + // null token -> token-less SABR -> cold-start failure. 60s + one retry (in getPoToken) is robust. + private static final long PIPELINE_TIMEOUT_MS = 60_000L; + // Persist minted tokens across process restarts so an app cold-start doesn't pay the ~45s mint + // again while the videoId-bound token is still valid (<6h). + private static final String PREFS = "sabr_potoken_cache"; + private static final int READY_RETRIES = 20; + private static final long READY_POLL_MS = 250L; + + private static final class CachedToken { + private final byte[] token; + private final long mintedAtMs; + + CachedToken(final byte[] token, final long mintedAtMs) { + this.token = token; + this.mintedAtMs = mintedAtMs; + } + } + + private final Context appContext; + private final Handler mainHandler; + private final android.content.SharedPreferences prefs; + private final Map* It adds more headers to {@code videoplayback} URLs, such as {@code Origin}, {@code Referer} @@ -182,7 +182,7 @@ public Factory setAllowCrossProtocolRedirects( * *
* Note that it must be not enabled on streams which are using a {@link - * com.google.android.exoplayer2.source.ProgressiveMediaSource}, as it will break playback + * androidx.media3.exoplayer.source.ProgressiveMediaSource}, as it will break playback * for them (some exceptions may be thrown). *
* diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerEventListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerEventListener.java index b5520e8bee..553b12c81f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/event/PlayerEventListener.java +++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerEventListener.java @@ -1,7 +1,7 @@ package org.schabi.newpipe.player.event; -import com.google.android.exoplayer2.PlaybackParameters; +import androidx.media3.common.PlaybackParameters; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.player.playqueue.PlayQueue; diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceEventListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceEventListener.java index 359eab8b28..5b52455bf5 100644 --- a/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceEventListener.java +++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceEventListener.java @@ -1,6 +1,6 @@ package org.schabi.newpipe.player.event; -import com.google.android.exoplayer2.PlaybackException; +import androidx.media3.common.PlaybackException; public interface PlayerServiceEventListener extends PlayerEventListener { void onFullscreenStateChanged(boolean fullscreen); diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java index 8613ef57bc..d880e6aed3 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java @@ -15,8 +15,8 @@ import androidx.media.AudioManagerCompat; import androidx.preference.PreferenceManager; -import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.analytics.AnalyticsListener; +import androidx.media3.exoplayer.ExoPlayer; +import androidx.media3.exoplayer.analytics.AnalyticsListener; import org.schabi.newpipe.R; public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, AnalyticsListener { diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java b/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java index 202ff85412..dc5e520978 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java @@ -6,16 +6,16 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.google.android.exoplayer2.database.StandaloneDatabaseProvider; -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DefaultDataSource; -import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; -import com.google.android.exoplayer2.upstream.FileDataSource; -import com.google.android.exoplayer2.upstream.TransferListener; -import com.google.android.exoplayer2.upstream.cache.CacheDataSink; -import com.google.android.exoplayer2.upstream.cache.CacheDataSource; -import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor; -import com.google.android.exoplayer2.upstream.cache.SimpleCache; +import androidx.media3.database.StandaloneDatabaseProvider; +import androidx.media3.datasource.DataSource; +import androidx.media3.datasource.DefaultDataSource; +import androidx.media3.datasource.DefaultHttpDataSource; +import androidx.media3.datasource.FileDataSource; +import androidx.media3.datasource.TransferListener; +import androidx.media3.datasource.cache.CacheDataSink; +import androidx.media3.datasource.cache.CacheDataSource; +import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor; +import androidx.media3.datasource.cache.SimpleCache; import org.schabi.newpipe.player.datasource.YoutubeHttpDataSource; diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/CustomMediaCodecVideoRenderer.java b/app/src/main/java/org/schabi/newpipe/player/helper/CustomMediaCodecVideoRenderer.java index 66ac6d50bc..5769346cd2 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/CustomMediaCodecVideoRenderer.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/CustomMediaCodecVideoRenderer.java @@ -5,10 +5,10 @@ import androidx.annotation.Nullable; -import com.google.android.exoplayer2.mediacodec.MediaCodecAdapter; -import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; -import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; -import com.google.android.exoplayer2.video.VideoRendererEventListener; +import androidx.media3.exoplayer.mediacodec.MediaCodecAdapter; +import androidx.media3.exoplayer.mediacodec.MediaCodecSelector; +import androidx.media3.exoplayer.video.MediaCodecVideoRenderer; +import androidx.media3.exoplayer.video.VideoRendererEventListener; /** * A {@link MediaCodecVideoRenderer} which always enable the output surface workaround that diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/CustomRenderersFactory.java b/app/src/main/java/org/schabi/newpipe/player/helper/CustomRenderersFactory.java index 668b48c306..d7f4066a2c 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/CustomRenderersFactory.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/CustomRenderersFactory.java @@ -3,10 +3,10 @@ import android.content.Context; import android.os.Handler; -import com.google.android.exoplayer2.DefaultRenderersFactory; -import com.google.android.exoplayer2.Renderer; -import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; -import com.google.android.exoplayer2.video.VideoRendererEventListener; +import androidx.media3.exoplayer.DefaultRenderersFactory; +import androidx.media3.exoplayer.Renderer; +import androidx.media3.exoplayer.mediacodec.MediaCodecSelector; +import androidx.media3.exoplayer.video.VideoRendererEventListener; import java.util.ArrayList; diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java b/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java index ec0e4e4a72..fa9abca983 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java @@ -1,12 +1,42 @@ package org.schabi.newpipe.player.helper; -import com.google.android.exoplayer2.DefaultLoadControl; +import androidx.media3.common.C; +import androidx.media3.exoplayer.DefaultLoadControl; +import androidx.media3.exoplayer.upstream.DefaultAllocator; public class LoadController extends DefaultLoadControl { public static final String TAG = "LoadController"; + + // Keep the player's buffer target well BELOW the SABR pump's read-ahead cushion (50s). The player + // reads ahead to fill maxBuffer (plus up to one whole segment, ~10s for audio), and if that runs + // past what the pump has fed it blocks on a segment the pump won't fetch yet (pump paces off the + // play head): cold start deadlocked, and the longer-segment track (audio) dropped out mid-play. + // 20s target -> ~30s real read-ahead, comfortably inside the 50s cushion. Fine for DASH/HLS. + private static final int MIN_BUFFER_MS = 12_000; + private static final int MAX_BUFFER_MS = 20_000; + private static final int BUFFER_FOR_PLAYBACK_MS = 2_000; + private static final int BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 3_000; + private boolean preloadingEnabled = true; + public LoadController() { + // media3 1.10 split every buffer param into a normal + a "ForLocalPlayback" variant; we use + // the same value for both so behaviour is unchanged whether the source is local or remote. + super(new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE), + MIN_BUFFER_MS, MIN_BUFFER_MS, + MAX_BUFFER_MS, MAX_BUFFER_MS, + BUFFER_FOR_PLAYBACK_MS, BUFFER_FOR_PLAYBACK_MS, + BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS, BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS, + C.LENGTH_UNSET, // no byte cap: the SABR cache bounds memory, time bounds the player + // MUST be true: with false, media3 prioritises its (huge, ~128MB) default byte target + // and ignores maxBufferMs, reading ~50s ahead = right at the pump cushion, so it + // starved at the edge. true makes maxBufferMs (time) the real limit. + true, true, + DEFAULT_BACK_BUFFER_DURATION_MS, + DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME); + } + @Override public void onPrepared() { preloadingEnabled = true; diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java b/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java index 72c1be0362..fa231b28c3 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java @@ -2,6 +2,7 @@ import android.content.Context; import android.content.Intent; +import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; @@ -15,38 +16,54 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.media.session.MediaButtonReceiver; - -import com.google.android.exoplayer2.ForwardingPlayer; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; -import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector.PlaybackPreparer; +import androidx.media3.common.Player; +import androidx.media3.common.util.Util; import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; import org.schabi.newpipe.player.NotificationUtil; import org.schabi.newpipe.player.mediasession.MediaSessionCallback; -import org.schabi.newpipe.player.mediasession.PlayQueueNavigator; +import org.schabi.newpipe.player.mediasession.PlaybackPreparer; import org.schabi.newpipe.player.playback.PlayerMediaSession; import org.schabi.newpipe.util.StreamTypeUtil; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Optional; -import static org.schabi.newpipe.player.PlayerService.*; +import static org.schabi.newpipe.player.PlayerService.ACTION_CHANGE_PLAY_MODE; +import static org.schabi.newpipe.player.PlayerService.ACTION_CLOSE; public class MediaSessionManager { private static final String TAG = MediaSessionManager.class.getSimpleName(); public static final boolean DEBUG = MainActivity.DEBUG; + private static final int MAX_QUEUE_SIZE = 10; + + private static final long PLAYBACK_ACTIONS = PlaybackStateCompat.ACTION_SEEK_TO + | PlaybackStateCompat.ACTION_PLAY + | PlaybackStateCompat.ACTION_PAUSE + | PlaybackStateCompat.ACTION_PLAY_PAUSE + | PlaybackStateCompat.ACTION_SKIP_TO_NEXT + | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS + | PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM + | PlaybackStateCompat.ACTION_SET_REPEAT_MODE + | PlaybackStateCompat.ACTION_STOP + | PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID; + @NonNull private final MediaSessionCompat mediaSession; @NonNull - private final MediaSessionConnector sessionConnector; + private final Player exoPlayer; + @NonNull + private final MediaSessionCallback callback; + @Nullable + private final PlaybackPreparer playbackPreparer; private final boolean isExternalSession; - private int lastTitleHashCode; - private int lastArtistHashCode; - private long lastDuration; - private int lastAlbumArtHashCode; + @Nullable + private PlaybackStateCompat.CustomAction errorAction; private org.schabi.newpipe.player.Player player; @@ -68,90 +85,205 @@ public MediaSessionManager(@NonNull final Context context, @NonNull final MediaSessionCallback callback, @Nullable final MediaSessionCompat existingSession, @Nullable final PlaybackPreparer playbackPreparer) { - if (DEBUG) { - Log.d(TAG, "MediaSessionManager called"); - } - mediaSession = existingSession != null ? existingSession : new MediaSessionCompat(context, TAG); + this.exoPlayer = player; + this.callback = callback; + this.playbackPreparer = playbackPreparer; + mediaSession = existingSession != null ? existingSession + : new MediaSessionCompat(context, TAG); isExternalSession = existingSession != null; mediaSession.setActive(true); - mediaSession.setPlaybackState(new PlaybackStateCompat.Builder() - .setState(PlaybackStateCompat.STATE_NONE, -1, 1) - .setActions(PlaybackStateCompat.ACTION_SEEK_TO - | PlaybackStateCompat.ACTION_PLAY - | PlaybackStateCompat.ACTION_PAUSE // was play and pause now play/pause - | PlaybackStateCompat.ACTION_SKIP_TO_NEXT - | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS - | PlaybackStateCompat.ACTION_SET_REPEAT_MODE - | PlaybackStateCompat.ACTION_STOP - | PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID) - .build()); - - sessionConnector = new MediaSessionConnector(mediaSession); - sessionConnector.setQueueNavigator(new PlayQueueNavigator(mediaSession, callback)); - sessionConnector.setPlayer(new ForwardingPlayer(player) { - @Override - public void play() { - callback.play(); + mediaSession.setCallback(sessionCallback); + + exoPlayer.addListener(playerListener); + updatePlaybackState(); + } + + private final Player.Listener playerListener = new Player.Listener() { + @Override + public void onEvents(@NonNull final Player p, @NonNull final Player.Events events) { + if (events.containsAny( + Player.EVENT_PLAYBACK_STATE_CHANGED, + Player.EVENT_PLAY_WHEN_READY_CHANGED, + Player.EVENT_IS_PLAYING_CHANGED, + Player.EVENT_POSITION_DISCONTINUITY, + Player.EVENT_REPEAT_MODE_CHANGED, + Player.EVENT_PLAYBACK_PARAMETERS_CHANGED)) { + updatePlaybackState(); + } + if (events.containsAny( + Player.EVENT_MEDIA_ITEM_TRANSITION, + Player.EVENT_MEDIA_METADATA_CHANGED, + Player.EVENT_TIMELINE_CHANGED)) { + updateMetadata(); + publishQueue(); } + } + }; + + private final MediaSessionCompat.Callback sessionCallback = new MediaSessionCompat.Callback() { + @Override + public void onPlay() { + callback.play(); + } - @Override - public void pause() { - callback.pause(); + @Override + public void onPause() { + callback.pause(); + } + + @Override + public void onSkipToNext() { + callback.playNext(); + } + + @Override + public void onSkipToPrevious() { + callback.playPrevious(); + } + + @Override + public void onSkipToQueueItem(final long id) { + callback.playItemAtIndex((int) id); + } + + @Override + public void onSeekTo(final long pos) { + exoPlayer.seekTo(pos); + } + + @Override + public void onStop() { + callback.close(); + } + + @Override + public void onCustomAction(final String action, final Bundle extras) { + if (ACTION_CHANGE_PLAY_MODE.equals(action)) { + callback.changePlayMode(); + } else if (ACTION_CLOSE.equals(action)) { + callback.close(); } - }); - MediaSessionConnector.CustomActionProvider[] providers = new MediaSessionConnector.CustomActionProvider[2]; - providers[0] = new MediaSessionConnector.CustomActionProvider() { - @Override - public void onCustomAction(@NonNull Player player, @NonNull String action, @Nullable Bundle extras) { - if (action.equals(ACTION_CHANGE_PLAY_MODE)) { - callback.changePlayMode(); - } + } + + @Override + public void onPrepare() { + if (playbackPreparer != null) { + playbackPreparer.onPrepare(false); } + } - @Nullable - @Override - public PlaybackStateCompat.CustomAction getCustomAction(Player player) { - switch (((PlayerMediaSession)callback).mode){ - case 0: - default: - return new android.support.v4.media.session.PlaybackStateCompat.CustomAction.Builder( - ACTION_CHANGE_PLAY_MODE, "Shuffle", R.drawable.shuffle_disabled).build(); - case 1: - return new android.support.v4.media.session.PlaybackStateCompat.CustomAction.Builder( - ACTION_CHANGE_PLAY_MODE, "Repeat all", R.drawable.exo_controls_shuffle_on).build(); - case 2: - return new android.support.v4.media.session.PlaybackStateCompat.CustomAction.Builder( - ACTION_CHANGE_PLAY_MODE, "Repeat none", R.drawable.exo_controls_repeat_one).build(); - case 3: - return new android.support.v4.media.session.PlaybackStateCompat.CustomAction.Builder( - ACTION_CHANGE_PLAY_MODE, "Repeat one", R.drawable.exo_controls_repeat_all).build(); - } + @Override + public void onPlayFromMediaId(final String mediaId, final Bundle extras) { + if (playbackPreparer != null) { + playbackPreparer.onPrepareFromMediaId(mediaId, true, extras); } - }; - providers[1] = new MediaSessionConnector.CustomActionProvider() { - @Override - public void onCustomAction(@NonNull Player player, @NonNull String action, @Nullable Bundle extras) { - if (action.equals(ACTION_CLOSE)) { - callback.close(); - } + } + + @Override + public void onPrepareFromMediaId(final String mediaId, final Bundle extras) { + if (playbackPreparer != null) { + playbackPreparer.onPrepareFromMediaId(mediaId, false, extras); } + } - @Nullable - @Override - public PlaybackStateCompat.CustomAction getCustomAction(Player player) { - // Close - return new android.support.v4.media.session.PlaybackStateCompat.CustomAction.Builder( - ACTION_CLOSE, "Close", R.drawable.ic_close).build(); + @Override + public void onPlayFromSearch(final String query, final Bundle extras) { + if (playbackPreparer != null) { + playbackPreparer.onPrepareFromSearch(query, true, extras); } - }; - sessionConnector.setCustomActionProviders(providers); - sessionConnector.setMetadataDeduplicationEnabled(true); - sessionConnector.setMediaMetadataProvider(exoPlayer -> buildMediaMetadata()); - - // Set PlaybackPreparer if provided (for Android Auto support) - if (playbackPreparer != null) { - sessionConnector.setPlaybackPreparer(playbackPreparer); } + + @Override + public void onPlayFromUri(final Uri uri, final Bundle extras) { + if (playbackPreparer != null) { + playbackPreparer.onPrepareFromUri(uri, true, extras); + } + } + }; + + private void updatePlaybackState() { + final PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder() + .setActions(PLAYBACK_ACTIONS + | (playbackPreparer != null + ? playbackPreparer.getSupportedPrepareActions() : 0)) + .setState(toPlaybackStateCompat(), exoPlayer.getCurrentPosition(), + exoPlayer.getPlaybackParameters().speed); + builder.addCustomAction(changePlayModeAction()); + builder.addCustomAction(new PlaybackStateCompat.CustomAction.Builder( + ACTION_CLOSE, "Close", R.drawable.ic_close).build()); + if (errorAction != null) { + builder.addCustomAction(errorAction); + } + mediaSession.setPlaybackState(builder.build()); + } + + @PlaybackStateCompat.State + private int toPlaybackStateCompat() { + switch (exoPlayer.getPlaybackState()) { + case Player.STATE_BUFFERING: + return PlaybackStateCompat.STATE_BUFFERING; + case Player.STATE_READY: + return exoPlayer.getPlayWhenReady() + ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED; + case Player.STATE_ENDED: + return PlaybackStateCompat.STATE_STOPPED; + case Player.STATE_IDLE: + default: + return PlaybackStateCompat.STATE_NONE; + } + } + + @NonNull + private PlaybackStateCompat.CustomAction changePlayModeAction() { + final int mode = callback instanceof PlayerMediaSession + ? ((PlayerMediaSession) callback).mode : 0; + switch (mode) { + case 1: + return new PlaybackStateCompat.CustomAction.Builder(ACTION_CHANGE_PLAY_MODE, + "Repeat all", R.drawable.exo_icon_shuffle_on).build(); + case 2: + return new PlaybackStateCompat.CustomAction.Builder(ACTION_CHANGE_PLAY_MODE, + "Repeat none", R.drawable.exo_icon_repeat_one).build(); + case 3: + return new PlaybackStateCompat.CustomAction.Builder(ACTION_CHANGE_PLAY_MODE, + "Repeat one", R.drawable.exo_icon_repeat_all).build(); + case 0: + default: + return new PlaybackStateCompat.CustomAction.Builder(ACTION_CHANGE_PLAY_MODE, + "Shuffle", R.drawable.shuffle_disabled).build(); + } + } + + private void publishQueue() { + final int windowCount = callback.getQueueSize(); + if (windowCount == 0) { + mediaSession.setQueue(Collections.emptyList()); + return; + } + final int currentWindowIndex = callback.getCurrentPlayingIndex(); + final int queueSize = Math.min(MAX_QUEUE_SIZE, windowCount); + final int startIndex = Util.constrainValue(currentWindowIndex - ((queueSize - 1) / 2), 0, + windowCount - queueSize); + + final ListThe default is {@code null}, which causes the default user agent of the underlying - * platform to be used. - * - * @param userAgent The user agent that will be used, or {@code null} to use the default user - * agent of the underlying platform. - * @return This factory. - */ - public NiconicoLiveHttpDataSource.Factory setUserAgent(@Nullable String userAgent) { - this.userAgent = userAgent; + public Factory setUserAgent(@Nullable final String userAgent) { + inner.setUserAgent(userAgent); return this; } - /** - * Sets the connect timeout, in milliseconds. - * - *
The default is {@link PurifiedHttpDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS}. - * - * @param connectTimeoutMs The connect timeout, in milliseconds, that will be used. - * @return This factory. - */ - public NiconicoLiveHttpDataSource.Factory setConnectTimeoutMs(int connectTimeoutMs) { - this.connectTimeoutMs = connectTimeoutMs; + public Factory setConnectTimeoutMs(final int connectTimeoutMs) { + inner.setConnectTimeoutMs(connectTimeoutMs); return this; } - /** - * Sets the read timeout, in milliseconds. - * - *
The default is {@link PurifiedHttpDataSource#DEFAULT_READ_TIMEOUT_MILLIS}. - * - * @param readTimeoutMs The connect timeout, in milliseconds, that will be used. - * @return This factory. - */ - public NiconicoLiveHttpDataSource.Factory setReadTimeoutMs(int readTimeoutMs) { - this.readTimeoutMs = readTimeoutMs; + public Factory setReadTimeoutMs(final int readTimeoutMs) { + inner.setReadTimeoutMs(readTimeoutMs); return this; } - /** - * Sets whether to allow cross protocol redirects. - * - *
The default is {@code false}. - * - * @param allowCrossProtocolRedirects Whether to allow cross protocol redirects. - * @return This factory. - */ - public NiconicoLiveHttpDataSource.Factory setAllowCrossProtocolRedirects(boolean allowCrossProtocolRedirects) { - this.allowCrossProtocolRedirects = allowCrossProtocolRedirects; + public Factory setAllowCrossProtocolRedirects(final boolean allowCrossProtocolRedirects) { + inner.setAllowCrossProtocolRedirects(allowCrossProtocolRedirects); return this; } - /** - * Sets a content type {@link Predicate}. If a content type is rejected by the predicate then a - * {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link - * PurifiedHttpDataSource#open(DataSpec)}. - * - *
The default is {@code null}.
- *
- * @param contentTypePredicate The content type {@link Predicate}, or {@code null} to clear a
- * predicate that was previously set.
- * @return This factory.
- */
- public NiconicoLiveHttpDataSource.Factory setContentTypePredicate(@Nullable Predicate The default is {@code null}.
- *
- * See {@link DataSource#addTransferListener(TransferListener)}.
- *
- * @param transferListener The listener that will be used.
- * @return This factory.
- */
- public NiconicoLiveHttpDataSource.Factory setTransferListener(@Nullable TransferListener transferListener) {
- this.transferListener = transferListener;
+ public Factory setTransferListener(@Nullable final TransferListener transferListener) {
+ inner.setTransferListener(transferListener);
return this;
}
- /**
- * Sets whether we should keep the POST method and body when we have HTTP 302 redirects for a
- * POST request.
- */
- public NiconicoLiveHttpDataSource.Factory setKeepPostFor302Redirects(boolean keepPostFor302Redirects) {
- this.keepPostFor302Redirects = keepPostFor302Redirects;
+ public Factory setKeepPostFor302Redirects(final boolean keepPostFor302Redirects) {
+ inner.setKeepPostFor302Redirects(keepPostFor302Redirects);
return this;
}
@Override
public NiconicoLiveHttpDataSource createDataSource() {
- NiconicoLiveHttpDataSource dataSource =
- new NiconicoLiveHttpDataSource(
- userAgent,
- connectTimeoutMs,
- readTimeoutMs,
- allowCrossProtocolRedirects,
- defaultRequestProperties,
- contentTypePredicate,
- keepPostFor302Redirects,
- url);
- if (transferListener != null) {
- dataSource.addTransferListener(transferListener);
- }
- return dataSource;
+ return new NiconicoLiveHttpDataSource(inner.createDataSource(), url);
}
}
- NiconicoLiveHttpDataSource(@Nullable String userAgent, int connectTimeoutMillis, int readTimeoutMillis, boolean allowCrossProtocolRedirects, @Nullable RequestProperties defaultRequestProperties
- , @Nullable Predicate The default is {@code null}, which causes the default user agent of the underlying
- * platform to be used.
- *
- * @param userAgent The user agent that will be used, or {@code null} to use the default user
- * agent of the underlying platform.
- * @return This factory.
- */
- public PurifiedHttpDataSource.Factory setUserAgent(@Nullable String userAgent) {
- this.userAgent = userAgent;
+ public Factory setUserAgent(@Nullable final String userAgent) {
+ inner.setUserAgent(userAgent);
return this;
}
- /**
- * Sets the connect timeout, in milliseconds.
- *
- * The default is {@link PurifiedHttpDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS}.
- *
- * @param connectTimeoutMs The connect timeout, in milliseconds, that will be used.
- * @return This factory.
- */
- public PurifiedHttpDataSource.Factory setConnectTimeoutMs(int connectTimeoutMs) {
- this.connectTimeoutMs = connectTimeoutMs;
+ public Factory setConnectTimeoutMs(final int connectTimeoutMs) {
+ inner.setConnectTimeoutMs(connectTimeoutMs);
return this;
}
- /**
- * Sets the read timeout, in milliseconds.
- *
- * The default is {@link PurifiedHttpDataSource#DEFAULT_READ_TIMEOUT_MILLIS}.
- *
- * @param readTimeoutMs The connect timeout, in milliseconds, that will be used.
- * @return This factory.
- */
- public PurifiedHttpDataSource.Factory setReadTimeoutMs(int readTimeoutMs) {
- this.readTimeoutMs = readTimeoutMs;
+ public Factory setReadTimeoutMs(final int readTimeoutMs) {
+ inner.setReadTimeoutMs(readTimeoutMs);
return this;
}
- /**
- * Sets whether to allow cross protocol redirects.
- *
- * The default is {@code false}.
- *
- * @param allowCrossProtocolRedirects Whether to allow cross protocol redirects.
- * @return This factory.
- */
- public PurifiedHttpDataSource.Factory setAllowCrossProtocolRedirects(boolean allowCrossProtocolRedirects) {
- this.allowCrossProtocolRedirects = allowCrossProtocolRedirects;
+ public Factory setAllowCrossProtocolRedirects(final boolean allowCrossProtocolRedirects) {
+ inner.setAllowCrossProtocolRedirects(allowCrossProtocolRedirects);
return this;
}
- /**
- * Sets a content type {@link Predicate}. If a content type is rejected by the predicate then a
- * {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link
- * PurifiedHttpDataSource#open(DataSpec)}.
- *
- * The default is {@code null}.
- *
- * @param contentTypePredicate The content type {@link Predicate}, or {@code null} to clear a
- * predicate that was previously set.
- * @return This factory.
- */
- public PurifiedHttpDataSource.Factory setContentTypePredicate(@Nullable Predicate The default is {@code null}.
- *
- * See {@link DataSource#addTransferListener(TransferListener)}.
- *
- * @param transferListener The listener that will be used.
- * @return This factory.
- */
- public PurifiedHttpDataSource.Factory setTransferListener(@Nullable TransferListener transferListener) {
- this.transferListener = transferListener;
+ public Factory setTransferListener(@Nullable final TransferListener transferListener) {
+ inner.setTransferListener(transferListener);
return this;
}
- /**
- * Sets whether we should keep the POST method and body when we have HTTP 302 redirects for a
- * POST request.
- */
- public PurifiedHttpDataSource.Factory setKeepPostFor302Redirects(boolean keepPostFor302Redirects) {
- this.keepPostFor302Redirects = keepPostFor302Redirects;
+ public Factory setKeepPostFor302Redirects(final boolean keepPostFor302Redirects) {
+ inner.setKeepPostFor302Redirects(keepPostFor302Redirects);
return this;
}
@Override
public PurifiedHttpDataSource createDataSource() {
- PurifiedHttpDataSource dataSource =
- new PurifiedHttpDataSource(
- userAgent,
- connectTimeoutMs,
- readTimeoutMs,
- allowCrossProtocolRedirects,
- defaultRequestProperties,
- contentTypePredicate,
- keepPostFor302Redirects);
- if (transferListener != null) {
- dataSource.addTransferListener(transferListener);
- }
- return dataSource;
+ return new PurifiedHttpDataSource(inner.createDataSource());
}
}
- PurifiedHttpDataSource(
- @Nullable String userAgent,
- int connectTimeoutMillis,
- int readTimeoutMillis,
- boolean allowCrossProtocolRedirects,
- @Nullable RequestProperties defaultRequestProperties,
- @Nullable Predicate
diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java b/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java
index 7376070015..e482646026 100644
--- a/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java
+++ b/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java
@@ -3,7 +3,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import com.google.android.exoplayer2.source.MediaSource;
+import androidx.media3.exoplayer.source.MediaSource;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/PlayerMediaSession.java b/app/src/main/java/org/schabi/newpipe/player/playback/PlayerMediaSession.java
index cd0f377155..265c50a1c7 100644
--- a/app/src/main/java/org/schabi/newpipe/player/playback/PlayerMediaSession.java
+++ b/app/src/main/java/org/schabi/newpipe/player/playback/PlayerMediaSession.java
@@ -9,15 +9,15 @@
import org.schabi.newpipe.player.mediasession.MediaSessionCallback;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
-import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
+import static androidx.media3.common.Player.REPEAT_MODE_OFF;
import static org.schabi.newpipe.player.helper.PlayerHelper.nextRepeatMode;
public class PlayerMediaSession implements MediaSessionCallback {
public final Player player;
- private final com.google.android.exoplayer2.Player exoPlayer;
+ private final androidx.media3.common.Player exoPlayer;
public int mode = 0;
- public PlayerMediaSession(final Player player, final com.google.android.exoplayer2.Player exoPlayer) {
+ public PlayerMediaSession(final Player player, final androidx.media3.common.Player exoPlayer) {
this.player = player;
this.exoPlayer = exoPlayer;
refresh();
@@ -114,10 +114,10 @@ public void changePlayMode() {
player.onShuffleClicked();
break;
case 1: // repeat_one
- player.setRepeatMode(com.google.android.exoplayer2.Player.REPEAT_MODE_ONE);
+ player.setRepeatMode(androidx.media3.common.Player.REPEAT_MODE_ONE);
break;
case 2: // repeat_all
- player.setRepeatMode(com.google.android.exoplayer2.Player.REPEAT_MODE_ALL);
+ player.setRepeatMode(androidx.media3.common.Player.REPEAT_MODE_ALL);
break;
case 3: // repeat_none
default:
@@ -132,9 +132,9 @@ public void close(){
public void refresh(){
if (exoPlayer.getShuffleModeEnabled()) {
this.mode = 1;
- } else if (exoPlayer.getRepeatMode() == com.google.android.exoplayer2.Player.REPEAT_MODE_ONE) {
+ } else if (exoPlayer.getRepeatMode() == androidx.media3.common.Player.REPEAT_MODE_ONE) {
this.mode = 2;
- } else if (exoPlayer.getRepeatMode() == com.google.android.exoplayer2.Player.REPEAT_MODE_ALL) {
+ } else if (exoPlayer.getRepeatMode() == androidx.media3.common.Player.REPEAT_MODE_ALL) {
this.mode = 3;
} else {
this.mode = 0;
diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/SurfaceHolderCallback.java b/app/src/main/java/org/schabi/newpipe/player/playback/SurfaceHolderCallback.java
index da6cb36d4f..b02a9240d3 100644
--- a/app/src/main/java/org/schabi/newpipe/player/playback/SurfaceHolderCallback.java
+++ b/app/src/main/java/org/schabi/newpipe/player/playback/SurfaceHolderCallback.java
@@ -3,8 +3,8 @@
import android.content.Context;
import android.view.SurfaceHolder;
-import com.google.android.exoplayer2.Player;
-import com.google.android.exoplayer2.video.PlaceholderSurface;
+import androidx.media3.common.Player;
+import androidx.media3.exoplayer.video.PlaceholderSurface;
/**
* Prevent error message: 'Unrecoverable player error occurred'
diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java
index 7e3f590078..d44b64b556 100644
--- a/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java
+++ b/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java
@@ -9,7 +9,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import com.google.android.exoplayer2.source.MediaSource;
+import androidx.media3.exoplayer.source.MediaSource;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/CustomDataSourceFactory.java b/app/src/main/java/org/schabi/newpipe/player/resolver/CustomDataSourceFactory.java
index 82dff6ec9b..06fe0b3656 100644
--- a/app/src/main/java/org/schabi/newpipe/player/resolver/CustomDataSourceFactory.java
+++ b/app/src/main/java/org/schabi/newpipe/player/resolver/CustomDataSourceFactory.java
@@ -4,9 +4,9 @@
import androidx.annotation.Nullable;
-import com.google.android.exoplayer2.upstream.ByteArrayDataSource;
-import com.google.android.exoplayer2.upstream.DataSource;
-import com.google.android.exoplayer2.upstream.TransferListener;
+import androidx.media3.datasource.ByteArrayDataSource;
+import androidx.media3.datasource.DataSource;
+import androidx.media3.datasource.TransferListener;
public class CustomDataSourceFactory implements DataSource.Factory {
private final Context context;
diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java
index d8d9ecb0d6..fbd6334f01 100644
--- a/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java
+++ b/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java
@@ -6,19 +6,19 @@
import android.net.Uri;
import android.util.Log;
-import com.google.android.exoplayer2.C;
-import com.google.android.exoplayer2.MediaItem;
-import com.google.android.exoplayer2.source.MediaSource;
-import com.google.android.exoplayer2.source.ProgressiveMediaSource;
-import com.google.android.exoplayer2.source.dash.DashMediaSource;
-import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
-import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;
-import com.google.android.exoplayer2.source.hls.HlsMediaSource;
-import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist;
-import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser;
-import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
-import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest;
-import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser;
+import androidx.media3.common.C;
+import androidx.media3.common.MediaItem;
+import androidx.media3.exoplayer.source.MediaSource;
+import androidx.media3.exoplayer.source.ProgressiveMediaSource;
+import androidx.media3.exoplayer.dash.DashMediaSource;
+import androidx.media3.exoplayer.dash.manifest.DashManifest;
+import androidx.media3.exoplayer.dash.manifest.DashManifestParser;
+import androidx.media3.exoplayer.hls.HlsMediaSource;
+import androidx.media3.exoplayer.hls.playlist.HlsPlaylist;
+import androidx.media3.exoplayer.hls.playlist.HlsPlaylistParser;
+import androidx.media3.exoplayer.smoothstreaming.SsMediaSource;
+import androidx.media3.exoplayer.smoothstreaming.manifest.SsManifest;
+import androidx.media3.exoplayer.smoothstreaming.manifest.SsManifestParser;
import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonParser;
import com.grack.nanojson.JsonParserException;
@@ -47,6 +47,12 @@
import org.schabi.newpipe.player.mediaitem.MediaItemTag;
import org.schabi.newpipe.player.mediaitem.StreamInfoTag;
import org.schabi.newpipe.util.StreamTypeUtil;
+import org.schabi.newpipe.App;
+import org.schabi.newpipe.extractor.exceptions.ExtractionException;
+import org.schabi.newpipe.extractor.localization.Localization;
+import org.schabi.newpipe.extractor.services.youtube.sabr.YoutubeSabrFormat;
+import org.schabi.newpipe.player.datasource.SabrMediaSource;
+import org.schabi.newpipe.player.datasource.SabrSessionStore;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -437,12 +443,42 @@ private static