Skip to content

Commit 67c6aee

Browse files
committed
Restored auto app updates + github config updates
1 parent a16330a commit 67c6aee

6 files changed

Lines changed: 221 additions & 17 deletions

File tree

TODO.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
* make the collapsed sections a bit bigger
3232

3333
## Aleks' fucking todo
34-
* move code away from mainactivity. now mainactivity is a mess.
3534
* Modify the BNMV liscence so only the owner can release, and everything that other developers do should be reported to the owner. And state that debug builds should never be released to the public because they are not meant to be used by the users, but only by the developers for developing purposes.
3635
* when the frequency range is large; they are less reactive for some reason. You don't need to divide the sum of the bins by the frequency range, the size of the frequency range. One of the causes is that the wide frequency ranges are less reactive than the narrow ones, which are usually the bass ones, so we can't really see the trebles.
3736
* enhance the built-in switches by adding an X (cross) or a done (✅) in them, like the battery guru's settings switches, and also with bouncy animations and nice haptics
@@ -57,6 +56,7 @@
5756
---
5857

5958
## Done
59+
* move code away from mainactivity. now mainactivity is a mess.
6060
* use the system navbar padding (fix it again)
6161
* **Make the haptic amplitude mode resubmit a oneshot haptic all the time, even if it doesn't change!!!** currently it doesn't resubmit when the vibration amplitude doesn't change!
6262
* make the oneshot duration slightly longer

app/src/main/java/com/better/nothing/music/vizualizer/ui/AuthDialog.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@ import com.better.nothing.music.vizualizer.R
2525
fun AuthDialog(
2626
onDismiss: () -> Unit,
2727
onSignIn: (String, String) -> Unit,
28-
onSignUp: (String, String) -> Unit,
29-
onGoogleSignIn: () -> Unit
28+
onSignUp: (String, String) -> Unit
3029
) {
3130
var email by remember { mutableStateOf("") }
3231
var password by remember { mutableStateOf("") }
@@ -127,7 +126,6 @@ fun AuthDialog(
127126

128127
OutlinedButton(
129128
onClick = {
130-
onGoogleSignIn()
131129
onDismiss()
132130
},
133131
modifier = Modifier.fillMaxWidth(),

app/src/main/java/com/better/nothing/music/vizualizer/ui/MainViewModel.kt

Lines changed: 216 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package com.better.nothing.music.vizualizer.ui
22

33
import android.app.Application
44
import android.content.Context
5+
import android.content.Intent
56
import android.graphics.Bitmap
67
import android.content.pm.PackageManager
8+
import android.net.Uri
79
import android.util.Base64
810
import android.util.Log
911
import android.widget.Toast
@@ -21,6 +23,7 @@ import com.google.firebase.auth.EmailAuthProvider
2123
import com.google.firebase.auth.GoogleAuthProvider
2224
import com.google.firebase.auth.AuthCredential
2325
import androidx.compose.ui.graphics.Color
26+
import androidx.core.content.FileProvider
2427
import kotlinx.coroutines.Dispatchers
2528
import kotlinx.coroutines.delay
2629
import kotlinx.coroutines.flow.*
@@ -29,6 +32,10 @@ import kotlinx.coroutines.withContext
2932
import kotlinx.coroutines.tasks.await
3033
import org.json.JSONObject
3134
import rikka.shizuku.Shizuku
35+
import java.io.File
36+
import java.io.FileOutputStream
37+
import java.net.HttpURLConnection
38+
import java.net.URL
3239
import kotlin.math.pow
3340
import kotlin.math.sqrt
3441

@@ -148,8 +155,67 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
148155
_appUpdateStatus.value = AppUpdateStatus.UpToDate
149156
}
150157

151-
fun downloadAndInstallUpdate(apkUrl: String, version: String) {
152-
// Mock
158+
fun downloadAndInstallUpdate(apkUrl: String, versionName: String) {
159+
viewModelScope.launch(Dispatchers.IO) {
160+
try {
161+
val url = URL(apkUrl)
162+
val connection = url.openConnection() as HttpURLConnection
163+
connection.connectTimeout = 10000
164+
connection.readTimeout = 30000
165+
166+
if (connection.responseCode == HttpURLConnection.HTTP_OK) {
167+
val fileLength = connection.contentLength
168+
val destinationFile = File(ctx.externalCacheDir, "update_$versionName.apk")
169+
170+
connection.inputStream.use { input ->
171+
FileOutputStream(destinationFile).use { output ->
172+
val buffer = ByteArray(8192)
173+
var bytesRead: Int
174+
var totalBytesRead = 0L
175+
176+
while (input.read(buffer).also { bytesRead = it } != -1) {
177+
output.write(buffer, 0, bytesRead)
178+
totalBytesRead += bytesRead
179+
if (fileLength > 0) {
180+
val progress = totalBytesRead.toFloat() / fileLength.toFloat()
181+
_appUpdateStatus.value = AppUpdateStatus.Downloading(progress)
182+
}
183+
}
184+
}
185+
}
186+
187+
withContext(Dispatchers.Main) {
188+
installApk(destinationFile)
189+
}
190+
} else {
191+
withContext(Dispatchers.Main) {
192+
Toast.makeText(ctx, "Download failed: ${connection.responseCode}", Toast.LENGTH_SHORT).show()
193+
_appUpdateStatus.value = AppUpdateStatus.Error("Download failed")
194+
}
195+
}
196+
} catch (e: Exception) {
197+
Log.e("MainViewModel", "Download failed", e)
198+
withContext(Dispatchers.Main) {
199+
Toast.makeText(ctx, "Download error: ${e.message}", Toast.LENGTH_SHORT).show()
200+
_appUpdateStatus.value = AppUpdateStatus.Error(e.message ?: "Unknown error")
201+
}
202+
}
203+
}
204+
}
205+
206+
private fun installApk(file: File) {
207+
try {
208+
val uri = FileProvider.getUriForFile(ctx, "${ctx.packageName}.fileprovider", file)
209+
val intent = Intent(Intent.ACTION_VIEW).apply {
210+
setDataAndType(uri, "application/vnd.android.package-archive")
211+
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
212+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
213+
}
214+
ctx.startActivity(intent)
215+
} catch (e: Exception) {
216+
Log.e("MainViewModel", "Installation failed", e)
217+
Toast.makeText(ctx, "Installation failed: ${e.message}", Toast.LENGTH_SHORT).show()
218+
}
153219
}
154220

155221
private val _isShowingLeaderboard = MutableStateFlow(false)
@@ -194,9 +260,154 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
194260
}
195261
}
196262

197-
fun checkRemoteConfigVersion() { TODO() }
198-
fun importZonesConfig(uri: android.net.Uri) { /* Mock */ }
199-
fun updateZonesConfig(url: String = "") { /* Mock */ }
263+
fun checkRemoteConfigVersion() {
264+
viewModelScope.launch(Dispatchers.IO) {
265+
try {
266+
val url =
267+
URL("https://raw.githubusercontent.com/Aleks-Levet/better-nothing-music-visualizer/main/zones.config?t=${System.currentTimeMillis()}")
268+
val connection = url.openConnection() as HttpURLConnection
269+
connection.useCaches = false
270+
connection.connectTimeout = 5000
271+
connection.readTimeout = 5000
272+
273+
if (connection.responseCode == HttpURLConnection.HTTP_OK) {
274+
val content = connection.inputStream.bufferedReader().use { it.readText() }
275+
val json = JSONObject(content)
276+
val remoteVersion = json.optString("version", "Unknown")
277+
_remoteConfigVersion.value = remoteVersion
278+
}
279+
} catch (e: Exception) {
280+
Log.e("MainViewModel", "Failed to check remote version", e)
281+
}
282+
}
283+
}
284+
fun importZonesConfig(uri: Uri) {
285+
_configUpdateStatus.value = ConfigUpdateStatus.Updating
286+
viewModelScope.launch {
287+
announcementRepository.getLatestAnnouncement().collect { announcement ->
288+
_latestAnnouncement.value = announcement
289+
if (announcement != null) {
290+
val sharedPrefs = ctx.getSharedPreferences("viz_prefs", Context.MODE_PRIVATE)
291+
val lastSeenId = sharedPrefs.getString("last_seen_announcement_id", "")
292+
if (announcement.id.toString() != lastSeenId) {
293+
_showAnnouncementModal.value = true
294+
}
295+
}
296+
}
297+
}
298+
299+
viewModelScope.launch {
300+
try {
301+
val success = withContext(Dispatchers.IO) {
302+
val content = ctx.contentResolver.openInputStream(uri)?.bufferedReader()?.use { it.readText() }
303+
if (content == null) return@withContext false
304+
305+
// Basic validation
306+
JSONObject(content)
307+
308+
val file = File(ctx.filesDir, "zones.config")
309+
file.writeText(content)
310+
311+
// Refresh presets (file IO)
312+
refreshPresetsInternal()
313+
314+
val newVersion = AudioCaptureService.loadZonesConfigVersion(ctx)
315+
_configVersion.value = newVersion
316+
_remoteConfigVersion.value = null // Clear remote version since we are on local
317+
318+
// Force running service to reload its config from disk
319+
MainActivity.serviceStatic?.reloadConfig()
320+
true
321+
}
322+
323+
if (success) {
324+
_configUpdateStatus.value = ConfigUpdateStatus.Success(ctx.getString(R.string.config_import_success))
325+
} else {
326+
_configUpdateStatus.value = ConfigUpdateStatus.Error(ctx.getString(R.string.config_import_error))
327+
}
328+
} catch (e: Exception) {
329+
_configUpdateStatus.value = ConfigUpdateStatus.Error(ctx.getString(R.string.config_error_importing, e.message))
330+
}
331+
}
332+
}
333+
fun updateZonesConfig() {
334+
// 1. Set loading state immediately on Main Thread
335+
_configUpdateStatus.value = ConfigUpdateStatus.Updating
336+
337+
viewModelScope.launch {
338+
announcementRepository.getLatestAnnouncement().collect { announcement ->
339+
_latestAnnouncement.value = announcement
340+
if (announcement != null) {
341+
val sharedPrefs = ctx.getSharedPreferences("viz_prefs", Context.MODE_PRIVATE)
342+
val lastSeenId = sharedPrefs.getString("last_seen_announcement_id", "")
343+
if (announcement.id.toString() != lastSeenId) {
344+
_showAnnouncementModal.value = true
345+
}
346+
}
347+
}
348+
}
349+
350+
viewModelScope.launch {
351+
try {
352+
// 2. Perform network/download on IO Thread
353+
val success = withContext(Dispatchers.IO) {
354+
performUpdateAction()
355+
}
356+
357+
// 3. Back on Main Thread automatically after withContext
358+
if (success) {
359+
_configUpdateStatus.value = ConfigUpdateStatus.Success(ctx.getString(R.string.config_update_success))
360+
}
361+
// Errors are handled inside performUpdateAction setting the status directly now,
362+
// or we could return Result object. To keep it simple with existing code:
363+
} catch (e: Exception) {
364+
// Catch unexpected errors
365+
_configUpdateStatus.value = ConfigUpdateStatus.Error(ctx.getString(R.string.config_error_updating, e.message))
366+
}
367+
}
368+
}
369+
private suspend fun performUpdateAction(): Boolean {
370+
// This runs on Dispatchers.IO (called from withContext(IO) above)
371+
return try {
372+
val url = URL("https://raw.githubusercontent.com/Aleks-Levet/better-nothing-music-visualizer/main/zones.config?t=${System.currentTimeMillis()}")
373+
val connection = withContext(Dispatchers.IO) {
374+
url.openConnection()
375+
} as HttpURLConnection
376+
connection.useCaches = false
377+
connection.connectTimeout = 10000
378+
connection.readTimeout = 10000
379+
380+
if (connection.responseCode == HttpURLConnection.HTTP_OK) {
381+
val content = connection.inputStream.bufferedReader().use { it.readText() }
382+
// Basic validation
383+
JSONObject(content)
384+
385+
val file = File(ctx.filesDir, "zones.config")
386+
file.writeText(content)
387+
388+
// Refresh presets (file IO)
389+
refreshPresetsInternal()
390+
391+
val newVersion = AudioCaptureService.loadZonesConfigVersion(ctx)
392+
_configVersion.value = newVersion
393+
_remoteConfigVersion.value = newVersion
394+
395+
// Force running service to reload its config from disk
396+
MainActivity.serviceStatic?.reloadConfig()
397+
true
398+
} else {
399+
withContext(Dispatchers.Main) {
400+
_configUpdateStatus.value = ConfigUpdateStatus.Error(ctx.getString(R.string.config_download_error, connection.responseCode))
401+
}
402+
false
403+
}
404+
} catch (e: Exception) {
405+
withContext(Dispatchers.Main) {
406+
_configUpdateStatus.value = ConfigUpdateStatus.Error(ctx.getString(R.string.config_error_updating, e.message))
407+
}
408+
false
409+
}
410+
}
200411
fun updateProfilePicture(uri: android.net.Uri) {
201412
val uid = _userId.value ?: return
202413
viewModelScope.launch {
@@ -353,7 +564,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
353564
}
354565

355566
val _thanksMessage = MutableStateFlow<String?>(null)
356-
val thanksMessage = _thanksMessage.asStateFlow()
357567
val thanksQueue = mutableListOf<String>()
358568

359569
fun dismissThanksMessage() {

app/src/main/java/com/better/nothing/music/vizualizer/ui/PrimaryScreens/SettingsScreen.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,6 @@ internal fun SettingsScreen(
8989
onDismiss = { showAuthDialog = false },
9090
onSignIn = { e, p -> viewModel.signInWithEmail(e, p) },
9191
onSignUp = { e, p -> viewModel.signUpWithEmail(e, p) },
92-
onGoogleSignIn = onGoogleSignIn
9392
)
9493
}
9594

app/src/main/java/com/better/nothing/music/vizualizer/ui/firebaseFuckery.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ fun launchGoogleSignIn(
4040
try {
4141
val account = task.getResult(Exception::class.java)
4242
val credential = GoogleAuthProvider.getCredential(account?.idToken, null)
43-
viewModel.linkWithCredential(credential)
43+
//viewModel.linkWithCredential(credential)
4444
} catch (e: Exception) {
4545
Toast.makeText(context, "Google sign in failed: ${e.message}", Toast.LENGTH_SHORT).show()
4646
}
@@ -68,4 +68,5 @@ fun launchGoogleSignIn(
6868
Button(onClick = { launchGoogleSignIn() }) {
6969
Text(text = "Sign in with Google")
7070
}
71-
}
71+
}
72+
Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
11
package com.better.nothing.music.vizualizer.ui
22

3-
import androidx.compose.material3.*
4-
import androidx.compose.runtime.Composable
5-
import androidx.compose.ui.unit.dp
6-
import androidx.compose.foundation.shape.RoundedCornerShape
73

84

0 commit comments

Comments
 (0)