Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement SplitInstallService #2500

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ buildscript {

ext.slf4jVersion = '1.7.36'
ext.volleyVersion = '1.2.1'
ext.wireVersion = '4.8.0'
ext.wireVersion = '4.9.9'

ext.androidBuildGradleVersion = '8.2.2'

Expand Down
12 changes: 12 additions & 0 deletions vending-app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="com.google.android.gms.permission.READ_SETTINGS" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>

<uses-permission
android:name="android.permission.USE_CREDENTIALS"
Expand Down Expand Up @@ -148,6 +149,13 @@
</intent-filter>
</activity>

<service android:name="com.google.android.finsky.splitinstallservice.SplitInstallService"
android:exported="true">
<intent-filter>
<action android:name="com.google.android.play.core.splitinstall.BIND_SPLIT_INSTALL_SERVICE"/>
</intent-filter>
</service>

<service
android:name="com.android.vending.billing.InAppBillingService"
android:exported="true">
Expand Down Expand Up @@ -175,5 +183,9 @@
</intent-filter>
</service>

<receiver
android:name="com.google.android.finsky.splitinstallservice.InstallResultReceiver"
android:exported="true"/>

</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.play.core.splitinstall.protocol;
import com.google.android.play.core.splitinstall.protocol.ISplitInstallServiceCallback;

interface ISplitInstallService {
void startInstall(String pkg,in List<Bundle> splits,in Bundle bundle, ISplitInstallServiceCallback callback) = 1;
void completeInstalls(String pkg, int sessionId,in Bundle bundle, ISplitInstallServiceCallback callback) = 2;
void cancelInstall(String pkg, int sessionId, ISplitInstallServiceCallback callback) = 3;
void getSessionState(String pkg, int sessionId, ISplitInstallServiceCallback callback) = 4;
void getSessionStates(String pkg, ISplitInstallServiceCallback callback) = 5;
void splitRemoval(String pkg,in List<Bundle> splits, ISplitInstallServiceCallback callback) = 6;
void splitDeferred(String pkg,in List<Bundle> splits,in Bundle bundle, ISplitInstallServiceCallback callback) = 7;
void getSessionState2(String pkg, int sessionId, ISplitInstallServiceCallback callback) = 8;
void getSessionStates2(String pkg, ISplitInstallServiceCallback callback) = 9;
void getSplitsAppUpdate(String pkg, ISplitInstallServiceCallback callback) = 10;
void completeInstallAppUpdate(String pkg, ISplitInstallServiceCallback callback) = 11;
void languageSplitInstall(String pkg,in List<Bundle> splits,in Bundle bundle, ISplitInstallServiceCallback callback) = 12;
void languageSplitUninstall(String pkg,in List<Bundle> splits, ISplitInstallServiceCallback callback) =13;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.play.core.splitinstall.protocol;


interface ISplitInstallServiceCallback {
oneway void onStartInstall(int status, in Bundle bundle) = 1;
oneway void onInstallCompleted(int status, in Bundle bundle) = 2;
oneway void onCancelInstall(int status, in Bundle bundle) = 3;
oneway void onGetSessionState(int status, in Bundle bundle) = 4;
oneway void onError(in Bundle bundle) = 5;
oneway void onGetSessionStates(in List<Bundle> list) = 6;
oneway void onDeferredUninstall(in Bundle bundle) = 7;
oneway void onDeferredInstall(in Bundle bundle) = 8;
oneway void onDeferredLanguageInstall(in Bundle bundle) = 11;
oneway void onDeferredLanguageUninstall(in Bundle bundle) = 12;
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ private const val TAG = "FakeLicenseRequest"
private const val BASE64_FLAGS = Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING
private const val FINSKY_VERSION = "Finsky/37.5.24-29%20%5B0%5D%20%5BPR%5D%20565477504"

internal fun getLicenseRequestHeaders(auth: String, androidId: Long): Map<String, String> {
internal fun getDefaultLicenseRequestHeaderBuilder(androidId: Long) : LicenseRequestHeader.Builder {
var millis = System.currentTimeMillis()
val timestamp = TimestampContainer.Builder()
.container2(
Expand Down Expand Up @@ -79,7 +79,7 @@ internal fun getLicenseRequestHeaders(auth: String, androidId: Long): Map<String
Base64.encode(locality.encode(), BASE64_FLAGS)
)

val header = LicenseRequestHeader.Builder()
return LicenseRequestHeader.Builder()
.encodedTimestamps(StringWrapper.Builder().string(encodedTimestamps).build())
.triple(
EncodedTripleWrapper.Builder().triple(
Expand Down Expand Up @@ -126,7 +126,10 @@ internal fun getLicenseRequestHeaders(auth: String, androidId: Long): Map<String
.unknown(2)
.build()
)
.build().encode()
}

internal fun getLicenseRequestHeaders(auth: String, androidId: Long): Map<String, String> {
val header = getDefaultLicenseRequestHeaderBuilder(androidId).build().encode()
val xPsRh = String(Base64.encode(header.encodeGzip(), BASE64_FLAGS))

Log.v(TAG, "X-PS-RH: $xPsRh")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,47 @@ import com.squareup.wire.Message
import com.squareup.wire.ProtoAdapter
import org.json.JSONObject
import org.microg.gms.utils.singleInstanceOf
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine

private const val POST_TIMEOUT = 8000

class HttpClient(context: Context) {
private val requestQueue = singleInstanceOf { Volley.newRequestQueue(context.applicationContext) }

val requestQueue = singleInstanceOf { Volley.newRequestQueue(context.applicationContext) }

suspend fun download(url: String, downloadFile: File, tag: String): String = suspendCoroutine { continuation ->
val uriBuilder = Uri.parse(url).buildUpon()
requestQueue.add(object : Request<String>(Method.GET, uriBuilder.build().toString(), null) {
override fun parseNetworkResponse(response: NetworkResponse): Response<String> {
if (response.statusCode != 200) throw VolleyError(response)
return try {
val parentDir = downloadFile.getParentFile()
if (parentDir != null && !parentDir.exists() && !parentDir.mkdirs()) {
throw IOException("Failed to create directories: ${parentDir.absolutePath}")
}
val fos = FileOutputStream(downloadFile)
fos.write(response.data)
Copy link
Contributor

@fynngodau fynngodau Sep 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Volley loads the entire response into memory. This is a problem for large downloads, as it will consume a lot of RAM (temporarily – or even fail the download if not enough memory is available). The data from the network stream should be written to disk immediately instead.

Per homepage:

Volley is not suitable for large download or streaming operations, since Volley holds all responses in memory during parsing.

fos.close()
Response.success(downloadFile.absolutePath, HttpHeaderParser.parseCacheHeaders(response))
} catch (e: Exception) {
Response.error(VolleyError(e))
}
}

override fun deliverResponse(response: String) {
continuation.resume(response)
}

override fun deliverError(error: VolleyError) {
continuation.resumeWithException(error)
}
}.setShouldCache(false).setTag(tag))
}

suspend fun <O> get(
url: String,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/

package com.google.android.finsky.splitinstallservice

import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.IBinder
import android.util.Log
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import com.google.android.gms.common.api.CommonStatusCodes
import com.google.android.play.core.splitinstall.protocol.ISplitInstallService
import com.google.android.play.core.splitinstall.protocol.ISplitInstallServiceCallback
import kotlinx.coroutines.launch
import org.microg.gms.profile.ProfileManager
import org.microg.vending.billing.core.HttpClient

private const val TAG = "SplitInstallServiceImpl"

class SplitInstallService : LifecycleService() {

private lateinit var httpClient: HttpClient

override fun onBind(intent: Intent): IBinder? {
super.onBind(intent)
Log.d(TAG, "onBind: ")
ProfileManager.ensureInitialized(this)
httpClient = HttpClient(this)
return SplitInstallServiceImpl(this.applicationContext, httpClient, lifecycle).asBinder()
}

override fun onUnbind(intent: Intent?): Boolean {
Log.d(TAG, "onUnbind: ")
httpClient.requestQueue.cancelAll(SPLIT_INSTALL_REQUEST_TAG)
return super.onUnbind(intent)
}
}

class SplitInstallServiceImpl(private val context: Context, private val httpClient: HttpClient, override val lifecycle: Lifecycle) : ISplitInstallService.Stub(), LifecycleOwner {

override fun startInstall(pkg: String, splits: List<Bundle>, bundle0: Bundle, callback: ISplitInstallServiceCallback) {
Log.d(TAG, "Method <startInstall> Called by package: $pkg")
lifecycleScope.launch {
trySplitInstall(context, httpClient, pkg, splits)
Log.d(TAG, "onStartInstall SUCCESS")
callback.onStartInstall(CommonStatusCodes.SUCCESS, Bundle())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to me like the identity of the caller is not verified. I would expect packages are only allowed to request split installs for themselves, right? We should ensure that pkg is indeed the caller themselves by calling PackageUtils.getAndCheckCallingPackage.

}
}

override fun completeInstalls(pkg: String, sessionId: Int, bundle0: Bundle, callback: ISplitInstallServiceCallback) {
Log.d(TAG, "Method (completeInstalls) called but not implement by package -> $pkg")
}

override fun cancelInstall(pkg: String, sessionId: Int, callback: ISplitInstallServiceCallback) {
Log.d(TAG, "Method (cancelInstall) called but not implement by package -> $pkg")
}

override fun getSessionState(pkg: String, sessionId: Int, callback: ISplitInstallServiceCallback) {
Log.d(TAG, "Method (getSessionState) called but not implement by package -> $pkg")
}

override fun getSessionStates(pkg: String, callback: ISplitInstallServiceCallback) {
Log.d(TAG, "Method (getSessionStates) called but not implement by package -> $pkg")
callback.onGetSessionStates(ArrayList<Bundle>(1))
}

override fun splitRemoval(pkg: String, splits: List<Bundle>, callback: ISplitInstallServiceCallback) {
Log.d(TAG, "Method (splitRemoval) called but not implement by package -> $pkg")
}

override fun splitDeferred(pkg: String, splits: List<Bundle>, bundle0: Bundle, callback: ISplitInstallServiceCallback) {
Log.d(TAG, "Method (splitDeferred) called but not implement by package -> $pkg")
callback.onDeferredInstall(Bundle())
}

override fun getSessionState2(pkg: String, sessionId: Int, callback: ISplitInstallServiceCallback) {
Log.d(TAG, "Method (getSessionState2) called but not implement by package -> $pkg")
}

override fun getSessionStates2(pkg: String, callback: ISplitInstallServiceCallback) {
Log.d(TAG, "Method (getSessionStates2) called but not implement by package -> $pkg")
}

override fun getSplitsAppUpdate(pkg: String, callback: ISplitInstallServiceCallback) {
Log.d(TAG, "Method (getSplitsAppUpdate) called but not implement by package -> $pkg")
}

override fun completeInstallAppUpdate(pkg: String, callback: ISplitInstallServiceCallback) {
Log.d(TAG, "Method (completeInstallAppUpdate) called but not implement by package -> $pkg")
}

override fun languageSplitInstall(pkg: String, splits: List<Bundle>, bundle0: Bundle, callback: ISplitInstallServiceCallback) {
Log.d(TAG, "Method <languageSplitInstall> Called by package: $pkg")
lifecycleScope.launch {
trySplitInstall(context, httpClient, pkg, splits)
}
}

override fun languageSplitUninstall(pkg: String, splits: List<Bundle>, callback: ISplitInstallServiceCallback) {
Log.d(TAG, "Method (languageSplitUninstall) called but not implement by package -> $pkg")
}

}
Loading