1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2026-02-10 20:20:16 +00:00

Compare commits

..

15 Commits
dev ... r8fixes

Author SHA1 Message Date
Aayush Gupta
d42058d99e FocusOverlayView: Avoid accessing restricted API
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-10 15:17:02 +08:00
Aayush Gupta
a06034f189 ic_smart_display: Fix invalid vector path on older devices
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-10 15:17:02 +08:00
Aayush Gupta
3635b40daa lint: Supress more translation related errors
They should be fixed by translators or weblate in general.

 ../../src/main/res/values-lt/strings.xml:87: For locale "lt" (Lithuanian) the following quantity should also be defined: many (e.g. "1.1 obuolio")
 ../../src/main/res/values-is/strings.xml:318: The quantity 'one' matches more than one specific number in this locale (1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …), but the message did not include a formatting argument (such as %d). This is usually an internationalization error. See full issue explanation for more.

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-10 15:17:02 +08:00
Aayush Gupta
81ee67b03b DownloadDialog: Avoid using restricted API for menuitem
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-10 15:17:02 +08:00
Aayush Gupta
b2d4fdb0fb activity_player_queue_control: Switch to app:tint instead of android:tint
../../src/main/res/layout/activity_player_queue_control.xml:208: Must use app:tint instead of android:tint

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-10 15:17:02 +08:00
Aayush Gupta
e7b5231708 DownloadRunnableFallback: Fix error with log tag being too long
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-10 15:17:02 +08:00
Aayush Gupta
d0edb9482d SearchFragment: Fix hint translation
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-10 15:17:02 +08:00
Aayush Gupta
4282f78dd4 Use correct constant for hiding keyboard
../../src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt:509: Must be one or more of: InputMethodManager.HIDE_IMPLICIT_ONLY, InputMethodManager.HIDE_NOT_ALWAYS

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-10 15:17:02 +08:00
Aayush Gupta
3150cae5f6 Use requireContext() instead of asserting non-null context
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-10 15:17:02 +08:00
Aayush Gupta
2c3da68329 FinishedMIssionStore: Throw exception if column is missing
../../src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java:105: Value must be ≥ 0 but getColumnIndex can be -1

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-10 15:17:02 +08:00
Aayush Gupta
9830c55563 ReCaptchaActivity: Supress lint error for missing super call
saveCookiesAndFinish method handles back navigation

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-10 15:17:02 +08:00
Aayush Gupta
ead12cec74 Add missing permission checks for notifications
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-10 15:17:02 +08:00
Aayush Gupta
ae7078e5b6 Introduce lint configuration and enable checks
Supress missing translation errors as they are done by volunteers

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-10 15:17:02 +08:00
Aayush Gupta
93d3909e19 Enable resources shrinking
AGP 9.0.0 has introduced additional resource shrinking tasks. Its better
to enable and fix this.

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-10 15:17:02 +08:00
Aayush Gupta
7ae7cb3b7e Address non-final resource IDs warnings
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-10 15:17:02 +08:00
49 changed files with 1154 additions and 1032 deletions

View File

@@ -79,7 +79,7 @@ configure<ApplicationExtension> {
resValue("string", "app_name", "NewPipe $suffix") resValue("string", "app_name", "NewPipe $suffix")
} }
isMinifyEnabled = true isMinifyEnabled = true
isShrinkResources = false // disabled to fix F-Droid"s reproducible build isShrinkResources = true
proguardFiles( proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro" "proguard-rules.pro"
@@ -88,13 +88,7 @@ configure<ApplicationExtension> {
} }
lint { lint {
checkReleaseBuilds = false lintConfig = file("lint.xml")
// Or, if you prefer, you can continue to check for errors in release builds,
// but continue the build even when errors are found:
abortOnError = false
// suppress false warning ("Resource IDs will be non-final in Android Gradle Plugin version
// 5.0, avoid using them in switch case statements"), which affects only library projects
disable += "NonConstantResourceId"
} }
compileOptions { compileOptions {

10
app/lint.xml Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
~ SPDX-License-Identifier: GPL-3.0-or-later
-->
<lint>
<issue id="MissingTranslation" severity="ignore" />
<issue id="MissingQuantity" severity="ignore" />
<issue id="ImpliedQuantity" severity="ignore" />
</lint>

View File

@@ -0,0 +1,50 @@
package org.schabi.newpipe;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import org.schabi.newpipe.util.NavigationHelper;
/*
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
* ExitActivity.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class ExitActivity extends Activity {
public static void exitAndRemoveFromRecentApps(final Activity activity) {
final Intent intent = new Intent(activity, ExitActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
| Intent.FLAG_ACTIVITY_CLEAR_TASK
| Intent.FLAG_ACTIVITY_NO_ANIMATION);
activity.startActivity(intent);
}
@SuppressLint("NewApi")
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
finishAndRemoveTask();
NavigationHelper.restartApp(this);
}
}

View File

@@ -1,36 +0,0 @@
/*
* SPDX-FileCopyrightText: 2016-2026 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import org.schabi.newpipe.util.NavigationHelper
class ExitActivity : Activity() {
@SuppressLint("NewApi")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
finishAndRemoveTask()
NavigationHelper.restartApp(this)
}
companion object {
@JvmStatic
fun exitAndRemoveFromRecentApps(activity: Activity) {
val intent = Intent(activity, ExitActivity::class.java)
intent.addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK
or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
or Intent.FLAG_ACTIVITY_CLEAR_TASK
or Intent.FLAG_ACTIVITY_NO_ANIMATION
)
activity.startActivity(intent)
}
}
}

View File

@@ -309,25 +309,21 @@ public class MainActivity extends AppCompatActivity {
} }
private boolean drawerItemSelected(final MenuItem item) { private boolean drawerItemSelected(final MenuItem item) {
switch (item.getGroupId()) { final int groupId = item.getGroupId();
case R.id.menu_services_group: if (groupId == R.id.menu_services_group) {
changeService(item); changeService(item);
break; } else if (groupId == R.id.menu_tabs_group) {
case R.id.menu_tabs_group: tabSelected(item);
tabSelected(item); } else if (groupId == R.id.menu_kiosks_group) {
break; try {
case R.id.menu_kiosks_group: kioskSelected(item);
try { } catch (final Exception e) {
kioskSelected(item); ErrorUtil.showUiErrorSnackbar(this, "Selecting drawer kiosk", e);
} catch (final Exception e) { }
ErrorUtil.showUiErrorSnackbar(this, "Selecting drawer kiosk", e); } else if (groupId == R.id.menu_options_about_group) {
} optionsAboutSelected(item);
break; } else {
case R.id.menu_options_about_group: return false;
optionsAboutSelected(item);
break;
default:
return false;
} }
mainBinding.getRoot().closeDrawers(); mainBinding.getRoot().closeDrawers();

View File

@@ -82,7 +82,9 @@ class NewVersionWorker(
) )
val notificationManager = NotificationManagerCompat.from(applicationContext) val notificationManager = NotificationManagerCompat.from(applicationContext)
notificationManager.notify(2000, notificationBuilder.build()) if (notificationManager.areNotificationsEnabled()) {
notificationManager.notify(2000, notificationBuilder.build())
}
} }
@Throws(IOException::class, ReCaptchaException::class) @Throws(IOException::class, ReCaptchaException::class)

View File

@@ -41,50 +41,50 @@ public final class QueueItemMenuUtil {
} }
popupMenu.setOnMenuItemClickListener(menuItem -> { popupMenu.setOnMenuItemClickListener(menuItem -> {
switch (menuItem.getItemId()) { final int itemId = menuItem.getItemId();
case R.id.menu_item_remove: if (itemId == R.id.menu_item_remove) {
final int index = playQueue.indexOf(item); final int index = playQueue.indexOf(item);
playQueue.remove(index); playQueue.remove(index);
return true; return true;
case R.id.menu_item_details: } else if (itemId == R.id.menu_item_details) {
// playQueue is null since we don't want any queue change // playQueue is null since we don't want any queue change
NavigationHelper.openVideoDetail(context, item.getServiceId(), NavigationHelper.openVideoDetail(context, item.getServiceId(),
item.getUrl(), item.getTitle(), null, item.getUrl(), item.getTitle(), null,
false); false);
return true; return true;
case R.id.menu_item_append_playlist: } else if (itemId == R.id.menu_item_append_playlist) {
PlaylistDialog.createCorrespondingDialog( PlaylistDialog.createCorrespondingDialog(
context, context,
List.of(new StreamEntity(item)), List.of(new StreamEntity(item)),
dialog -> dialog.show( dialog -> dialog.show(
fragmentManager, fragmentManager,
"QueueItemMenuUtil@append_playlist" "QueueItemMenuUtil@append_playlist"
) )
); );
return true; return true;
case R.id.menu_item_channel_details: } else if (itemId == R.id.menu_item_channel_details) {
SparseItemUtil.fetchUploaderUrlIfSparse(context, item.getServiceId(), SparseItemUtil.fetchUploaderUrlIfSparse(context, item.getServiceId(),
item.getUrl(), item.getUploaderUrl(), item.getUrl(), item.getUploaderUrl(),
// An intent must be used here. // An intent must be used here.
// Opening with FragmentManager transactions is not working, // Opening with FragmentManager transactions is not working,
// as PlayQueueActivity doesn't use fragments. // as PlayQueueActivity doesn't use fragments.
uploaderUrl -> NavigationHelper.openChannelFragmentUsingIntent( uploaderUrl -> NavigationHelper.openChannelFragmentUsingIntent(
context, item.getServiceId(), uploaderUrl, item.getUploader() context, item.getServiceId(), uploaderUrl, item.getUploader()
)); ));
return true; return true;
case R.id.menu_item_share: } else if (itemId == R.id.menu_item_share) {
shareText(context, item.getTitle(), item.getUrl(), shareText(context, item.getTitle(), item.getUrl(),
item.getThumbnails()); item.getThumbnails());
return true; return true;
case R.id.menu_item_download: } else if (itemId == R.id.menu_item_download) {
fetchStreamInfoAndSaveToDatabase(context, item.getServiceId(), item.getUrl(), fetchStreamInfoAndSaveToDatabase(context, item.getServiceId(), item.getUrl(),
info -> { info -> {
final DownloadDialog downloadDialog = new DownloadDialog(context, final DownloadDialog downloadDialog = new DownloadDialog(context,
info); info);
downloadDialog.show(fragmentManager, "downloadDialog"); downloadDialog.show(fragmentManager, "downloadDialog");
}); });
return true; return true;
} }
return false; return false;
}); });

View File

@@ -62,7 +62,11 @@ data class PlaylistRemoteEntity(
orderingName = playlistInfo.name, orderingName = playlistInfo.name,
url = playlistInfo.url, url = playlistInfo.url,
thumbnailUrl = ImageStrategy.imageListToDbUrl( thumbnailUrl = ImageStrategy.imageListToDbUrl(
playlistInfo.thumbnails.ifEmpty { playlistInfo.uploaderAvatars } if (playlistInfo.thumbnails.isEmpty()) {
playlistInfo.uploaderAvatars
} else {
playlistInfo.thumbnails
}
), ),
uploader = playlistInfo.uploaderName, uploader = playlistInfo.uploaderName,
streamCount = playlistInfo.streamCount streamCount = playlistInfo.streamCount

View File

@@ -16,6 +16,7 @@ import android.os.IBinder;
import android.provider.Settings; import android.provider.Settings;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.AdapterView; import android.widget.AdapterView;
@@ -31,7 +32,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.view.menu.ActionMenuItemView;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
import androidx.collection.SparseArrayCompat; import androidx.collection.SparseArrayCompat;
import androidx.documentfile.provider.DocumentFile; import androidx.documentfile.provider.DocumentFile;
@@ -113,7 +113,7 @@ public class DownloadDialog extends DialogFragment
private StoredDirectoryHelper mainStorageAudio = null; private StoredDirectoryHelper mainStorageAudio = null;
private StoredDirectoryHelper mainStorageVideo = null; private StoredDirectoryHelper mainStorageVideo = null;
private DownloadManager downloadManager = null; private DownloadManager downloadManager = null;
private ActionMenuItemView okButton = null; private MenuItem okButton = null;
private Context context = null; private Context context = null;
private boolean askForSavePath; private boolean askForSavePath;
@@ -558,17 +558,13 @@ public class DownloadDialog extends DialogFragment
} }
boolean flag = true; boolean flag = true;
switch (checkedId) { if (checkedId == R.id.audio_button) {
case R.id.audio_button: setupAudioSpinner();
setupAudioSpinner(); } else if (checkedId == R.id.video_button) {
break; setupVideoSpinner();
case R.id.video_button: } else if (checkedId == R.id.subtitle_button) {
setupVideoSpinner(); setupSubtitleSpinner();
break; flag = false;
case R.id.subtitle_button:
setupSubtitleSpinner();
flag = false;
break;
} }
dialogBinding.threads.setEnabled(flag); dialogBinding.threads.setEnabled(flag);
@@ -585,29 +581,26 @@ public class DownloadDialog extends DialogFragment
+ "position = [" + position + "], id = [" + id + "]"); + "position = [" + position + "], id = [" + id + "]");
} }
switch (parent.getId()) { final int parentId = parent.getId();
case R.id.quality_spinner: if (parentId == R.id.quality_spinner) {
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) { final int checkedRadioButtonId = dialogBinding.videoAudioGroup
case R.id.video_button: .getCheckedRadioButtonId();
selectedVideoIndex = position; if (checkedRadioButtonId == R.id.video_button) {
onVideoStreamSelected(); selectedVideoIndex = position;
break; onVideoStreamSelected();
case R.id.subtitle_button: } else if (checkedRadioButtonId == R.id.subtitle_button) {
selectedSubtitleIndex = position; selectedSubtitleIndex = position;
break; }
} onItemSelectedSetFileName();
onItemSelectedSetFileName(); } else if (parentId == R.id.audio_track_spinner) {
break; final boolean trackChanged = selectedAudioTrackIndex != position;
case R.id.audio_track_spinner: selectedAudioTrackIndex = position;
final boolean trackChanged = selectedAudioTrackIndex != position; if (trackChanged) {
selectedAudioTrackIndex = position; updateSecondaryStreams();
if (trackChanged) { fetchStreamsSize();
updateSecondaryStreams(); }
fetchStreamsSize(); } else if (parentId == R.id.audio_stream_spinner) {
} selectedAudioIndex = position;
break;
case R.id.audio_stream_spinner:
selectedAudioIndex = position;
} }
} }
@@ -622,23 +615,20 @@ public class DownloadDialog extends DialogFragment
|| prevFileName.startsWith(getString(R.string.caption_file_name, fileName, ""))) { || prevFileName.startsWith(getString(R.string.caption_file_name, fileName, ""))) {
// only update the file name field if it was not edited by the user // only update the file name field if it was not edited by the user
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) { final int radioButtonId = dialogBinding.videoAudioGroup
case R.id.audio_button: .getCheckedRadioButtonId();
case R.id.video_button: if (radioButtonId == R.id.audio_button || radioButtonId == R.id.video_button) {
if (!prevFileName.equals(fileName)) { if (!prevFileName.equals(fileName)) {
// since the user might have switched between audio and video, the correct // since the user might have switched between audio and video, the correct
// text might already be in place, so avoid resetting the cursor position // text might already be in place, so avoid resetting the cursor position
dialogBinding.fileName.setText(fileName); dialogBinding.fileName.setText(fileName);
} }
break; } else if (radioButtonId == R.id.subtitle_button) {
final String setSubtitleLanguageCode = subtitleStreamsAdapter
case R.id.subtitle_button: .getItem(selectedSubtitleIndex).getLanguageTag();
final String setSubtitleLanguageCode = subtitleStreamsAdapter // this will reset the cursor position, which is bad UX, but it can't be avoided
.getItem(selectedSubtitleIndex).getLanguageTag(); dialogBinding.fileName.setText(getString(
// this will reset the cursor position, which is bad UX, but it can't be avoided R.string.caption_file_name, fileName, setSubtitleLanguageCode));
dialogBinding.fileName.setText(getString(
R.string.caption_file_name, fileName, setSubtitleLanguageCode));
break;
} }
} }
} }
@@ -770,47 +760,44 @@ public class DownloadDialog extends DialogFragment
filenameTmp = getNameEditText().concat("."); filenameTmp = getNameEditText().concat(".");
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) { final int checkedRadioButtonId = dialogBinding.videoAudioGroup.getCheckedRadioButtonId();
case R.id.audio_button: if (checkedRadioButtonId == R.id.audio_button) {
selectedMediaType = getString(R.string.last_download_type_audio_key); selectedMediaType = getString(R.string.last_download_type_audio_key);
mainStorage = mainStorageAudio; mainStorage = mainStorageAudio;
format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat(); format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
size = getWrappedAudioStreams().getSizeInBytes(selectedAudioIndex); size = getWrappedAudioStreams().getSizeInBytes(selectedAudioIndex);
if (format == MediaFormat.WEBMA_OPUS) { if (format == MediaFormat.WEBMA_OPUS) {
mimeTmp = "audio/ogg"; mimeTmp = "audio/ogg";
filenameTmp += "opus"; filenameTmp += "opus";
} else if (format != null) { } else if (format != null) {
mimeTmp = format.mimeType; mimeTmp = format.mimeType;
filenameTmp += format.getSuffix(); filenameTmp += format.getSuffix();
} }
break; } else if (checkedRadioButtonId == R.id.video_button) {
case R.id.video_button: selectedMediaType = getString(R.string.last_download_type_video_key);
selectedMediaType = getString(R.string.last_download_type_video_key); mainStorage = mainStorageVideo;
mainStorage = mainStorageVideo; format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat(); size = wrappedVideoStreams.getSizeInBytes(selectedVideoIndex);
size = wrappedVideoStreams.getSizeInBytes(selectedVideoIndex); if (format != null) {
if (format != null) { mimeTmp = format.mimeType;
mimeTmp = format.mimeType; filenameTmp += format.getSuffix();
filenameTmp += format.getSuffix(); }
} } else if (checkedRadioButtonId == R.id.subtitle_button) {
break; selectedMediaType = getString(R.string.last_download_type_subtitle_key);
case R.id.subtitle_button: mainStorage = mainStorageVideo; // subtitle & video files go together
selectedMediaType = getString(R.string.last_download_type_subtitle_key); format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
mainStorage = mainStorageVideo; // subtitle & video files go together size = wrappedSubtitleStreams.getSizeInBytes(selectedSubtitleIndex);
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat(); if (format != null) {
size = wrappedSubtitleStreams.getSizeInBytes(selectedSubtitleIndex); mimeTmp = format.mimeType;
if (format != null) { }
mimeTmp = format.mimeType;
}
if (format == MediaFormat.TTML) { if (format == MediaFormat.TTML) {
filenameTmp += MediaFormat.SRT.getSuffix(); filenameTmp += MediaFormat.SRT.getSuffix();
} else if (format != null) { } else if (format != null) {
filenameTmp += format.getSuffix(); filenameTmp += format.getSuffix();
} }
break; } else {
default: throw new RuntimeException("No stream selected");
throw new RuntimeException("No stream selected");
} }
if (!askForSavePath && (mainStorage == null if (!askForSavePath && (mainStorage == null
@@ -1057,59 +1044,56 @@ public class DownloadDialog extends DialogFragment
long nearLength = 0; long nearLength = 0;
// more download logic: select muxer, subtitle converter, etc. // more download logic: select muxer, subtitle converter, etc.
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) { final int checkedRadioButtonId = dialogBinding.videoAudioGroup.getCheckedRadioButtonId();
case R.id.audio_button: if (checkedRadioButtonId == R.id.audio_button) {
kind = 'a'; kind = 'a';
selectedStream = audioStreamsAdapter.getItem(selectedAudioIndex); selectedStream = audioStreamsAdapter.getItem(selectedAudioIndex);
if (selectedStream.getFormat() == MediaFormat.M4A) { if (selectedStream.getFormat() == MediaFormat.M4A) {
psName = Postprocessing.ALGORITHM_M4A_NO_DASH; psName = Postprocessing.ALGORITHM_M4A_NO_DASH;
} else if (selectedStream.getFormat() == MediaFormat.WEBMA_OPUS) { } else if (selectedStream.getFormat() == MediaFormat.WEBMA_OPUS) {
psName = Postprocessing.ALGORITHM_OGG_FROM_WEBM_DEMUXER; psName = Postprocessing.ALGORITHM_OGG_FROM_WEBM_DEMUXER;
}
} else if (checkedRadioButtonId == R.id.video_button) {
kind = 'v';
selectedStream = videoStreamsAdapter.getItem(selectedVideoIndex);
final SecondaryStreamHelper<AudioStream> secondary = videoStreamsAdapter
.getAllSecondary()
.get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream));
if (secondary != null) {
secondaryStream = secondary.getStream();
if (selectedStream.getFormat() == MediaFormat.MPEG_4) {
psName = Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER;
} else {
psName = Postprocessing.ALGORITHM_WEBM_MUXER;
} }
break;
case R.id.video_button:
kind = 'v';
selectedStream = videoStreamsAdapter.getItem(selectedVideoIndex);
final SecondaryStreamHelper<AudioStream> secondary = videoStreamsAdapter final long videoSize = wrappedVideoStreams.getSizeInBytes(
.getAllSecondary() (VideoStream) selectedStream);
.get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream));
if (secondary != null) { // set nearLength, only, if both sizes are fetched or known. This probably
secondaryStream = secondary.getStream(); // does not work on slow networks but is later updated in the downloader
if (secondary.getSizeInBytes() > 0 && videoSize > 0) {
if (selectedStream.getFormat() == MediaFormat.MPEG_4) { nearLength = secondary.getSizeInBytes() + videoSize;
psName = Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER;
} else {
psName = Postprocessing.ALGORITHM_WEBM_MUXER;
}
final long videoSize = wrappedVideoStreams.getSizeInBytes(
(VideoStream) selectedStream);
// set nearLength, only, if both sizes are fetched or known. This probably
// does not work on slow networks but is later updated in the downloader
if (secondary.getSizeInBytes() > 0 && videoSize > 0) {
nearLength = secondary.getSizeInBytes() + videoSize;
}
} }
break; }
case R.id.subtitle_button: } else if (checkedRadioButtonId == R.id.subtitle_button) {
threads = 1; // use unique thread for subtitles due small file size threads = 1; // use unique thread for subtitles due small file size
kind = 's'; kind = 's';
selectedStream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex); selectedStream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex);
if (selectedStream.getFormat() == MediaFormat.TTML) { if (selectedStream.getFormat() == MediaFormat.TTML) {
psName = Postprocessing.ALGORITHM_TTML_CONVERTER; psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
psArgs = new String[] { psArgs = new String[]{
selectedStream.getFormat().getSuffix(), selectedStream.getFormat().getSuffix(),
"false" // ignore empty frames "false" // ignore empty frames
}; };
} }
break; } else {
default: return;
return;
} }
if (secondaryStream == null) { if (secondaryStream == null) {

View File

@@ -133,17 +133,16 @@ public class ErrorActivity extends AppCompatActivity {
@Override @Override
public boolean onOptionsItemSelected(final MenuItem item) { public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) { final int itemId = item.getItemId();
case android.R.id.home: if (itemId == android.R.id.home) {
onBackPressed(); onBackPressed();
return true; return true;
case R.id.menu_item_share_error: } else if (itemId == R.id.menu_item_share_error) {
ShareUtils.shareText(getApplicationContext(), ShareUtils.shareText(getApplicationContext(),
getString(R.string.error_report_title), buildJson()); getString(R.string.error_report_title), buildJson());
return true; return true;
default:
return false;
} }
return false;
} }
private void openPrivacyPolicyDialog(final Context context, final String action) { private void openPrivacyPolicyDialog(final Context context, final String action) {

View File

@@ -134,8 +134,11 @@ class ErrorUtil {
) )
) )
NotificationManagerCompat.from(context) val notificationManager = NotificationManagerCompat.from(context)
.notify(ERROR_REPORT_NOTIFICATION_ID, notificationBuilder.build()) if (notificationManager.areNotificationsEnabled()) {
notificationManager
.notify(ERROR_REPORT_NOTIFICATION_ID, notificationBuilder.build())
}
ContextCompat.getMainExecutor(context).execute { ContextCompat.getMainExecutor(context).execute {
// since the notification is silent, also show a toast, otherwise the user is confused // since the notification is silent, also show a toast, otherwise the user is confused

View File

@@ -126,6 +126,7 @@ public class ReCaptchaActivity extends AppCompatActivity {
} }
@Override @Override
@SuppressLint("MissingSuperCall")
public void onBackPressed() { public void onBackPressed() {
saveCookiesAndFinish(); saveCookiesAndFinish();
} }

View File

@@ -160,34 +160,29 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
@Override @Override
public boolean onMenuItemSelected(@NonNull final MenuItem item) { public boolean onMenuItemSelected(@NonNull final MenuItem item) {
switch (item.getItemId()) { final int itemId = item.getItemId();
case R.id.menu_item_notify: if (itemId == R.id.menu_item_notify) {
final boolean value = !item.isChecked(); final boolean value = !item.isChecked();
item.setEnabled(false); item.setEnabled(false);
setNotify(value); setNotify(value);
break; } else if (itemId == R.id.action_settings) {
case R.id.action_settings: NavigationHelper.openSettings(requireContext());
NavigationHelper.openSettings(requireContext()); } else if (itemId == R.id.menu_item_rss) {
break; if (currentInfo != null) {
case R.id.menu_item_rss: ShareUtils.openUrlInApp(requireContext(), currentInfo.getFeedUrl());
if (currentInfo != null) { }
ShareUtils.openUrlInApp(requireContext(), currentInfo.getFeedUrl()); } else if (itemId == R.id.menu_item_openInBrowser) {
} if (currentInfo != null) {
break; ShareUtils.openUrlInBrowser(requireContext(),
case R.id.menu_item_openInBrowser: currentInfo.getOriginalUrl());
if (currentInfo != null) { }
ShareUtils.openUrlInBrowser(requireContext(), } else if (itemId == R.id.menu_item_share) {
currentInfo.getOriginalUrl()); if (currentInfo != null) {
} ShareUtils.shareText(requireContext(), name,
break; currentInfo.getOriginalUrl(), currentInfo.getAvatars());
case R.id.menu_item_share: }
if (currentInfo != null) { } else {
ShareUtils.shareText(requireContext(), name, return false;
currentInfo.getOriginalUrl(), currentInfo.getAvatars());
}
break;
default:
return false;
} }
return true; return true;
} }

View File

@@ -232,35 +232,30 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
@Override @Override
public boolean onOptionsItemSelected(final MenuItem item) { public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) { final int itemId = item.getItemId();
case R.id.action_settings: if (itemId == R.id.action_settings) {
NavigationHelper.openSettings(requireContext()); NavigationHelper.openSettings(requireContext());
break; } else if (itemId == R.id.menu_item_openInBrowser) {
case R.id.menu_item_openInBrowser: ShareUtils.openUrlInBrowser(requireContext(), url);
ShareUtils.openUrlInBrowser(requireContext(), url); } else if (itemId == R.id.menu_item_share) {
break; ShareUtils.shareText(requireContext(), name, url,
case R.id.menu_item_share: currentInfo == null ? List.of() : currentInfo.getThumbnails());
ShareUtils.shareText(requireContext(), name, url, } else if (itemId == R.id.menu_item_bookmark) {
currentInfo == null ? List.of() : currentInfo.getThumbnails()); onBookmarkClicked();
break; } else if (itemId == R.id.menu_item_append_playlist) {
case R.id.menu_item_bookmark: if (currentInfo != null) {
onBookmarkClicked(); disposables.add(PlaylistDialog.createCorrespondingDialog(
break; getContext(),
case R.id.menu_item_append_playlist: getPlayQueue()
if (currentInfo != null) { .getStreams()
disposables.add(PlaylistDialog.createCorrespondingDialog( .stream()
getContext(), .map(StreamEntity::new)
getPlayQueue() .collect(Collectors.toList()),
.getStreams() dialog -> dialog.show(getFM(), TAG)
.stream() ));
.map(StreamEntity::new) }
.collect(Collectors.toList()), } else {
dialog -> dialog.show(getFM(), TAG) return super.onOptionsItemSelected(item);
));
}
break;
default:
return super.onOptionsItemSelected(item);
} }
return true; return true;
} }

View File

@@ -255,7 +255,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
viewModel.getShowFutureItemsFromPreferences() viewModel.getShowFutureItemsFromPreferences()
) )
AlertDialog.Builder(context!!) AlertDialog.Builder(requireContext())
.setTitle(R.string.feed_hide_streams_title) .setTitle(R.string.feed_hide_streams_title)
.setMultiChoiceItems(dialogItems, checkedDialogItems) { _, which, isChecked -> .setMultiChoiceItems(dialogItems, checkedDialogItems) { _, which, isChecked ->
checkedDialogItems[which] = isChecked checkedDialogItems[which] = isChecked

View File

@@ -129,7 +129,8 @@ class FeedViewModel(
fun setSaveShowPlayedItems(showPlayedItems: Boolean) { fun setSaveShowPlayedItems(showPlayedItems: Boolean) {
this.showPlayedItems.onNext(showPlayedItems) this.showPlayedItems.onNext(showPlayedItems)
PreferenceManager.getDefaultSharedPreferences(application).edit { PreferenceManager.getDefaultSharedPreferences(application).edit {
putBoolean(application.getString(R.string.feed_show_watched_items_key), showPlayedItems) this.putBoolean(application.getString(R.string.feed_show_watched_items_key), showPlayedItems)
this.apply()
} }
} }
@@ -138,7 +139,8 @@ class FeedViewModel(
fun setSaveShowPartiallyPlayedItems(showPartiallyPlayedItems: Boolean) { fun setSaveShowPartiallyPlayedItems(showPartiallyPlayedItems: Boolean) {
this.showPartiallyPlayedItems.onNext(showPartiallyPlayedItems) this.showPartiallyPlayedItems.onNext(showPartiallyPlayedItems)
PreferenceManager.getDefaultSharedPreferences(application).edit { PreferenceManager.getDefaultSharedPreferences(application).edit {
putBoolean(application.getString(R.string.feed_show_partially_watched_items_key), showPartiallyPlayedItems) this.putBoolean(application.getString(R.string.feed_show_partially_watched_items_key), showPartiallyPlayedItems)
this.apply()
} }
} }
@@ -147,7 +149,8 @@ class FeedViewModel(
fun setSaveShowFutureItems(showFutureItems: Boolean) { fun setSaveShowFutureItems(showFutureItems: Boolean) {
this.showFutureItems.onNext(showFutureItems) this.showFutureItems.onNext(showFutureItems)
PreferenceManager.getDefaultSharedPreferences(application).edit { PreferenceManager.getDefaultSharedPreferences(application).edit {
putBoolean(application.getString(R.string.feed_show_future_items_key), showFutureItems) this.putBoolean(application.getString(R.string.feed_show_future_items_key), showFutureItems)
this.apply()
} }
} }

View File

@@ -92,8 +92,10 @@ class NotificationHelper(val context: Context) {
// Show individual stream notifications, set channel icon only if there is actually // Show individual stream notifications, set channel icon only if there is actually
// one // one
showStreamNotifications(newStreams, data.serviceId, data.url, bitmap) showStreamNotifications(newStreams, data.serviceId, data.url, bitmap)
// Show summary notification // Show summary notification if enabled
manager.notify(data.pseudoId, summaryBuilder.build()) if (manager.areNotificationsEnabled()) {
manager.notify(data.pseudoId, summaryBuilder.build())
}
iconLoadingTargets.remove(this) // allow it to be garbage-collected iconLoadingTargets.remove(this) // allow it to be garbage-collected
} }
@@ -101,8 +103,10 @@ class NotificationHelper(val context: Context) {
override fun onBitmapFailed(e: Exception, errorDrawable: Drawable) { override fun onBitmapFailed(e: Exception, errorDrawable: Drawable) {
// Show individual stream notifications // Show individual stream notifications
showStreamNotifications(newStreams, data.serviceId, data.url, null) showStreamNotifications(newStreams, data.serviceId, data.url, null)
// Show summary notification // Show summary notification if enabled
manager.notify(data.pseudoId, summaryBuilder.build()) if (manager.areNotificationsEnabled()) {
manager.notify(data.pseudoId, summaryBuilder.build())
}
iconLoadingTargets.remove(this) // allow it to be garbage-collected iconLoadingTargets.remove(this) // allow it to be garbage-collected
} }
@@ -126,7 +130,9 @@ class NotificationHelper(val context: Context) {
) { ) {
for (stream in newStreams) { for (stream in newStreams) {
val notification = createStreamNotification(stream, serviceId, channelUrl, channelIcon) val notification = createStreamNotification(stream, serviceId, channelUrl, channelIcon)
manager.notify(stream.url.hashCode(), notification) if (manager.areNotificationsEnabled()) {
manager.notify(stream.url.hashCode(), notification)
}
} }
} }

View File

@@ -185,7 +185,9 @@ class FeedLoadService : Service() {
} }
} }
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()) if (notificationManager.areNotificationsEnabled()) {
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build())
}
} }
// ///////////////////////////////////////////////////////////////////////// // /////////////////////////////////////////////////////////////////////////

View File

@@ -327,7 +327,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
groupIcon = feedGroupEntity?.icon groupIcon = feedGroupEntity?.icon
groupSortOrder = feedGroupEntity?.sortOrder ?: -1 groupSortOrder = feedGroupEntity?.sortOrder ?: -1
val feedGroupIcon = selectedIcon ?: icon val feedGroupIcon = if (selectedIcon == null) icon else selectedIcon!!
feedGroupCreateBinding.iconPreview.setImageResource(feedGroupIcon.getDrawableRes()) feedGroupCreateBinding.iconPreview.setImageResource(feedGroupIcon.getDrawableRes())
if (feedGroupCreateBinding.groupNameInput.text.isNullOrBlank()) { if (feedGroupCreateBinding.groupNameInput.text.isNullOrBlank()) {
@@ -506,7 +506,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
private fun hideKeyboardSearch() { private fun hideKeyboardSearch() {
inputMethodManager.hideSoftInputFromWindow( inputMethodManager.hideSoftInputFromWindow(
searchLayoutBinding.toolbarSearchEditText.windowToken, searchLayoutBinding.toolbarSearchEditText.windowToken,
InputMethodManager.RESULT_UNCHANGED_SHOWN InputMethodManager.HIDE_NOT_ALWAYS
) )
searchLayoutBinding.toolbarSearchEditText.clearFocus() searchLayoutBinding.toolbarSearchEditText.clearFocus()
} }
@@ -523,7 +523,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
private fun hideKeyboard() { private fun hideKeyboard() {
inputMethodManager.hideSoftInputFromWindow( inputMethodManager.hideSoftInputFromWindow(
feedGroupCreateBinding.groupNameInput.windowToken, feedGroupCreateBinding.groupNameInput.windowToken,
InputMethodManager.RESULT_UNCHANGED_SHOWN InputMethodManager.HIDE_NOT_ALWAYS
) )
feedGroupCreateBinding.groupNameInput.clearFocus() feedGroupCreateBinding.groupNameInput.clearFocus()
} }

View File

@@ -144,7 +144,9 @@ public abstract class BaseImportExportService extends Service {
notificationBuilder.setContentText(text); notificationBuilder.setContentText(text);
} }
notificationManager.notify(getNotificationId(), notificationBuilder.build()); if (notificationManager.areNotificationsEnabled()) {
notificationManager.notify(getNotificationId(), notificationBuilder.build());
}
} }
protected void stopService() { protected void stopService() {
@@ -174,7 +176,10 @@ public abstract class BaseImportExportService extends Service {
.setContentTitle(title) .setContentTitle(title)
.setStyle(new NotificationCompat.BigTextStyle().bigText(textOrEmpty)) .setStyle(new NotificationCompat.BigTextStyle().bigText(textOrEmpty))
.setContentText(textOrEmpty); .setContentText(textOrEmpty);
notificationManager.notify(getNotificationId(), notificationBuilder.build());
if (notificationManager.areNotificationsEnabled()) {
notificationManager.notify(getNotificationId(), notificationBuilder.build());
}
} }
protected NotificationCompat.Builder createNotification() { protected NotificationCompat.Builder createNotification() {

View File

@@ -127,39 +127,39 @@ public final class PlayQueueActivity extends AppCompatActivity
@Override @Override
public boolean onOptionsItemSelected(final MenuItem item) { public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) { final int itemId = item.getItemId();
case android.R.id.home: if (itemId == android.R.id.home) {
finish(); finish();
return true; return true;
case R.id.action_settings: } else if (itemId == R.id.action_settings) {
NavigationHelper.openSettings(this); NavigationHelper.openSettings(this);
return true; return true;
case R.id.action_append_playlist: } else if (itemId == R.id.action_append_playlist) {
PlaylistDialog.showForPlayQueue(player, getSupportFragmentManager()); PlaylistDialog.showForPlayQueue(player, getSupportFragmentManager());
return true; return true;
case R.id.action_playback_speed: } else if (itemId == R.id.action_playback_speed) {
openPlaybackParameterDialog(); openPlaybackParameterDialog();
return true; return true;
case R.id.action_mute: } else if (itemId == R.id.action_mute) {
player.toggleMute(); player.toggleMute();
return true; return true;
case R.id.action_system_audio: } else if (itemId == R.id.action_system_audio) {
startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS)); startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS));
return true; return true;
case R.id.action_switch_main: } else if (itemId == R.id.action_switch_main) {
this.player.setRecovery();
NavigationHelper.playOnMainPlayer(this, player.getPlayQueue(), true);
return true;
} else if (itemId == R.id.action_switch_popup) {
if (PermissionHelper.isPopupEnabledElseAsk(this)) {
this.player.setRecovery(); this.player.setRecovery();
NavigationHelper.playOnMainPlayer(this, player.getPlayQueue(), true); NavigationHelper.playOnPopupPlayer(this, player.getPlayQueue(), true);
return true; }
case R.id.action_switch_popup: return true;
if (PermissionHelper.isPopupEnabledElseAsk(this)) { } else if (itemId == R.id.action_switch_background) {
this.player.setRecovery(); this.player.setRecovery();
NavigationHelper.playOnPopupPlayer(this, player.getPlayQueue(), true); NavigationHelper.playOnBackgroundPlayer(this, player.getPlayQueue(), true);
} return true;
return true;
case R.id.action_switch_background:
this.player.setRecovery();
NavigationHelper.playOnBackgroundPlayer(this, player.getPlayQueue(), true);
return true;
} }
if (item.getGroupId() == MENU_ID_AUDIO_TRACK) { if (item.getGroupId() == MENU_ID_AUDIO_TRACK) {

View File

@@ -72,7 +72,9 @@ public final class NotificationUtil {
notificationBuilder = createNotification(); notificationBuilder = createNotification();
} }
updateNotification(); updateNotification();
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()); if (notificationManager.areNotificationsEnabled()) {
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
}
} }
public synchronized void updateThumbnail() { public synchronized void updateThumbnail() {
@@ -84,7 +86,9 @@ public final class NotificationUtil {
} }
setLargeIcon(notificationBuilder); setLargeIcon(notificationBuilder);
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()); if (notificationManager.areNotificationsEnabled()) {
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
}
} }
} }

View File

@@ -0,0 +1,58 @@
package org.schabi.newpipe.settings.export;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
import java.util.Set;
/**
* An {@link ObjectInputStream} that only allows preferences-related types to be deserialized, to
* prevent injections. The only allowed types are: all primitive types, all boxed primitive types,
* null, strings. HashMap, HashSet and arrays of previously defined types are also allowed. Sources:
* <a href="https://wiki.sei.cmu.edu/confluence/display/java/SER00-J.+Enable+serialization+compatibility+during+class+evolution">
* cmu.edu
* </a>,
* <a href="https://cheatsheetseries.owasp.org/cheatsheets/Deserialization_Cheat_Sheet.html#harden-your-own-javaioobjectinputstream">
* OWASP cheatsheet
* </a>,
* <a href="https://commons.apache.org/proper/commons-io/apidocs/src-html/org/apache/commons/io/serialization/ValidatingObjectInputStream.html#line-118">
* Apache's {@code ValidatingObjectInputStream}
* </a>
*/
public class PreferencesObjectInputStream extends ObjectInputStream {
/**
* Primitive types, strings and other built-in types do not pass through resolveClass() but
* instead have a custom encoding; see
* <a href="https://docs.oracle.com/javase/6/docs/platform/serialization/spec/protocol.html#10152">
* official docs</a>.
*/
private static final Set<String> CLASS_WHITELIST = Set.of(
"java.lang.Boolean",
"java.lang.Byte",
"java.lang.Character",
"java.lang.Short",
"java.lang.Integer",
"java.lang.Long",
"java.lang.Float",
"java.lang.Double",
"java.lang.Void",
"java.util.HashMap",
"java.util.HashSet"
);
public PreferencesObjectInputStream(final InputStream in) throws IOException {
super(in);
}
@Override
protected Class<?> resolveClass(final ObjectStreamClass desc)
throws ClassNotFoundException, IOException {
if (CLASS_WHITELIST.contains(desc.getName())) {
return super.resolveClass(desc);
} else {
throw new ClassNotFoundException("Class not allowed: " + desc.getName());
}
}
}

View File

@@ -1,52 +0,0 @@
/*
* SPDX-FileCopyrightText: 2024-2026 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.settings.export
import java.io.IOException
import java.io.InputStream
import java.io.ObjectInputStream
import java.io.ObjectStreamClass
/**
* An [ObjectInputStream] that only allows preferences-related types to be deserialized, to
* prevent injections. The only allowed types are: all primitive types, all boxed primitive types,
* null, strings. HashMap, HashSet and arrays of previously defined types are also allowed. Sources:
* [cmu.edu](https://wiki.sei.cmu.edu/confluence/display/java/SER00-J.+Enable+serialization+compatibility+during+class+evolution) * ,
* [OWASP cheatsheet](https://cheatsheetseries.owasp.org/cheatsheets/Deserialization_Cheat_Sheet.html#harden-your-own-javaioobjectinputstream) * ,
* [Apache's `ValidatingObjectInputStream`](https://commons.apache.org/proper/commons-io/apidocs/src-html/org/apache/commons/io/serialization/ValidatingObjectInputStream.html#line-118) *
*/
class PreferencesObjectInputStream(stream: InputStream) : ObjectInputStream(stream) {
@Throws(ClassNotFoundException::class, IOException::class)
override fun resolveClass(desc: ObjectStreamClass): Class<*> {
if (desc.name in CLASS_WHITELIST) {
return super.resolveClass(desc)
} else {
throw ClassNotFoundException("Class not allowed: $desc.name")
}
}
companion object {
/**
* Primitive types, strings and other built-in types do not pass through resolveClass() but
* instead have a custom encoding; see
* [
* official docs](https://docs.oracle.com/javase/6/docs/platform/serialization/spec/protocol.html#10152).
*/
private val CLASS_WHITELIST = setOf<String>(
"java.lang.Boolean",
"java.lang.Byte",
"java.lang.Character",
"java.lang.Short",
"java.lang.Integer",
"java.lang.Long",
"java.lang.Float",
"java.lang.Double",
"java.lang.Void",
"java.util.HashMap",
"java.util.HashSet"
)
}
}

View File

@@ -26,13 +26,14 @@ data class PreferenceSearchItem(
val breadcrumbs: String, val breadcrumbs: String,
@XmlRes val searchIndexItemResId: Int @XmlRes val searchIndexItemResId: Int
) { ) {
val allRelevantSearchFields: List<String>
get() = listOf(title, summary, entries, breadcrumbs)
fun hasData(): Boolean { fun hasData(): Boolean {
return !key.isEmpty() && !title.isEmpty() return !key.isEmpty() && !title.isEmpty()
} }
fun getAllRelevantSearchFields(): MutableList<String?> {
return mutableListOf(title, summary, entries, breadcrumbs)
}
override fun toString(): String { override fun toString(): String {
return "PreferenceItem: $title $summary $key" return "PreferenceItem: $title $summary $key"
} }

View File

@@ -0,0 +1,51 @@
package org.schabi.newpipe.util;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R;
/**
* For preferences with dependencies and multiple use case,
* this class can be used to reduce the lines of code.
*/
public final class DependentPreferenceHelper {
private DependentPreferenceHelper() {
// no instance
}
/**
* Option `Resume playback` depends on `Watch history`, this method can be used to retrieve if
* `Resume playback` and its dependencies are all enabled.
*
* @param context the Android context
* @return returns true if `Resume playback` and `Watch history` are both enabled
*/
public static boolean getResumePlaybackEnabled(final Context context) {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
return prefs.getBoolean(context.getString(
R.string.enable_watch_history_key), true)
&& prefs.getBoolean(context.getString(
R.string.enable_playback_resume_key), true);
}
/**
* Option `Position in lists` depends on `Watch history`, this method can be used to retrieve if
* `Position in lists` and its dependencies are all enabled.
*
* @param context the Android context
* @return returns true if `Positions in lists` and `Watch history` are both enabled
*/
public static boolean getPositionsInListsEnabled(final Context context) {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
return prefs.getBoolean(context.getString(
R.string.enable_watch_history_key), true)
&& prefs.getBoolean(context.getString(
R.string.enable_playback_state_lists_key), true);
}
}

View File

@@ -1,46 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023-2026 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.util
import android.content.Context
import androidx.preference.PreferenceManager
import org.schabi.newpipe.R
/**
* For preferences with dependencies and multiple use case,
* this class can be used to reduce the lines of code.
*/
object DependentPreferenceHelper {
/**
* Option `Resume playback` depends on `Watch history`, this method can be used to retrieve if
* `Resume playback` and its dependencies are all enabled.
*
* @param context the Android context
* @return returns true if `Resume playback` and `Watch history` are both enabled
*/
@JvmStatic
fun getResumePlaybackEnabled(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true) &&
prefs.getBoolean(context.getString(R.string.enable_playback_resume_key), true)
}
/**
* Option `Position in lists` depends on `Watch history`, this method can be used to retrieve if
* `Position in lists` and its dependencies are all enabled.
*
* @param context the Android context
* @return returns true if `Positions in lists` and `Watch history` are both enabled
*/
@JvmStatic
fun getPositionsInListsEnabled(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true) &&
prefs.getBoolean(context.getString(R.string.enable_playback_state_lists_key), true)
}
}

View File

@@ -48,7 +48,7 @@ public final class KeyboardUtil {
final InputMethodManager imm = ContextCompat.getSystemService(activity, final InputMethodManager imm = ContextCompat.getSystemService(activity,
InputMethodManager.class); InputMethodManager.class);
imm.hideSoftInputFromWindow(editText.getWindowToken(), imm.hideSoftInputFromWindow(editText.getWindowToken(),
InputMethodManager.RESULT_UNCHANGED_SHOWN); InputMethodManager.HIDE_NOT_ALWAYS);
editText.clearFocus(); editText.clearFocus();
} }

View File

@@ -0,0 +1,61 @@
package org.schabi.newpipe.util;
import android.content.Context;
import android.text.Selection;
import android.text.Spannable;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.views.NewPipeEditText;
import org.schabi.newpipe.views.NewPipeTextView;
public final class NewPipeTextViewHelper {
private NewPipeTextViewHelper() {
}
/**
* Share the selected text of {@link NewPipeTextView NewPipeTextViews} and
* {@link NewPipeEditText NewPipeEditTexts} with
* {@link ShareUtils#shareText(Context, String, String)}.
*
* <p>
* This allows EMUI users to get the Android share sheet instead of the EMUI share sheet when
* using the {@code Share} command of the popup menu which appears when selecting text.
* </p>
*
* @param textView the {@link TextView} on which sharing the selected text. It should be a
* {@link NewPipeTextView} or a {@link NewPipeEditText} (even if
* {@link TextView standard TextViews} are supported).
*/
public static void shareSelectedTextWithShareUtils(@NonNull final TextView textView) {
final CharSequence textViewText = textView.getText();
shareSelectedTextIfNotNullAndNotEmpty(textView, getSelectedText(textView, textViewText));
if (textViewText instanceof Spannable) {
Selection.setSelection((Spannable) textViewText, textView.getSelectionEnd());
}
}
@Nullable
private static CharSequence getSelectedText(@NonNull final TextView textView,
@Nullable final CharSequence text) {
if (!textView.hasSelection() || text == null) {
return null;
}
final int start = textView.getSelectionStart();
final int end = textView.getSelectionEnd();
return String.valueOf(start > end ? text.subSequence(end, start)
: text.subSequence(start, end));
}
private static void shareSelectedTextIfNotNullAndNotEmpty(
@NonNull final TextView textView,
@Nullable final CharSequence selectedText) {
if (selectedText != null && selectedText.length() != 0) {
ShareUtils.shareText(textView.getContext(), "", selectedText.toString());
}
}
}

View File

@@ -1,60 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021-2026 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.util
import android.text.Selection
import android.text.Spannable
import android.widget.TextView
import org.schabi.newpipe.util.external_communication.ShareUtils
object NewPipeTextViewHelper {
/**
* Share the selected text of [NewPipeTextViews][org.schabi.newpipe.views.NewPipeTextView] and
* [NewPipeEditTexts][org.schabi.newpipe.views.NewPipeEditText] with
* [ShareUtils.shareText].
*
*
*
* This allows EMUI users to get the Android share sheet instead of the EMUI share sheet when
* using the `Share` command of the popup menu which appears when selecting text.
*
*
* @param textView the [TextView] on which sharing the selected text. It should be a
* [org.schabi.newpipe.views.NewPipeTextView] or a [org.schabi.newpipe.views.NewPipeEditText]
* (even if [standard TextViews][TextView] are supported).
*/
@JvmStatic
fun shareSelectedTextWithShareUtils(textView: TextView) {
val textViewText = textView.getText()
shareSelectedTextIfNotNullAndNotEmpty(textView, getSelectedText(textView, textViewText))
if (textViewText is Spannable) {
Selection.setSelection(textViewText, textView.selectionEnd)
}
}
private fun getSelectedText(textView: TextView, text: CharSequence?): CharSequence? {
if (!textView.hasSelection() || text == null) {
return null
}
val start = textView.selectionStart
val end = textView.selectionEnd
return if (start > end) {
text.subSequence(end, start)
} else {
text.subSequence(start, end)
}
}
private fun shareSelectedTextIfNotNullAndNotEmpty(
textView: TextView,
selectedText: CharSequence?
) {
if (!selectedText.isNullOrEmpty()) {
ShareUtils.shareText(textView.context, "", selectedText.toString())
}
}
}

View File

@@ -0,0 +1,69 @@
package org.schabi.newpipe.util;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.preference.PreferenceManager;
import com.grack.nanojson.JsonArray;
import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonParser;
import com.grack.nanojson.JsonParserException;
import com.grack.nanojson.JsonStringWriter;
import com.grack.nanojson.JsonWriter;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
import java.util.ArrayList;
import java.util.List;
public final class PeertubeHelper {
private PeertubeHelper() { }
public static List<PeertubeInstance> getInstanceList(final Context context) {
final SharedPreferences sharedPreferences = PreferenceManager
.getDefaultSharedPreferences(context);
final String savedInstanceListKey = context.getString(R.string.peertube_instance_list_key);
final String savedJson = sharedPreferences.getString(savedInstanceListKey, null);
if (null == savedJson) {
return List.of(getCurrentInstance());
}
try {
final JsonArray array = JsonParser.object().from(savedJson).getArray("instances");
final List<PeertubeInstance> result = new ArrayList<>();
for (final Object o : array) {
if (o instanceof JsonObject) {
final JsonObject instance = (JsonObject) o;
final String name = instance.getString("name");
final String url = instance.getString("url");
result.add(new PeertubeInstance(url, name));
}
}
return result;
} catch (final JsonParserException e) {
return List.of(getCurrentInstance());
}
}
public static PeertubeInstance selectInstance(final PeertubeInstance instance,
final Context context) {
final SharedPreferences sharedPreferences = PreferenceManager
.getDefaultSharedPreferences(context);
final String selectedInstanceKey =
context.getString(R.string.peertube_selected_instance_key);
final JsonStringWriter jsonWriter = JsonWriter.string().object();
jsonWriter.value("name", instance.getName());
jsonWriter.value("url", instance.getUrl());
final String jsonToSave = jsonWriter.end().done();
sharedPreferences.edit().putString(selectedInstanceKey, jsonToSave).apply();
ServiceList.PeerTube.setInstance(instance);
return instance;
}
public static PeertubeInstance getCurrentInstance() {
return ServiceList.PeerTube.getInstance();
}
}

View File

@@ -1,52 +0,0 @@
/*
* SPDX-FileCopyrightText: 2019-2026 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.util
import android.content.Context
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import com.grack.nanojson.JsonObject
import com.grack.nanojson.JsonParser
import com.grack.nanojson.JsonWriter
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.ServiceList
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance
object PeertubeHelper {
@JvmStatic
val currentInstance: PeertubeInstance
get() = ServiceList.PeerTube.instance
@JvmStatic
fun getInstanceList(context: Context): List<PeertubeInstance> {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val savedInstanceListKey = context.getString(R.string.peertube_instance_list_key)
val savedJson = sharedPreferences.getString(savedInstanceListKey, null)
?: return listOf(currentInstance)
return runCatching {
JsonParser.`object`().from(savedJson).getArray("instances")
.filterIsInstance<JsonObject>()
.map { PeertubeInstance(it.getString("url"), it.getString("name")) }
}.getOrDefault(listOf(currentInstance))
}
@JvmStatic
fun selectInstance(instance: PeertubeInstance, context: Context): PeertubeInstance {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val selectedInstanceKey = context.getString(R.string.peertube_selected_instance_key)
val jsonWriter = JsonWriter.string().`object`()
jsonWriter.value("name", instance.name)
jsonWriter.value("url", instance.url)
val jsonToSave = jsonWriter.end().done()
sharedPreferences.edit { putString(selectedInstanceKey, jsonToSave) }
ServiceList.PeerTube.instance = instance
return instance
}
}

View File

@@ -0,0 +1,94 @@
package org.schabi.newpipe.util;
import android.content.Context;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.PlaylistControlBinding;
import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder;
import org.schabi.newpipe.player.PlayerType;
/**
* Utility class for play buttons and their respective click listeners.
*/
public final class PlayButtonHelper {
private PlayButtonHelper() {
// utility class
}
/**
* Initialize {@link android.view.View.OnClickListener OnClickListener}
* and {@link android.view.View.OnLongClickListener OnLongClickListener} for playlist control
* buttons defined in {@link R.layout#playlist_control}.
*
* @param activity The activity to use for the {@link android.widget.Toast Toast}.
* @param playlistControlBinding The binding of the
* {@link R.layout#playlist_control playlist control layout}.
* @param fragment The fragment to get the play queue from.
*/
public static void initPlaylistControlClickListener(
@NonNull final AppCompatActivity activity,
@NonNull final PlaylistControlBinding playlistControlBinding,
@NonNull final PlaylistControlViewHolder fragment) {
// click listener
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(view -> {
NavigationHelper.playOnMainPlayer(activity, fragment.getPlayQueue());
showHoldToAppendToastIfNeeded(activity);
});
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(view -> {
NavigationHelper.playOnPopupPlayer(activity, fragment.getPlayQueue(), false);
showHoldToAppendToastIfNeeded(activity);
});
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(view -> {
NavigationHelper.playOnBackgroundPlayer(activity, fragment.getPlayQueue(), false);
showHoldToAppendToastIfNeeded(activity);
});
// long click listener
playlistControlBinding.playlistCtrlPlayAllButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.MAIN);
return true;
});
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.POPUP);
return true;
});
playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.AUDIO);
return true;
});
}
/**
* Show the "hold to append" toast if the corresponding preference is enabled.
*
* @param context The context to show the toast.
*/
private static void showHoldToAppendToastIfNeeded(@NonNull final Context context) {
if (shouldShowHoldToAppendTip(context)) {
Toast.makeText(context, R.string.hold_to_append, Toast.LENGTH_SHORT).show();
}
}
/**
* Check if the "hold to append" toast should be shown.
*
* <p>
* The tip is shown if the corresponding preference is enabled.
* This is the default behaviour.
* </p>
*
* @param context The context to get the preference.
* @return {@code true} if the tip should be shown, {@code false} otherwise.
*/
public static boolean shouldShowHoldToAppendTip(@NonNull final Context context) {
return PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.show_hold_to_append_key), true);
}
}

View File

@@ -1,96 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023-2026 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.util
import android.content.Context
import android.view.View
import android.view.View.OnLongClickListener
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceManager
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.PlaylistControlBinding
import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder
import org.schabi.newpipe.player.PlayerType
/**
* Utility class for play buttons and their respective click listeners.
*/
object PlayButtonHelper {
/**
* Initialize [OnClickListener][View.OnClickListener]
* and [OnLongClickListener][OnLongClickListener] for playlist control
* buttons defined in [R.layout.playlist_control].
*
* @param activity The activity to use for the [Toast][Toast].
* @param playlistControlBinding The binding of the
* [playlist control layout][R.layout.playlist_control].
* @param fragment The fragment to get the play queue from.
*/
@JvmStatic
fun initPlaylistControlClickListener(
activity: AppCompatActivity,
playlistControlBinding: PlaylistControlBinding,
fragment: PlaylistControlViewHolder
) {
// click listener
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener {
NavigationHelper.playOnMainPlayer(activity, fragment.getPlayQueue())
showHoldToAppendToastIfNeeded(activity)
}
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener {
NavigationHelper.playOnPopupPlayer(activity, fragment.getPlayQueue(), false)
showHoldToAppendToastIfNeeded(activity)
}
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener {
NavigationHelper.playOnBackgroundPlayer(activity, fragment.getPlayQueue(), false)
showHoldToAppendToastIfNeeded(activity)
}
// long click listener
playlistControlBinding.playlistCtrlPlayAllButton.setOnLongClickListener {
NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.MAIN)
true
}
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener {
NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.POPUP)
true
}
playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener {
NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.AUDIO)
true
}
}
/**
* Show the "hold to append" toast if the corresponding preference is enabled.
*
* @param context The context to show the toast.
*/
private fun showHoldToAppendToastIfNeeded(context: Context) {
if (shouldShowHoldToAppendTip(context)) {
Toast.makeText(context, R.string.hold_to_append, Toast.LENGTH_SHORT).show()
}
}
/**
* Check if the "hold to append" toast should be shown.
*
*
*
* The tip is shown if the corresponding preference is enabled.
* This is the default behaviour.
*
*
* @param context The context to get the preference.
* @return `true` if the tip should be shown, `false` otherwise.
*/
@JvmStatic
fun shouldShowHoldToAppendTip(context: Context): Boolean {
return PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.show_hold_to_append_key), true)
}
}

View File

@@ -0,0 +1,213 @@
package org.schabi.newpipe.util;
import static org.schabi.newpipe.extractor.ServiceList.SoundCloud;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.preference.PreferenceManager;
import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonParser;
import com.grack.nanojson.JsonParserException;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
public final class ServiceHelper {
private static final StreamingService DEFAULT_FALLBACK_SERVICE = ServiceList.YouTube;
private ServiceHelper() { }
@DrawableRes
public static int getIcon(final int serviceId) {
switch (serviceId) {
case 0:
return R.drawable.ic_smart_display;
case 1:
return R.drawable.ic_cloud;
case 2:
return R.drawable.ic_placeholder_media_ccc;
case 3:
return R.drawable.ic_placeholder_peertube;
case 4:
return R.drawable.ic_placeholder_bandcamp;
default:
return R.drawable.ic_circle;
}
}
public static String getTranslatedFilterString(final String filter, final Context c) {
switch (filter) {
case "all":
return c.getString(R.string.all);
case "videos":
case "sepia_videos":
case "music_videos":
return c.getString(R.string.videos_string);
case "channels":
return c.getString(R.string.channels);
case "playlists":
case "music_playlists":
return c.getString(R.string.playlists);
case "tracks":
return c.getString(R.string.tracks);
case "users":
return c.getString(R.string.users);
case "conferences":
return c.getString(R.string.conferences);
case "events":
return c.getString(R.string.events);
case "music_songs":
return c.getString(R.string.songs);
case "music_albums":
return c.getString(R.string.albums);
case "music_artists":
return c.getString(R.string.artists);
default:
return filter;
}
}
/**
* Get a resource string with instructions for importing subscriptions for each service.
*
* @param serviceId service to get the instructions for
* @return the string resource containing the instructions or -1 if the service don't support it
*/
@StringRes
public static int getImportInstructions(final int serviceId) {
switch (serviceId) {
case 0:
return R.string.import_youtube_instructions;
case 1:
return R.string.import_soundcloud_instructions;
default:
return -1;
}
}
/**
* For services that support importing from a channel url, return a hint that will
* be used in the EditText that the user will type in his channel url.
*
* @param serviceId service to get the hint for
* @return the hint's string resource or -1 if the service don't support it
*/
@StringRes
public static int getImportInstructionsHint(final int serviceId) {
switch (serviceId) {
case 1:
return R.string.import_soundcloud_instructions_hint;
default:
return -1;
}
}
public static int getSelectedServiceId(final Context context) {
return Optional.ofNullable(getSelectedService(context))
.orElse(DEFAULT_FALLBACK_SERVICE)
.getServiceId();
}
@Nullable
public static StreamingService getSelectedService(final Context context) {
final String serviceName = PreferenceManager.getDefaultSharedPreferences(context)
.getString(context.getString(R.string.current_service_key),
context.getString(R.string.default_service_value));
try {
return NewPipe.getService(serviceName);
} catch (final ExtractionException e) {
return null;
}
}
@NonNull
public static String getNameOfServiceById(final int serviceId) {
return ServiceList.all().stream()
.filter(s -> s.getServiceId() == serviceId)
.findFirst()
.map(StreamingService::getServiceInfo)
.map(StreamingService.ServiceInfo::getName)
.orElse("<unknown>");
}
/**
* @param serviceId the id of the service
* @return the service corresponding to the provided id
* @throws java.util.NoSuchElementException if there is no service with the provided id
*/
@NonNull
public static StreamingService getServiceById(final int serviceId) {
return ServiceList.all().stream()
.filter(s -> s.getServiceId() == serviceId)
.findFirst()
.orElseThrow();
}
public static void setSelectedServiceId(final Context context, final int serviceId) {
String serviceName;
try {
serviceName = NewPipe.getService(serviceId).getServiceInfo().getName();
} catch (final ExtractionException e) {
serviceName = DEFAULT_FALLBACK_SERVICE.getServiceInfo().getName();
}
setSelectedServicePreferences(context, serviceName);
}
private static void setSelectedServicePreferences(final Context context,
final String serviceName) {
PreferenceManager.getDefaultSharedPreferences(context).edit().
putString(context.getString(R.string.current_service_key), serviceName).apply();
}
public static long getCacheExpirationMillis(final int serviceId) {
if (serviceId == SoundCloud.getServiceId()) {
return TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES);
} else {
return TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS);
}
}
public static void initService(final Context context, final int serviceId) {
if (serviceId == ServiceList.PeerTube.getServiceId()) {
final SharedPreferences sharedPreferences = PreferenceManager
.getDefaultSharedPreferences(context);
final String json = sharedPreferences.getString(context.getString(
R.string.peertube_selected_instance_key), null);
if (null == json) {
return;
}
final JsonObject jsonObject;
try {
jsonObject = JsonParser.object().from(json);
} catch (final JsonParserException e) {
return;
}
final String name = jsonObject.getString("name");
final String url = jsonObject.getString("url");
final PeertubeInstance instance = new PeertubeInstance(url, name);
ServiceList.PeerTube.setInstance(instance);
}
}
public static void initServices(final Context context) {
for (final StreamingService s : ServiceList.all()) {
initService(context, s.getServiceId());
}
}
}

View File

@@ -1,168 +0,0 @@
/*
* SPDX-FileCopyrightText: 2018-2026 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.util
import android.content.Context
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import com.grack.nanojson.JsonParser
import java.util.concurrent.TimeUnit
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.NewPipe
import org.schabi.newpipe.extractor.ServiceList
import org.schabi.newpipe.extractor.StreamingService
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance
import org.schabi.newpipe.ktx.getStringSafe
object ServiceHelper {
private val DEFAULT_FALLBACK_SERVICE: StreamingService = ServiceList.YouTube
@JvmStatic
@DrawableRes
fun getIcon(serviceId: Int): Int {
return when (serviceId) {
0 -> R.drawable.ic_smart_display
1 -> R.drawable.ic_cloud
2 -> R.drawable.ic_placeholder_media_ccc
3 -> R.drawable.ic_placeholder_peertube
4 -> R.drawable.ic_placeholder_bandcamp
else -> R.drawable.ic_circle
}
}
@JvmStatic
fun getTranslatedFilterString(filter: String, context: Context): String {
return when (filter) {
"all" -> context.getString(R.string.all)
"videos", "sepia_videos", "music_videos" -> context.getString(R.string.videos_string)
"channels" -> context.getString(R.string.channels)
"playlists", "music_playlists" -> context.getString(R.string.playlists)
"tracks" -> context.getString(R.string.tracks)
"users" -> context.getString(R.string.users)
"conferences" -> context.getString(R.string.conferences)
"events" -> context.getString(R.string.events)
"music_songs" -> context.getString(R.string.songs)
"music_albums" -> context.getString(R.string.albums)
"music_artists" -> context.getString(R.string.artists)
else -> filter
}
}
/**
* Get a resource string with instructions for importing subscriptions for each service.
*
* @param serviceId service to get the instructions for
* @return the string resource containing the instructions or -1 if the service don't support it
*/
@JvmStatic
@StringRes
fun getImportInstructions(serviceId: Int): Int {
return when (serviceId) {
0 -> R.string.import_youtube_instructions
1 -> R.string.import_soundcloud_instructions
else -> -1
}
}
/**
* For services that support importing from a channel url, return a hint that will
* be used in the EditText that the user will type in his channel url.
*
* @param serviceId service to get the hint for
* @return the hint's string resource or -1 if the service don't support it
*/
@JvmStatic
@StringRes
fun getImportInstructionsHint(serviceId: Int): Int {
return when (serviceId) {
1 -> R.string.import_soundcloud_instructions_hint
else -> -1
}
}
@JvmStatic
fun getSelectedServiceId(context: Context): Int {
return (getSelectedService(context) ?: DEFAULT_FALLBACK_SERVICE).serviceId
}
@JvmStatic
fun getSelectedService(context: Context): StreamingService? {
val serviceName: String = PreferenceManager.getDefaultSharedPreferences(context)
.getStringSafe(
context.getString(R.string.current_service_key),
context.getString(R.string.default_service_value)
)
return runCatching { NewPipe.getService(serviceName) }.getOrNull()
}
@JvmStatic
fun getNameOfServiceById(serviceId: Int): String {
return ServiceList.all().stream()
.filter { it.serviceId == serviceId }
.findFirst()
.map(StreamingService::getServiceInfo)
.map(StreamingService.ServiceInfo::getName)
.orElse("<unknown>")
}
/**
* @param serviceId the id of the service
* @return the service corresponding to the provided id
* @throws java.util.NoSuchElementException if there is no service with the provided id
*/
@JvmStatic
fun getServiceById(serviceId: Int): StreamingService {
return ServiceList.all().firstNotNullOf { it.takeIf { it.serviceId == serviceId } }
}
@JvmStatic
fun setSelectedServiceId(context: Context, serviceId: Int) {
val serviceName = runCatching { NewPipe.getService(serviceId).serviceInfo.name }
.getOrDefault(DEFAULT_FALLBACK_SERVICE.serviceInfo.name)
setSelectedServicePreferences(context, serviceName)
}
private fun setSelectedServicePreferences(context: Context, serviceName: String?) {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
sharedPreferences.edit { putString(context.getString(R.string.current_service_key), serviceName) }
}
@JvmStatic
fun getCacheExpirationMillis(serviceId: Int): Long {
return if (serviceId == ServiceList.SoundCloud.serviceId) {
TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES)
} else {
TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS)
}
}
fun initService(context: Context, serviceId: Int) {
if (serviceId == ServiceList.PeerTube.serviceId) {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val json = sharedPreferences.getString(
context.getString(R.string.peertube_selected_instance_key),
null
) ?: return
val jsonObject = runCatching { JsonParser.`object`().from(json) }
.getOrElse { return@initService }
ServiceList.PeerTube.instance = PeertubeInstance(
jsonObject.getString("url"),
jsonObject.getString("name")
)
}
}
@JvmStatic
fun initServices(context: Context) {
ServiceList.all().forEach { initService(context, it.serviceId) }
}
}

View File

@@ -0,0 +1,50 @@
package org.schabi.newpipe.util;
import org.schabi.newpipe.extractor.stream.StreamType;
/**
* Utility class for {@link StreamType}.
*/
public final class StreamTypeUtil {
private StreamTypeUtil() {
// No impl pls
}
/**
* Check if the {@link StreamType} of a stream is a livestream.
*
* @param streamType the stream type of the stream
* @return whether the stream type is {@link StreamType#AUDIO_STREAM},
* {@link StreamType#AUDIO_LIVE_STREAM} or {@link StreamType#POST_LIVE_AUDIO_STREAM}
*/
public static boolean isAudio(final StreamType streamType) {
return streamType == StreamType.AUDIO_STREAM
|| streamType == StreamType.AUDIO_LIVE_STREAM
|| streamType == StreamType.POST_LIVE_AUDIO_STREAM;
}
/**
* Check if the {@link StreamType} of a stream is a livestream.
*
* @param streamType the stream type of the stream
* @return whether the stream type is {@link StreamType#VIDEO_STREAM},
* {@link StreamType#LIVE_STREAM} or {@link StreamType#POST_LIVE_STREAM}
*/
public static boolean isVideo(final StreamType streamType) {
return streamType == StreamType.VIDEO_STREAM
|| streamType == StreamType.LIVE_STREAM
|| streamType == StreamType.POST_LIVE_STREAM;
}
/**
* Check if the {@link StreamType} of a stream is a livestream.
*
* @param streamType the stream type of the stream
* @return whether the stream type is {@link StreamType#LIVE_STREAM} or
* {@link StreamType#AUDIO_LIVE_STREAM}
*/
public static boolean isLiveStream(final StreamType streamType) {
return streamType == StreamType.LIVE_STREAM
|| streamType == StreamType.AUDIO_LIVE_STREAM;
}
}

View File

@@ -1,54 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021-2026 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.util
import org.schabi.newpipe.extractor.stream.StreamType
/**
* Utility class for [StreamType].
*/
object StreamTypeUtil {
/**
* Check if the [StreamType] of a stream is a livestream.
*
* @param streamType the stream type of the stream
* @return whether the stream type is [StreamType.AUDIO_STREAM],
* [StreamType.AUDIO_LIVE_STREAM] or [StreamType.POST_LIVE_AUDIO_STREAM]
*/
@JvmStatic
fun isAudio(streamType: StreamType): Boolean {
return streamType == StreamType.AUDIO_STREAM ||
streamType == StreamType.AUDIO_LIVE_STREAM ||
streamType == StreamType.POST_LIVE_AUDIO_STREAM
}
/**
* Check if the [StreamType] of a stream is a livestream.
*
* @param streamType the stream type of the stream
* @return whether the stream type is [StreamType.VIDEO_STREAM],
* [StreamType.LIVE_STREAM] or [StreamType.POST_LIVE_STREAM]
*/
@JvmStatic
fun isVideo(streamType: StreamType): Boolean {
return streamType == StreamType.VIDEO_STREAM ||
streamType == StreamType.LIVE_STREAM ||
streamType == StreamType.POST_LIVE_STREAM
}
/**
* Check if the [StreamType] of a stream is a livestream.
*
* @param streamType the stream type of the stream
* @return whether the stream type is [StreamType.LIVE_STREAM] or
* [StreamType.AUDIO_LIVE_STREAM]
*/
@JvmStatic
fun isLiveStream(streamType: StreamType): Boolean {
return streamType == StreamType.LIVE_STREAM ||
streamType == StreamType.AUDIO_LIVE_STREAM
}
}

View File

@@ -40,7 +40,6 @@ import android.view.Window;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
import androidx.appcompat.view.WindowCallbackWrapper;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
@@ -232,7 +231,7 @@ public final class FocusOverlayView extends Drawable implements
// Unfortunately many such forms of "scrolling" do not count as scrolling for purpose // Unfortunately many such forms of "scrolling" do not count as scrolling for purpose
// of dispatching ViewTreeObserver callbacks, so we have to intercept them by directly // of dispatching ViewTreeObserver callbacks, so we have to intercept them by directly
// receiving keys from Window. // receiving keys from Window.
window.setCallback(new WindowCallbackWrapper(window.getCallback()) { window.setCallback(new SimpleWindowCallback(window.getCallback()) {
@Override @Override
public boolean dispatchKeyEvent(final KeyEvent event) { public boolean dispatchKeyEvent(final KeyEvent event) {
final boolean res = super.dispatchKeyEvent(event); final boolean res = super.dispatchKeyEvent(event);

View File

@@ -0,0 +1,35 @@
/*
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.views
import android.os.Build
import android.view.KeyEvent
import android.view.KeyboardShortcutGroup
import android.view.Menu
import android.view.Window
import androidx.annotation.RequiresApi
open class SimpleWindowCallback(private val baseCallback: Window.Callback) :
Window.Callback by baseCallback {
override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
return baseCallback.dispatchKeyEvent(event)
}
@RequiresApi(Build.VERSION_CODES.O)
override fun onPointerCaptureChanged(hasCapture: Boolean) {
baseCallback.onPointerCaptureChanged(hasCapture)
}
@RequiresApi(Build.VERSION_CODES.N)
override fun onProvideKeyboardShortcuts(
data: List<KeyboardShortcutGroup?>?,
menu: Menu?,
deviceId: Int
) {
baseCallback.onProvideKeyboardShortcuts(data, menu, deviceId)
}
}

View File

@@ -21,7 +21,7 @@ import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_FORBIDDEN;
* Single-threaded fallback mode * Single-threaded fallback mode
*/ */
public class DownloadRunnableFallback extends Thread { public class DownloadRunnableFallback extends Thread {
private static final String TAG = "DownloadRunnableFallback"; private static final String TAG = DownloadRunnableFallback.class.getSimpleName();
private final DownloadMission mMission; private final DownloadMission mMission;

View File

@@ -102,14 +102,23 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
db.beginTransaction(); db.beginTransaction();
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(KEY_SOURCE, cursor.getString(cursor.getColumnIndex(KEY_SOURCE))); values.put(
values.put(KEY_DONE, cursor.getString(cursor.getColumnIndex(KEY_DONE))); KEY_SOURCE,
values.put(KEY_TIMESTAMP, cursor.getLong(cursor.getColumnIndex(KEY_TIMESTAMP))); cursor.getString(cursor.getColumnIndexOrThrow(KEY_SOURCE))
values.put(KEY_KIND, cursor.getString(cursor.getColumnIndex(KEY_KIND))); );
values.put(
KEY_DONE,
cursor.getString(cursor.getColumnIndexOrThrow(KEY_DONE))
);
values.put(
KEY_TIMESTAMP,
cursor.getLong(cursor.getColumnIndexOrThrow(KEY_TIMESTAMP))
);
values.put(KEY_KIND, cursor.getString(cursor.getColumnIndexOrThrow(KEY_KIND)));
values.put(KEY_PATH, Uri.fromFile( values.put(KEY_PATH, Uri.fromFile(
new File( new File(
cursor.getString(cursor.getColumnIndex(KEY_LOCATION)), cursor.getString(cursor.getColumnIndexOrThrow(KEY_LOCATION)),
cursor.getString(cursor.getColumnIndex(KEY_NAME)) cursor.getString(cursor.getColumnIndexOrThrow(KEY_NAME))
) )
).toString()); ).toString());
@@ -141,7 +150,8 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
} }
private FinishedMission getMissionFromCursor(Cursor cursor) { private FinishedMission getMissionFromCursor(Cursor cursor) {
String kind = Objects.requireNonNull(cursor).getString(cursor.getColumnIndex(KEY_KIND)); String kind = Objects.requireNonNull(cursor)
.getString(cursor.getColumnIndexOrThrow(KEY_KIND));
if (kind == null || kind.isEmpty()) kind = "?"; if (kind == null || kind.isEmpty()) kind = "?";
String path = cursor.getString(cursor.getColumnIndexOrThrow(KEY_PATH)); String path = cursor.getString(cursor.getColumnIndexOrThrow(KEY_PATH));

View File

@@ -632,103 +632,95 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
DownloadMission mission = h.item.mission instanceof DownloadMission ? (DownloadMission) h.item.mission : null; DownloadMission mission = h.item.mission instanceof DownloadMission ? (DownloadMission) h.item.mission : null;
if (mission != null) { if (mission != null) {
switch (id) { if (id == R.id.start) {
case R.id.start: h.status.setText(UNDEFINED_PROGRESS);
h.status.setText(UNDEFINED_PROGRESS); mDownloadManager.resumeMission(mission);
mDownloadManager.resumeMission(mission); return true;
return true; } else if (id == R.id.pause) {
case R.id.pause: mDownloadManager.pauseMission(mission);
mDownloadManager.pauseMission(mission); return true;
return true; } else if (id == R.id.error_message_view) {
case R.id.error_message_view: showError(mission);
showError(mission); return true;
return true; } else if (id == R.id.queue) {
case R.id.queue: boolean flag = !h.queue.isChecked();
boolean flag = !h.queue.isChecked(); h.queue.setChecked(flag);
h.queue.setChecked(flag); mission.setEnqueued(flag);
mission.setEnqueued(flag); updateProgress(h);
updateProgress(h); return true;
return true; } else if (id == R.id.retry) {
case R.id.retry: if (mission.isPsRunning()) {
if (mission.isPsRunning()) { mission.psContinue(true);
mission.psContinue(true); } else {
} else { mDownloadManager.tryRecover(mission);
mDownloadManager.tryRecover(mission); if (mission.storage.isInvalid())
if (mission.storage.isInvalid()) mRecover.tryRecover(mission);
mRecover.tryRecover(mission); else
else recoverMission(mission);
recoverMission(mission); }
} return true;
return true; } else if (id == R.id.cancel) {
case R.id.cancel: mission.psContinue(false);
mission.psContinue(false); return false;
return false;
} }
} }
switch (id) { if (id == R.id.menu_item_share) {
case R.id.menu_item_share: shareFile(h.item.mission);
shareFile(h.item.mission); return true;
return true; } else if (id == R.id.delete) {// delete the entry and the file
case R.id.delete: mDeleter.append(h.item.mission, true);
// delete the entry and the file applyChanges();
checkMasterButtonsVisibility();
return true;
} else if (id == R.id.delete_entry) {// just delete the entry
mDeleter.append(h.item.mission, false);
applyChanges();
checkMasterButtonsVisibility();
return true;
} else if (id == R.id.md5 || id == R.id.sha1) {
final StoredFileHelper storage = h.item.mission.storage;
if (!storage.existsAsFile()) {
Toast.makeText(mContext, R.string.missing_file, Toast.LENGTH_SHORT).show();
mDeleter.append(h.item.mission, true); mDeleter.append(h.item.mission, true);
applyChanges(); applyChanges();
checkMasterButtonsVisibility();
return true; return true;
case R.id.delete_entry: }
// just delete the entry final NotificationManager notificationManager
mDeleter.append(h.item.mission, false); = ContextCompat.getSystemService(mContext, NotificationManager.class);
applyChanges(); final NotificationCompat.Builder progressNotificationBuilder
checkMasterButtonsVisibility(); = new NotificationCompat.Builder(mContext,
return true; mContext.getString(R.string.hash_channel_id))
case R.id.md5: .setPriority(NotificationCompat.PRIORITY_HIGH)
case R.id.sha1: .setSmallIcon(R.drawable.ic_newpipe_triangle_white)
final StoredFileHelper storage = h.item.mission.storage; .setContentTitle(mContext.getString(R.string.msg_calculating_hash))
if (!storage.existsAsFile()) { .setContentText(mContext.getString(R.string.msg_wait))
Toast.makeText(mContext, R.string.missing_file, Toast.LENGTH_SHORT).show(); .setProgress(0, 0, true)
mDeleter.append(h.item.mission, true); .setOngoing(true);
applyChanges();
return true;
}
final NotificationManager notificationManager
= ContextCompat.getSystemService(mContext, NotificationManager.class);
final NotificationCompat.Builder progressNotificationBuilder
= new NotificationCompat.Builder(mContext,
mContext.getString(R.string.hash_channel_id))
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
.setContentTitle(mContext.getString(R.string.msg_calculating_hash))
.setContentText(mContext.getString(R.string.msg_wait))
.setProgress(0, 0, true)
.setOngoing(true);
notificationManager.notify(HASH_NOTIFICATION_ID, progressNotificationBuilder notificationManager.notify(HASH_NOTIFICATION_ID, progressNotificationBuilder
.build()); .build());
compositeDisposable.add( compositeDisposable.add(
Observable.fromCallable(() -> Utility.checksum(storage, id)) Observable.fromCallable(() -> Utility.checksum(storage, id))
.subscribeOn(Schedulers.computation()) .subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> { .subscribe(result -> {
ShareUtils.copyToClipboard(mContext, result); ShareUtils.copyToClipboard(mContext, result);
notificationManager.cancel(HASH_NOTIFICATION_ID); notificationManager.cancel(HASH_NOTIFICATION_ID);
}) })
); );
return true; return true;
case R.id.source: } else if (id == R.id.source) {
/*Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(h.item.mission.source)); try {
mContext.startActivity(intent);*/ Intent intent = NavigationHelper.getIntentByLink(mContext, h.item.mission.source);
try { intent.addFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);
Intent intent = NavigationHelper.getIntentByLink(mContext, h.item.mission.source); mContext.startActivity(intent);
intent.addFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP); } catch (Exception e) {
mContext.startActivity(intent); Log.w(TAG, "Selected item has a invalid source", e);
} catch (Exception e) { }
Log.w(TAG, "Selected item has a invalid source", e); return true;
}
return true;
default:
return false;
} }
return false;
} }
public void applyChanges() { public void applyChanges() {

View File

@@ -186,23 +186,24 @@ public class MissionsFragment extends Fragment {
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) { int itemId = item.getItemId();
case R.id.switch_mode: if (itemId == R.id.switch_mode) {
mLinear = !mLinear; mLinear = !mLinear;
updateList(); updateList();
return true; return true;
case R.id.clear_list: } else if (itemId == R.id.clear_list) {
showClearDownloadHistoryPrompt(); showClearDownloadHistoryPrompt();
return true; return true;
case R.id.start_downloads: } else if (itemId == R.id.start_downloads) {
mBinder.getDownloadManager().startAllMissions(); mBinder.getDownloadManager().startAllMissions();
return true; return true;
case R.id.pause_downloads: } else if (itemId == R.id.pause_downloads) {
mBinder.getDownloadManager().pauseAllMissions(false); mBinder.getDownloadManager().pauseAllMissions(false);
mAdapter.refreshMissionItems();// update items view mAdapter.refreshMissionItems();// update items view
default:
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
return super.onOptionsItemSelected(item);
} }
public void showClearDownloadHistoryPrompt() { public void showClearDownloadHistoryPrompt() {

View File

@@ -5,6 +5,6 @@
android:viewportWidth="24" android:viewportWidth="24"
android:tint="@color/defaultIconTint"> android:tint="@color/defaultIconTint">
<path <path
android:pathData="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zM9.5 16.5v-9l7 4.5-7 4.5z" android:pathData="M20 4H4c-1.1 0-2 0.9-2 2v12c0 1.1 0.9 2 2 2h16c1.1 0 2-0.9 2-2V6c0-1.1-0.9-2-2-2zM9.5 16.5v-9l7 4.5-7 4.5z"
android:fillColor="#FF000000"/> android:fillColor="#FF000000"/>
</vector> </vector>

View File

@@ -122,8 +122,8 @@
android:focusable="true" android:focusable="true"
android:scaleType="fitCenter" android:scaleType="fitCenter"
android:src="@drawable/exo_controls_rewind" android:src="@drawable/exo_controls_rewind"
android:tint="?attr/colorAccent" android:contentDescription="@string/rewind"
android:contentDescription="@string/rewind" /> app:tint="?attr/colorAccent" />
<ImageButton <ImageButton
android:id="@+id/control_play_pause" android:id="@+id/control_play_pause"
@@ -139,8 +139,8 @@
android:focusable="true" android:focusable="true"
android:scaleType="fitCenter" android:scaleType="fitCenter"
android:src="@drawable/ic_pause" android:src="@drawable/ic_pause"
android:tint="?attr/colorAccent" android:contentDescription="@string/pause"
android:contentDescription="@string/pause" /> app:tint="?attr/colorAccent" />
<ProgressBar <ProgressBar
android:id="@+id/control_progress_bar" android:id="@+id/control_progress_bar"
@@ -172,8 +172,8 @@
android:focusable="true" android:focusable="true"
android:scaleType="fitCenter" android:scaleType="fitCenter"
android:src="@drawable/exo_controls_fastforward" android:src="@drawable/exo_controls_fastforward"
android:tint="?attr/colorAccent" android:contentDescription="@string/forward"
android:contentDescription="@string/forward" /> app:tint="?attr/colorAccent" />
</RelativeLayout> </RelativeLayout>
<RelativeLayout <RelativeLayout
@@ -215,8 +215,8 @@
android:focusable="true" android:focusable="true"
android:scaleType="fitXY" android:scaleType="fitXY"
android:src="@drawable/ic_repeat" android:src="@drawable/ic_repeat"
android:tint="?attr/colorAccent" android:contentDescription="@string/notification_action_repeat"
android:contentDescription="@string/notification_action_repeat" /> app:tint="?attr/colorAccent" />
<View <View
android:id="@+id/anchor" android:id="@+id/anchor"
@@ -236,8 +236,8 @@
android:focusable="true" android:focusable="true"
android:scaleType="fitXY" android:scaleType="fitXY"
android:src="@drawable/ic_shuffle" android:src="@drawable/ic_shuffle"
android:tint="?attr/colorAccent" android:contentDescription="@string/notification_action_shuffle"
android:contentDescription="@string/notification_action_shuffle" /> app:tint="?attr/colorAccent" />
<androidx.appcompat.widget.AppCompatImageButton <androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/control_forward" android:id="@+id/control_forward"

View File

@@ -175,7 +175,7 @@
android:focusable="true" android:focusable="true"
android:scaleType="fitXY" android:scaleType="fitXY"
android:src="@drawable/ic_repeat" android:src="@drawable/ic_repeat"
android:tint="?attr/colorAccent" app:tint="?attr/colorAccent"
tools:ignore="ContentDescription" /> tools:ignore="ContentDescription" />
<androidx.appcompat.widget.AppCompatImageButton <androidx.appcompat.widget.AppCompatImageButton
@@ -205,7 +205,7 @@
android:focusable="true" android:focusable="true"
android:scaleType="fitCenter" android:scaleType="fitCenter"
android:src="@drawable/exo_controls_rewind" android:src="@drawable/exo_controls_rewind"
android:tint="?attr/colorAccent" /> app:tint="?attr/colorAccent" />
<ImageButton <ImageButton
android:id="@+id/control_play_pause" android:id="@+id/control_play_pause"
@@ -220,7 +220,7 @@
android:focusable="true" android:focusable="true"
android:scaleType="fitCenter" android:scaleType="fitCenter"
android:src="@drawable/ic_pause" android:src="@drawable/ic_pause"
android:tint="?attr/colorAccent" app:tint="?attr/colorAccent"
tools:ignore="ContentDescription" /> tools:ignore="ContentDescription" />
<ProgressBar <ProgressBar
@@ -255,7 +255,7 @@
android:focusable="true" android:focusable="true"
android:scaleType="fitCenter" android:scaleType="fitCenter"
android:src="@drawable/exo_controls_fastforward" android:src="@drawable/exo_controls_fastforward"
android:tint="?attr/colorAccent" /> app:tint="?attr/colorAccent" />
<androidx.appcompat.widget.AppCompatImageButton <androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/control_forward" android:id="@+id/control_forward"
@@ -285,7 +285,7 @@
android:focusable="true" android:focusable="true"
android:scaleType="fitXY" android:scaleType="fitXY"
android:src="@drawable/ic_shuffle" android:src="@drawable/ic_shuffle"
android:tint="?attr/colorAccent" app:tint="?attr/colorAccent"
tools:ignore="ContentDescription" /> tools:ignore="ContentDescription" />
</RelativeLayout> </RelativeLayout>

View File

@@ -309,6 +309,6 @@
<string name="notification_action_nothing">কিছু না</string> <string name="notification_action_nothing">কিছু না</string>
<string name="yes">হ্যাঁ</string> <string name="yes">হ্যাঁ</string>
<string name="no">না</string> <string name="no">না</string>
<string name="search_with_service_name">সার্চ</string> <string name="search_with_service_name">সার্চ %1$s</string>
<string name="search_with_service_name_and_filter">খুঁজুন</string> <string name="search_with_service_name_and_filter">খুঁজুন %1$s (%2$s)</string>
</resources> </resources>

View File

@@ -1,4 +1,3 @@
android.nonFinalResIds=false
android.useAndroidX=true android.useAndroidX=true
org.gradle.jvmargs=-Xmx2048M --add-opens jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED org.gradle.jvmargs=-Xmx2048M --add-opens jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED
systemProp.file.encoding=utf-8 systemProp.file.encoding=utf-8