Add support for checking app updates

This commit is contained in:
Mygod
2021-10-24 17:35:38 -04:00
parent cd6c72f559
commit aa2d92e6a8
10 changed files with 227 additions and 18 deletions

View File

@@ -6,26 +6,46 @@ import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import be.mygod.vpnhotspot.client.ClientViewModel
import be.mygod.vpnhotspot.client.ClientsFragment
import be.mygod.vpnhotspot.databinding.ActivityMainBinding
import be.mygod.vpnhotspot.manage.TetheringFragment
import be.mygod.vpnhotspot.net.IpNeighbour
import be.mygod.vpnhotspot.net.wifi.WifiDoubleLock
import be.mygod.vpnhotspot.util.AppUpdate
import be.mygod.vpnhotspot.util.ServiceForegroundConnector
import be.mygod.vpnhotspot.util.Services
import be.mygod.vpnhotspot.util.UpdateChecker
import be.mygod.vpnhotspot.widget.SmartSnackbar
import com.google.android.material.badge.BadgeDrawable
import com.google.android.material.navigation.NavigationBarView
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import java.net.Inet4Address
class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListener {
lateinit var binding: ActivityMainBinding
private lateinit var updateItem: MenuItem
private lateinit var updateBadge: BadgeDrawable
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.navigation.setOnItemSelectedListener(this)
val badge = binding.navigation.getOrCreateBadge(R.id.navigation_clients).apply {
backgroundColor = ContextCompat.getColor(this@MainActivity, R.color.colorSecondary)
badgeTextColor = ContextCompat.getColor(this@MainActivity, R.color.primary_text_default_material_light)
}
updateItem = binding.navigation.menu.findItem(R.id.navigation_update)
updateItem.isCheckable = false
updateBadge = binding.navigation.getOrCreateBadge(R.id.navigation_update).apply {
backgroundColor = ContextCompat.getColor(this@MainActivity, R.color.colorSecondary)
badgeTextColor = ContextCompat.getColor(this@MainActivity, R.color.primary_text_default_material_light)
}
if (savedInstanceState == null) displayFragment(TetheringFragment())
val model by viewModels<ClientViewModel>()
lifecycle.addObserver(model)
@@ -34,38 +54,57 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
val count = clients.count {
it.ip.any { (ip, state) -> ip is Inet4Address && state == IpNeighbour.State.VALID }
}
if (count > 0) binding.navigation.getOrCreateBadge(R.id.navigation_clients).apply {
backgroundColor = ContextCompat.getColor(this@MainActivity, R.color.colorSecondary)
badgeTextColor = ContextCompat.getColor(this@MainActivity, R.color.primary_text_default_material_light)
number = count
} else binding.navigation.removeBadge(R.id.navigation_clients)
badge.isVisible = count > 0
badge.number = count
}
SmartSnackbar.Register(binding.fragmentHolder)
WifiDoubleLock.ActivityListener(this)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
onAppUpdateAvailable(null)
UpdateChecker.check().collect(this@MainActivity::onAppUpdateAvailable)
}
}
}
private var lastUpdate: AppUpdate? = null
private fun onAppUpdateAvailable(update: AppUpdate?) {
lastUpdate = update
updateItem.isVisible = update != null
if (update == null) return
updateItem.isEnabled = update.downloaded != false
updateItem.setIcon(when (update.downloaded) {
null -> R.drawable.ic_action_update
false -> R.drawable.ic_file_downloading
true -> R.drawable.ic_action_autorenew
})
updateItem.title = update.message ?: "Update"
updateBadge.isVisible = when (val days = update.stalenessDays) {
null -> false
else -> {
if (days > 0) updateBadge.number = days else updateBadge.clearNumber()
true
}
}
}
override fun onNavigationItemSelected(item: MenuItem) = when (item.itemId) {
R.id.navigation_clients -> {
if (!item.isChecked) {
item.isChecked = true
displayFragment(ClientsFragment())
}
displayFragment(ClientsFragment())
true
}
R.id.navigation_tethering -> {
if (!item.isChecked) {
item.isChecked = true
displayFragment(TetheringFragment())
}
displayFragment(TetheringFragment())
true
}
R.id.navigation_settings -> {
if (!item.isChecked) {
item.isChecked = true
displayFragment(SettingsPreferenceFragment())
}
displayFragment(SettingsPreferenceFragment())
true
}
R.id.navigation_update -> {
lastUpdate!!.updateForResult(this, 1)
false
}
else -> false
}

View File

@@ -7,6 +7,7 @@ import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.room.AppDatabase
import be.mygod.vpnhotspot.util.disconnectCompat
import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
@@ -36,7 +37,7 @@ object MacLookup {
@MainThread
fun abort(mac: MacAddressCompat) = macLookupBusy.remove(mac)?.let { (conn, job) ->
job.cancel()
if (Build.VERSION.SDK_INT < 26) GlobalScope.launch(Dispatchers.IO) { conn.disconnect() } else conn.disconnect()
conn.disconnectCompat()
}
@MainThread

View File

@@ -0,0 +1,10 @@
package be.mygod.vpnhotspot.util
import android.app.Activity
interface AppUpdate {
val downloaded: Boolean? get() = null
val message: String? get() = null
val stalenessDays: Int? get() = null
fun updateForResult(activity: Activity, requestCode: Int): Unit = error("Update not supported")
}

View File

@@ -24,6 +24,9 @@ import androidx.fragment.app.FragmentManager
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.File
import java.io.FileNotFoundException
@@ -32,6 +35,7 @@ import java.lang.invoke.MethodHandles
import java.lang.reflect.InvocationHandler
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method
import java.net.HttpURLConnection
import java.net.InetAddress
import java.net.NetworkInterface
import java.net.SocketException
@@ -66,6 +70,10 @@ fun Method.matchesCompat(name: String, args: Array<out Any?>?, vararg classes: C
}
} else matches(name, *classes)
fun HttpURLConnection.disconnectCompat() {
if (Build.VERSION.SDK_INT < 26) GlobalScope.launch(Dispatchers.IO) { disconnect() } else disconnect()
}
fun Context.ensureReceiverUnregistered(receiver: BroadcastReceiver) {
try {
unregisterReceiver(receiver)

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M21,10.12h-6.78l2.74,-2.82c-2.73,-2.7 -7.15,-2.8 -9.88,-0.1c-2.73,2.71 -2.73,7.08 0,9.79s7.15,2.71 9.88,0C18.32,15.65 19,14.08 19,12.1h2c0,1.98 -0.88,4.55 -2.64,6.29c-3.51,3.48 -9.21,3.48 -12.72,0c-3.5,-3.47 -3.53,-9.11 -0.02,-12.58s9.14,-3.47 12.65,0L21,3V10.12zM12.5,8v4.25l3.5,2.08l-0.72,1.21L11,13V8H12.5z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M18.32,4.26C16.84,3.05 15.01,2.25 13,2.05v2.02c1.46,0.18 2.79,0.76 3.9,1.62L18.32,4.26zM19.93,11h2.02c-0.2,-2.01 -1,-3.84 -2.21,-5.32L18.31,7.1C19.17,8.21 19.75,9.54 19.93,11zM18.31,16.9l1.43,1.43c1.21,-1.48 2.01,-3.32 2.21,-5.32h-2.02C19.75,14.46 19.17,15.79 18.31,16.9zM13,19.93v2.02c2.01,-0.2 3.84,-1 5.32,-2.21l-1.43,-1.43C15.79,19.17 14.46,19.75 13,19.93zM13,12V7h-2v5H7l5,5l5,-5H13zM11,19.93v2.02c-5.05,-0.5 -9,-4.76 -9,-9.95s3.95,-9.45 9,-9.95v2.02C7.05,4.56 4,7.92 4,12S7.05,19.44 11,19.93z"/>
</vector>

View File

@@ -16,4 +16,10 @@
android:icon="@drawable/ic_action_settings"
android:title="@string/title_settings"/>
<item
android:id="@+id/navigation_update"
android:icon="@drawable/ic_action_update"
android:title="Update"
android:visible="false"/>
</menu>