Skip to content

Commit 187a537

Browse files
author
Dennis Mantz
committed
v2.1.1 bugfixes: AM demodulation. Crash in Zoom Slider. Adding FOSS build flavor.
1 parent 062a8e3 commit 187a537

File tree

22 files changed

+511
-195
lines changed

22 files changed

+511
-195
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ local.properties
1515
.kotlin
1616
build/
1717
app/release/
18+
app/play/release/
19+
app/foss/release/
1820

1921
# mkdocs generated files
2022
build_site/

Readme.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ You can also donate to support my work:
8888

8989
[![liberapay](https://liberapay.com/assets/widgets/donate.svg)](https://liberapay.com/DM4NTZ/donate)
9090

91+
Paypal: https://paypal.me/dennismantz
92+
93+
Bitcoin: bc1qnzuvkpxd08grw505aurw45y7rty60554kd2ja2
94+
9195
---
9296

9397
## About the Author

app/build.gradle.kts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,31 @@ android {
1414
applicationId = "com.mantz_it.rfanalyzer"
1515
minSdk = 28
1616
targetSdk = 36
17-
versionCode = 20106
18-
versionName = "2.1.0"
17+
versionCode = 20107
18+
versionName = "2.1.1"
1919

2020
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
2121
}
2222

23+
flavorDimensions += "distribution"
24+
25+
productFlavors {
26+
create("play") {
27+
dimension = "distribution"
28+
buildConfigField("boolean", "IS_FOSS", "false")
29+
resValue("string", "flavor_name", "Google Play")
30+
resValue("string", "app_name", "RF Analyzer")
31+
}
32+
create("foss") {
33+
dimension = "distribution"
34+
applicationIdSuffix = ".foss"
35+
versionNameSuffix = "-foss"
36+
buildConfigField("boolean", "IS_FOSS", "true")
37+
resValue("string", "flavor_name", "FOSS")
38+
resValue("string", "app_name", "RF Analyzer (FOSS)")
39+
}
40+
}
41+
2342
buildTypes {
2443
release {
2544
isMinifyEnabled = true
@@ -98,7 +117,9 @@ dependencies {
98117
androidTestImplementation(libs.androidx.espresso.core)
99118
debugImplementation(libs.androidx.ui.tooling)
100119
implementation(libs.androidx.security.crypto)
101-
implementation(libs.billing)
102120
implementation(libs.dagger.hilt)
103121
ksp(libs.dagger.hilt.compiler)
122+
123+
// Flavor-specific dependencies:
124+
add("playImplementation", libs.billing) // "play" is the flavor
104125
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package com.mantz_it.rfanalyzer.database
2+
3+
import android.app.Activity
4+
import android.content.Context
5+
6+
/**
7+
* <h1>RF Analyzer - Billing Repository (FOSS)</h1>
8+
*
9+
* Module: BillingRepository.kt
10+
* Description: Mock Interface (without Google Play Billing API) for FOSS version
11+
*
12+
* @author Dennis Mantz
13+
*
14+
* Copyright (C) 2025 Dennis Mantz
15+
* License: http://www.gnu.org/licenses/gpl.html GPL version 2 or higher
16+
*
17+
* This library is free software; you can redistribute it and/or
18+
* modify it under the terms of the GNU General Public
19+
* License as published by the Free Software Foundation; either
20+
* version 2 of the License, or (at your option) any later version.
21+
*
22+
* This library is distributed in the hope that it will be useful,
23+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
24+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
25+
* General Public License for more details.
26+
*
27+
* You should have received a copy of the GNU General Public
28+
* License along with this library; if not, write to the Free Software
29+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
30+
*/
31+
32+
33+
interface BillingRepositoryInterface {
34+
fun queryPurchases()
35+
fun purchaseFullVersion(activity: Activity)
36+
}
37+
38+
class BillingRepository(val context: Context, val appStateRepository: AppStateRepository) : BillingRepositoryInterface {
39+
override fun queryPurchases() {
40+
// do nothing in foss
41+
}
42+
override fun purchaseFullVersion(activity: Activity) {
43+
// do nothing in foss
44+
}
45+
}

app/src/main/java/com/mantz_it/rfanalyzer/database/MockedBillingRepository.kt renamed to app/src/foss/kotlin/com/mantz_it/rfanalyzer/database/MockedBillingRepository.kt

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,6 @@ import kotlinx.coroutines.launch
4343

4444
class MockedBillingRepository(val context: Context, val appStateRepository: AppStateRepository) : BillingRepositoryInterface {
4545

46-
private val _remainingTrialPeriodDays = MutableStateFlow(calculateRemainingDays())
47-
override val remainingTrialPeriodDays: StateFlow<Int> = _remainingTrialPeriodDays.asStateFlow()
48-
49-
init {
50-
CoroutineScope(Dispatchers.Default).launch {
51-
while (isActive) {
52-
_remainingTrialPeriodDays.value = calculateRemainingDays()
53-
delay(TimeUnit.HOURS.toMillis(1)) // update every hour
54-
}
55-
}
56-
}
57-
5846
override fun queryPurchases() {
5947
// do nothing in mock
6048
}
@@ -63,26 +51,4 @@ class MockedBillingRepository(val context: Context, val appStateRepository: AppS
6351
//Log.d("MockedBillingRepository", "purchaseFullVersion: DISABLED")
6452
appStateRepository.isFullVersion.set(true)
6553
}
66-
67-
private fun calculateRemainingDays(): Int {
68-
val installTimestamp = getInstallTimestamp(context) // todo: this should be 'purchase time'
69-
val currentTime = System.currentTimeMillis()
70-
val installedDays = TimeUnit.MILLISECONDS.toDays(currentTime - installTimestamp).toInt()
71-
val trialPeriod= 7 // 7-day trial period
72-
Log.d("MockedBillingRepository", "Install: $installTimestamp ; Now: $currentTime ; Diff: ${currentTime-installTimestamp} ; InstalledDays: $installedDays")
73-
return (trialPeriod - installedDays).coerceAtLeast(0)
74-
}
75-
76-
override fun isTrialPeriodExpired(): Boolean {
77-
return remainingTrialPeriodDays.value <= 0
78-
}
79-
80-
private fun getInstallTimestamp(context: Context): Long {
81-
return try {
82-
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
83-
packageInfo.firstInstallTime // Returns install time in milliseconds
84-
} catch (e: PackageManager.NameNotFoundException) {
85-
0L
86-
}
87-
}
8854
}

app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
<?xml version="1.0" encoding="utf-8"?>
2-
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3-
package="com.mantz_it.rfanalyzer" >
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android" >
43

54
<!-- Internet is used to connect to local and remote rtl_tcp instances -->
65
<uses-permission android:name="android.permission.INTERNET" />

app/src/main/java/com/mantz_it/rfanalyzer/AppModule.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ object AppModule {
8282
@Singleton
8383
fun provideBillingRepository(@ApplicationContext context: Context, appStateRepository: AppStateRepository): BillingRepositoryInterface {
8484
val buildType = BuildConfig.BUILD_TYPE
85-
return if (buildType == "debug")
85+
val isFoss = BuildConfig.IS_FOSS
86+
return if (isFoss || buildType == "debug")
8687
MockedBillingRepository(context, appStateRepository)
8788
else
8889
BillingRepository(context, appStateRepository)

app/src/main/java/com/mantz_it/rfanalyzer/analyzer/Demodulator.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ class Demodulator(
299299

300300
// normalize values:
301301
val gain = 0.75f / lastMax
302-
for (i in 0..<output.size()) reOut[i] = (reOut[i] - avg) * gain
302+
for (i in 0..<input.size()) reOut[i] = (reOut[i] - avg) * gain
303303

304304
output.setSize(input.size())
305305
output.sampleRate = demodulationMode.quadratureRate

app/src/main/java/com/mantz_it/rfanalyzer/database/AppStateRepository.kt

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,10 @@ class AppStateRepository @Inject constructor(
258258
val viewportVerticalScaleMax = Setting("viewportVerticalScaleMax", 0f, scope, dataStore)
259259
val viewportStartFrequency = DerivedState(viewportFrequency, viewportSampleRate) { viewportFrequency.value - viewportSampleRate.value/2 }
260260
val viewportEndFrequency = DerivedState(viewportFrequency, viewportSampleRate) { viewportFrequency.value + viewportSampleRate.value/2 }
261-
val viewportZoom = DerivedState(sourceSampleRate, viewportSampleRate) { (1f - (viewportSampleRate.value.toFloat()/sourceSampleRate.value)).coerceIn(0f, 1f) }
261+
val viewportZoom = DerivedState(sourceSampleRate, viewportSampleRate) {
262+
val tmp = (1f - (viewportSampleRate.value.toFloat()/sourceSampleRate.value)).coerceIn(0f, 1f)
263+
if (tmp.isNaN()) 0f else tmp
264+
}
262265

263266
// Analyzer State
264267
val analyzerRunning = MutableState(false)
@@ -277,6 +280,10 @@ class AppStateRepository @Inject constructor(
277280
val isPurchasePending = Setting("isPurchasePending", false, scope, dataStore)
278281
val isAppUsageTimeUsedUp = DerivedState(appUsageTimeInSeconds) { appUsageTimeInSeconds.value > TRIAL_VERSTION_USAGE_TIME }
279282

283+
// Donation Dialog:
284+
val timestampOfLastDonationDialog = Setting("timestampOfLastDonationDialog", 0, scope, dataStore)
285+
val donationDialogCounter = Setting("donationDialogCounter", 0, scope, dataStore)
286+
280287

281288
// FFT Data
282289
val fftProcessorData = FftProcessorData()
@@ -325,32 +332,57 @@ class AppStateRepository @Inject constructor(
325332
) : MutableState<T>(default) {
326333
init {
327334
settingsTotalCount++
328-
val key: Preferences.Key<T> = when (default) {
335+
val key: Preferences.Key<T>? = when (default) {
329336
is Boolean -> booleanPreferencesKey(keyName)
330337
is Int -> intPreferencesKey(keyName)
331338
is Long -> longPreferencesKey(keyName)
332339
is Float -> floatPreferencesKey(keyName)
333340
is String -> stringPreferencesKey(keyName)
334-
is Enum<*> -> intPreferencesKey(keyName) // Enum stored as ordinal
335-
is List<*> -> stringPreferencesKey(keyName) // Store lists as comma separated list (string)
341+
is Enum<*> -> null
342+
is List<*> -> null
336343
else -> throw IllegalArgumentException("Unsupported setting type (setting: ${keyName}")
337-
} as Preferences.Key<T>
344+
} as Preferences.Key<T>?
338345

339346
scope.launch {
340347
// Load initial value
341348
val saved = dataStore.data
342349
.map { prefs -> when(default) {
343-
is Enum<*> -> {
350+
is Enum<*> -> { // up until 2.1 this was stored as int (ordinal) but now we store the enum name (using a different key)
344351
val enumClass = default!!::class.java
345-
val enumIndex = prefs[key as Preferences.Key<Int>] ?: default.ordinal
346-
enumClass.enumConstants[enumIndex.coerceAtMost(enumClass.enumConstants.size - 1)]
352+
// If we find the string type key take it, otherwise try using the legacy int key:
353+
val stringKey = stringPreferencesKey(keyName + "Enum") // name new key with 'Enum' suffix
354+
val legacyIntKey = intPreferencesKey(keyName)
355+
val prefsString = prefs[stringKey]
356+
val prefsInt = prefs[legacyIntKey]
357+
val enumValue = when {
358+
prefsString != null -> enumClass.enumConstants.firstOrNull { it.name == prefsString }
359+
prefsInt != null -> enumClass.enumConstants.getOrNull(prefsInt)
360+
else -> default
361+
} ?: default
362+
363+
// If we loaded from legacy int, immediately migrate to new key
364+
if (prefsString == null && prefsInt != null) {
365+
scope.launch {
366+
dataStore.edit { editPrefs ->
367+
editPrefs[stringKey] = enumValue.name
368+
editPrefs.remove(legacyIntKey) // cleanup
369+
}
370+
}
371+
}
372+
373+
enumValue
347374
}
348375
is List<*> -> {
349-
val stringValue = prefs[key as Preferences.Key<String>]
376+
// Store lists as comma separated list (string)
377+
val stringKey = stringPreferencesKey(keyName)
378+
val stringValue = prefs[stringKey]
350379
val list = stringValue?.split(",")?.mapNotNull { it.toIntOrNull() } ?: default
351380
list as T
352381
}
353-
else -> prefs[key] ?: default
382+
else -> {
383+
if (key == null) throw IllegalArgumentException("Unsupported setting type (setting: ${keyName}")
384+
else prefs[key] ?: default
385+
}
354386
}}
355387
.firstOrNull()
356388

@@ -367,9 +399,9 @@ class AppStateRepository @Inject constructor(
367399
.collectLatest { newValue ->
368400
dataStore.edit { prefs ->
369401
when (newValue) {
370-
is Enum<*> -> prefs[key as Preferences.Key<Int>] = newValue.ordinal
371-
is List<*> -> prefs[key as Preferences.Key<String>] = newValue.joinToString(",")
372-
else -> prefs[key] = newValue
402+
is Enum<*> -> prefs[stringPreferencesKey(keyName + "Enum")] = newValue.name
403+
is List<*> -> prefs[stringPreferencesKey(keyName)] = newValue.joinToString(",")
404+
else -> key?.let { prefs[it] = newValue }
373405
}
374406
}
375407
}

app/src/main/java/com/mantz_it/rfanalyzer/ui/AnalyzerSurface.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import android.view.ScaleGestureDetector
2121
import android.view.SurfaceHolder
2222
import android.view.SurfaceView
2323
import androidx.core.content.ContextCompat
24+
import com.mantz_it.rfanalyzer.BuildConfig
2425
import com.mantz_it.rfanalyzer.database.AppStateRepository
2526
import com.mantz_it.rfanalyzer.database.AppStateRepository.Companion.VERTICAL_SCALE_LOWER_BOUNDARY
2627
import com.mantz_it.rfanalyzer.database.AppStateRepository.Companion.VERTICAL_SCALE_UPPER_BOUNDARY
@@ -696,7 +697,7 @@ class AnalyzerSurface(context: Context,
696697
peakAvg = 0f
697698
counter = 0
698699
var j = (i * samplesPerPx).toInt()
699-
while (j < (i + 1) * samplesPerPx) {
700+
while (j < (i + 1) * samplesPerPx && (j+start)<fftSize) {
700701
avg += fftRow[j + start]
701702
if (rowNumber == 0 && calcPeaks) peakAvg += peaks[j + start]
702703
counter++
@@ -1206,7 +1207,11 @@ class AnalyzerSurface(context: Context,
12061207
fun drawWatermark() {
12071208
var c: Canvas? = null
12081209
val bitmap: Bitmap = BitmapFactory.decodeResource(resources, R.drawable.rfanalyzer2)
1209-
var text = if(isFullVersion.value) "FULL VERSION" else "TRIAL VERSION"
1210+
var text = if(BuildConfig.IS_FOSS) {
1211+
"FOSS VERSION"
1212+
} else {
1213+
if(isFullVersion.value) "FULL VERSION" else "TRIAL VERSION"
1214+
}
12101215
val paint = Paint()
12111216
val textPaint = Paint()
12121217
textPaint.color = Color.DKGRAY

0 commit comments

Comments
 (0)