Skip to content

Commit 705b5e5

Browse files
committed
Fix ghost notifications on Android 10
Fixes #12400, see there for explanation. Citing from there: So apparently the problem is onGetRoot always returning a BrowserRoot instance. Making it return null solved the issue (but again, breaks Android Auto compatibility). It turns out (see https://stackoverflow.com/q/63818988/) that onGetRoot is also used for media resumption https://developer.android.com/media/implement/surfaces/mobile#mediabrowserservice_implementation, which causes a new notification to pop up (in this case a useless notification because our onGetRoot does not return something that can be used for resumption). So what needs to be done is to check if rootHints?.getBoolean(EXTRA_RECENT) == true and if that's the case not return anything (as EXTRA_RECENT is used by the system for resumption). The PackageValidator file is taken from https://github.com/android/uamp/blob/329a21b63c247e9bd35f6858d4fc0e448fa38603/common/src/main/java/com/example/android/uamp/media/PackageValidator.kt .
1 parent 2dd11f7 commit 705b5e5

3 files changed

Lines changed: 257 additions & 2 deletions

File tree

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
@@ -9,6 +9,7 @@ import android.support.v4.media.MediaDescriptionCompat
99
import android.util.Log
1010
import androidx.annotation.DrawableRes
1111
import androidx.media.MediaBrowserServiceCompat
12+
import androidx.media.MediaBrowserServiceCompat.BrowserRoot.EXTRA_RECENT
1213
import androidx.media.MediaBrowserServiceCompat.Result
1314
import androidx.media.utils.MediaConstants
1415
import io.reactivex.rxjava3.core.Flowable
@@ -47,6 +48,7 @@ class MediaBrowserImpl(
4748
private val context: Context,
4849
notifyChildrenChanged: Consumer<String>, // parentId
4950
) {
51+
private val packageValidator = PackageValidator(context)
5052
private val database = NewPipeDatabase.getInstance(context)
5153
private var disposables = CompositeDisposable()
5254

@@ -68,11 +70,22 @@ class MediaBrowserImpl(
6870
clientPackageName: String,
6971
clientUid: Int,
7072
rootHints: Bundle?
71-
): MediaBrowserServiceCompat.BrowserRoot {
73+
): MediaBrowserServiceCompat.BrowserRoot? {
7274
if (DEBUG) {
7375
Log.d(TAG, "onGetRoot($clientPackageName, $clientUid, $rootHints)")
7476
}
7577

78+
if (!packageValidator.isKnownCaller(clientPackageName, clientUid)) {
79+
// this is a caller we can't trust (see PackageValidator's rules taken from uamp)
80+
return null
81+
}
82+
83+
if (rootHints?.getBoolean(EXTRA_RECENT, false) == true) {
84+
// the system is asking for a root to do media resumption, but we can't handle that yet,
85+
// see https://developer.android.com/media/implement/surfaces/mobile#mediabrowserservice_implementation
86+
return null
87+
}
88+
7689
val extras = Bundle()
7790
extras.putBoolean(
7891
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"

0 commit comments

Comments
 (0)