EN: Handle confirmation via resolution/pending intent instead of new task

This commit is contained in:
Marvin W 2020-09-05 23:51:00 +02:00
parent fd6d915f0a
commit d33391ebce
No known key found for this signature in database
GPG Key ID: 072E9235DB996F2A
4 changed files with 82 additions and 50 deletions

View File

@ -11,12 +11,14 @@ import android.widget.Button
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.google.android.gms.R import com.google.android.gms.R
import com.google.android.gms.nearby.exposurenotification.ExposureNotificationStatusCodes.*
import org.microg.gms.nearby.exposurenotification.* import org.microg.gms.nearby.exposurenotification.*
class ExposureNotificationsConfirmActivity : AppCompatActivity() { class ExposureNotificationsConfirmActivity : AppCompatActivity() {
private var resultCode: Int = FAILED private var resultCode: Int = RESULT_CANCELED
private val resultData: Bundle = Bundle() set(value) {
setResult(value)
field = value
}
private val receiver: ResultReceiver? private val receiver: ResultReceiver?
get() = intent.getParcelableExtra(KEY_CONFIRM_RECEIVER) get() = intent.getParcelableExtra(KEY_CONFIRM_RECEIVER)
private val action: String? private val action: String?
@ -47,22 +49,22 @@ class ExposureNotificationsConfirmActivity : AppCompatActivity() {
findViewById<Button>(android.R.id.button1).text = getString(R.string.exposure_confirm_keys_button) findViewById<Button>(android.R.id.button1).text = getString(R.string.exposure_confirm_keys_button)
} }
else -> { else -> {
resultCode = INTERNAL_ERROR resultCode = RESULT_CANCELED
finish() finish()
} }
} }
findViewById<Button>(android.R.id.button1).setOnClickListener { findViewById<Button>(android.R.id.button1).setOnClickListener {
resultCode = SUCCESS resultCode = RESULT_OK
finish() finish()
} }
findViewById<Button>(android.R.id.button2).setOnClickListener { findViewById<Button>(android.R.id.button2).setOnClickListener {
resultCode = FAILED_REJECTED_OPT_IN resultCode = RESULT_CANCELED
finish() finish()
} }
} }
override fun onStop() { override fun finish() {
super.onStop() receiver?.send(resultCode, Bundle())
receiver?.send(resultCode, resultData) super.finish()
} }
} }

View File

@ -25,6 +25,7 @@ const val KEY_CONFIRM_PACKAGE = "package"
const val CONFIRM_ACTION_START = "start" const val CONFIRM_ACTION_START = "start"
const val CONFIRM_ACTION_STOP = "stop" const val CONFIRM_ACTION_STOP = "stop"
const val CONFIRM_ACTION_KEYS = "keys" const val CONFIRM_ACTION_KEYS = "keys"
const val CONFIRM_PERMISSION_VALIDITY = 60 * 60 * 1000L
const val PERMISSION_EXPOSURE_CALLBACK = "com.google.android.gms.nearby.exposurenotification.EXPOSURE_CALLBACK" const val PERMISSION_EXPOSURE_CALLBACK = "com.google.android.gms.nearby.exposurenotification.EXPOSURE_CALLBACK"

View File

@ -54,6 +54,9 @@ class ExposureDatabase private constructor(private val context: Context) : SQLit
db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_DIAGNOSIS(package TEXT NOT NULL, token TEXT NOT NULL, tcid INTEGER REFERENCES $TABLE_TEK_CHECK(tcid) ON DELETE CASCADE, transmissionRiskLevel INTEGER NOT NULL);") db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_DIAGNOSIS(package TEXT NOT NULL, token TEXT NOT NULL, tcid INTEGER REFERENCES $TABLE_TEK_CHECK(tcid) ON DELETE CASCADE, transmissionRiskLevel INTEGER NOT NULL);")
db.execSQL("CREATE INDEX IF NOT EXISTS index_${TABLE_DIAGNOSIS}_package_token ON $TABLE_DIAGNOSIS(package, token);") db.execSQL("CREATE INDEX IF NOT EXISTS index_${TABLE_DIAGNOSIS}_package_token ON $TABLE_DIAGNOSIS(package, token);")
} }
if (oldVersion < 3) {
db.execSQL("CREATE TABLE $TABLE_APP_PERMS(package TEXT NOT NULL, sig TEXT NOT NULL, perm TEXT NOT NULL, timestamp INTEGER NOT NULL);")
}
} }
fun SQLiteDatabase.delete(table: String, whereClause: String, args: LongArray): Int = fun SQLiteDatabase.delete(table: String, whereClause: String, args: LongArray): Int =
@ -68,7 +71,23 @@ class ExposureDatabase private constructor(private val context: Context) : SQLit
val appLogEntries = delete(TABLE_APP_LOG, "timestamp < ?", longArrayOf(rollingStartTime)) val appLogEntries = delete(TABLE_APP_LOG, "timestamp < ?", longArrayOf(rollingStartTime))
val temporaryExposureKeys = delete(TABLE_TEK, "(rollingStartNumber + rollingPeriod) < ?", longArrayOf(rollingStartTime / ROLLING_WINDOW_LENGTH_MS)) val temporaryExposureKeys = delete(TABLE_TEK, "(rollingStartNumber + rollingPeriod) < ?", longArrayOf(rollingStartTime / ROLLING_WINDOW_LENGTH_MS))
val checkedTemporaryExposureKeys = delete(TABLE_TEK_CHECK, "(rollingStartNumber + rollingPeriod) < ?", longArrayOf(rollingStartTime / ROLLING_WINDOW_LENGTH_MS)) val checkedTemporaryExposureKeys = delete(TABLE_TEK_CHECK, "(rollingStartNumber + rollingPeriod) < ?", longArrayOf(rollingStartTime / ROLLING_WINDOW_LENGTH_MS))
Log.d(TAG, "Deleted on daily cleanup: $advertisements adv, $appLogEntries applogs, $temporaryExposureKeys teks, $checkedTemporaryExposureKeys cteks") val appPerms = delete(TABLE_APP_PERMS, "timestamp < ?", longArrayOf(System.currentTimeMillis() - CONFIRM_PERMISSION_VALIDITY))
Log.d(TAG, "Deleted on daily cleanup: $advertisements adv, $appLogEntries applogs, $temporaryExposureKeys teks, $checkedTemporaryExposureKeys cteks, $appPerms perms")
}
fun grantPermission(packageName: String, signatureDigest: String, permission: String, timestamp: Long = System.currentTimeMillis()) = writableDatabase.run {
insert(TABLE_APP_PERMS, "NULL", ContentValues().apply {
put("package", packageName)
put("sig", signatureDigest)
put("perm", permission)
put("timestamp", timestamp)
})
}
fun hasPermission(packageName: String, signatureDigest: String, permission: String, maxAge: Long = CONFIRM_PERMISSION_VALIDITY) = readableDatabase.run {
query(TABLE_APP_PERMS, arrayOf("MAX(timestamp)"), "package = ? AND sig = ? and perm = ?", arrayOf(packageName, signatureDigest, permission), null, null, null).use { cursor ->
cursor.moveToNext() && cursor.getLong(0) + maxAge > System.currentTimeMillis()
}
} }
fun noteAdvertisement(rpi: ByteArray, aem: ByteArray, rssi: Int, timestamp: Long = Date().time) = writableDatabase.run { fun noteAdvertisement(rpi: ByteArray, aem: ByteArray, rssi: Int, timestamp: Long = Date().time) = writableDatabase.run {
@ -496,13 +515,14 @@ class ExposureDatabase private constructor(private val context: Context) : SQLit
companion object { companion object {
private const val DB_NAME = "exposure.db" private const val DB_NAME = "exposure.db"
private const val DB_VERSION = 2 private const val DB_VERSION = 3
private const val TABLE_ADVERTISEMENTS = "advertisements" private const val TABLE_ADVERTISEMENTS = "advertisements"
private const val TABLE_APP_LOG = "app_log" private const val TABLE_APP_LOG = "app_log"
private const val TABLE_TEK = "tek" private const val TABLE_TEK = "tek"
private const val TABLE_TEK_CHECK = "tek_check" private const val TABLE_TEK_CHECK = "tek_check"
private const val TABLE_DIAGNOSIS = "diagnosis" private const val TABLE_DIAGNOSIS = "diagnosis"
private const val TABLE_CONFIGURATIONS = "configurations" private const val TABLE_CONFIGURATIONS = "configurations"
private const val TABLE_APP_PERMS = "app_perms"
private var instance: ExposureDatabase? = null private var instance: ExposureDatabase? = null
fun ref(context: Context): ExposureDatabase = synchronized(this) { fun ref(context: Context): ExposureDatabase = synchronized(this) {

View File

@ -5,6 +5,8 @@
package org.microg.gms.nearby.exposurenotification package org.microg.gms.nearby.exposurenotification
import android.app.Activity
import android.app.PendingIntent
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@ -13,12 +15,14 @@ import android.os.*
import android.util.Log import android.util.Log
import com.google.android.gms.common.api.CommonStatusCodes import com.google.android.gms.common.api.CommonStatusCodes
import com.google.android.gms.common.api.Status import com.google.android.gms.common.api.Status
import com.google.android.gms.nearby.exposurenotification.ExposureNotificationStatusCodes
import com.google.android.gms.nearby.exposurenotification.ExposureNotificationStatusCodes.* import com.google.android.gms.nearby.exposurenotification.ExposureNotificationStatusCodes.*
import com.google.android.gms.nearby.exposurenotification.ExposureSummary import com.google.android.gms.nearby.exposurenotification.ExposureSummary
import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey
import com.google.android.gms.nearby.exposurenotification.internal.* import com.google.android.gms.nearby.exposurenotification.internal.*
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import org.microg.gms.common.PackageUtils
import org.microg.gms.nearby.exposurenotification.Constants.* import org.microg.gms.nearby.exposurenotification.Constants.*
import org.microg.gms.nearby.exposurenotification.proto.TemporaryExposureKeyExport import org.microg.gms.nearby.exposurenotification.proto.TemporaryExposureKeyExport
import org.microg.gms.nearby.exposurenotification.proto.TemporaryExposureKeyProto import org.microg.gms.nearby.exposurenotification.proto.TemporaryExposureKeyProto
@ -26,48 +30,51 @@ import java.util.*
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
class ExposureNotificationServiceImpl(private val context: Context, private val packageName: String) : INearbyExposureNotificationService.Stub() { class ExposureNotificationServiceImpl(private val context: Context, private val packageName: String) : INearbyExposureNotificationService.Stub() {
private fun confirm(action: String, callback: (resultCode: Int, resultData: Bundle?) -> Unit) { private fun pendingConfirm(permission: String): PendingIntent {
val intent = Intent(ACTION_CONFIRM) val intent = Intent(ACTION_CONFIRM)
intent.`package` = context.packageName intent.`package` = context.packageName
intent.putExtra(KEY_CONFIRM_PACKAGE, packageName) intent.putExtra(KEY_CONFIRM_PACKAGE, packageName)
intent.putExtra(KEY_CONFIRM_ACTION, action) intent.putExtra(KEY_CONFIRM_ACTION, permission)
intent.putExtra(KEY_CONFIRM_RECEIVER, object : ResultReceiver(Handler(Looper.getMainLooper())) { intent.putExtra(KEY_CONFIRM_RECEIVER, object : ResultReceiver(null) {
override fun onReceiveResult(resultCode: Int, resultData: Bundle?) { override fun onReceiveResult(resultCode: Int, resultData: Bundle?) {
Log.d(TAG, "Result from action $action: ${getStatusCodeString(resultCode)}") if (resultCode == Activity.RESULT_OK) {
callback(resultCode, resultData) ExposureDatabase.with(context) { database -> database.grantPermission(packageName, PackageUtils.firstSignatureDigest(context, packageName)!!, permission) }
}
} }
}) })
intent.addFlags(FLAG_ACTIVITY_NEW_TASK)
intent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK)
intent.addFlags(FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
try { try {
intent.component = ComponentName(context, context.packageManager.resolveActivity(intent, 0)?.activityInfo?.name!!) intent.component = ComponentName(context, context.packageManager.resolveActivity(intent, 0)?.activityInfo?.name!!)
context.startActivity(intent)
} catch (e: Exception) { } catch (e: Exception) {
Log.w(TAG, e) Log.w(TAG, e)
callback(CommonStatusCodes.INTERNAL_ERROR, null) }
Log.d(TAG, "Pending: $intent")
val pi = PendingIntent.getActivity(context, permission.hashCode(), intent, PendingIntent.FLAG_ONE_SHOT)
Log.d(TAG, "Pending: $pi")
return pi
}
private fun confirmPermission(permission: String): Status {
if (packageName == context.packageName) return Status.SUCCESS
return ExposureDatabase.with(context) { database ->
if (!database.hasPermission(packageName, PackageUtils.firstSignatureDigest(context, packageName)!!, permission)) {
Status(RESOLUTION_REQUIRED, "Permission EN#$permission required.", pendingConfirm(permission))
} else {
Status.SUCCESS
}
} }
} }
override fun start(params: StartParams) { override fun start(params: StartParams) {
if (ExposurePreferences(context).enabled) { if (ExposurePreferences(context).enabled) return
params.callback.onResult(Status(FAILED_ALREADY_STARTED)) val status = confirmPermission(CONFIRM_ACTION_START)
return if (status.isSuccess) {
ExposurePreferences(context).enabled = true
ExposureDatabase.with(context) { database -> database.noteAppAction(packageName, "start") }
} }
confirm(CONFIRM_ACTION_START) { resultCode, resultData -> try {
if (resultCode == SUCCESS) { params.callback.onResult(status)
ExposurePreferences(context).enabled = true } catch (e: Exception) {
} Log.w(TAG, "Callback failed", e)
ExposureDatabase.with(context) { database ->
database.noteAppAction(packageName, "start", JSONObject().apply {
put("result", resultCode)
}.toString())
}
try {
params.callback.onResult(Status(if (resultCode == SUCCESS) SUCCESS else FAILED_REJECTED_OPT_IN, resultData?.getString("message")))
} catch (e: Exception) {
Log.w(TAG, "Callback failed", e)
}
} }
} }
@ -91,23 +98,25 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
} }
} }
override fun getTemporaryExposureKeyHistory(params: GetTemporaryExposureKeyHistoryParams): Unit = ExposureDatabase.with(context) { database -> override fun getTemporaryExposureKeyHistory(params: GetTemporaryExposureKeyHistoryParams) {
confirm(CONFIRM_ACTION_START) { resultCode, resultData -> val status = confirmPermission(CONFIRM_ACTION_KEYS)
val (status, response) = if (resultCode == SUCCESS) { val response = when {
SUCCESS to database.allKeys status.isSuccess -> ExposureDatabase.with(context) { database ->
} else { database.allKeys
FAILED_REJECTED_OPT_IN to emptyList()
} }
else -> emptyList()
}
ExposureDatabase.with(context) { database ->
database.noteAppAction(packageName, "getTemporaryExposureKeyHistory", JSONObject().apply { database.noteAppAction(packageName, "getTemporaryExposureKeyHistory", JSONObject().apply {
put("result", resultCode) put("result", status.statusCode)
put("response_keys_size", response.size) put("response_keys_size", response.size)
}.toString()) }.toString())
try { }
params.callback.onResult(Status(status, resultData?.getString("message")), response) try {
} catch (e: Exception) { params.callback.onResult(status, response)
Log.w(TAG, "Callback failed", e) } catch (e: Exception) {
} Log.w(TAG, "Callback failed", e)
} }
} }