Skip to content

Commit

Permalink
Add support for opening AppInvite / Dynamic links
Browse files Browse the repository at this point in the history
  • Loading branch information
mar-v-in committed Aug 24, 2023
1 parent a45de90 commit e00b985
Show file tree
Hide file tree
Showing 4 changed files with 274 additions and 14 deletions.
8 changes: 8 additions & 0 deletions play-services-appinvite/core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@
*/

apply plugin: 'com.android.library'
apply plugin: 'com.squareup.wire'
apply plugin: 'kotlin-android'

dependencies {
api project(':play-services-appinvite')
implementation project(':play-services-base-core')

implementation "androidx.appcompat:appcompat:$appcompatVersion"

implementation "com.android.volley:volley:$volleyVersion"
implementation "com.squareup.wire:wire-runtime:$wireVersion"
}

android {
Expand Down Expand Up @@ -38,3 +42,7 @@ android {
jvmTarget = 1.8
}
}

wire {
kotlin {}
}
13 changes: 6 additions & 7 deletions play-services-appinvite/core/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,13 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</service>

<activity
android:process=":ui"
android:name="org.microg.gms.appinivite.AppInviteActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<activity-alias
android:name="com.google.android.gms.appinvite.AppInviteAcceptInvitationActivity"
android:targetActivity="org.microg.gms.appinivite.AppInviteActivity"
android:exported="true">
android:excludeFromRecents="true"
android:process=":ui"
android:exported="true"
android:theme="@style/Theme.AppCompat.Light.Dialog.NoActionBar">
<intent-filter
android:priority="900"
android:autoVerify="true">
Expand All @@ -46,6 +45,6 @@
<data android:scheme="https" />
<data android:scheme="http" />
</intent-filter>
</activity-alias>
</activity>
</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,181 @@

package org.microg.gms.appinivite

import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Build

This comment has been minimized.

Copy link
@ale5000-git

ale5000-git Aug 24, 2023

Member

@mar-v-in
Here android.os.Build is imported but in the other previous commit you said:

Stop importing android.os.Build
Import fields in android.os.Build directly if really needed.
Build, if ever imported, should point to our Build implementation that's populated by ProfileManager
import android.os.Bundle
import android.os.LocaleList
import android.util.Log
import android.view.ViewGroup
import android.view.Window
import android.widget.ProgressBar
import android.widget.RelativeLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.pm.PackageInfoCompat
import androidx.core.os.bundleOf
import androidx.core.view.setPadding
import androidx.lifecycle.lifecycleScope
import com.android.volley.*
import com.android.volley.toolbox.Volley
import com.squareup.wire.Message
import com.squareup.wire.ProtoAdapter
import kotlinx.coroutines.CompletableDeferred
import okio.ByteString.Companion.decodeHex
import org.microg.gms.appinvite.*
import org.microg.gms.common.Constants
import java.util.*

private const val TAG = "AppInviteActivity"

class AppInviteActivity : Activity() {
private const val APPINVITE_DEEP_LINK = "com.google.android.gms.appinvite.DEEP_LINK"
private const val APPINVITE_INVITATION_ID = "com.google.android.gms.appinvite.INVITATION_ID"
private const val APPINVITE_OPENED_FROM_PLAY_STORE = "com.google.android.gms.appinvite.OPENED_FROM_PLAY_STORE"
private const val APPINVITE_REFERRAL_BUNDLE = "com.google.android.gms.appinvite.REFERRAL_BUNDLE"

class AppInviteActivity : AppCompatActivity() {
private val queue by lazy { Volley.newRequestQueue(this) }

private val Int.px: Int get() = (this * resources.displayMetrics.density).toInt()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val uri = intent?.data
if (uri == null) {
requestWindowFeature(Window.FEATURE_NO_TITLE)
setContentView(ProgressBar(this).apply {
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
setPadding(20.px)
isIndeterminate = true
})
val extras = intent.extras
extras?.keySet()
Log.d(TAG, "Intent: $intent $extras")
if (intent?.data == null) return finish()
lifecycleScope.launchWhenStarted { run() }
}

private fun redirectToBrowser() {
try {
startActivity(Intent(Intent.ACTION_VIEW).apply {
addCategory(Intent.CATEGORY_DEFAULT)
data = intent.data
})
} catch (e: Exception) {
Log.w(TAG, e)
}
finish()
}

private fun open(appInviteLink: MutateAppInviteLinkResponse) {
val intent = Intent(Intent.ACTION_VIEW).apply {
addCategory(Intent.CATEGORY_DEFAULT)
data = appInviteLink.data_?.intentData?.let { Uri.parse(it) }
`package` = appInviteLink.data_?.packageName
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
putExtra(
APPINVITE_REFERRAL_BUNDLE, bundleOf(
APPINVITE_DEEP_LINK to appInviteLink,
APPINVITE_INVITATION_ID to "",
APPINVITE_OPENED_FROM_PLAY_STORE to false
)
)
}
val fallbackIntent = Intent(Intent.ACTION_VIEW).apply {
addCategory(Intent.CATEGORY_DEFAULT)
data = appInviteLink.data_?.fallbackUrl?.let { Uri.parse(it) }
}
val installedVersionCode = runCatching {
intent.resolveActivity(packageManager)?.let {
PackageInfoCompat.getLongVersionCode(packageManager.getPackageInfo(it.packageName, 0))
}
}.getOrNull()
if (installedVersionCode != null && (appInviteLink.data_?.app?.minAppVersion == null || installedVersionCode >= appInviteLink.data_.app.minAppVersion)) {
startActivity(intent)
finish()
} else {
try {
startActivity(fallbackIntent)
} catch (e: Exception) {
Log.w(TAG, e)
}
finish()
return
}
Log.d(TAG, "uri: $uri")
// TODO datamixer-pa.googleapis.com/
}

private suspend fun run() {
val request = ProtobufPostRequest(
"https://datamixer-pa.googleapis.com/v1/mutateonekey?alt=proto&key=AIzaSyAP-gfH3qvi6vgHZbSYwQ_XHqV_mXHhzIk", MutateOperation(
id = MutateOperationId.AppInviteLink,
mutateRequest = MutateDataRequest(
appInviteLink = MutateAppInviteLinkRequest(
client = ClientIdInfo(
packageName = Constants.GMS_PACKAGE_NAME,
signature = Constants.GMS_PACKAGE_SIGNATURE_SHA1.decodeHex().base64(),
language = Locale.getDefault().language
),
link = LinkInfo(
empty = "",
uri = intent.data.toString()
),
system = SystemInfo(
gms = SystemInfo.GmsInfo(
versionCode = Constants.GMS_VERSION_CODE
)
)
)
)
), MutateDataResponseWithError.ADAPTER
)
val response = try {
request.sendAndAwait(queue)
} catch (e: Exception) {
Log.w(TAG, e)
return redirectToBrowser()
}
if (response.errorStatus != null || response.dataResponse?.appInviteLink == null) return redirectToBrowser()
open(response.dataResponse.appInviteLink)
}
}

class ProtobufPostRequest<I : Message<I, *>, O>(url: String, private val i: I, private val oAdapter: ProtoAdapter<O>) :
Request<O>(Method.POST, url, null) {
private val deferred = CompletableDeferred<O>()

override fun getHeaders(): Map<String, String> {
val headers = HashMap(super.getHeaders())
headers["Accept-Language"] = if (Build.VERSION.SDK_INT >= 24) LocaleList.getDefault().toLanguageTags() else Locale.getDefault().language
headers["X-Android-Package"] = Constants.GMS_PACKAGE_NAME
headers["X-Android-Cert"] = Constants.GMS_PACKAGE_SIGNATURE_SHA1
return headers
}

override fun getBody(): ByteArray = i.encode()

override fun getBodyContentType(): String = "application/x-protobuf"

override fun parseNetworkResponse(response: NetworkResponse): Response<O> {
try {
return Response.success(oAdapter.decode(response.data), null)
} catch (e: VolleyError) {
return Response.error(e)
} catch (e: Exception) {
return Response.error(VolleyError())
}
}

override fun deliverResponse(response: O) {
Log.d(TAG, "Got response: $response")
deferred.complete(response)
}

override fun deliverError(error: VolleyError) {
deferred.completeExceptionally(error)
}

suspend fun await(): O = deferred.await()

suspend fun sendAndAwait(queue: RequestQueue): O {
Log.d(TAG, "Sending request: $i")
queue.add(this)
return await()
}
}
93 changes: 93 additions & 0 deletions play-services-appinvite/core/src/main/proto/datamixer.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
syntax = "proto2";
option java_package = "org.microg.gms.appinvite";

message ClientIdInfo {
optional string packageName = 3; // e.g. com.google.android.gms
optional string signature = 4; // Signing certificate sha-1 base64 with padding, e.g. WOHEEz90Qew9LCcCcKFIAtpHug4=
optional string language = 6; // e.g. en
}

message LinkInfo {
optional string empty = 1; // e.g. ""
optional string uri = 2;
}

message SystemInfo {
message GmsInfo {
optional uint32 versionCode = 1; // 212423054
}
optional GmsInfo gms = 1;
}

message MutateAppInviteLinkRequest {
optional ClientIdInfo client = 1;
optional LinkInfo link = 4;
optional SystemInfo system = 5;
}

message MutateDataRequest {
oneof request {
MutateAppInviteLinkRequest appInviteLink = 84453462;
}
}

message AppInviteLinkInfo {
optional int32 type = 1;
optional string url = 2;
optional string name = 3;
}

message AppInviteAppData {
optional string packageName = 1; // apn
optional uint64 minAppVersion = 2; // amv
optional string altPackageName = 3; //apn
}

message AppInviteLinkData {
optional string fallbackUrl = 1; // afl
optional string packageName = 2; // apn
optional string intentData = 3; // link
optional AppInviteAppData app = 6;
}

message AppInviteLinkMetadata {
optional string source = 2; // utm_source
optional string medium = 3; // utm_medium
optional string campaign = 4; // utm_campaign
optional string id = 5;
optional string appCode = 6;
optional AppInviteLinkInfo info = 8;
optional string sessionId = 9;
optional string domainUriPrefix = 10;
optional string content = 11; // utm_content
optional string term = 12; // utm_term
}

message MutateAppInviteLinkResponse {
optional AppInviteLinkData data = 1;
optional AppInviteLinkMetadata metadata = 4;
}

message MutateDataResponse {
oneof response {
MutateAppInviteLinkResponse appInviteLink = 84453462;
}
}

enum MutateOperationId {
AppInviteLink = 84453462;
}

message MutateOperation {
optional MutateOperationId id = 1; // 84453462
optional MutateDataRequest mutateRequest = 2;
}

message StatusProto {
optional int32 code = 1;
}

message MutateDataResponseWithError {
optional MutateDataResponse dataResponse = 1;
optional StatusProto errorStatus = 2;
}

1 comment on commit e00b985

@mar-v-in
Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, you're right, I'll fix that later.

Please sign in to comment.