Use PendingIntentCompat for mutable flag
This commit is contained in:
parent
bdaa619267
commit
00bb582614
16 changed files with 1272 additions and 11 deletions
|
|
@ -25,6 +25,7 @@ import android.telephony.SmsMessage
|
|||
import android.text.TextUtils
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import androidx.core.app.PendingIntentCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
|
|
@ -160,7 +161,7 @@ class SmsRetrieverCore(private val context: Context, override val lifecycle: Lif
|
|||
private fun getTimeoutPendingIntent(context: Context, packageName: String): PendingIntent {
|
||||
val intent = Intent(ACTION_SMS_RETRIEVE_TIMEOUT)
|
||||
intent.setPackage(packageName)
|
||||
return PendingIntent.getBroadcast(context, ++requestCode, intent, PendingIntent.FLAG_IMMUTABLE)
|
||||
return PendingIntentCompat.getBroadcast(context, ++requestCode, intent, 0, false)!!
|
||||
}
|
||||
|
||||
private fun tryHandleIncomingMessageAsRetrieverMessage(message: SmsMessage): Boolean {
|
||||
|
|
|
|||
61
play-services-base/core/build.gradle
Normal file
61
play-services-base/core/build.gradle
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2020, microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'maven-publish'
|
||||
apply plugin: 'signing'
|
||||
|
||||
dependencies {
|
||||
api project(':play-services-basement-ktx')
|
||||
|
||||
implementation "androidx.annotation:annotation:$annotationVersion"
|
||||
implementation "androidx.appcompat:appcompat:$appcompatVersion"
|
||||
implementation "androidx.core:core-ktx:$coreVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-service:$lifecycleVersion"
|
||||
implementation "androidx.navigation:navigation-fragment-ktx:$navigationVersion"
|
||||
implementation "androidx.navigation:navigation-ui-ktx:$navigationVersion"
|
||||
implementation "androidx.preference:preference-ktx:$preferenceVersion"
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
|
||||
}
|
||||
|
||||
android {
|
||||
namespace "org.microg.gms.base.core"
|
||||
|
||||
compileSdkVersion androidCompileSdk
|
||||
buildToolsVersion "$androidBuildVersionTools"
|
||||
|
||||
defaultConfig {
|
||||
versionName version
|
||||
minSdkVersion androidMinSdk
|
||||
targetSdkVersion androidTargetSdk
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
dataBinding = true
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = 1.8
|
||||
targetCompatibility = 1.8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = 1.8
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
disable 'MissingTranslation'
|
||||
}
|
||||
}
|
||||
|
||||
apply from: '../../gradle/publish-android.gradle'
|
||||
|
||||
description = 'microG service implementation for play-services-base'
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.microg.gms.utils
|
||||
|
||||
import android.app.AlarmManager
|
||||
import android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP
|
||||
import android.app.PendingIntent.FLAG_NO_CREATE
|
||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.os.Parcelable
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import androidx.core.app.PendingIntentCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import java.util.UUID
|
||||
|
||||
class IntentCacheManager<S : Service, T : Parcelable>(private val context: Context, private val clazz: Class<S>, private val type: Int) {
|
||||
private val lock = Any()
|
||||
private lateinit var content: ArrayList<T>
|
||||
private lateinit var id: String
|
||||
private var isReady: Boolean = false
|
||||
private val pendingActions: MutableList<() -> Unit> = arrayListOf()
|
||||
|
||||
init {
|
||||
val pendingIntent = PendingIntentCompat.getService(context, type, getIntent(), 0, true)!!
|
||||
val alarmManager = context.getSystemService<AlarmManager>()
|
||||
if (SDK_INT >= 19) {
|
||||
alarmManager?.setWindow(ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + TEN_YEARS, -1, pendingIntent)
|
||||
} else {
|
||||
alarmManager?.set(ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + TEN_YEARS, pendingIntent)
|
||||
}
|
||||
pendingIntent.send()
|
||||
}
|
||||
|
||||
private fun getIntent() = Intent(context, clazz).apply {
|
||||
action = ACTION
|
||||
putExtra(EXTRA_IS_CACHE, true)
|
||||
putExtra(EXTRA_CACHE_TYPE, this@IntentCacheManager.type)
|
||||
}
|
||||
|
||||
fun add(entry: T, check: (T) -> Boolean = { false }) = runIfReady {
|
||||
val iterator = content.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
if (check(iterator.next())) {
|
||||
iterator.remove()
|
||||
}
|
||||
}
|
||||
content.add(entry)
|
||||
updateIntent()
|
||||
}
|
||||
|
||||
fun remove(entry: T) = runIfReady {
|
||||
if (content.remove(entry)) updateIntent()
|
||||
}
|
||||
|
||||
fun removeIf(check: (T) -> Boolean) = runIfReady {
|
||||
var removed = false
|
||||
val iterator = content.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
if (check(iterator.next())) {
|
||||
iterator.remove()
|
||||
removed = true
|
||||
}
|
||||
}
|
||||
if (removed) updateIntent()
|
||||
}
|
||||
|
||||
fun clear() = runIfReady {
|
||||
content.clear()
|
||||
updateIntent()
|
||||
}
|
||||
|
||||
fun getId(): String? = if (this::id.isInitialized) id else null
|
||||
|
||||
fun getEntries(): List<T> = if (this::content.isInitialized) content else emptyList()
|
||||
|
||||
fun processIntent(intent: Intent) {
|
||||
if (isCache(intent) && getType(intent) == type) {
|
||||
synchronized(lock) {
|
||||
content = intent.getParcelableArrayListExtra(EXTRA_DATA) ?: arrayListOf()
|
||||
id = intent.getStringExtra(EXTRA_ID) ?: UUID.randomUUID().toString()
|
||||
if (!intent.hasExtra(EXTRA_ID)) {
|
||||
Log.d(TAG, "Created new intent cache with id $id")
|
||||
} else if (intent.hasExtra(EXTRA_DATA)) {
|
||||
Log.d(TAG, "Recovered data from intent cache with id $id")
|
||||
}
|
||||
pendingActions.forEach { it() }
|
||||
pendingActions.clear()
|
||||
isReady = true
|
||||
updateIntent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun runIfReady(action: () -> Unit) {
|
||||
synchronized(lock) {
|
||||
if (isReady) {
|
||||
action()
|
||||
} else {
|
||||
pendingActions.add(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateIntent() {
|
||||
synchronized(lock) {
|
||||
if (isReady) {
|
||||
val intent = getIntent().apply {
|
||||
putExtra(EXTRA_ID, id)
|
||||
putParcelableArrayListExtra(EXTRA_DATA, content)
|
||||
}
|
||||
val pendingIntent = PendingIntentCompat.getService(context, type, intent, FLAG_NO_CREATE or FLAG_UPDATE_CURRENT, true)
|
||||
if (pendingIntent == null) {
|
||||
Log.w(TAG, "Failed to update existing pending intent, will likely have a loss of information")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "IntentCacheManager"
|
||||
private const val TEN_YEARS = 315360000000L
|
||||
private const val ACTION = "org.microg.gms.ACTION_INTENT_CACHE_MANAGER"
|
||||
private const val EXTRA_IS_CACHE = "org.microg.gms.IntentCacheManager.is_cache"
|
||||
private const val EXTRA_CACHE_TYPE = "org.microg.gms.IntentCacheManager.cache_type"
|
||||
private const val EXTRA_ID = "org.microg.gms.IntentCacheManager.id"
|
||||
private const val EXTRA_DATA = "org.microg.gms.IntentCacheManager.data"
|
||||
|
||||
inline fun<reified S: Service, T: Parcelable> create(context: Context, type: Int) = IntentCacheManager<S, T>(context, S::class.java, type)
|
||||
|
||||
fun isCache(intent: Intent): Boolean = try {
|
||||
intent.getBooleanExtra(EXTRA_IS_CACHE, false)
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
|
||||
fun getType(intent: Intent): Int {
|
||||
val ret = intent.getIntExtra(EXTRA_CACHE_TYPE, -1)
|
||||
if (ret == -1) throw IllegalArgumentException()
|
||||
return ret
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import android.os.Parcel
|
||||
import android.util.Log
|
||||
import androidx.core.app.PendingIntentCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
|
|
@ -51,10 +52,10 @@ class GamesConnectServiceImpl(val context: Context, override val lifecycle: Life
|
|||
override fun signIn(callback: IGamesConnectCallbacks?, request: GamesSignInRequest?) {
|
||||
Log.d(TAG, "signIn($request)")
|
||||
fun sendSignInRequired() {
|
||||
val resolution = PendingIntent.getActivity(context, packageName.hashCode(), Intent(context, GamesSignInActivity::class.java).apply {
|
||||
val resolution = PendingIntentCompat.getActivity(context, packageName.hashCode(), Intent(context, GamesSignInActivity::class.java).apply {
|
||||
putExtra(EXTRA_GAME_PACKAGE_NAME, packageName)
|
||||
putExtra(EXTRA_SCOPES, arrayOf(Scopes.GAMES_LITE))
|
||||
}, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
}, PendingIntent.FLAG_UPDATE_CURRENT, false)
|
||||
when (request?.signInType) {
|
||||
0 -> { // Manual sign in, provide resolution
|
||||
callback?.onSignIn(Status(CommonStatusCodes.SIGN_IN_REQUIRED, null, resolution), null)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import android.os.Bundle
|
|||
import android.os.IBinder
|
||||
import android.os.Parcel
|
||||
import android.util.Log
|
||||
import androidx.core.app.PendingIntentCompat
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
|
|
@ -55,7 +56,7 @@ class GamesService : BaseService(TAG, GmsService.GAMES) {
|
|||
Log.d(TAG, "Sending SIGN_IN_REQUIRED to $packageName")
|
||||
callback.onPostInitCompleteWithConnectionInfo(ConnectionResult.SIGN_IN_REQUIRED, null, ConnectionInfo().apply {
|
||||
params = bundleOf(
|
||||
"pendingIntent" to PendingIntent.getActivity(
|
||||
"pendingIntent" to PendingIntentCompat.getActivity(
|
||||
this@GamesService,
|
||||
packageName.hashCode(),
|
||||
Intent(this@GamesService, GamesSignInActivity::class.java).apply {
|
||||
|
|
@ -63,7 +64,8 @@ class GamesService : BaseService(TAG, GmsService.GAMES) {
|
|||
putExtra(EXTRA_ACCOUNT, request.account)
|
||||
putExtra(EXTRA_SCOPES, request.scopes)
|
||||
},
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
PendingIntent.FLAG_UPDATE_CURRENT,
|
||||
false
|
||||
)
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -6,14 +6,13 @@
|
|||
package org.microg.gms.fido.core.privileged
|
||||
|
||||
import android.app.KeyguardManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.PendingIntent.FLAG_IMMUTABLE
|
||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
import android.content.Context
|
||||
import android.content.Context.KEYGUARD_SERVICE
|
||||
import android.content.Intent
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.os.Parcel
|
||||
import androidx.core.app.PendingIntentCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
|
|
@ -70,7 +69,7 @@ class Fido2PrivilegedServiceImpl(private val context: Context, override val life
|
|||
.putExtra(KEY_OPTIONS, options.serializeToBytes())
|
||||
|
||||
val pendingIntent =
|
||||
PendingIntent.getActivity(context, options.hashCode(), intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
|
||||
PendingIntentCompat.getActivity(context, options.hashCode(), intent, FLAG_UPDATE_CURRENT, false)
|
||||
callbacks.onPendingIntent(Status.SUCCESS, pendingIntent)
|
||||
}
|
||||
}
|
||||
|
|
@ -84,7 +83,7 @@ class Fido2PrivilegedServiceImpl(private val context: Context, override val life
|
|||
.putExtra(KEY_OPTIONS, options.serializeToBytes())
|
||||
|
||||
val pendingIntent =
|
||||
PendingIntent.getActivity(context, options.hashCode(), intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
|
||||
PendingIntentCompat.getActivity(context, options.hashCode(), intent, FLAG_UPDATE_CURRENT, false)
|
||||
callbacks.onPendingIntent(Status.SUCCESS, pendingIntent)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.microg.gms.fido.core.transport.nfc
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.nfc.NfcAdapter
|
||||
import android.nfc.Tag
|
||||
import android.nfc.tech.IsoDep
|
||||
import android.util.Log
|
||||
import androidx.core.app.OnNewIntentProvider
|
||||
import androidx.core.app.PendingIntentCompat
|
||||
import androidx.core.util.Consumer
|
||||
import com.google.android.gms.fido.fido2.api.common.AuthenticatorAssertionResponse
|
||||
import com.google.android.gms.fido.fido2.api.common.AuthenticatorAttestationResponse
|
||||
import com.google.android.gms.fido.fido2.api.common.AuthenticatorResponse
|
||||
import com.google.android.gms.fido.fido2.api.common.RequestOptions
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import org.microg.gms.fido.core.RequestOptionsType
|
||||
import org.microg.gms.fido.core.transport.Transport
|
||||
import org.microg.gms.fido.core.transport.TransportHandler
|
||||
import org.microg.gms.fido.core.transport.TransportHandlerCallback
|
||||
import org.microg.gms.fido.core.type
|
||||
|
||||
class NfcTransportHandler(private val activity: Activity, callback: TransportHandlerCallback? = null) :
|
||||
TransportHandler(Transport.NFC, callback) {
|
||||
override val isSupported: Boolean
|
||||
get() = NfcAdapter.getDefaultAdapter(activity)?.isEnabled == true && activity is OnNewIntentProvider
|
||||
|
||||
private var deferred = CompletableDeferred<Tag>()
|
||||
|
||||
private suspend fun waitForNewNfcTag(adapter: NfcAdapter): Tag {
|
||||
val intent = Intent(activity, activity.javaClass).apply { addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) }
|
||||
val pendingIntent: PendingIntent = PendingIntentCompat.getActivity(activity, 0, intent, 0, true)!!
|
||||
adapter.enableForegroundDispatch(
|
||||
activity,
|
||||
pendingIntent,
|
||||
arrayOf(IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED)),
|
||||
arrayOf(arrayOf(IsoDep::class.java.name))
|
||||
)
|
||||
invokeStatusChanged(TransportHandlerCallback.STATUS_WAITING_FOR_DEVICE)
|
||||
val tag = deferred.await()
|
||||
deferred = CompletableDeferred()
|
||||
return tag
|
||||
}
|
||||
|
||||
suspend fun register(
|
||||
options: RequestOptions,
|
||||
callerPackage: String,
|
||||
tag: Tag
|
||||
): AuthenticatorAttestationResponse {
|
||||
return CtapNfcConnection(activity, tag).open {
|
||||
register(it, activity, options, callerPackage)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sign(
|
||||
options: RequestOptions,
|
||||
callerPackage: String,
|
||||
tag: Tag
|
||||
): AuthenticatorAssertionResponse {
|
||||
return CtapNfcConnection(activity, tag).open {
|
||||
sign(it, activity, options, callerPackage)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
suspend fun handle(
|
||||
options: RequestOptions,
|
||||
callerPackage: String,
|
||||
tag: Tag
|
||||
): AuthenticatorResponse {
|
||||
return when (options.type) {
|
||||
RequestOptionsType.REGISTER -> register(options, callerPackage, tag)
|
||||
RequestOptionsType.SIGN -> sign(options, callerPackage, tag)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override suspend fun start(options: RequestOptions, callerPackage: String): AuthenticatorResponse {
|
||||
val adapter = NfcAdapter.getDefaultAdapter(activity)
|
||||
val newIntentListener = Consumer<Intent> {
|
||||
if (it?.action != NfcAdapter.ACTION_TECH_DISCOVERED) return@Consumer
|
||||
val tag = it.getParcelableExtra<Tag>(NfcAdapter.EXTRA_TAG) ?: return@Consumer
|
||||
deferred.complete(tag)
|
||||
}
|
||||
try {
|
||||
(activity as OnNewIntentProvider).addOnNewIntentListener(newIntentListener)
|
||||
while (true) {
|
||||
val tag = waitForNewNfcTag(adapter)
|
||||
try {
|
||||
return handle(options, callerPackage, tag)
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, e)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
(activity as OnNewIntentProvider).removeOnNewIntentListener(newIntentListener)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "FidoNfcHandler"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.microg.gms.fido.core.transport.usb
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.hardware.usb.UsbDevice
|
||||
import android.hardware.usb.UsbManager
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import androidx.core.app.PendingIntentCompat
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
|
||||
private val Context.usbPermissionCallbackAction
|
||||
get() = "$packageName.USB_PERMISSION_CALLBACK"
|
||||
|
||||
private object UsbDevicePermissionReceiver : BroadcastReceiver() {
|
||||
private var registered = false
|
||||
private val pendingRequests = hashMapOf<UsbDevice, MutableList<CompletableDeferred<Boolean>>>()
|
||||
|
||||
fun register(context: Context) = synchronized(this) {
|
||||
if (!registered) {
|
||||
context.registerReceiver(this, IntentFilter(context.usbPermissionCallbackAction))
|
||||
registered = true
|
||||
}
|
||||
}
|
||||
|
||||
fun addDeferred(device: UsbDevice, deferred: CompletableDeferred<Boolean>) = synchronized(this) {
|
||||
if (pendingRequests.containsKey(device)) {
|
||||
pendingRequests[device]!!.add(deferred)
|
||||
false
|
||||
} else {
|
||||
pendingRequests[device] = arrayListOf(deferred)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fun unregister(context: Context) = synchronized(this) {
|
||||
if (registered) {
|
||||
context.unregisterReceiver(this)
|
||||
registered = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
if (intent == null || context == null) return
|
||||
if (intent.action == context.usbPermissionCallbackAction) {
|
||||
val device = intent.getParcelableExtra<UsbDevice>(UsbManager.EXTRA_DEVICE)
|
||||
if (device != null) {
|
||||
synchronized(this) {
|
||||
if (pendingRequests.containsKey(device)) {
|
||||
val hasPermission = context.usbManager?.hasPermission(device) == true
|
||||
for (deferred in pendingRequests[device].orEmpty()) {
|
||||
deferred.complete(hasPermission)
|
||||
}
|
||||
pendingRequests.remove(device)
|
||||
if (pendingRequests.isEmpty()) {
|
||||
unregister(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class UsbDevicePermissionManager(private val context: Context) {
|
||||
|
||||
suspend fun awaitPermission(device: UsbDevice): Boolean {
|
||||
if (context.usbManager?.hasPermission(device) == true) return true
|
||||
val res = CompletableDeferred<Boolean>()
|
||||
if (UsbDevicePermissionReceiver.addDeferred(device, res)) {
|
||||
UsbDevicePermissionReceiver.register(context)
|
||||
val intent = PendingIntentCompat.getBroadcast(context, 0, Intent(context.usbPermissionCallbackAction).apply { `package` = context.packageName }, 0, true)
|
||||
context.usbManager?.requestPermission(device, intent)
|
||||
}
|
||||
return res.await()
|
||||
}
|
||||
}
|
||||
57
play-services-location/core/provider/build.gradle
Normal file
57
play-services-location/core/provider/build.gradle
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
dependencies {
|
||||
api project(':play-services-location')
|
||||
compileOnly project(':play-services-location-core-system-api')
|
||||
implementation project(':play-services-base-core')
|
||||
implementation project(':play-services-location-core-base')
|
||||
|
||||
implementation "androidx.core:core-ktx:$coreVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-service:$lifecycleVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion"
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion"
|
||||
|
||||
implementation "com.android.volley:volley:$volleyVersion"
|
||||
|
||||
implementation 'org.microg:address-formatter:0.3.1'
|
||||
}
|
||||
|
||||
android {
|
||||
namespace "org.microg.gms.location.provider"
|
||||
|
||||
compileSdkVersion androidCompileSdk
|
||||
buildToolsVersion "$androidBuildVersionTools"
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
versionName version
|
||||
minSdkVersion androidMinSdk
|
||||
targetSdkVersion androidTargetSdk
|
||||
buildConfigField "String", "ICHNAEA_KEY", "\"${localProperties.get("ichnaea.key", "")}\""
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = 1.8
|
||||
targetCompatibility = 1.8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = 1.8
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.microg.gms.location.provider
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.location.Criteria
|
||||
import android.location.Location
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.os.WorkSource
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.PendingIntentCompat
|
||||
import com.android.location.provider.ProviderPropertiesUnbundled
|
||||
import com.android.location.provider.ProviderRequestUnbundled
|
||||
import org.microg.gms.location.*
|
||||
import org.microg.gms.location.network.LOCATION_EXTRA_PRECISION
|
||||
import org.microg.gms.location.network.NetworkLocationService
|
||||
import org.microg.gms.location.provider.NetworkLocationProviderService.Companion.ACTION_REPORT_LOCATION
|
||||
import java.io.PrintWriter
|
||||
import kotlin.math.max
|
||||
|
||||
class NetworkLocationProviderPreTiramisu : AbstractLocationProviderPreTiramisu {
|
||||
@Deprecated("Use only with SDK < 31")
|
||||
constructor(context: Context, legacy: Unit) : super(properties) {
|
||||
this.context = context
|
||||
}
|
||||
|
||||
@RequiresApi(31)
|
||||
constructor(context: Context) : super(context, properties) {
|
||||
this.context = context
|
||||
}
|
||||
|
||||
private val context: Context
|
||||
private var enabled = false
|
||||
private var currentRequest: ProviderRequestUnbundled? = null
|
||||
private var pendingIntent: PendingIntent? = null
|
||||
private var lastReportedLocation: Location? = null
|
||||
|
||||
private fun updateRequest() {
|
||||
if (enabled) {
|
||||
val forceNow: Boolean
|
||||
val intervalMillis: Long
|
||||
if (currentRequest?.reportLocation == true) {
|
||||
forceNow = true
|
||||
intervalMillis = max(currentRequest?.interval ?: Long.MAX_VALUE, MIN_INTERVAL_MILLIS)
|
||||
} else {
|
||||
forceNow = false
|
||||
intervalMillis = Long.MAX_VALUE
|
||||
}
|
||||
val intent = Intent(context, NetworkLocationService::class.java)
|
||||
intent.putExtra(EXTRA_PENDING_INTENT, pendingIntent)
|
||||
intent.putExtra(EXTRA_ENABLE, true)
|
||||
intent.putExtra(EXTRA_INTERVAL_MILLIS, intervalMillis)
|
||||
intent.putExtra(EXTRA_FORCE_NOW, forceNow)
|
||||
if (SDK_INT >= 31) {
|
||||
intent.putExtra(EXTRA_LOW_POWER, currentRequest?.isLowPower ?: false)
|
||||
intent.putExtra(EXTRA_WORK_SOURCE, currentRequest?.workSource)
|
||||
}
|
||||
if (SDK_INT >= 29) {
|
||||
intent.putExtra(EXTRA_BYPASS, currentRequest?.isLocationSettingsIgnored ?: false)
|
||||
}
|
||||
context.startService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun dump(writer: PrintWriter) {
|
||||
writer.println("Enabled: $enabled")
|
||||
writer.println("Current request: $currentRequest")
|
||||
writer.println("Last reported: $lastReportedLocation")
|
||||
}
|
||||
|
||||
override fun onSetRequest(request: ProviderRequestUnbundled, source: WorkSource) {
|
||||
synchronized(this) {
|
||||
currentRequest = request
|
||||
updateRequest()
|
||||
}
|
||||
}
|
||||
|
||||
override fun enable() {
|
||||
synchronized(this) {
|
||||
if (enabled) throw IllegalStateException()
|
||||
val intent = Intent(context, NetworkLocationProviderService::class.java)
|
||||
intent.action = ACTION_REPORT_LOCATION
|
||||
pendingIntent = PendingIntentCompat.getService(context, 0, intent, FLAG_UPDATE_CURRENT, true)
|
||||
currentRequest = null
|
||||
enabled = true
|
||||
when {
|
||||
SDK_INT >= 30 -> isAllowed = true
|
||||
SDK_INT >= 29 -> isEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun disable() {
|
||||
synchronized(this) {
|
||||
if (!enabled) throw IllegalStateException()
|
||||
val intent = Intent(context, NetworkLocationService::class.java)
|
||||
intent.putExtra(EXTRA_PENDING_INTENT, pendingIntent)
|
||||
intent.putExtra(EXTRA_ENABLE, false)
|
||||
context.startService(intent)
|
||||
pendingIntent?.cancel()
|
||||
pendingIntent = null
|
||||
currentRequest = null
|
||||
enabled = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun reportLocationToSystem(location: Location) {
|
||||
location.provider = "network"
|
||||
location.extras?.remove(LOCATION_EXTRA_PRECISION)
|
||||
lastReportedLocation = location
|
||||
super.reportLocation(location)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MIN_INTERVAL_MILLIS = 20000L
|
||||
private val properties = ProviderPropertiesUnbundled.create(false, false, false, false, true, true, true, Criteria.POWER_LOW, Criteria.ACCURACY_COARSE)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,268 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2020, microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.microg.gms.nearby.exposurenotification
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.annotation.TargetApi
|
||||
import android.app.AlarmManager
|
||||
import android.app.PendingIntent.FLAG_ONE_SHOT
|
||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
import android.bluetooth.BluetoothAdapter.*
|
||||
import android.bluetooth.le.*
|
||||
import android.bluetooth.le.AdvertiseSettings.*
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import androidx.core.app.PendingIntentCompat
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.microg.gms.common.ForegroundServiceContext
|
||||
import org.microg.gms.common.ForegroundServiceInfo
|
||||
import java.io.FileDescriptor
|
||||
import java.io.PrintWriter
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.*
|
||||
|
||||
@TargetApi(21)
|
||||
@ForegroundServiceInfo("Exposure Notification")
|
||||
class AdvertiserService : LifecycleService() {
|
||||
private val version = VERSION_1_1
|
||||
private var advertising = false
|
||||
private var wantStartAdvertising = false
|
||||
private val advertiser: BluetoothLeAdvertiser?
|
||||
get() = getDefaultAdapter()?.bluetoothLeAdvertiser
|
||||
private val alarmManager: AlarmManager
|
||||
get() = getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
private val callback: AdvertiseCallback = object : AdvertiseCallback() {
|
||||
override fun onStartSuccess(settingsInEffect: AdvertiseSettings?) {
|
||||
Log.d(TAG, "Advertising active for ${settingsInEffect?.timeout}ms")
|
||||
}
|
||||
|
||||
override fun onStartFailure(errorCode: Int) {
|
||||
Log.w(TAG, "Advertising failed: $errorCode")
|
||||
stopOrRestartAdvertising()
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(23)
|
||||
private var setCallback: Any? = null
|
||||
private val trigger = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
if (intent?.action == ACTION_STATE_CHANGED) {
|
||||
when (intent.getIntExtra(EXTRA_STATE, -1)) {
|
||||
STATE_TURNING_OFF, STATE_OFF -> stopOrRestartAdvertising()
|
||||
STATE_ON -> startAdvertisingIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private val startLaterRunnable = Runnable { startAdvertisingIfNeeded() }
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
registerReceiver(trigger, IntentFilter().apply { addAction(ACTION_STATE_CHANGED) })
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
ForegroundServiceContext.completeForegroundService(this, intent, TAG)
|
||||
Log.d(TAG, "AdvertisingService.start: $intent")
|
||||
super.onStartCommand(intent, flags, startId)
|
||||
if (intent?.action == ACTION_RESTART_ADVERTISING && advertising) {
|
||||
stopOrRestartAdvertising()
|
||||
} else {
|
||||
startAdvertisingIfNeeded()
|
||||
}
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
unregisterReceiver(trigger)
|
||||
stopOrRestartAdvertising()
|
||||
handler.removeCallbacks(startLaterRunnable)
|
||||
}
|
||||
|
||||
private fun startAdvertisingIfNeeded() {
|
||||
if (ExposurePreferences(this).enabled) {
|
||||
lifecycleScope.launchWhenStarted {
|
||||
withContext(Dispatchers.IO) {
|
||||
startAdvertising()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
|
||||
private var lastStartTime = System.currentTimeMillis()
|
||||
private var sendingBytes = ByteArray(0)
|
||||
private var starting = false
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private suspend fun startAdvertising() {
|
||||
val advertiser = synchronized(this) {
|
||||
if (advertising || starting) return
|
||||
val advertiser = advertiser ?: return
|
||||
wantStartAdvertising = false
|
||||
starting = true
|
||||
advertiser
|
||||
}
|
||||
try {
|
||||
val aemBytes = when (version) {
|
||||
VERSION_1_0 -> byteArrayOf(
|
||||
version, // Version and flags
|
||||
currentDeviceInfo.txPowerCorrection, // TX power
|
||||
0x00, // Reserved
|
||||
0x00 // Reserved
|
||||
)
|
||||
VERSION_1_1 -> byteArrayOf(
|
||||
(version + currentDeviceInfo.confidence.toByte() * 4).toByte(), // Version and flags
|
||||
currentDeviceInfo.txPowerCorrection, // TX power
|
||||
0x00, // Reserved
|
||||
0x00 // Reserved
|
||||
)
|
||||
else -> return
|
||||
}
|
||||
var nextSend = nextKeyMillis.coerceAtLeast(10000)
|
||||
val payload = ExposureDatabase.with(this@AdvertiserService) { database ->
|
||||
database.generateCurrentPayload(aemBytes)
|
||||
}
|
||||
val data = AdvertiseData.Builder().addServiceUuid(SERVICE_UUID).addServiceData(SERVICE_UUID, payload).build()
|
||||
Log.i(TAG, "Starting advertiser")
|
||||
if (SDK_INT >= 26) {
|
||||
setCallback = SetCallback()
|
||||
val params = AdvertisingSetParameters.Builder()
|
||||
.setInterval(AdvertisingSetParameters.INTERVAL_MEDIUM)
|
||||
.setLegacyMode(true)
|
||||
.setTxPowerLevel(AdvertisingSetParameters.TX_POWER_LOW)
|
||||
.setConnectable(false)
|
||||
.build()
|
||||
try {
|
||||
advertiser.startAdvertisingSet(params, data, null, null, null, setCallback as AdvertisingSetCallback)
|
||||
} catch (e: SecurityException) {
|
||||
Log.e(TAG, "Couldn't start advertising: Need android.permission.BLUETOOTH_ADVERTISE permission.", )
|
||||
}
|
||||
} else {
|
||||
nextSend = nextSend.coerceAtMost(180000)
|
||||
val settings = Builder()
|
||||
.setTimeout(nextSend.toInt())
|
||||
.setAdvertiseMode(ADVERTISE_MODE_BALANCED)
|
||||
.setTxPowerLevel(ADVERTISE_TX_POWER_LOW)
|
||||
.setConnectable(false)
|
||||
.build()
|
||||
try {
|
||||
advertiser.startAdvertising(settings, data, callback)
|
||||
} catch (e: SecurityException) {
|
||||
Log.e(TAG, "Couldn't start advertising.", )
|
||||
}
|
||||
}
|
||||
synchronized(this) { advertising = true }
|
||||
sendingBytes = payload
|
||||
lastStartTime = System.currentTimeMillis()
|
||||
scheduleRestartAdvertising(nextSend)
|
||||
} finally {
|
||||
synchronized(this) { starting = false }
|
||||
}
|
||||
}
|
||||
|
||||
override fun dump(fd: FileDescriptor?, writer: PrintWriter?, args: Array<out String>?) {
|
||||
writer?.println("Advertising: $advertising")
|
||||
try {
|
||||
val startTime = lastStartTime
|
||||
val bytes = sendingBytes
|
||||
val (uuid, aem) = ByteBuffer.wrap(bytes).let { UUID(it.long, it.long) to it.int }
|
||||
writer?.println("""
|
||||
Last advertising:
|
||||
Since: ${Date(startTime)}
|
||||
RPI: $uuid
|
||||
Version: 0x${version.toString(16)}
|
||||
TX Power: ${currentDeviceInfo.txPowerCorrection}
|
||||
AEM: 0x${aem.toLong().let { if (it < 0) 0x100000000L + it else it }.toString(16)}
|
||||
""".trimIndent())
|
||||
} catch (e: Exception) {
|
||||
writer?.println("Last advertising: ${e.message ?: e.toString()}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun scheduleRestartAdvertising(nextSend: Long) {
|
||||
val intent = Intent(this, AdvertiserService::class.java).apply { action = ACTION_RESTART_ADVERTISING }
|
||||
val pendingIntent = PendingIntentCompat.getService(this, ACTION_RESTART_ADVERTISING.hashCode(), intent, FLAG_ONE_SHOT or FLAG_UPDATE_CURRENT, false)!!
|
||||
when {
|
||||
SDK_INT >= 23 ->
|
||||
alarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + nextSend, pendingIntent)
|
||||
else ->
|
||||
alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + nextSend, pendingIntent)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun stopOrRestartAdvertising() {
|
||||
if (!advertising) return
|
||||
Log.i(TAG, "Stopping advertiser")
|
||||
advertising = false
|
||||
if (SDK_INT >= 26) {
|
||||
wantStartAdvertising = true
|
||||
try {
|
||||
advertiser?.stopAdvertisingSet(setCallback as AdvertisingSetCallback)
|
||||
} catch (e: SecurityException) {
|
||||
Log.i(TAG, "Tried calling stopAdvertisingSet without android.permission.BLUETOOTH_ADVERTISE permission.", )
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
advertiser?.stopAdvertising(callback)
|
||||
} catch (e: SecurityException) {
|
||||
Log.i(TAG, "stopAdvertising() failed with a SecurityException. Maybe some permissions are missing?", )
|
||||
}
|
||||
}
|
||||
handler.postDelayed(startLaterRunnable, 1000)
|
||||
}
|
||||
|
||||
@TargetApi(26)
|
||||
inner class SetCallback : AdvertisingSetCallback() {
|
||||
override fun onAdvertisingSetStarted(advertisingSet: AdvertisingSet?, txPower: Int, status: Int) {
|
||||
Log.d(TAG, "Advertising active, status=$status")
|
||||
}
|
||||
|
||||
override fun onAdvertisingSetStopped(advertisingSet: AdvertisingSet?) {
|
||||
Log.d(TAG, "Advertising stopped")
|
||||
if (wantStartAdvertising) {
|
||||
startAdvertisingIfNeeded()
|
||||
} else {
|
||||
stopOrRestartAdvertising()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
private const val ACTION_RESTART_ADVERTISING = "org.microg.gms.nearby.exposurenotification.RESTART_ADVERTISING"
|
||||
|
||||
fun isNeeded(context: Context): Boolean {
|
||||
return ExposurePreferences(context).enabled
|
||||
}
|
||||
|
||||
fun isSupported(context: Context): Boolean? {
|
||||
val adapter = getDefaultAdapter()
|
||||
return when {
|
||||
adapter == null -> false
|
||||
SDK_INT >= 26 && (adapter.isLeExtendedAdvertisingSupported || adapter.isLePeriodicAdvertisingSupported) -> true
|
||||
adapter.state != STATE_ON -> null
|
||||
adapter.bluetoothLeAdvertiser != null -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2020, microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.microg.gms.nearby.exposurenotification
|
||||
|
||||
import android.app.AlarmManager
|
||||
import android.app.PendingIntent.FLAG_ONE_SHOT
|
||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import androidx.core.app.PendingIntentCompat
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.microg.gms.common.ForegroundServiceContext
|
||||
import org.microg.gms.common.ForegroundServiceInfo
|
||||
|
||||
@ForegroundServiceInfo("Exposure Notification")
|
||||
class CleanupService : LifecycleService() {
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
ForegroundServiceContext.completeForegroundService(this, intent, TAG)
|
||||
Log.d(TAG, "CleanupService.start: $intent")
|
||||
super.onStartCommand(intent, flags, startId)
|
||||
if (isNeeded(this, true)) {
|
||||
lifecycleScope.launchWhenStarted {
|
||||
withContext(Dispatchers.IO) {
|
||||
var workPending = true
|
||||
while (workPending) {
|
||||
ExposureDatabase.with(this@CleanupService) {
|
||||
workPending = !it.dailyCleanup()
|
||||
}
|
||||
if (workPending) delay(5000L)
|
||||
}
|
||||
ExposurePreferences(this@CleanupService).lastCleanup = System.currentTimeMillis()
|
||||
}
|
||||
stop()
|
||||
}
|
||||
} else {
|
||||
stop()
|
||||
}
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
val pendingIntent = PendingIntentCompat.getService(applicationContext, CleanupService::class.java.name.hashCode(), Intent(applicationContext, CleanupService::class.java), FLAG_ONE_SHOT or FLAG_UPDATE_CURRENT, false)!!
|
||||
alarmManager.set(AlarmManager.RTC, ExposurePreferences(this).lastCleanup + CLEANUP_INTERVAL, pendingIntent)
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun isNeeded(context: Context, now: Boolean = false): Boolean {
|
||||
return ExposurePreferences(context).let {
|
||||
(it.enabled && !now) || it.lastCleanup < System.currentTimeMillis() - CLEANUP_INTERVAL
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ import android.location.LocationManager
|
|||
import android.os.*
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import androidx.core.app.PendingIntentCompat
|
||||
import androidx.core.location.LocationManagerCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
|
|
@ -113,7 +114,7 @@ class ExposureNotificationServiceImpl(private val context: Context, override val
|
|||
Log.w(TAG, e)
|
||||
}
|
||||
Log.d(TAG, "Pending: $intent")
|
||||
val pi = PendingIntent.getActivity(context, permission.hashCode(), intent, PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE)
|
||||
val pi = PendingIntentCompat.getActivity(context, permission.hashCode(), intent, PendingIntent.FLAG_ONE_SHOT, false)!!
|
||||
Log.d(TAG, "Pending: $pi")
|
||||
return pi
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2020, microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.microg.gms.nearby.exposurenotification
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.app.*
|
||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Color
|
||||
import android.location.LocationManager
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.app.PendingIntentCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.location.LocationManagerCompat
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient
|
||||
import org.microg.gms.common.ForegroundServiceContext
|
||||
import org.microg.gms.common.ForegroundServiceInfo
|
||||
import org.microg.gms.nearby.core.R
|
||||
|
||||
@ForegroundServiceInfo("Exposure Notification")
|
||||
class NotifyService : LifecycleService() {
|
||||
private val notificationId = NotifyService::class.java.name.hashCode()
|
||||
private val trigger = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
updateNotification()
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(26)
|
||||
private fun createNotificationChannel(): String {
|
||||
val channel = NotificationChannel("exposure-notifications", "Exposure Notifications", NotificationManager.IMPORTANCE_HIGH)
|
||||
channel.setSound(null, null)
|
||||
channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
||||
channel.setShowBadge(true)
|
||||
if (SDK_INT >= 29) {
|
||||
channel.setAllowBubbles(false)
|
||||
}
|
||||
channel.vibrationPattern = longArrayOf(0)
|
||||
getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
|
||||
return channel.id
|
||||
}
|
||||
|
||||
@TargetApi(21)
|
||||
private fun updateNotification() {
|
||||
val location = !LocationManagerCompat.isLocationEnabled(getSystemService(Context.LOCATION_SERVICE) as LocationManager)
|
||||
val bluetooth = BluetoothAdapter.getDefaultAdapter()?.state.let { it != BluetoothAdapter.STATE_ON && it != BluetoothAdapter.STATE_TURNING_ON }
|
||||
val nearbyPermissions = arrayOf("android.permission.BLUETOOTH_ADVERTISE", "android.permission.BLUETOOTH_SCAN")
|
||||
val permissionNeedsHandling = SDK_INT >= 31 && nearbyPermissions.any {
|
||||
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
Log.d( TAG,"notify: location: $location, bluetooth: $bluetooth, permissionNeedsHandling: $permissionNeedsHandling")
|
||||
|
||||
val text: String = when {
|
||||
permissionNeedsHandling -> getString(R.string.exposure_notify_off_nearby)
|
||||
location && bluetooth -> getString(R.string.exposure_notify_off_bluetooth_location)
|
||||
location -> getString(R.string.exposure_notify_off_location)
|
||||
bluetooth -> getString(R.string.exposure_notify_off_bluetooth)
|
||||
else -> {
|
||||
NotificationManagerCompat.from(this).cancel(notificationId)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (SDK_INT >= 26) {
|
||||
NotificationCompat.Builder(this, createNotificationChannel())
|
||||
} else {
|
||||
NotificationCompat.Builder(this)
|
||||
}.apply {
|
||||
val typedValue = TypedValue()
|
||||
try {
|
||||
var resolved = theme.resolveAttribute(androidx.appcompat.R.attr.colorError, typedValue, true)
|
||||
if (!resolved && SDK_INT >= 26) resolved = theme.resolveAttribute(android.R.attr.colorError, typedValue, true)
|
||||
color = if (resolved) {
|
||||
ContextCompat.getColor(this@NotifyService, typedValue.resourceId)
|
||||
} else {
|
||||
Color.RED
|
||||
}
|
||||
if (SDK_INT >= 26) setColorized(true)
|
||||
} catch (e: Exception) {
|
||||
// Ignore
|
||||
}
|
||||
setSmallIcon(R.drawable.ic_virus_outline)
|
||||
setContentTitle(getString(R.string.exposure_notify_off_title))
|
||||
setContentText(text)
|
||||
setStyle(NotificationCompat.BigTextStyle())
|
||||
try {
|
||||
val intent = Intent(ExposureNotificationClient.ACTION_EXPOSURE_NOTIFICATION_SETTINGS).apply { `package` = packageName }
|
||||
intent.resolveActivity(packageManager)
|
||||
setContentIntent(PendingIntentCompat.getActivity(this@NotifyService, notificationId, Intent(ExposureNotificationClient.ACTION_EXPOSURE_NOTIFICATION_SETTINGS).apply { `package` = packageName }, FLAG_UPDATE_CURRENT, false))
|
||||
} catch (e: Exception) {
|
||||
// Ignore
|
||||
}
|
||||
}.let {
|
||||
NotificationManagerCompat.from(this).notify(notificationId, it.build())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
registerReceiver(trigger, IntentFilter().apply {
|
||||
addAction(BluetoothAdapter.ACTION_STATE_CHANGED)
|
||||
if (SDK_INT >= 19) addAction(LocationManager.MODE_CHANGED_ACTION)
|
||||
addAction(LocationManager.PROVIDERS_CHANGED_ACTION)
|
||||
addAction(NOTIFICATION_UPDATE_ACTION)
|
||||
})
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
ForegroundServiceContext.completeForegroundService(this, intent, TAG)
|
||||
Log.d(TAG, "NotifyService.start: $intent")
|
||||
super.onStartCommand(intent, flags, startId)
|
||||
updateNotification()
|
||||
return Service.START_STICKY
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
NotificationManagerCompat.from(this).cancel(notificationId)
|
||||
unregisterReceiver(trigger)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun isNeeded(context: Context): Boolean {
|
||||
return ExposurePreferences(context).let { it.enabled }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,197 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2020, microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.microg.gms.nearby.exposurenotification
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.annotation.TargetApi
|
||||
import android.app.AlarmManager
|
||||
import android.app.PendingIntent.FLAG_ONE_SHOT
|
||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
import android.bluetooth.BluetoothAdapter.*
|
||||
import android.bluetooth.le.*
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.*
|
||||
import android.util.Log
|
||||
import androidx.core.app.PendingIntentCompat
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import org.microg.gms.common.ForegroundServiceContext
|
||||
import org.microg.gms.common.ForegroundServiceInfo
|
||||
import java.io.FileDescriptor
|
||||
import java.io.PrintWriter
|
||||
import java.util.*
|
||||
|
||||
@TargetApi(21)
|
||||
@ForegroundServiceInfo("Exposure Notification")
|
||||
class ScannerService : LifecycleService() {
|
||||
private var scanning = false
|
||||
private var lastStartTime = 0L
|
||||
private var seenAdvertisements = 0L
|
||||
private var lastAdvertisement = 0L
|
||||
private val callback = object : ScanCallback() {
|
||||
override fun onScanResult(callbackType: Int, result: ScanResult?) {
|
||||
result?.let { onScanResult(it) }
|
||||
}
|
||||
|
||||
override fun onBatchScanResults(results: MutableList<ScanResult>) {
|
||||
for (result in results) {
|
||||
onScanResult(result)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onScanFailed(errorCode: Int) {
|
||||
Log.w(TAG, "onScanFailed: $errorCode")
|
||||
stopScan()
|
||||
}
|
||||
}
|
||||
private val trigger = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
if (intent?.action == ACTION_STATE_CHANGED) {
|
||||
when (intent.getIntExtra(EXTRA_STATE, -1)) {
|
||||
STATE_TURNING_OFF, STATE_OFF -> stopScan()
|
||||
STATE_ON -> startScanIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private val stopLaterRunnable = Runnable { stopScan() }
|
||||
|
||||
// Wake lock for the duration of scan. Otherwise we might fall asleep while scanning
|
||||
// resulting in potentially very long scan times
|
||||
private val wakeLock: PowerManager.WakeLock by lazy {
|
||||
powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, ScannerService::class.java.canonicalName).apply { setReferenceCounted(false) }
|
||||
}
|
||||
|
||||
private val scanner: BluetoothLeScanner?
|
||||
get() = getDefaultAdapter()?.bluetoothLeScanner
|
||||
private val alarmManager: AlarmManager
|
||||
get() = getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
private val powerManager: PowerManager
|
||||
get() = getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
ForegroundServiceContext.completeForegroundService(this, intent, TAG)
|
||||
Log.d(TAG, "ScannerService.start: $intent")
|
||||
super.onStartCommand(intent, flags, startId)
|
||||
startScanIfNeeded()
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
fun onScanResult(result: ScanResult) {
|
||||
val data = result.scanRecord?.serviceData?.get(SERVICE_UUID) ?: return
|
||||
if (data.size < 16) return // Ignore invalid advertisements
|
||||
seenAdvertisements++
|
||||
lastAdvertisement = System.currentTimeMillis()
|
||||
lifecycleScope.launchWhenStarted {
|
||||
ExposureDatabase.with(this@ScannerService) { database ->
|
||||
database.noteAdvertisement(data.sliceArray(0..15), data.drop(16).toByteArray(), result.rssi)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun startScanIfNeeded() {
|
||||
if (ExposurePreferences(this).enabled) {
|
||||
startScan()
|
||||
} else {
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
registerReceiver(trigger, IntentFilter().apply { addAction(ACTION_STATE_CHANGED) })
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
unregisterReceiver(trigger)
|
||||
stopScan()
|
||||
}
|
||||
|
||||
@SuppressLint("WakelockTimeout", "MissingPermission")
|
||||
@Synchronized
|
||||
private fun startScan() {
|
||||
if (scanning) return
|
||||
val scanner = scanner ?: return
|
||||
Log.i(TAG, "Starting scanner for service $SERVICE_UUID for ${SCANNING_TIME_MS}ms")
|
||||
seenAdvertisements = 0
|
||||
wakeLock.acquire()
|
||||
try {
|
||||
scanner.startScan(
|
||||
listOf(ScanFilter.Builder()
|
||||
.setServiceUuid(SERVICE_UUID)
|
||||
.setServiceData(SERVICE_UUID, byteArrayOf(0), byteArrayOf(0))
|
||||
.build()),
|
||||
ScanSettings.Builder().build(),
|
||||
callback
|
||||
)
|
||||
} catch (e: SecurityException) {
|
||||
Log.e(TAG, "Couldn't start ScannerService, need android.permission.BLUETOOTH_SCAN permission.")
|
||||
}
|
||||
scanning = true
|
||||
lastStartTime = System.currentTimeMillis()
|
||||
handler.postDelayed(stopLaterRunnable, SCANNING_TIME_MS)
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
@Synchronized
|
||||
private fun stopScan() {
|
||||
if (!scanning) return
|
||||
Log.i(TAG, "Stopping scanner for service $SERVICE_UUID, had seen $seenAdvertisements advertisements")
|
||||
handler.removeCallbacks(stopLaterRunnable)
|
||||
scanning = false
|
||||
try {
|
||||
scanner?.stopScan(callback)
|
||||
} catch (e: Exception) {
|
||||
// Ignored
|
||||
}
|
||||
if (ExposurePreferences(this).enabled) {
|
||||
scheduleStartScan(((lastStartTime + SCANNING_INTERVAL_MS) - System.currentTimeMillis()).coerceIn(0, SCANNING_INTERVAL_MS))
|
||||
}
|
||||
wakeLock.release()
|
||||
}
|
||||
|
||||
private fun scheduleStartScan(nextScan: Long) {
|
||||
val intent = Intent(this, ScannerService::class.java)
|
||||
val pendingIntent = PendingIntentCompat.getService(this, ScannerService::class.java.hashCode(), intent, FLAG_ONE_SHOT or FLAG_UPDATE_CURRENT, false)!!
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
// Note: there is no setWindowAndAllowWhileIdle()
|
||||
alarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + nextScan, pendingIntent)
|
||||
} else {
|
||||
alarmManager.setWindow(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + nextScan - SCANNING_TIME_MS / 2, SCANNING_TIME_MS, pendingIntent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun dump(fd: FileDescriptor?, writer: PrintWriter?, args: Array<out String>?) {
|
||||
writer?.println("Scanning now: $scanning")
|
||||
writer?.println("Last scan start: ${Date(lastStartTime)}")
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
writer?.println("Scan stop pending: ${handler.hasCallbacks(stopLaterRunnable)}")
|
||||
}
|
||||
writer?.println("Seen advertisements since last scan start: $seenAdvertisements")
|
||||
writer?.println("Last advertisement seen: ${Date(lastAdvertisement)}")
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun isNeeded(context: Context): Boolean {
|
||||
return ExposurePreferences(context).enabled
|
||||
}
|
||||
|
||||
fun isSupported(context: Context): Boolean? {
|
||||
val adapter = getDefaultAdapter()
|
||||
return when {
|
||||
adapter == null -> false
|
||||
adapter.state != STATE_ON -> null
|
||||
adapter.bluetoothLeScanner != null -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -114,7 +114,7 @@ public abstract class Task<TResult> {
|
|||
public abstract Task<TResult> addOnFailureListener(Activity activity, OnFailureListener listener);
|
||||
|
||||
/**
|
||||
* Adds an Activity-scoped listener that is called if the Task fails.
|
||||
* Adds a listener that is called if the Task fails.
|
||||
* <p/>
|
||||
* The listener will be called on main application thread. If the Task has already failed, a
|
||||
* call to the listener will be immediately scheduled. If multiple listeners are added, they
|
||||
|
|
|
|||
Loading…
Reference in a new issue