Skip to content

Commit 4099b31

Browse files
Fix split install issue (#2898)
Co-authored-by: Marvin W <git@larma.de>
1 parent 3321044 commit 4099b31

File tree

15 files changed

+530
-178
lines changed

15 files changed

+530
-178
lines changed

vending-app/src/main/AndroidManifest.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,11 @@
221221
android:exported="false"
222222
tools:targetApi="21" />
223223

224+
<receiver
225+
android:name=".installer.InstallReceiver"
226+
android:exported="false"
227+
tools:targetApi="21" />
228+
224229
<!-- Work account store -->
225230
<activity android:name="org.microg.vending.ui.WorkAppsActivity"
226231
android:exported="true"

vending-app/src/main/java/org/microg/vending/billing/core/GooglePlayApi.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,6 @@ class GooglePlayApi {
1616
const val URL_DELIVERY = "$URL_FDFE/delivery"
1717
const val URL_ENTERPRISE_CLIENT_POLICY = "$URL_FDFE/getEnterpriseClientPolicy"
1818
const val URL_SYNC = "$URL_FDFE/sync"
19+
const val URL_BULK = "$URL_FDFE/bulkGrantEntitlement"
1920
}
2021
}

vending-app/src/main/java/org/microg/vending/billing/core/HttpClient.kt

Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package org.microg.vending.billing.core
22

33
import android.content.Context
44
import android.net.Uri
5+
import android.util.Log
56
import com.squareup.wire.Message
67
import com.squareup.wire.ProtoAdapter
78
import io.ktor.client.HttpClient
@@ -32,6 +33,7 @@ import java.io.IOException
3233
import java.io.OutputStream
3334

3435
private const val POST_TIMEOUT = 8000L
36+
private const val TAG = "HttpClient"
3537

3638
class HttpClient {
3739

@@ -60,32 +62,44 @@ class HttpClient {
6062
}
6163

6264
suspend fun download(
63-
url: String,
64-
downloadTo: OutputStream,
65-
params: Map<String, String> = emptyMap(),
66-
emitProgress: (bytesDownloaded: Long) -> Unit = {}
65+
url: String,
66+
downloadTo: OutputStream,
67+
params: Map<String, String> = emptyMap(),
68+
downloadedBytes: Long = 0,
69+
emitProgress: (bytesDownloaded: Long) -> Unit = {}
6770
) {
68-
client.prepareGet(url.asUrl(params)).execute { response ->
69-
val body: ByteReadChannel = response.body()
70-
71-
// Modified version of `ByteReadChannel.copyTo(OutputStream, Long)` to indicate progress
72-
val buffer = ByteArrayPool.borrow()
73-
try {
74-
var copied = 0L
75-
val bufferSize = buffer.size
76-
77-
do {
78-
val rc = body.readAvailable(buffer, 0, bufferSize)
79-
copied += rc
80-
if (rc > 0) {
81-
downloadTo.write(buffer, 0, rc)
82-
emitProgress(copied)
71+
try {
72+
Log.d(TAG, "download downloadedBytes:$downloadedBytes")
73+
client.prepareGet(url.asUrl(params)){
74+
if (downloadedBytes > 0) {
75+
headers {
76+
append(HttpHeaders.Range, "bytes=$downloadedBytes-")
8377
}
84-
} while (rc > 0)
85-
} finally {
86-
ByteArrayPool.recycle(buffer)
78+
}
79+
}.execute { response ->
80+
val body: ByteReadChannel = response.body()
81+
// Modified version of `ByteReadChannel.copyTo(OutputStream, Long)` to indicate progress
82+
val buffer = ByteArrayPool.borrow()
83+
try {
84+
var copied = downloadedBytes
85+
val bufferSize = buffer.size
86+
87+
do {
88+
val rc = body.readAvailable(buffer, 0, bufferSize)
89+
copied += rc
90+
if (rc > 0) {
91+
downloadTo.write(buffer, 0, rc)
92+
emitProgress(copied)
93+
}
94+
} while (rc > 0)
95+
} finally {
96+
ByteArrayPool.recycle(buffer)
97+
}
98+
// don't close `downloadTo` yet
8799
}
88-
// don't close `downloadTo` yet
100+
} catch (e: Exception) {
101+
Log.w(TAG, "download error : $e")
102+
throw e
89103
}
90104
}
91105

vending-app/src/main/java/org/microg/vending/delivery/Delivery.kt

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,20 @@
55

66
package org.microg.vending.delivery
77

8+
import android.accounts.AccountManager
9+
import android.content.Context
810
import android.util.Log
911
import com.android.vending.buildRequestHeaders
12+
import com.google.android.finsky.BulkGrant
13+
import com.google.android.finsky.BulkRequest
14+
import com.google.android.finsky.BulkRequestWrapper
15+
import com.google.android.finsky.BulkResponseWrapper
16+
import com.google.android.finsky.DeviceSyncInfo
17+
import com.google.android.finsky.SyncResponse
1018
import com.google.android.finsky.splitinstallservice.PackageComponent
19+
import com.google.android.finsky.syncDeviceInfo
1120
import org.microg.vending.billing.core.AuthData
21+
import org.microg.vending.billing.core.GooglePlayApi
1222
import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_DELIVERY
1323
import org.microg.vending.billing.core.HttpClient
1424
import org.microg.vending.billing.proto.GoogleApiResponse
@@ -22,18 +32,19 @@ private const val TAG = "GmsVendingDelivery"
2232
* only those will be contained in the result.
2333
*/
2434
suspend fun HttpClient.requestDownloadUrls(
25-
packageName: String,
26-
versionCode: Long,
27-
auth: AuthData,
28-
requestSplitPackages: List<String>? = null,
29-
deliveryToken: String? = null,
35+
context: Context,
36+
packageName: String,
37+
versionCode: Long,
38+
auth: AuthData,
39+
requestSplitPackages: List<String>? = null,
40+
deliveryToken: String? = null,
3041
): List<PackageComponent> {
3142

3243
val requestUrl = StringBuilder("$URL_DELIVERY?doc=$packageName&ot=1&vc=$versionCode")
3344

3445
requestSplitPackages?.apply {
3546
requestUrl.append(
36-
"&bvc=$versionCode&pf=1&pf=2&pf=3&pf=4&pf=5&pf=7&pf=8&pf=9&pf=10&da=4&bda=4&bf=4&fdcf=1&fdcf=2&ch="
47+
"&bvc=$versionCode&pf=1&pf=2&pf=3&pf=4&pf=5&pf=7&pf=8&pf=9&pf=10&bda=4&bf=4&fdcf=1&fdcf=2&ch="
3748
)
3849
forEach { requestUrl.append("&mn=").append(it) }
3950
}
@@ -48,16 +59,31 @@ suspend fun HttpClient.requestDownloadUrls(
4859
}
4960
Log.d(TAG, "requestDownloadUrls languages: $languages")
5061

62+
val androidId = auth.gsfId.toLong(16)
5163
val headers = buildRequestHeaders(
5264
auth = auth.authToken,
53-
// TODO: understand behavior. Using proper Android ID doesn't work when downloading split APKs
54-
androidId = if (requestSplitPackages != null) 1 else auth.gsfId.toLong(16),
65+
androidId = androidId,
5566
languages
5667
).minus(
5768
// TODO: understand behavior. According to tests, these headers break split install queries but may be needed for normal ones
5869
(if (requestSplitPackages != null) listOf("X-DFE-Encoded-Targets", "X-DFE-Phenotype", "X-DFE-Device-Id", "X-DFE-Client-Id") else emptyList()).toSet()
5970
)
6071

72+
kotlin.runCatching {
73+
//Authorize the account to prevent the inability to obtain split information
74+
post(
75+
url = GooglePlayApi.URL_BULK,
76+
headers = headers,
77+
payload = BulkRequestWrapper.build {
78+
request(BulkRequest.build {
79+
packageName(packageName)
80+
grant(BulkGrant.build { grantLevel = 1 })
81+
})
82+
},
83+
adapter = BulkResponseWrapper.ADAPTER
84+
)
85+
}
86+
6187
val response = get(
6288
url = requestUrl.toString(),
6389
headers = headers,
@@ -76,9 +102,9 @@ suspend fun HttpClient.requestDownloadUrls(
76102
if (requestSplitPackages != null) {
77103
// Only download requested, if specific components were requested
78104
requestSplitPackages.firstOrNull { requestComponent ->
79-
requestComponent.contains(it.splitPackageName!!)
105+
(it.splitPackageName?.contains(requestComponent) == true || requestComponent.contains(it.splitPackageName!!))
80106
}?.let { requestComponent ->
81-
PackageComponent(packageName, requestComponent, it.downloadUrl!!, it.size!!.toLong())
107+
PackageComponent(packageName, it.splitPackageName!!, it.downloadUrl!!, it.size!!.toLong())
82108
}
83109
} else {
84110
// Download all offered components (server chooses)

vending-app/src/main/java/org/microg/vending/enterprise/InstallProgress.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
package org.microg.vending.enterprise
77

8+
import android.app.PendingIntent
9+
810
internal sealed interface InstallProgress
911

1012
internal data class Downloading(

vending-app/src/main/java/org/microg/vending/ui/InstallProgressNotification.kt

Lines changed: 69 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ package org.microg.vending.ui
88
import android.app.Notification
99
import android.app.NotificationChannel
1010
import android.app.NotificationManager
11+
import android.app.PendingIntent
1112
import android.content.Context
13+
import android.content.pm.ApplicationInfo
14+
import android.content.pm.PackageManager
1215
import android.content.pm.PackageManager.NameNotFoundException
1316
import android.os.Build
1417
import android.util.Log
@@ -24,11 +27,31 @@ import org.microg.vending.enterprise.InstallProgress
2427

2528
private const val INSTALL_NOTIFICATION_CHANNEL_ID = "packageInstall"
2629

30+
internal fun Context.notifyInstallPrompt(packageName: String, sessionId: Int, installIntent: PendingIntent, deleteIntent: PendingIntent) {
31+
val notificationManager = NotificationManagerCompat.from(this)
32+
val label = try {
33+
val applicationInfo = packageManager.getPackageInfo(packageName, 0).applicationInfo
34+
applicationInfo?.loadLabel(packageManager) ?: return
35+
} catch (e: NameNotFoundException) {
36+
Log.e(TAG, "Couldn't load label for $packageName (${e.message}). Is it not installed?")
37+
return
38+
}
39+
getInstallPromptNotificationBuilder().apply {
40+
setDeleteIntent(deleteIntent)
41+
setContentTitle(getString(R.string.installer_notification_progress_splitinstall_click_install, label))
42+
addAction(R.drawable.ic_download, getString(R.string.vending_overview_row_action_install), installIntent)
43+
setContentIntent(installIntent)
44+
setAutoCancel(true)
45+
}.apply {
46+
notificationManager.notify(sessionId, this.build())
47+
}
48+
}
49+
2750
internal fun Context.notifySplitInstallProgress(packageName: String, sessionId: Int, progress: InstallProgress) {
2851

2952
val label = try {
30-
packageManager.getPackageInfo(packageName, 0).applicationInfo!!
31-
.loadLabel(packageManager)
53+
val applicationInfo = packageManager.getPackageInfo(packageName, 0).applicationInfo
54+
applicationInfo?.loadLabel(packageManager) ?: return
3255
} catch (e: NameNotFoundException) {
3356
Log.e(TAG, "Couldn't load label for $packageName (${e.message}). Is it not installed?")
3457
return
@@ -44,11 +67,20 @@ internal fun Context.notifySplitInstallProgress(packageName: String, sessionId:
4467
when (progress) {
4568
is Downloading -> getDownloadNotificationBuilder().apply {
4669
setContentTitle(getString(R.string.installer_notification_progress_splitinstall_downloading, label))
47-
setProgress(progress.bytesDownloaded.toInt(), progress.bytesTotal.toInt(), false)
70+
setProgress(100, ((progress.bytesDownloaded.toFloat() / progress.bytesTotal) * 100).toInt().coerceIn(0, 100), false)
4871
}
49-
CommitingSession -> getDownloadNotificationBuilder().apply {
50-
setContentTitle(getString(R.string.installer_notification_progress_splitinstall_commiting, label))
51-
setProgress(0, 1, true)
72+
CommitingSession -> {
73+
// Check whether silent installation is possible. Only show the notification if silent installation is supported,
74+
// to prevent cases where the user cancels the install page and the notification is not removed.
75+
if (isSystem(packageManager)) {
76+
getDownloadNotificationBuilder().apply {
77+
setContentTitle(getString(R.string.installer_notification_progress_splitinstall_commiting, label))
78+
setProgress(0, 1, true)
79+
}
80+
} else {
81+
notificationManager.cancel(sessionId)
82+
null
83+
}
5284
}
5385
else -> null.also { notificationManager.cancel(sessionId) }
5486
}?.apply {
@@ -88,16 +120,23 @@ internal fun Context.notifyInstallProgress(
88120
return this.build().also { notificationManager.notify(sessionId, it) }
89121
}
90122
CommitingSession -> {
91-
setContentTitle(
92-
getString(
93-
if (isDependency) R.string.installer_notification_progress_splitinstall_commiting
94-
else R.string.installer_notification_progress_commiting,
95-
displayName
123+
// Check whether silent installation is possible. Only show the notification if silent installation is supported,
124+
// to prevent cases where the user cancels the install page and the notification is not removed.
125+
if (isSystem(packageManager)) {
126+
setContentTitle(
127+
getString(
128+
if (isDependency) R.string.installer_notification_progress_splitinstall_commiting
129+
else R.string.installer_notification_progress_commiting,
130+
displayName
131+
)
96132
)
97-
)
98-
setProgress(0, 0, true)
99-
setOngoing(true)
100-
return this.build().also { notificationManager.notify(sessionId, it) }
133+
setProgress(0, 0, true)
134+
setOngoing(true)
135+
return this.build().also { notificationManager.notify(sessionId, it) }
136+
} else {
137+
notificationManager.cancel(sessionId)
138+
return null
139+
}
101140
}
102141
InstallComplete -> {
103142
if (!isDependency) {
@@ -135,6 +174,12 @@ internal fun Context.notifyInstallProgress(
135174

136175
}
137176

177+
private fun Context.getInstallPromptNotificationBuilder() =
178+
NotificationCompat.Builder(this, INSTALL_NOTIFICATION_CHANNEL_ID)
179+
.setPriority(NotificationCompat.PRIORITY_LOW)
180+
.setSmallIcon(android.R.drawable.stat_sys_download)
181+
.setLocalOnly(true)
182+
138183
private fun Context.getDownloadNotificationBuilder() =
139184
NotificationCompat.Builder(this, INSTALL_NOTIFICATION_CHANNEL_ID)
140185
.setSmallIcon(android.R.drawable.stat_sys_download)
@@ -157,4 +202,13 @@ private fun Context.createNotificationChannel() {
157202
}
158203
)
159204
}
205+
}
206+
207+
private fun Context.isSystem(pm: PackageManager): Boolean {
208+
try {
209+
val ai = pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA)
210+
return (ai.flags and (ApplicationInfo.FLAG_SYSTEM or ApplicationInfo.FLAG_UPDATED_SYSTEM_APP)) != 0
211+
} catch (e: NameNotFoundException) {
212+
return false
213+
}
160214
}

vending-app/src/main/kotlin/com/android/vending/installer/Constants.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ private const val FILE_SAVE_PATH = "phonesky-download-service"
1212
internal const val TAG = "GmsPackageInstaller"
1313

1414
const val KEY_BYTES_DOWNLOADED = "bytes_downloaded"
15+
const val VENDING_INSTALL_ACTION = "com.android.vending.ACTION_INSTALL"
16+
const val VENDING_INSTALL_DELETE_ACTION = "com.android.vending.ACTION_INSTALL_DELETE"
17+
const val SESSION_ID = "session_id"
18+
const val SESSION_RESULT_RECEIVER_INTENT = "session_result_receiver_intent"
19+
const val SPLIT_LANGUAGE_TAG = "config."
1520

1621
fun Context.packageDownloadLocation() = File(cacheDir, FILE_SAVE_PATH).apply {
1722
if (!exists()) mkdir()

0 commit comments

Comments
 (0)