VancedMicroG/play-services-core/src/main/kotlin/org/microg/gms/gcm/PushRegisterService.kt

349 lines
13 KiB
Kotlin

/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
@file:Suppress("DEPRECATION")
package org.microg.gms.gcm
import android.app.Activity
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.*
import android.util.Log
import androidx.legacy.content.WakefulBroadcastReceiver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import org.microg.gms.checkin.CheckinPrefs
import org.microg.gms.checkin.CheckinService
import org.microg.gms.checkin.LastCheckinInfo
import org.microg.gms.common.ForegroundServiceContext
import org.microg.gms.common.PackageUtils
import org.microg.gms.common.Utils
import org.microg.gms.gcm.GcmConstants.*
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
private const val TAG = "GmsGcmRegister"
private suspend fun ensureCheckinIsUpToDate(context: Context) {
if (!CheckinPrefs.isEnabled(context)) throw RuntimeException("Checkin disabled")
val lastCheckin = LastCheckinInfo.read(context).lastCheckin
if (lastCheckin < System.currentTimeMillis() - CheckinService.MAX_VALID_CHECKIN_AGE) {
val resultData: Bundle = suspendCoroutine { continuation ->
val intent = Intent(context, CheckinService::class.java)
val continued = AtomicBoolean(false)
intent.putExtra(CheckinService.EXTRA_RESULT_RECEIVER, object : ResultReceiver(null) {
override fun onReceiveResult(resultCode: Int, resultData: Bundle?) {
if (continued.compareAndSet(false, true)) continuation.resume(resultData ?: Bundle.EMPTY)
}
})
ForegroundServiceContext(context).startService(intent)
Handler().postDelayed({
if (continued.compareAndSet(false, true)) continuation.resume(Bundle.EMPTY)
}, 10000L)
}
if (resultData.getLong(CheckinService.EXTRA_NEW_CHECKIN_TIME, 0L) + lastCheckin == 0L) {
throw RuntimeException("No checkin available")
}
}
}
private suspend fun ensureAppRegistrationAllowed(context: Context, database: GcmDatabase, packageName: String) {
if (!GcmPrefs.get(context).isEnabled) throw RuntimeException("GCM disabled")
val app = database.getApp(packageName)
if (app?.allowRegister == false) {
throw RuntimeException("Push permission not granted to $packageName")
}
}
suspend fun completeRegisterRequest(context: Context, database: GcmDatabase, request: RegisterRequest, requestId: String? = null): Bundle = suspendCoroutine { continuation ->
PushRegisterManager.completeRegisterRequest(context, database, requestId, request) { continuation.resume(it) }
}
private val Intent.requestId: String?
get() {
val kidString = getStringExtra(GcmConstants.EXTRA_KID) ?: return null
if (kidString.startsWith("|")) {
val kid = kidString.split("\\|".toRegex()).toTypedArray()
if (kid.size >= 3 && "ID" == kid[1]) {
return kid[2]
}
}
return null
}
private val Intent.app: PendingIntent?
get() = getParcelableExtra(EXTRA_APP)
private val Intent.appPackageName: String?
get() = PackageUtils.packageFromPendingIntent(app)
class PushRegisterService : LifecycleService() {
private lateinit var database: GcmDatabase
override fun onCreate() {
super.onCreate()
database = GcmDatabase(this)
}
override fun onDestroy() {
super.onDestroy()
database.close()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent != null) {
WakefulBroadcastReceiver.completeWakefulIntent(intent)
Log.d(TAG, "onStartCommand: $intent")
lifecycleScope.launchWhenStarted {
handleIntent(intent)
}
}
return super.onStartCommand(intent, flags, startId)
}
private suspend fun handleIntent(intent: Intent) {
try {
ensureCheckinIsUpToDate(this)
if (ACTION_C2DM_UNREGISTER == intent.action || ACTION_C2DM_REGISTER == intent.action && "1" == intent.getStringExtra(EXTRA_DELETE)) {
unregister(intent)
} else if (ACTION_C2DM_REGISTER == intent.action) {
register(intent)
}
} catch (e: Exception) {
Log.w(TAG, e)
replyNotAvailable(intent)
}
}
private fun replyNotAvailable(intent: Intent) {
val outIntent = Intent(ACTION_C2DM_REGISTRATION)
outIntent.putExtra(EXTRA_ERROR, PushRegisterManager.attachRequestId(ERROR_SERVICE_NOT_AVAILABLE, intent.requestId))
sendReply(intent, intent.appPackageName, outIntent)
}
private suspend fun register(intent: Intent) {
val packageName = intent.appPackageName ?: throw RuntimeException("No package provided")
ensureAppRegistrationAllowed(this, database, packageName)
Log.d(TAG, "register[req]: " + intent.toString() + " extras=" + intent.extras)
val bundle = completeRegisterRequest(this, database,
RegisterRequest()
.build(Utils.getBuild(this))
.sender(intent.getStringExtra(EXTRA_SENDER))
.checkin(LastCheckinInfo.read(this))
.app(packageName)
.extraParams(intent.extras))
val outIntent = Intent(ACTION_C2DM_REGISTRATION)
outIntent.putExtras(bundle)
Log.d(TAG, "register[res]: " + outIntent.toString() + " extras=" + outIntent.extras)
sendReply(intent, packageName, outIntent)
}
private suspend fun unregister(intent: Intent) {
val packageName = intent.appPackageName ?: throw RuntimeException("No package provided")
Log.d(TAG, "unregister[req]: " + intent.toString() + " extras=" + intent.extras)
val bundle = completeRegisterRequest(this, database, RegisterRequest()
.build(Utils.getBuild(this))
.sender(intent.getStringExtra(EXTRA_SENDER))
.checkin(LastCheckinInfo.read(this))
.app(packageName)
.extraParams(intent.extras)
)
val outIntent = Intent(ACTION_C2DM_REGISTRATION)
outIntent.putExtras(bundle)
Log.d(TAG, "unregister[res]: " + outIntent.toString() + " extras=" + outIntent.extras)
sendReply(intent, packageName, outIntent)
}
private fun sendReply(intent: Intent, packageName: String?, outIntent: Intent) {
if (sendReplyToMessenger(intent, outIntent)) return
outIntent.setPackage(packageName)
sendOrderedBroadcast(outIntent, null)
}
private fun sendReplyToMessenger(intent: Intent, outIntent: Intent): Boolean {
try {
val messenger = intent.getParcelableExtra<Messenger>(EXTRA_MESSENGER) ?: return false
val message = Message.obtain()
message.obj = outIntent
messenger.send(message)
return true
} catch (e: Exception) {
Log.w(TAG, e)
return false
}
}
override fun onBind(intent: Intent): IBinder? {
Log.d(TAG, "onBind: $intent")
super.onBind(intent)
if (ACTION_C2DM_REGISTER == intent.action) {
val messenger = Messenger(PushRegisterHandler(this, database, lifecycle))
return messenger.binder
}
return null
}
}
internal class PushRegisterHandler(private val context: Context, private val database: GcmDatabase, private val lifecycle: Lifecycle) : Handler(), LifecycleOwner {
override fun getLifecycle(): Lifecycle = lifecycle
private var callingUid = 0
override fun sendMessageAtTime(msg: Message, uptimeMillis: Long): Boolean {
callingUid = Binder.getCallingUid()
return super.sendMessageAtTime(msg, uptimeMillis)
}
private fun sendReplyViaMessage(what: Int, id: Int, replyTo: Messenger, messageData: Bundle) {
val response = Message.obtain()
response.what = what
response.arg1 = id
response.data = messageData
try {
replyTo.send(response)
} catch (e: RemoteException) {
Log.w(TAG, e)
}
}
private fun sendReplyViaIntent(outIntent: Intent, replyTo: Messenger) {
val message = Message.obtain()
message.obj = outIntent
try {
replyTo.send(message)
} catch (e: RemoteException) {
Log.w(TAG, e)
}
}
private fun sendReply(what: Int, id: Int, replyTo: Messenger, data: Bundle) {
if (what == 0) {
val outIntent = Intent(ACTION_C2DM_REGISTRATION)
outIntent.putExtras(data)
sendReplyViaIntent(outIntent, replyTo)
return
}
val messageData = Bundle()
messageData.putBundle("data", data)
sendReplyViaMessage(what, id, replyTo, messageData)
}
private fun replyError(what: Int, id: Int, replyTo: Messenger, errorMessage: String) {
val bundle = Bundle()
bundle.putString(EXTRA_ERROR, errorMessage)
sendReply(what, id, replyTo, bundle)
}
private fun replyNotAvailable(what: Int, id: Int, replyTo: Messenger) {
replyError(what, id, replyTo, ERROR_SERVICE_NOT_AVAILABLE)
}
private val selfAuthIntent: PendingIntent
get() {
val intent = Intent()
intent.setPackage("com.google.example.invalidpackage")
return PendingIntent.getBroadcast(context, 0, intent, 0)
}
override fun handleMessage(msg: Message) {
var getmsg = msg
val obj = getmsg.obj
if (getmsg.what == 0) {
if (obj is Intent) {
val nuMsg = Message.obtain()
nuMsg.what = getmsg.what
nuMsg.arg1 = 0
nuMsg.replyTo = null
val packageName = obj.appPackageName
val data = Bundle()
data.putBoolean("oneWay", false)
data.putString("pkg", packageName)
data.putBundle("data", getmsg.data)
nuMsg.data = data
getmsg = nuMsg
} else {
return
}
}
val what = getmsg.what
val id = getmsg.arg1
val replyTo = getmsg.replyTo
if (replyTo == null) {
Log.w(TAG, "replyTo is null")
return
}
val data = getmsg.data
val packageName = data.getString("pkg") ?: return
val subdata = data.getBundle("data")
try {
PackageUtils.checkPackageUid(context, packageName, callingUid)
} catch (e: SecurityException) {
Log.w(TAG, e)
return
}
Log.d(TAG, "handleMessage: package=$packageName what=$what id=$id")
val oneWay = data.getBoolean("oneWay", false)
when (what) {
0, 1 -> {
lifecycleScope.launchWhenStarted {
try {
val sender = subdata?.getString("sender")
val delete = subdata?.get("delete") != null
ensureCheckinIsUpToDate(context)
if (!delete) ensureAppRegistrationAllowed(context, database, packageName)
val bundle = completeRegisterRequest(context, database,
RegisterRequest()
.build(Utils.getBuild(context))
.sender(sender)
.checkin(LastCheckinInfo.read(context))
.app(packageName)
.delete(delete)
.extraParams(subdata))
sendReply(what, id, replyTo, bundle)
} catch (e: Exception) {
Log.w(TAG, e)
replyNotAvailable(what, id, replyTo)
}
}
}
2 -> {
val messageId = subdata!!.getString("google.message_id")
Log.d(TAG, "Ack $messageId for $packageName")
val i = Intent(context, McsService::class.java)
i.action = McsConstants.ACTION_ACK
i.putExtra(EXTRA_APP, selfAuthIntent)
ForegroundServiceContext(context).startService(i)
}
else -> {
val bundle = Bundle()
bundle.putBoolean("unsupported", true)
sendReplyViaMessage(what, id, replyTo, bundle)
return
}
}
if (oneWay) {
val bundle = Bundle()
bundle.putBoolean("ack", true)
sendReplyViaMessage(what, id, replyTo, bundle)
}
}
}
class PushRegisterReceiver : WakefulBroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val intent2 = Intent(context, PushRegisterService::class.java)
if (intent.extras!!["delete"] != null) {
intent2.action = ACTION_C2DM_UNREGISTER
} else {
intent2.action = ACTION_C2DM_REGISTER
}
intent2.putExtras(intent.extras!!)
startWakefulService(context, intent2)
}
}