app/src/main/java/dev/equwal/inkupdate/MainActivity.kt (6433 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 99 /** One watched app: what is installed, and what the check found. */ 100 private fun appRow(col: LinearLayout, r: Checker.Row) { 101 val result = r.result 102 val state = when (result) { 103 is Result.Update -> getString(R.string.update_to, result.versionName) 104 is Result.UpToDate -> getString(R.string.up_to_date) 105 is Result.LeftToPlay -> getString(R.string.play_updates) 106 else -> getString(R.string.no_answer) 107 } 108 val page = when (result) { 109 is Result.Update -> result.page 110 is Result.LeftToPlay -> r.app.watched.playPage() 111 else -> null 112 } 113 col.row( 114 title = r.app.watched.title, 115 subtitle = r.app.versionName, 116 state = state, 117 onClick = if (page == null) null else ({ open(page) }) 118 ) 119 } 120 121 /** The one line that says when the app last looked. */ 122 private fun status(): String = when { 123 checking -> getString(R.string.checking) 124 store.checkedAt == 0L -> getString(R.string.never_checked) 125 else -> { 126 val ago = DateUtils.getRelativeTimeSpanString( 127 store.checkedAt, System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS 128 ).toString() 129 if (store.lastOk) getString(R.string.checked, ago) 130 else getString(R.string.could_not_check, ago) 131 } 132 } 133 134 // ---- the work ---------------------------------------------------------- 135 136 private fun check() { 137 if (checking) return 138 checking = true 139 draw() 140 Thread { 141 try { 142 Checker.checkAll(applicationContext, store) 143 } catch (e: Exception) { 144 store.checked(System.currentTimeMillis(), false) 145 } 146 runOnUiThread { 147 checking = false 148 if (!isFinishing && !isDestroyed) draw() 149 } 150 }.start() 151 } 152 153 private fun setDaily(on: Boolean) { 154 store.daily = on 155 if (on) CheckJob.schedule(this) else CheckJob.cancel(this) 156 draw() 157 } 158 159 // ---- notifications ----------------------------------------------------- 160 161 private fun notificationsOn(): Boolean = 162 getSystemService(NotificationManager::class.java)?.areNotificationsEnabled() ?: false 163 164 /** Asks for the notification permission. Android shows the box once. */ 165 private fun ask() { 166 if (notificationsOn()) return 167 if (checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) 168 != PackageManager.PERMISSION_GRANTED 169 ) { 170 requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), 1) 171 } 172 } 173 174 override fun onRequestPermissionsResult( 175 code: Int, 176 permissions: Array<out String>, 177 results: IntArray 178 ) { 179 super.onRequestPermissionsResult(code, permissions, results) 180 if (!checking) draw() 181 } 182 183 // ---- links ------------------------------------------------------------- 184 185 private fun open(url: String) { 186 try { 187 startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) 188 } catch (e: Exception) { 189 // No app takes the link. There is nothing to do about it. 190 } 191 } 192 193 private companion object { 194 const val SOURCE = "https://github.com/equwal/ink-update" 195 const val KOFI = "https://ko-fi.com/truex" 196 } 197 }