app/src/main/java/dev/equwal/inkupdate/MainActivity.kt (6626 bytes)
1 // SPDX-License-Identifier: GPL-3.0-or-later 2 // Copyright (c) 2026 equwal 3 package dev.equwal.inkupdate 4 5 import android.Manifest 6 import android.app.Activity 7 import android.app.NotificationManager 8 import android.content.Intent 9 import android.content.pm.PackageManager 10 import android.net.Uri 11 import android.os.Bundle 12 import android.text.format.DateUtils 13 import android.widget.LinearLayout 14 import dev.equwal.inkupdate.Ui.button 15 import dev.equwal.inkupdate.Ui.header 16 import dev.equwal.inkupdate.Ui.note 17 import dev.equwal.inkupdate.Ui.page 18 import dev.equwal.inkupdate.Ui.primaryButton 19 import dev.equwal.inkupdate.Ui.row 20 import dev.equwal.inkupdate.Ui.title 21 22 /** 23 * The one screen. 24 * 25 * It shows each installed watched app, what is installed, and what the last 26 * check found. The check runs on a background thread, because the main thread 27 * must not wait for the network. 28 */ 29 class MainActivity : Activity() { 30 31 /** Another app asks for a check with this action. Rebind uses it. */ 32 private val checkAction = "dev.equwal.inkupdate.CHECK" 33 34 private lateinit var store: Store 35 private var checking = false 36 37 override fun onCreate(saved: Bundle?) { 38 super.onCreate(saved) 39 store = Store(this) 40 41 val first = !store.started 42 if (first) { 43 store.started = true 44 if (store.daily) CheckJob.schedule(this) 45 ask() 46 } 47 draw() 48 if (first || intent?.action == checkAction) check() 49 } 50 51 override fun onNewIntent(i: Intent?) { 52 super.onNewIntent(i) 53 intent = i 54 if (i?.action == checkAction) check() 55 } 56 57 override fun onResume() { 58 super.onResume() 59 if (!checking) draw() 60 } 61 62 // ---- the screen -------------------------------------------------------- 63 64 private fun draw() { 65 val col = page(this) 66 col.title(getString(R.string.app_name)) 67 col.note(status()) 68 69 val rows = Checker.rows(this, store) 70 if (rows.isEmpty()) { 71 col.note(getString(R.string.none_installed)) 72 } else { 73 for (r in rows) appRow(col, r) 74 } 75 76 col.primaryButton( 77 if (checking) getString(R.string.checking) else getString(R.string.check_now) 78 ) { check() } 79 80 col.row( 81 title = getString(R.string.daily), 82 state = if (store.daily) getString(R.string.on) else getString(R.string.off) 83 ) { setDaily(!store.daily) } 84 85 if (!notificationsOn()) { 86 col.row( 87 title = getString(R.string.notifications), 88 state = getString(R.string.off) 89 ) { ask() } 90 } 91 92 col.header(getString(R.string.about)) 93 col.row(title = getString(R.string.version), subtitle = BuildConfig.VERSION_NAME) 94 col.row(title = getString(R.string.licence), subtitle = "GPL-3.0-or-later") 95 col.row(title = getString(R.string.source_code)) { open(SOURCE) } 96 col.row(title = getString(R.string.coffee)) { open(KOFI) } 97 98 col.header(getString(R.string.more_apps)) 99 for (app in MORE_APPS) { 100 col.row(title = getString(app.name), subtitle = getString(app.line)) { open(app.url) } 101 } 102 } 103 104 /** One watched app: what is installed, and what the check found. */ 105 private fun appRow(col: LinearLayout, r: Checker.Row) { 106 val result = r.result 107 val state = when (result) { 108 is Result.Update -> getString(R.string.update_to, result.versionName) 109 is Result.UpToDate -> getString(R.string.up_to_date) 110 is Result.LeftToPlay -> getString(R.string.play_updates) 111 else -> getString(R.string.no_answer) 112 } 113 val page = when (result) { 114 is Result.Update -> result.page 115 is Result.LeftToPlay -> r.app.watched.playPage() 116 else -> null 117 } 118 col.row( 119 title = r.app.watched.title, 120 subtitle = r.app.versionName, 121 state = state, 122 onClick = if (page == null) null else ({ open(page) }) 123 ) 124 } 125 126 /** The one line that says when the app last looked. */ 127 private fun status(): String = when { 128 checking -> getString(R.string.checking) 129 store.checkedAt == 0L -> getString(R.string.never_checked) 130 else -> { 131 val ago = DateUtils.getRelativeTimeSpanString( 132 store.checkedAt, System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS 133 ).toString() 134 if (store.lastOk) getString(R.string.checked, ago) 135 else getString(R.string.could_not_check, ago) 136 } 137 } 138 139 // ---- the work ---------------------------------------------------------- 140 141 private fun check() { 142 if (checking) return 143 checking = true 144 draw() 145 Thread { 146 try { 147 Checker.checkAll(applicationContext, store) 148 } catch (e: Exception) { 149 store.checked(System.currentTimeMillis(), false) 150 } 151 runOnUiThread { 152 checking = false 153 if (!isFinishing && !isDestroyed) draw() 154 } 155 }.start() 156 } 157 158 private fun setDaily(on: Boolean) { 159 store.daily = on 160 if (on) CheckJob.schedule(this) else CheckJob.cancel(this) 161 draw() 162 } 163 164 // ---- notifications ----------------------------------------------------- 165 166 private fun notificationsOn(): Boolean = 167 getSystemService(NotificationManager::class.java)?.areNotificationsEnabled() ?: false 168 169 /** Asks for the notification permission. Android shows the box once. */ 170 private fun ask() { 171 if (notificationsOn()) return 172 if (checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) 173 != PackageManager.PERMISSION_GRANTED 174 ) { 175 requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), 1) 176 } 177 } 178 179 override fun onRequestPermissionsResult( 180 code: Int, 181 permissions: Array<out String>, 182 results: IntArray 183 ) { 184 super.onRequestPermissionsResult(code, permissions, results) 185 if (!checking) draw() 186 } 187 188 // ---- links ------------------------------------------------------------- 189 190 private fun open(url: String) { 191 try { 192 startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) 193 } catch (e: Exception) { 194 // No app takes the link. There is nothing to do about it. 195 } 196 } 197 198 private companion object { 199 const val SOURCE = "https://github.com/equwal/ink-update" 200 const val KOFI = "https://ko-fi.com/truex" 201 } 202 }