Skip to content

Commit 6850a27

Browse files
Merge branch 'dev' into fix-soundcloud-hls-expiry
2 parents 552c1b0 + 938265d commit 6850a27

94 files changed

Lines changed: 1303 additions & 323 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ dependencies {
209209
// Or you can use a commit you pushed to GitHub by just replacing TeamNewPipe with your GitHub
210210
// name and the commit hash with the commit hash of the (pushed) commit you want to test
211211
// This works thanks to JitPack: https://jitpack.io/
212-
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
212+
implementation 'com.github.TeamNewPipe:nanojson:e9d656ddb49a412a5a0a5d5ef20ca7ef09549996'
213213
// WORKAROUND: if you get errors with the NewPipeExtractor dependency, replace `v0.24.3` with
214214
// the corresponding commit hash, since JitPack sometimes deletes artifacts.
215215
// If there’s already a git hash, just add more of it to the end (or remove a letter)

app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,15 @@ public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGrou
219219
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
220220
searchBinding = FragmentSearchBinding.bind(rootView);
221221
super.onViewCreated(rootView, savedInstanceState);
222+
223+
updateService();
224+
// Add the service name to search string hint
225+
// to make it more obvious which platform is being searched.
226+
if (service != null) {
227+
searchEditText.setHint(
228+
getString(R.string.search_with_service_name,
229+
service.getServiceInfo().getName()));
230+
}
222231
showSearchOnStart();
223232
initSearchListeners();
224233
}
@@ -936,6 +945,20 @@ private void changeContentFilter(final MenuItem item, final List<String> theCont
936945
filterItemCheckedId = item.getItemId();
937946
item.setChecked(true);
938947

948+
if (service != null) {
949+
final boolean isNotFiltered = theContentFilter.isEmpty()
950+
|| "all".equals(theContentFilter.get(0));
951+
if (isNotFiltered) {
952+
searchEditText.setHint(
953+
getString(R.string.search_with_service_name,
954+
service.getServiceInfo().getName()));
955+
} else {
956+
searchEditText.setHint(getString(R.string.search_with_service_name_and_filter,
957+
service.getServiceInfo().getName(),
958+
item.getTitle()));
959+
}
960+
}
961+
939962
contentFilter = theContentFilter.toArray(new String[0]);
940963

941964
if (!TextUtils.isEmpty(searchString)) {

app/src/main/java/org/schabi/newpipe/player/PlayerService.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,6 @@ public void setPlayerListener(@Nullable final Consumer<Player> listener) {
330330
public BrowserRoot onGetRoot(@NonNull final String clientPackageName,
331331
final int clientUid,
332332
@Nullable final Bundle rootHints) {
333-
// TODO check if the accessing package has permission to view data
334333
return mediaBrowserImpl.onGetRoot(clientPackageName, clientUid, rootHints);
335334
}
336335

app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserImpl.kt

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import android.util.Log
1010
import androidx.annotation.DrawableRes
1111
import androidx.core.net.toUri
1212
import androidx.media.MediaBrowserServiceCompat
13+
import androidx.media.MediaBrowserServiceCompat.BrowserRoot.EXTRA_RECENT
1314
import androidx.media.MediaBrowserServiceCompat.Result
1415
import androidx.media.utils.MediaConstants
1516
import io.reactivex.rxjava3.core.Flowable
@@ -48,6 +49,7 @@ class MediaBrowserImpl(
4849
private val context: Context,
4950
notifyChildrenChanged: Consumer<String>, // parentId
5051
) {
52+
private val packageValidator = PackageValidator(context)
5153
private val database = NewPipeDatabase.getInstance(context)
5254
private var disposables = CompositeDisposable()
5355

@@ -69,11 +71,22 @@ class MediaBrowserImpl(
6971
clientPackageName: String,
7072
clientUid: Int,
7173
rootHints: Bundle?
72-
): MediaBrowserServiceCompat.BrowserRoot {
74+
): MediaBrowserServiceCompat.BrowserRoot? {
7375
if (DEBUG) {
7476
Log.d(TAG, "onGetRoot($clientPackageName, $clientUid, $rootHints)")
7577
}
7678

79+
if (!packageValidator.isKnownCaller(clientPackageName, clientUid)) {
80+
// this is a caller we can't trust (see PackageValidator's rules taken from uamp)
81+
return null
82+
}
83+
84+
if (rootHints?.getBoolean(EXTRA_RECENT, false) == true) {
85+
// the system is asking for a root to do media resumption, but we can't handle that yet,
86+
// see https://developer.android.com/media/implement/surfaces/mobile#mediabrowserservice_implementation
87+
return null
88+
}
89+
7790
val extras = Bundle()
7891
extras.putBoolean(
7992
MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
/*
2+
* Copyright 2018 Google Inc. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
// THIS FILE WAS TAKEN FROM UAMP, EXCEPT FOR THINGS RELATED TO THE WHITELIST. UPDATE IT WHEN NEEDED.
18+
// https://github.com/android/uamp/blob/329a21b63c247e9bd35f6858d4fc0e448fa38603/common/src/main/java/com/example/android/uamp/media/PackageValidator.kt
19+
20+
package org.schabi.newpipe.player.mediabrowser
21+
22+
import android.Manifest.permission.MEDIA_CONTENT_CONTROL
23+
import android.annotation.SuppressLint
24+
import android.content.Context
25+
import android.content.pm.PackageInfo
26+
import android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED
27+
import android.content.pm.PackageManager
28+
import android.os.Process
29+
import android.support.v4.media.session.MediaSessionCompat
30+
import android.util.Log
31+
import androidx.core.app.NotificationManagerCompat
32+
import androidx.media.MediaBrowserServiceCompat
33+
import org.schabi.newpipe.BuildConfig
34+
import java.security.MessageDigest
35+
import java.security.NoSuchAlgorithmException
36+
37+
/**
38+
* Validates that the calling package is authorized to browse a [MediaBrowserServiceCompat].
39+
*
40+
* The list of allowed signing certificates and their corresponding package names is defined in
41+
* res/xml/allowed_media_browser_callers.xml.
42+
*
43+
* If you want to add a new caller to allowed_media_browser_callers.xml and you don't know
44+
* its signature, this class will print to logcat (INFO level) a message with the proper
45+
* xml tags to add to allow the caller.
46+
*
47+
* For more information, see res/xml/allowed_media_browser_callers.xml.
48+
*/
49+
internal class PackageValidator(context: Context) {
50+
private val context: Context = context.applicationContext
51+
private val packageManager: PackageManager = this.context.packageManager
52+
private val platformSignature: String = getSystemSignature()
53+
private val callerChecked = mutableMapOf<String, Pair<Int, Boolean>>()
54+
55+
/**
56+
* Checks whether the caller attempting to connect to a [MediaBrowserServiceCompat] is known.
57+
* See [MusicService.onGetRoot] for where this is utilized.
58+
*
59+
* @param callingPackage The package name of the caller.
60+
* @param callingUid The user id of the caller.
61+
* @return `true` if the caller is known, `false` otherwise.
62+
*/
63+
fun isKnownCaller(callingPackage: String, callingUid: Int): Boolean {
64+
// If the caller has already been checked, return the previous result here.
65+
val (checkedUid, checkResult) = callerChecked[callingPackage] ?: Pair(0, false)
66+
if (checkedUid == callingUid) {
67+
return checkResult
68+
}
69+
70+
/**
71+
* Because some of these checks can be slow, we save the results in [callerChecked] after
72+
* this code is run.
73+
*
74+
* In particular, there's little reason to recompute the calling package's certificate
75+
* signature (SHA-256) each call.
76+
*
77+
* This is safe to do as we know the UID matches the package's UID (from the check above),
78+
* and app UIDs are set at install time. Additionally, a package name + UID is guaranteed to
79+
* be constant until a reboot. (After a reboot then a previously assigned UID could be
80+
* reassigned.)
81+
*/
82+
83+
// Build the caller info for the rest of the checks here.
84+
val callerPackageInfo = buildCallerInfo(callingPackage)
85+
?: throw IllegalStateException("Caller wasn't found in the system?")
86+
87+
// Verify that things aren't ... broken. (This test should always pass.)
88+
if (callerPackageInfo.uid != callingUid) {
89+
throw IllegalStateException("Caller's package UID doesn't match caller's actual UID?")
90+
}
91+
92+
val callerSignature = callerPackageInfo.signature
93+
94+
val isCallerKnown = when {
95+
// If it's our own app making the call, allow it.
96+
callingUid == Process.myUid() -> true
97+
// If the system is making the call, allow it.
98+
callingUid == Process.SYSTEM_UID -> true
99+
// If the app was signed by the same certificate as the platform itself, also allow it.
100+
callerSignature == platformSignature -> true
101+
/**
102+
* [MEDIA_CONTENT_CONTROL] permission is only available to system applications, and
103+
* while it isn't required to allow these apps to connect to a
104+
* [MediaBrowserServiceCompat], allowing this ensures optimal compatability with apps
105+
* such as Android TV and the Google Assistant.
106+
*/
107+
callerPackageInfo.permissions.contains(MEDIA_CONTENT_CONTROL) -> true
108+
/**
109+
* If the calling app has a notification listener it is able to retrieve notifications
110+
* and can connect to an active [MediaSessionCompat].
111+
*
112+
* It's not required to allow apps with a notification listener to
113+
* connect to your [MediaBrowserServiceCompat], but it does allow easy compatibility
114+
* with apps such as Wear OS.
115+
*/
116+
NotificationManagerCompat.getEnabledListenerPackages(this.context)
117+
.contains(callerPackageInfo.packageName) -> true
118+
119+
// If none of the previous checks succeeded, then the caller is unrecognized.
120+
else -> false
121+
}
122+
123+
if (!isCallerKnown) {
124+
logUnknownCaller(callerPackageInfo)
125+
}
126+
127+
// Save our work for next time.
128+
callerChecked[callingPackage] = Pair(callingUid, isCallerKnown)
129+
return isCallerKnown
130+
}
131+
132+
/**
133+
* Logs an info level message with details of how to add a caller to the allowed callers list
134+
* when the app is debuggable.
135+
*/
136+
private fun logUnknownCaller(callerPackageInfo: CallerPackageInfo) {
137+
if (BuildConfig.DEBUG) {
138+
Log.w(TAG, "Unknown caller $callerPackageInfo")
139+
}
140+
}
141+
142+
/**
143+
* Builds a [CallerPackageInfo] for a given package that can be used for all the
144+
* various checks that are performed before allowing an app to connect to a
145+
* [MediaBrowserServiceCompat].
146+
*/
147+
private fun buildCallerInfo(callingPackage: String): CallerPackageInfo? {
148+
val packageInfo = getPackageInfo(callingPackage) ?: return null
149+
150+
val appName = packageInfo.applicationInfo.loadLabel(packageManager).toString()
151+
val uid = packageInfo.applicationInfo.uid
152+
val signature = getSignature(packageInfo)
153+
154+
val requestedPermissions = packageInfo.requestedPermissions
155+
val permissionFlags = packageInfo.requestedPermissionsFlags
156+
val activePermissions = mutableSetOf<String>()
157+
requestedPermissions?.forEachIndexed { index, permission ->
158+
if (permissionFlags[index] and REQUESTED_PERMISSION_GRANTED != 0) {
159+
activePermissions += permission
160+
}
161+
}
162+
163+
return CallerPackageInfo(appName, callingPackage, uid, signature, activePermissions.toSet())
164+
}
165+
166+
/**
167+
* Looks up the [PackageInfo] for a package name.
168+
* This requests both the signatures (for checking if an app is on the allow list) and
169+
* the app's permissions, which allow for more flexibility in the allow list.
170+
*
171+
* @return [PackageInfo] for the package name or null if it's not found.
172+
*/
173+
@Suppress("deprecation")
174+
@SuppressLint("PackageManagerGetSignatures")
175+
private fun getPackageInfo(callingPackage: String): PackageInfo? =
176+
packageManager.getPackageInfo(
177+
callingPackage,
178+
PackageManager.GET_SIGNATURES or PackageManager.GET_PERMISSIONS
179+
)
180+
181+
/**
182+
* Gets the signature of a given package's [PackageInfo].
183+
*
184+
* The "signature" is a SHA-256 hash of the public key of the signing certificate used by
185+
* the app.
186+
*
187+
* If the app is not found, or if the app does not have exactly one signature, this method
188+
* returns `null` as the signature.
189+
*/
190+
@Suppress("deprecation")
191+
private fun getSignature(packageInfo: PackageInfo): String? =
192+
if (packageInfo.signatures == null || packageInfo.signatures.size != 1) {
193+
// Security best practices dictate that an app should be signed with exactly one (1)
194+
// signature. Because of this, if there are multiple signatures, reject it.
195+
null
196+
} else {
197+
val certificate = packageInfo.signatures[0].toByteArray()
198+
getSignatureSha256(certificate)
199+
}
200+
201+
/**
202+
* Finds the Android platform signing key signature. This key is never null.
203+
*/
204+
private fun getSystemSignature(): String =
205+
getPackageInfo(ANDROID_PLATFORM)?.let { platformInfo ->
206+
getSignature(platformInfo)
207+
} ?: throw IllegalStateException("Platform signature not found")
208+
209+
/**
210+
* Creates a SHA-256 signature given a certificate byte array.
211+
*/
212+
private fun getSignatureSha256(certificate: ByteArray): String {
213+
val md: MessageDigest
214+
try {
215+
md = MessageDigest.getInstance("SHA256")
216+
} catch (noSuchAlgorithmException: NoSuchAlgorithmException) {
217+
Log.e(TAG, "No such algorithm: $noSuchAlgorithmException")
218+
throw RuntimeException("Could not find SHA256 hash algorithm", noSuchAlgorithmException)
219+
}
220+
md.update(certificate)
221+
222+
// This code takes the byte array generated by `md.digest()` and joins each of the bytes
223+
// to a string, applying the string format `%02x` on each digit before it's appended, with
224+
// a colon (':') between each of the items.
225+
// For example: input=[0,2,4,6,8,10,12], output="00:02:04:06:08:0a:0c"
226+
return md.digest().joinToString(":") { String.format("%02x", it) }
227+
}
228+
229+
/**
230+
* Convenience class to hold all of the information about an app that's being checked
231+
* to see if it's a known caller.
232+
*/
233+
private data class CallerPackageInfo(
234+
val name: String,
235+
val packageName: String,
236+
val uid: Int,
237+
val signature: String?,
238+
val permissions: Set<String>
239+
)
240+
}
241+
242+
private const val TAG = "PackageValidator"
243+
private const val ANDROID_PLATFORM = "android"

app/src/main/java/org/schabi/newpipe/util/ChannelTabHelper.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public static boolean isStreamsTab(final String tab) {
2424
switch (tab) {
2525
case ChannelTabs.VIDEOS:
2626
case ChannelTabs.TRACKS:
27+
case ChannelTabs.LIKES:
2728
case ChannelTabs.SHORTS:
2829
case ChannelTabs.LIVESTREAMS:
2930
return true;
@@ -62,6 +63,8 @@ private static int getShowTabKey(final String tab) {
6263
return R.string.show_channel_tabs_playlists;
6364
case ChannelTabs.ALBUMS:
6465
return R.string.show_channel_tabs_albums;
66+
case ChannelTabs.LIKES:
67+
return R.string.show_channel_tabs_likes;
6568
default:
6669
return -1;
6770
}
@@ -78,6 +81,8 @@ private static int getFetchFeedTabKey(final String tab) {
7881
return R.string.fetch_channel_tabs_shorts;
7982
case ChannelTabs.LIVESTREAMS:
8083
return R.string.fetch_channel_tabs_livestreams;
84+
case ChannelTabs.LIKES:
85+
return R.string.fetch_channel_tabs_likes;
8186
default:
8287
return -1;
8388
}
@@ -100,6 +105,8 @@ public static int getTranslationKey(final String tab) {
100105
return R.string.channel_tab_playlists;
101106
case ChannelTabs.ALBUMS:
102107
return R.string.channel_tab_albums;
108+
case ChannelTabs.LIKES:
109+
return R.string.channel_tab_likes;
103110
default:
104111
return R.string.unknown_content;
105112
}

app/src/main/res/values-ar/strings.xml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
<string name="show_play_with_kodi_summary">اعرض خيار لتشغيل الفيديو عبر مركز وسائط Kodi</string>
3838
<string name="show_play_with_kodi_title">عرض خيار التشغيل بواسطة كودي</string>
3939
<string name="theme_title">السمة</string>
40-
<string name="upload_date_text">تم النشر في %1$s</string>
40+
<string name="upload_date_text">منشورة على %1$s</string>
4141
<string name="unsupported_url">رابط غير مدعوم</string>
4242
<string name="use_external_audio_player_title">استخدام مشغل صوت خارجي</string>
4343
<string name="use_external_video_player_title">استخدام مشغل فيديو خارجي</string>
@@ -881,4 +881,9 @@
881881
<string name="no">لا</string>
882882
<string name="import_settings_vulnerable_format">تستخدم الإعدادات الموجودة في عملية التصدير التي يتم استيرادها تنسيقًا عرضة للاختراق تم إهماله منذ NewPipe 0.27.0. تأكد من أن التصدير الذي يتم استيراده من مصدر موثوق به، ويفضل استخدام عمليات التصدير التي تم الحصول عليها من NewPipe 0.27.0 أو الأحدث في المستقبل فقط. سيتم قريبًا إزالة دعم استيراد الإعدادات بهذا التنسيق الضعيف تمامًا، وبعد ذلك لن تتمكن الإصدارات القديمة من NewPipe من استيراد إعدادات التصدير من الإصدارات الجديدة بعد الآن.</string>
883883
<string name="audio_track_type_secondary">الثانوي</string>
884+
<string name="share_playlist_as_youtube_temporary_playlist">المشاركة كقائمة تشغيل مؤقتة على YouTube</string>
885+
<string name="tab_bookmarks_short">قوائم التشغيل</string>
886+
<string name="feed_group_page_summary">صفحة مجموعة القناة</string>
887+
<string name="select_a_feed_group">حدد مجموعة المحتوى</string>
888+
<string name="no_feed_group_created_yet">لم تنشئ مجموعة محتوى</string>
884889
</resources>

0 commit comments

Comments
 (0)