mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab619cb073 | ||
|
|
2d96822014 | ||
|
|
887adc87de | ||
|
|
4219bd02c5 | ||
|
|
48544464ae | ||
|
|
9b4f2477e4 | ||
|
|
26e8840484 | ||
|
|
b6ef6d3ce9 | ||
|
|
cdfcd24049 | ||
|
|
95a8e0b125 | ||
|
|
84b7509f33 | ||
|
|
a53baec9dc | ||
|
|
4b4bc86cf7 |
@@ -68,7 +68,9 @@ jobs:
|
|||||||
tag_name: ${{ steps.tagger.outputs.tag }}
|
tag_name: ${{ steps.tagger.outputs.tag }}
|
||||||
name: ${{ steps.tagger.outputs.tag }}
|
name: ${{ steps.tagger.outputs.tag }}
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
files: app/build/outputs/apk/foss/beta/*.apk
|
files: |
|
||||||
|
app/build/outputs/apk/foss/beta/*.apk
|
||||||
|
app-wear/build/outputs/apk/foss/beta/*.apk
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
@@ -80,7 +82,9 @@ jobs:
|
|||||||
tag_name: ${{ steps.tagger.outputs.tag }}
|
tag_name: ${{ steps.tagger.outputs.tag }}
|
||||||
name: ${{ steps.tagger.outputs.tag }}
|
name: ${{ steps.tagger.outputs.tag }}
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
files: app/build/outputs/apk/foss/release/*.apk
|
files: |
|
||||||
|
app/build/outputs/apk/foss/release/*.apk
|
||||||
|
app-wear/build/outputs/apk/foss/release/*.apk
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,13 @@ enum class Permission(
|
|||||||
isGranted = {
|
isGranted = {
|
||||||
android.provider.Settings.canDrawOverlays(it)
|
android.provider.Settings.canDrawOverlays(it)
|
||||||
},
|
},
|
||||||
)
|
),
|
||||||
|
POST_NOTIFICATIONS(
|
||||||
|
minApiLevel = Build.VERSION_CODES.S,
|
||||||
|
labelRes = R.string.permission_post_notifications_label,
|
||||||
|
descriptionRes = R.string.permission_post_notifications_description,
|
||||||
|
permissionId = "android.permission.POST_NOTIFICATIONS",
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Permission.isRequired(context: Context): Boolean = when {
|
fun Permission.isRequired(context: Context): Boolean = when {
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ interface UpgradeRepo {
|
|||||||
|
|
||||||
fun launchBillingFlow(activity: Activity)
|
fun launchBillingFlow(activity: Activity)
|
||||||
|
|
||||||
|
fun getSponsorUrl(): String? = null
|
||||||
|
|
||||||
interface Info {
|
interface Info {
|
||||||
val type: Type
|
val type: Type
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import android.bluetooth.le.ScanFilter
|
|||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.bluetooth.BleScanner
|
import eu.darken.capod.common.bluetooth.BleScanner
|
||||||
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
||||||
import eu.darken.capod.common.bluetooth.ScannerMode
|
|
||||||
import eu.darken.capod.common.coroutine.AppScope
|
import eu.darken.capod.common.coroutine.AppScope
|
||||||
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
||||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||||
@@ -14,6 +13,7 @@ import eu.darken.capod.common.debug.logging.log
|
|||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.common.flow.replayingShare
|
import eu.darken.capod.common.flow.replayingShare
|
||||||
import eu.darken.capod.main.core.GeneralSettings
|
import eu.darken.capod.main.core.GeneralSettings
|
||||||
|
import eu.darken.capod.main.core.PermissionTool
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.PodFactory
|
import eu.darken.capod.pods.core.PodFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
@@ -36,14 +36,103 @@ class PodMonitor @Inject constructor(
|
|||||||
private val bluetoothManager: BluetoothManager2,
|
private val bluetoothManager: BluetoothManager2,
|
||||||
private val debugSettings: DebugSettings,
|
private val debugSettings: DebugSettings,
|
||||||
private val podDeviceCache: PodDeviceCache,
|
private val podDeviceCache: PodDeviceCache,
|
||||||
|
private val permissionTool: PermissionTool,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val deviceCache = mutableMapOf<PodDevice.Id, PodDevice>()
|
private val deviceCache = mutableMapOf<PodDevice.Id, PodDevice>()
|
||||||
private val cacheLock = Mutex()
|
private val cacheLock = Mutex()
|
||||||
|
|
||||||
private suspend fun List<BleScanResult>.preFilterAndMap(
|
val devices: Flow<List<PodDevice>> = combine(
|
||||||
scannerMode: ScannerMode
|
permissionTool.missingPermissions,
|
||||||
): List<PodFactory.Result> = this
|
bluetoothManager.isBluetoothEnabled
|
||||||
|
) { missingPermissions, isBluetoothEnabled ->
|
||||||
|
log(TAG) { "devices: missingPermissions=$missingPermissions, isBluetoothEnabled=$isBluetoothEnabled" }
|
||||||
|
// We just want to retrigger if permissions change.
|
||||||
|
isBluetoothEnabled
|
||||||
|
}
|
||||||
|
.flatMapLatest { isReady ->
|
||||||
|
if (!isReady) {
|
||||||
|
log(TAG, WARN) { "Bluetooth is not ready" }
|
||||||
|
flowOf(null)
|
||||||
|
} else {
|
||||||
|
createBleScanner()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.map { newPods ->
|
||||||
|
val pods = processWithCache(newPods)
|
||||||
|
|
||||||
|
val presorted = sortPodsToInterest(pods.values)
|
||||||
|
val main = determineMainDevice(presorted)
|
||||||
|
newPods?.firstOrNull { it.device.identifier == main?.identifier }?.let {
|
||||||
|
podDeviceCache.saveMainDevice(it.scanResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
presorted.sortedByDescending { it == main }
|
||||||
|
}
|
||||||
|
.retryWhen { cause, attempt ->
|
||||||
|
log(TAG, WARN) { "PodMonitor failed (attempt=$attempt), will retry: ${cause.asLog()}" }
|
||||||
|
delay(3000)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
.onStart { emit(emptyList()) }
|
||||||
|
.replayingShare(appScope)
|
||||||
|
|
||||||
|
val mainDevice: Flow<PodDevice?> = devices
|
||||||
|
.map { determineMainDevice(it) }
|
||||||
|
.replayingShare(appScope)
|
||||||
|
|
||||||
|
private fun createBleScanner() = combine(
|
||||||
|
generalSettings.scannerMode.flow,
|
||||||
|
generalSettings.compatibilityMode.flow,
|
||||||
|
debugSettings.showUnfiltered.flow
|
||||||
|
) { scannerMode, compatMode, unfiltered ->
|
||||||
|
Triple(scannerMode, compatMode, unfiltered)
|
||||||
|
}
|
||||||
|
.flatMapLatest { (mode, compat, unfiltered) ->
|
||||||
|
val filters = when {
|
||||||
|
unfiltered -> {
|
||||||
|
log(TAG, WARN) { "Using unfiltered scan mode" }
|
||||||
|
setOf(getUnfilteredFilter())
|
||||||
|
}
|
||||||
|
else -> ProximityPairing.getBleScanFilter()
|
||||||
|
}
|
||||||
|
|
||||||
|
bleScanner.scan(
|
||||||
|
filters = filters,
|
||||||
|
scannerMode = mode,
|
||||||
|
compatMode = compat,
|
||||||
|
).map { preFilterAndMap(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun processWithCache(
|
||||||
|
newPods: List<PodFactory.Result>?
|
||||||
|
): Map<PodDevice.Id, PodDevice> = cacheLock.withLock {
|
||||||
|
if (newPods == null) {
|
||||||
|
log(TAG) { "Null result, Bluetooth is disabled." }
|
||||||
|
deviceCache.clear()
|
||||||
|
return emptyMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
val now = Instant.now()
|
||||||
|
deviceCache.toList().forEach { (key, value) ->
|
||||||
|
if (Duration.between(value.seenLastAt, now) > Duration.ofSeconds(20)) {
|
||||||
|
log(TAG, VERBOSE) { "Removing stale device from cache: $value" }
|
||||||
|
deviceCache.remove(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val pods = mutableMapOf<PodDevice.Id, PodDevice>()
|
||||||
|
|
||||||
|
pods.putAll(deviceCache)
|
||||||
|
|
||||||
|
newPods.map { it.device }.forEach {
|
||||||
|
deviceCache[it.identifier] = it
|
||||||
|
pods[it.identifier] = it
|
||||||
|
}
|
||||||
|
return pods
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun preFilterAndMap(rawResults: List<BleScanResult>): List<PodFactory.Result> = rawResults
|
||||||
.groupBy { it.address }
|
.groupBy { it.address }
|
||||||
.values
|
.values
|
||||||
.map { sameAdrDevs ->
|
.map { sameAdrDevs ->
|
||||||
@@ -56,115 +145,36 @@ class PodMonitor @Inject constructor(
|
|||||||
}
|
}
|
||||||
.mapNotNull { podFactory.createPod(it) }
|
.mapNotNull { podFactory.createPod(it) }
|
||||||
|
|
||||||
val devices: Flow<List<PodDevice>> = bluetoothManager.isBluetoothEnabled
|
private fun sortPodsToInterest(pods: Collection<PodDevice>): List<PodDevice> {
|
||||||
.flatMapLatest { isBluetoothEnabled ->
|
|
||||||
if (isBluetoothEnabled) {
|
|
||||||
log(TAG) { "Bluetooth is enabled" }
|
|
||||||
combine(
|
|
||||||
generalSettings.scannerMode.flow,
|
|
||||||
generalSettings.compatibilityMode.flow,
|
|
||||||
debugSettings.showUnfiltered.flow
|
|
||||||
) { scannerMode, compatMode, unfiltered ->
|
|
||||||
Triple(scannerMode, compatMode, unfiltered)
|
|
||||||
}.flatMapLatest { (mode, compat, unfiltered) ->
|
|
||||||
log(TAG, VERBOSE) { "Starting BLEScanner mode=$mode, compat=$compat, unfiltered=$unfiltered" }
|
|
||||||
val filters = if (unfiltered) {
|
|
||||||
setOf(getUnfilteredFilter())
|
|
||||||
} else {
|
|
||||||
ProximityPairing.getBleScanFilter()
|
|
||||||
}
|
|
||||||
bleScanner.scan(
|
|
||||||
filters = filters,
|
|
||||||
scannerMode = mode,
|
|
||||||
compatMode = compat,
|
|
||||||
).map { it.preFilterAndMap(mode) }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log(TAG, WARN) { "Bluetooth is currently disabled" }
|
|
||||||
flowOf(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.map { newPods ->
|
|
||||||
val pods = mutableMapOf<PodDevice.Id, PodDevice>()
|
|
||||||
|
|
||||||
cacheLock.withLock {
|
|
||||||
if (newPods == null) {
|
|
||||||
log(TAG) { "Null result, Bluetooth is disabled." }
|
|
||||||
deviceCache.clear()
|
|
||||||
return@map emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
val now = Instant.now()
|
|
||||||
deviceCache.toList().forEach { (key, value) ->
|
|
||||||
if (Duration.between(value.seenLastAt, now) > Duration.ofSeconds(20)) {
|
|
||||||
log(TAG, VERBOSE) { "Removing stale device from cache: $value" }
|
|
||||||
deviceCache.remove(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pods.putAll(deviceCache)
|
|
||||||
|
|
||||||
newPods.map { it.device }.forEach {
|
|
||||||
deviceCache[it.identifier] = it
|
|
||||||
pods[it.identifier] = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val presorted = pods.values.sortPodsToInterest()
|
|
||||||
val main = presorted.determineMainDevice()
|
|
||||||
newPods?.firstOrNull { it.device.identifier == main?.identifier }?.let {
|
|
||||||
podDeviceCache.saveMainDevice(it.scanResult)
|
|
||||||
}
|
|
||||||
|
|
||||||
presorted.sortedByDescending { it == main }
|
|
||||||
}
|
|
||||||
.onStart { emit(emptyList()) }
|
|
||||||
.retryWhen { cause, attempt ->
|
|
||||||
log(TAG, WARN) { "PodMonitor failed (attempt=$attempt), will retry: ${cause.asLog()}" }
|
|
||||||
delay(3000)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
.replayingShare(appScope)
|
|
||||||
|
|
||||||
val mainDevice: Flow<PodDevice?>
|
|
||||||
get() = devices
|
|
||||||
.map { it.determineMainDevice() }
|
|
||||||
.replayingShare(appScope)
|
|
||||||
|
|
||||||
private fun Collection<PodDevice>.sortPodsToInterest(): List<PodDevice> = this.let { devices ->
|
|
||||||
val now = Instant.now()
|
val now = Instant.now()
|
||||||
|
|
||||||
return@let devices.sortedWith(
|
return pods.sortedWith(
|
||||||
compareByDescending<PodDevice> { true }
|
compareByDescending<PodDevice> { true }
|
||||||
.thenBy {
|
.thenBy {
|
||||||
val age = Duration.between(it.seenLastAt, now).seconds
|
val age = Duration.between(it.seenLastAt, now).seconds
|
||||||
if (age < 5) 0L else (age / 3L).toLong()
|
if (age < 5) 0L else (age / 3L)
|
||||||
}
|
}
|
||||||
.thenByDescending { it.signalQuality }
|
.thenByDescending { it.signalQuality }
|
||||||
.thenByDescending { (it.seenCounter / 10) }
|
.thenByDescending { (it.seenCounter / 10) }
|
||||||
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun List<PodDevice>.determineMainDevice(): PodDevice? = this
|
private fun determineMainDevice(pods: List<PodDevice>): PodDevice? {
|
||||||
.sortPodsToInterest()
|
val mainDeviceModel = generalSettings.mainDeviceModel.value
|
||||||
.let { devices ->
|
|
||||||
val mainDeviceModel = generalSettings.mainDeviceModel.value
|
|
||||||
|
|
||||||
val presorted = devices.sortedByDescending {
|
val presorted = sortPodsToInterest(pods).sortedByDescending {
|
||||||
it.model == mainDeviceModel && it.model != PodDevice.Model.UNKNOWN
|
it.model == mainDeviceModel && it.model != PodDevice.Model.UNKNOWN
|
||||||
}
|
|
||||||
|
|
||||||
return@let presorted.firstOrNull()?.let { candidate ->
|
|
||||||
when {
|
|
||||||
candidate.model == PodDevice.Model.UNKNOWN -> null
|
|
||||||
mainDeviceModel != PodDevice.Model.UNKNOWN && candidate.model != mainDeviceModel -> null
|
|
||||||
candidate.signalQuality <= generalSettings.minimumSignalQuality.value -> null
|
|
||||||
else -> candidate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return presorted.firstOrNull()?.let { candidate ->
|
||||||
|
when {
|
||||||
|
candidate.model == PodDevice.Model.UNKNOWN -> null
|
||||||
|
mainDeviceModel != PodDevice.Model.UNKNOWN && candidate.model != mainDeviceModel -> null
|
||||||
|
candidate.signalQuality <= generalSettings.minimumSignalQuality.value -> null
|
||||||
|
else -> candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun getUnfilteredFilter(): ScanFilter {
|
private fun getUnfilteredFilter(): ScanFilter {
|
||||||
return ScanFilter.Builder().build()
|
return ScanFilter.Builder().build()
|
||||||
|
|||||||
@@ -65,4 +65,6 @@
|
|||||||
|
|
||||||
<string name="last_seen_x">Last seen: %s</string>
|
<string name="last_seen_x">Last seen: %s</string>
|
||||||
<string name="first_seen_x">First seen: %s</string>
|
<string name="first_seen_x">First seen: %s</string>
|
||||||
|
<string name="permission_post_notifications_label">Post notifications</string>
|
||||||
|
<string name="permission_post_notifications_description">"Allow CAPod to show you notifications about your AirPods, e.g. their current status while connected."</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -17,7 +17,7 @@ android {
|
|||||||
minSdk = ProjectConfig.minSdk
|
minSdk = ProjectConfig.minSdk
|
||||||
targetSdk = ProjectConfig.targetSdk
|
targetSdk = ProjectConfig.targetSdk
|
||||||
|
|
||||||
versionCode = ProjectConfig.Version.code
|
versionCode = ProjectConfig.Version.code + 1 // Wear app
|
||||||
versionName = ProjectConfig.Version.name
|
versionName = ProjectConfig.Version.name
|
||||||
|
|
||||||
testInstrumentationRunner = "eu.darken.capod.HiltTestRunner"
|
testInstrumentationRunner = "eu.darken.capod.HiltTestRunner"
|
||||||
@@ -90,7 +90,7 @@ android {
|
|||||||
val variantName: String = variantOutputImpl.name
|
val variantName: String = variantOutputImpl.name
|
||||||
|
|
||||||
if (listOf("release", "beta").any { variantName.toLowerCase().contains(it) }) {
|
if (listOf("release", "beta").any { variantName.toLowerCase().contains(it) }) {
|
||||||
val outputFileName = ProjectConfig.packageName +
|
val outputFileName = ProjectConfig.packageName + "-WEAROS" +
|
||||||
"-v${defaultConfig.versionName}-${defaultConfig.versionCode}" +
|
"-v${defaultConfig.versionName}-${defaultConfig.versionCode}" +
|
||||||
"-${variantName.toUpperCase()}-${lastCommitHash()}.apk"
|
"-${variantName.toUpperCase()}-${lastCommitHash()}.apk"
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ open class App : Application(), Configuration.Provider {
|
|||||||
private fun setupWorker() {
|
private fun setupWorker() {
|
||||||
log(TAG) { "setupWorker()" }
|
log(TAG) { "setupWorker()" }
|
||||||
val workRequest = PeriodicWorkRequestBuilder<MonitorWorker>(
|
val workRequest = PeriodicWorkRequestBuilder<MonitorWorker>(
|
||||||
Duration.ofMinutes(15),
|
Duration.ofMinutes(25),
|
||||||
Duration.ofMinutes(5)
|
Duration.ofMinutes(5)
|
||||||
).apply {
|
).apply {
|
||||||
setInputData(Data.Builder().build())
|
setInputData(Data.Builder().build())
|
||||||
|
|||||||
@@ -86,12 +86,12 @@ class MonitorWorker @AssistedInject constructor(
|
|||||||
|
|
||||||
val monitorJob = podMonitor.mainDevice
|
val monitorJob = podMonitor.mainDevice
|
||||||
.filterNotNull()
|
.filterNotNull()
|
||||||
.take(5)
|
.take(3)
|
||||||
.setupCommonEventHandlers(TAG) { "monitorJob" }
|
.setupCommonEventHandlers(TAG) { "monitorJob" }
|
||||||
.launchIn(workerScope)
|
.launchIn(workerScope)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
withTimeout(60 * 1000) {
|
withTimeout(15 * 1000) {
|
||||||
monitorJob.join()
|
monitorJob.join()
|
||||||
}
|
}
|
||||||
log(TAG) { "Monitor job quit after a few takes." }
|
log(TAG) { "Monitor job quit after a few takes." }
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ android {
|
|||||||
minSdk = ProjectConfig.minSdk
|
minSdk = ProjectConfig.minSdk
|
||||||
targetSdk = ProjectConfig.targetSdk
|
targetSdk = ProjectConfig.targetSdk
|
||||||
|
|
||||||
versionCode = ProjectConfig.Version.code
|
versionCode = ProjectConfig.Version.code + 0 // Base app
|
||||||
versionName = ProjectConfig.Version.name
|
versionName = ProjectConfig.Version.name
|
||||||
|
|
||||||
testInstrumentationRunner = "eu.darken.capod.HiltTestRunner"
|
testInstrumentationRunner = "eu.darken.capod.HiltTestRunner"
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class UpgradeControlFoss @Inject constructor(
|
|||||||
upgradedAt = Instant.now(),
|
upgradedAt = Instant.now(),
|
||||||
reason = FossUpgrade.Reason.DONATED
|
reason = FossUpgrade.Reason.DONATED
|
||||||
)
|
)
|
||||||
webpageTool.open("https://github.com/d4rken-org/capod#support-the-project")
|
webpageTool.open("https://github.com/sponsors/d4rken")
|
||||||
Toast.makeText(activity, R.string.general_thank_you_label, Toast.LENGTH_SHORT).show()
|
Toast.makeText(activity, R.string.general_thank_you_label, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
setNegativeButton(R.string.foss_upgrade_alreadydonated_label) { _, _ ->
|
setNegativeButton(R.string.foss_upgrade_alreadydonated_label) { _, _ ->
|
||||||
@@ -68,4 +68,6 @@ class UpgradeControlFoss @Inject constructor(
|
|||||||
override val type: UpgradeRepo.Type = UpgradeRepo.Type.FOSS
|
override val type: UpgradeRepo.Type = UpgradeRepo.Type.FOSS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getSponsorUrl(): String? = "https://github.com/sponsors/d4rken"
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
<uses-permission-sdk-23 android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
<uses-permission-sdk-23 android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.BLUETOOTH"
|
android:name="android.permission.BLUETOOTH"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package eu.darken.capod.main.ui.overview.cards
|
|||||||
import android.text.Html
|
import android.text.Html
|
||||||
import android.text.method.LinkMovementMethod
|
import android.text.method.LinkMovementMethod
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.isGone
|
||||||
import eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
import eu.darken.capod.common.PrivacyPolicy
|
import eu.darken.capod.common.PrivacyPolicy
|
||||||
import eu.darken.capod.common.lists.binding
|
import eu.darken.capod.common.lists.binding
|
||||||
@@ -33,6 +34,12 @@ class PermissionCardVH(parent: ViewGroup) :
|
|||||||
val ppText = getString(R.string.settings_privacy_policy_label)
|
val ppText = getString(R.string.settings_privacy_policy_label)
|
||||||
val ppLink = PrivacyPolicy.URL
|
val ppLink = PrivacyPolicy.URL
|
||||||
text = Html.fromHtml("<html><a href=\"$ppLink\">$ppText</a></html>", 0)
|
text = Html.fromHtml("<html><a href=\"$ppLink\">$ppText</a></html>", 0)
|
||||||
|
val ppp = setOf(
|
||||||
|
Permission.ACCESS_FINE_LOCATION,
|
||||||
|
Permission.ACCESS_BACKGROUND_LOCATION,
|
||||||
|
Permission.BLUETOOTH_SCAN
|
||||||
|
)
|
||||||
|
isGone = !ppp.contains(item.permission)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import eu.darken.capod.common.PrivacyPolicy
|
|||||||
import eu.darken.capod.common.WebpageTool
|
import eu.darken.capod.common.WebpageTool
|
||||||
import eu.darken.capod.common.preferences.Settings
|
import eu.darken.capod.common.preferences.Settings
|
||||||
import eu.darken.capod.common.uix.PreferenceFragment2
|
import eu.darken.capod.common.uix.PreferenceFragment2
|
||||||
|
import eu.darken.capod.common.upgrade.UpgradeRepo
|
||||||
import eu.darken.capod.main.core.GeneralSettings
|
import eu.darken.capod.main.core.GeneralSettings
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ class SettingsIndexFragment : PreferenceFragment2() {
|
|||||||
override val preferenceFile: Int = R.xml.preferences_index
|
override val preferenceFile: Int = R.xml.preferences_index
|
||||||
|
|
||||||
@Inject lateinit var webpageTool: WebpageTool
|
@Inject lateinit var webpageTool: WebpageTool
|
||||||
|
@Inject lateinit var upgradeRepo: UpgradeRepo
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
setupMenu(R.menu.menu_settings_index) { item ->
|
setupMenu(R.menu.menu_settings_index) { item ->
|
||||||
@@ -30,7 +32,13 @@ class SettingsIndexFragment : PreferenceFragment2() {
|
|||||||
webpageTool.open("https://twitter.com/d4rken")
|
webpageTool.open("https://twitter.com/d4rken")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
when (item.itemId) {
|
||||||
|
R.id.menu_item_sponsor -> {
|
||||||
|
upgradeRepo.getSponsorUrl()?.let { webpageTool.open(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
toolbar.menu?.findItem(R.id.menu_item_sponsor)?.isVisible = !upgradeRepo.getSponsorUrl().isNullOrEmpty()
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ class ReactionSettingsFragment : PreferenceFragment2() {
|
|||||||
autoConnectConditionPref.isEnabled = it
|
autoConnectConditionPref.isEnabled = it
|
||||||
}
|
}
|
||||||
|
|
||||||
vm.isPro.observe2 { isPro = true }
|
vm.isPro.observe2 { isPro = it }
|
||||||
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,19 @@
|
|||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
tools:context="eu.darken.androidstarter.main.ui.MainActivity">
|
tools:context="eu.darken.androidstarter.main.ui.MainActivity">
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_item_sponsor"
|
||||||
|
android:icon="@drawable/ic_heart"
|
||||||
|
android:orderInCategory="100"
|
||||||
|
android:title="Sponsor development"
|
||||||
|
android:tooltipText="Sponsor development"
|
||||||
|
android:visible="false"
|
||||||
|
app:showAsAction="always" />
|
||||||
<item
|
<item
|
||||||
android:id="@+id/menu_item_twitter"
|
android:id="@+id/menu_item_twitter"
|
||||||
android:icon="@drawable/ic_twitter"
|
android:icon="@drawable/ic_twitter"
|
||||||
android:orderInCategory="100"
|
android:orderInCategory="100"
|
||||||
android:tooltipText="Twitter"
|
|
||||||
android:title="Twitter"
|
android:title="Twitter"
|
||||||
|
android:tooltipText="Twitter"
|
||||||
app:showAsAction="always" />
|
app:showAsAction="always" />
|
||||||
</menu>
|
</menu>
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ object ProjectConfig {
|
|||||||
|
|
||||||
object Version {
|
object Version {
|
||||||
const val major = 2
|
const val major = 2
|
||||||
const val minor = 0
|
const val minor = 1
|
||||||
const val patch = 0
|
const val patch = 1
|
||||||
const val build = 0
|
const val build = 0
|
||||||
|
|
||||||
const val name = "${major}.${minor}.${patch}-rc${build}"
|
const val name = "${major}.${minor}.${patch}-rc${build}"
|
||||||
const val code = major * 1000000 + minor * 10000 + patch * 100 + build
|
const val code = major * 10000000 + minor * 100000 + patch * 1000 + build * 10
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ platform :android do
|
|||||||
skip_upload_images: 'true',
|
skip_upload_images: 'true',
|
||||||
skip_upload_screenshots: 'true',
|
skip_upload_screenshots: 'true',
|
||||||
skip_upload_metadata: 'true',
|
skip_upload_metadata: 'true',
|
||||||
|
aab_paths: [
|
||||||
|
"app/build/outputs/bundle/gplayRelease/app-gplay-beta.aab",
|
||||||
|
"app-wear/build/outputs/bundle/gplayRelease/app-wear-gplay-beta.aab",
|
||||||
|
],
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -45,6 +49,10 @@ platform :android do
|
|||||||
skip_upload_images: 'true',
|
skip_upload_images: 'true',
|
||||||
skip_upload_screenshots: 'true',
|
skip_upload_screenshots: 'true',
|
||||||
skip_upload_metadata: 'true',
|
skip_upload_metadata: 'true',
|
||||||
|
aab_paths: [
|
||||||
|
"app/build/outputs/bundle/gplayRelease/app-gplay-release.aab",
|
||||||
|
"app-wear/build/outputs/bundle/gplayRelease/app-wear-gplay-release.aab",
|
||||||
|
],
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user