Android Network Connectivity

This commit is contained in:
shabinder 2021-02-26 05:29:54 +05:30
parent 81a0b9a85c
commit bf6463a3e1
26 changed files with 328 additions and 35 deletions

View File

@ -4,6 +4,9 @@ plugins {
id("com.android.application")
kotlin("android")
id("org.jetbrains.compose")
id("com.google.gms.google-services")
id("com.google.firebase.crashlytics")
id("com.google.firebase.firebase-perf")
}
group = "com.shabinder"
@ -75,6 +78,12 @@ dependencies {
implementation(Decompose.decompose)
implementation(Decompose.extensionsCompose)
//Firebase
implementation(platform("com.google.firebase:firebase-bom:26.5.0"))
implementation("com.google.firebase:firebase-analytics-ktx")
implementation("com.google.firebase:firebase-crashlytics-ktx")
implementation("com.google.firebase:firebase-perf-ktx")
/*
//Lifecycle
Versions.androidLifecycle.let{
@ -85,6 +94,7 @@ dependencies {
}
*/
Extras.Android.apply {
implementation(appUpdator)
implementation(razorpay)

View File

@ -21,6 +21,8 @@
#-renamesourcefileattribute SourceFile
-keepattributes *Annotation*, InnerClasses
-dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations
-keepattributes SourceFile,LineNumberTable # Keep file names and line numbers.
-keep public class * extends java.lang.Exception # Optional: Keep custom exceptions.
# kotlinx-serialization-json specific. Add this if you have java.lang.NoClassDefFoundError kotlinx.serialization.json.JsonObjectSerializer
-keepclassmembers class kotlinx.serialization.json.** {
@ -29,11 +31,14 @@
-keepclasseswithmembers class kotlinx.serialization.json.** {
kotlinx.serialization.KSerializer serializer(...);
}
-keep,includedescriptorclasses class com.shabinder.spotiflyer.**$$serializer { *; } # <-- change package name to your app's
-keepclassmembers class com.shabinder.spotiflyer.** { # <-- change package name to your app's
-keep class com.shabinder.** { *; }
-keep,includedescriptorclasses class com.shabinder.**$$serializer { *; } # <-- change package name to your app's
-keepclassmembers class com.shabinder.** { # <-- change package name to your app's
*** Companion;
}
-keepclasseswithmembers class com.shabinder.spotiflyer.** { # <-- change package name to your app's
-keepclasseswithmembers class com.shabinder.** { # <-- change package name to your app's
kotlinx.serialization.KSerializer serializer(...);
}
}
# Ktor
-keep class io.ktor.** { *; }
-keep class kotlinx.coroutines.** { *; }

View File

@ -12,6 +12,7 @@
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

View File

@ -45,9 +45,7 @@ import com.shabinder.common.root.SpotiFlyerRoot
import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks
import com.shabinder.common.uikit.*
import com.shabinder.database.Database
import com.shabinder.spotiflyer.utils.checkIfLatestVersion
import com.shabinder.spotiflyer.utils.disableDozeMode
import com.shabinder.spotiflyer.utils.requestStoragePermission
import com.shabinder.spotiflyer.utils.*
import com.tonyodev.fetch2.Status
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
@ -100,6 +98,7 @@ class MainActivity : ComponentActivity(), PaymentResultListener {
if(askForPermission && !permissionGranted.value) permissionDialog()
NetworkDialog()
root = SpotiFlyerRootContent(rememberRootComponent(::spotiFlyerRoot),statusBarHeight)
}
}

View File

@ -0,0 +1,73 @@
package com.shabinder.spotiflyer.utils
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.material.AlertDialog
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.CloudOff
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.shabinder.common.di.isInternetAvailable
import com.shabinder.common.di.isInternetAvailableState
import com.shabinder.common.uikit.SpotiFlyerShapes
import com.shabinder.common.uikit.SpotiFlyerTypography
import com.shabinder.common.uikit.colorOffWhite
import kotlinx.coroutines.delay
@Composable
fun NetworkDialog(
networkAvailability: State<Boolean?> = isInternetAvailableState()
){
var visible by remember { mutableStateOf(false) }
LaunchedEffect(Unit){
delay(2600)
visible = true
}
if(networkAvailability.value == false){
Crossfade(visible){
if(it){
AlertDialog(
onDismissRequest = {},
buttons = {
/* TextButton({
//Retry Network Connection
},
Modifier.padding(bottom = 16.dp,start = 16.dp,end = 16.dp).fillMaxWidth().background(Color(0xFFFC5C7D),shape = RoundedCornerShape(size = 8.dp)).padding(horizontal = 8.dp),
){
Text("Retry",color = Color.Black,fontSize = 18.sp,textAlign = TextAlign.Center)
Icon(Icons.Rounded.SyncProblem,"Check Network Connection Again")
}
*/},
title = { Text("No Internet Connection!",
style = SpotiFlyerTypography.h5,
textAlign = TextAlign.Center) },
backgroundColor = Color.DarkGray,
text = {
Column(horizontalAlignment = Alignment.CenterHorizontally,verticalArrangement = Arrangement.Center){
Spacer(modifier = Modifier.padding(8.dp))
Row(verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp)
) {
Image(Icons.Rounded.CloudOff,"No Internet.",Modifier.size(42.dp),colorFilter = ColorFilter.tint(
colorOffWhite))
Spacer(modifier = Modifier.padding(start = 16.dp))
Text(
text = "Please Check Your Network Connection.",
style = SpotiFlyerTypography.subtitle1
)
}
}
}
,shape = SpotiFlyerShapes.medium)
}
}
}
}

View File

@ -16,6 +16,9 @@ repositories {
dependencies {
implementation("com.android.tools.build:gradle:4.0.1")
implementation("com.google.gms:google-services:4.3.5")
implementation("com.google.firebase:perf-plugin:1.3.4")
implementation("com.google.firebase:firebase-crashlytics-gradle:2.5.0")
implementation(JetBrains.Compose.gradlePlugin)
implementation(JetBrains.Kotlin.gradlePlugin)
implementation(JetBrains.Kotlin.serialization)

View File

@ -27,7 +27,7 @@ object Versions {
//Android
const val versionCode = 15
const val minSdkVersion = 24
const val compileSdkVersion = 30
const val compileSdkVersion = 29
const val targetSdkVersion = 29
const val androidLifecycle = "2.3.0"
}

View File

@ -1,2 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.shabinder.common.ui"/>
<manifest package="com.shabinder.common.uikit"/>

View File

@ -1,2 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.shabinder.common.ui"/>
<manifest package="com.shabinder.common.models"/>

View File

@ -19,4 +19,4 @@ actual fun createDatabase(): Database {
val driver = AndroidSqliteDriver(Database.Schema, appContext, "Database.db")
return Database(driver)
}
actual fun getLogger(): Logger = LogcatLogger()
actual fun getLogger(): Logger = LogcatLogger()

View File

@ -1,3 +1,5 @@
import org.jetbrains.compose.compose
plugins {
id("multiplatform-compose-setup")
id("android-setup")
@ -8,6 +10,7 @@ kotlin {
sourceSets {
commonMain {
dependencies {
implementation(compose.materialIconsExtended)
implementation(project(":common:data-models"))
implementation(project(":common:database"))
implementation(project(":fuzzywuzzy:app"))

View File

@ -1,2 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.shabinder.common.ui"/>
<manifest package="com.shabinder.common.di" xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest>

View File

@ -4,8 +4,13 @@ import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import androidx.compose.material.MaterialTheme
import androidx.compose.material.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.core.content.ContextCompat
import com.github.kiulian.downloader.model.YoutubeVideo
import com.github.kiulian.downloader.model.formats.Format

View File

@ -0,0 +1,122 @@
package com.shabinder.common.di
import android.content.Context
import android.content.Context.CONNECTIVITY_SERVICE
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
import android.net.NetworkRequest
import android.util.Log
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.lifecycle.LiveData
import com.shabinder.common.database.appContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.IOException
import java.net.InetSocketAddress
import javax.net.SocketFactory
const val TAG = "C-Manager"
val isInternetAvailable by lazy { ConnectionLiveData(appContext) }
@Composable
fun isInternetAvailableState(): State<Boolean?>{
return isInternetAvailable.observeAsState()
}
/**
* Save all available networks with an internet connection to a set (@validNetworks).
* As long as the size of the set > 0, this LiveData emits true.
* MinSdk = 21.
*
* Inspired by:
* https://github.com/AlexSheva-mason/Rick-Morty-Database/blob/master/app/src/main/java/com/shevaalex/android/rickmortydatabase/utils/networking/ConnectionLiveData.kt
*/
class ConnectionLiveData(context: Context = appContext) : LiveData<Boolean>() {
private lateinit var networkCallback: ConnectivityManager.NetworkCallback
private val cm = context.getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
private val validNetworks: MutableSet<Network> = HashSet()
private fun checkValidNetworks() {
postValue(validNetworks.size > 0)
}
override fun onActive() {
checkValidNetworks()
networkCallback = createNetworkCallback()
val networkRequest = NetworkRequest.Builder()
.addCapability(NET_CAPABILITY_INTERNET)
.build()
cm.registerNetworkCallback(networkRequest, networkCallback)
}
override fun onInactive() {
cm.unregisterNetworkCallback(networkCallback)
}
private fun createNetworkCallback() = object : ConnectivityManager.NetworkCallback() {
/*
Called when a network is detected. If that network has internet, save it in the Set.
Source: https://developer.android.com/reference/android/net/ConnectivityManager.NetworkCallback#onAvailable(android.net.Network)
*/
override fun onAvailable(network: Network) {
Log.d(TAG, "onAvailable: ${network}")
val networkCapabilities = cm.getNetworkCapabilities(network)
val hasInternetCapability = networkCapabilities?.hasCapability(NET_CAPABILITY_INTERNET)
Log.d(TAG, "onAvailable: ${network}, $hasInternetCapability")
if (hasInternetCapability == true) {
// check if this network actually has internet
CoroutineScope(Dispatchers.IO).launch {
val hasInternet = DoesNetworkHaveInternet.execute(network.socketFactory)
if(hasInternet){
withContext(Dispatchers.Main){
Log.d(TAG, "onAvailable: adding network. ${network}")
validNetworks.add(network)
checkValidNetworks()
}
}
}
}
}
/*
If the callback was registered with registerNetworkCallback() it will be called for each network which no longer satisfies the criteria of the callback.
Source: https://developer.android.com/reference/android/net/ConnectivityManager.NetworkCallback#onLost(android.net.Network)
*/
override fun onLost(network: Network) {
Log.d(TAG, "onLost: ${network}")
validNetworks.remove(network)
checkValidNetworks()
}
}
/**
* Send a ping to googles primary DNS.
* If successful, that means we have internet.
*/
object DoesNetworkHaveInternet {
// Make sure to execute this on a background thread.
fun execute(socketFactory: SocketFactory): Boolean {
return try{
Log.d(TAG, "PINGING google.")
val socket = socketFactory.createSocket() ?: throw IOException("Socket is null.")
socket.connect(InetSocketAddress("8.8.8.8", 53), 1500)
socket.close()
Log.d(TAG, "PING success.")
true
}catch (e: IOException){
Log.e(TAG, "No internet connection. $e")
false
}
}
}
}

View File

@ -0,0 +1,33 @@
package com.shabinder.common.di
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
@Composable
fun <T> LiveData<T>.observeAsState(): State<T?> = observeAsState(value)
/**
* Starts observing this [LiveData] and represents its values via [State]. Every time there would
* be new value posted into the [LiveData] the returned [State] will be updated causing
* recomposition of every [State.value] usage.
*
* The inner observer will automatically be removed when this composable disposes or the current
* [LifecycleOwner] moves to the [Lifecycle.State.DESTROYED] state.
*
* @sample androidx.compose.runtime.livedata.samples.LiveDataWithInitialSample
*/
@Composable
fun <R, T : R> LiveData<T>.observeAsState(initial: R): State<R> {
val lifecycleOwner = LocalLifecycleOwner.current
val state = remember { mutableStateOf(initial) }
DisposableEffect(this, lifecycleOwner) {
val observer = Observer<T> { state.value = it }
observe(lifecycleOwner, observer)
onDispose { removeObserver(observer) }
}
return state
}

View File

@ -14,6 +14,7 @@ import io.ktor.client.features.logging.*
import io.ktor.client.request.*
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import org.koin.core.context.startKoin
import org.koin.dsl.KoinAppDeclaration
@ -46,20 +47,18 @@ val kotlinxSerializer = KotlinxSerializer( Json {
/*
* Refactor This
* */
fun isInternetAvailable(): Boolean {
var result = false
val job = GlobalScope.launch {
suspend fun isInternetAvailable(): Boolean {
return withContext(dispatcherIO) {
try {
ktorHttpClient.head<String>("http://google.com")
result = true
true
} catch (e: Exception) {
println(e.message)
result = false
false
}
}
while (job.isActive){}
return result
}
fun createHttpClient(enableNetworkLogs: Boolean = false,serializer: KotlinxSerializer = kotlinxSerializer) = HttpClient {
install(JsonFeature) {
this.serializer = serializer

View File

@ -0,0 +1,6 @@
package com.shabinder.common.di
sealed class NetworkResponse<out T> {
data class Success<T>(val value:T):NetworkResponse<T>()
data class Error(val message:String):NetworkResponse<Nothing>()
}

View File

@ -21,7 +21,7 @@ class TokenStore(
db.add(token.access_token!!, token.expiry!! + Clock.System.now().epochSeconds)
}
suspend fun getToken(): TokenData {
suspend fun getToken(): TokenData? {
var token: TokenData? = db.select().executeAsOneOrNull()?.let {
TokenData(it.accessToken,null,it.expiry)
}
@ -29,7 +29,7 @@ class TokenStore(
if(Clock.System.now().epochSeconds > token?.expiry ?:0 || token == null){
logger.d{"Requesting New Token"}
token = authenticateSpotify()
GlobalScope.launch { token.access_token?.let { save(token) } }
GlobalScope.launch { token?.access_token?.let { save(token) } }
}
return token
}

View File

@ -18,10 +18,7 @@ package com.shabinder.common.di.providers
import co.touchlab.kermit.Kermit
import com.shabinder.common.database.DownloadRecordDatabaseQueries
import com.shabinder.common.di.Dir
import com.shabinder.common.di.TokenStore
import com.shabinder.common.di.finalOutputDir
import com.shabinder.common.di.kotlinxSerializer
import com.shabinder.common.di.*
import com.shabinder.common.di.spotify.SpotifyRequests
import com.shabinder.common.models.PlatformQueryResult
import com.shabinder.common.models.TrackDetails
@ -48,8 +45,17 @@ class SpotifyProvider(
init {
logger.d { "Creating Spotify Provider" }
GlobalScope.launch(Dispatchers.Default) {
val token = tokenStore.getToken()
GlobalScope.launch(Dispatchers.Default) {authenticateSpotify()}
}
override suspend fun authenticateSpotify(): HttpClient?{
val token = tokenStore.getToken()
return if(token == null) {
showPopUpMessage("Please Check your Network Connection")
null
}
else{
logger.d { "Spotify Provider Created with $token" }
httpClient = HttpClient {
defaultRequest {
header("Authorization","Bearer ${token.access_token}")
@ -58,7 +64,7 @@ class SpotifyProvider(
serializer = kotlinxSerializer
}
}
logger.d { "Spotify Provider Created with $token" }
httpClient
}
}
@ -68,6 +74,11 @@ class SpotifyProvider(
get() = database.downloadRecordDatabaseQueries
suspend fun query(fullLink: String): PlatformQueryResult?{
if(!this::httpClient.isInitialized){
authenticateSpotify()
}
var spotifyLink =
"https://" + fullLink.substringAfterLast("https://").substringBefore(" ").trim()

View File

@ -1,5 +1,6 @@
package com.shabinder.common.di.spotify
import com.shabinder.common.di.isInternetAvailable
import com.shabinder.common.di.kotlinxSerializer
import com.shabinder.common.models.spotify.TokenData
import io.ktor.client.*
@ -10,10 +11,10 @@ import io.ktor.client.request.*
import io.ktor.client.request.forms.*
import io.ktor.http.*
suspend fun authenticateSpotify(): TokenData {
return spotifyAuthClient.post("https://accounts.spotify.com/api/token"){
suspend fun authenticateSpotify(): TokenData? {
return if(isInternetAvailable()) spotifyAuthClient.post("https://accounts.spotify.com/api/token"){
body = FormDataContent(Parameters.build { append("grant_type","client_credentials") })
}
} else null
}
private val spotifyAuthClient by lazy {

View File

@ -13,6 +13,8 @@ interface SpotifyRequests {
val httpClient:HttpClient
suspend fun authenticateSpotify():HttpClient?
suspend fun getPlaylist(playlistID: String): Playlist {
return httpClient.get("$BASE_URL/playlists/$playlistID")
}

View File

@ -1,5 +1,9 @@
package com.shabinder.common.di
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import com.github.kiulian.downloader.YoutubeDownloader
import com.github.kiulian.downloader.model.YoutubeVideo
import com.github.kiulian.downloader.model.formats.Format
@ -22,6 +26,19 @@ actual fun giveDonation(){
//TODO
}
@Composable
actual fun AlertDialog(
onDismissRequest: () -> Unit,
buttons: @Composable () -> Unit,
modifier: Modifier,
title: (@Composable () -> Unit)?,
text: @Composable (() -> Unit)?,
shape: Shape,
backgroundColor: Color,
contentColor: Color,
){}
actual fun queryActiveTracks(){}
val DownloadProgressFlow: MutableSharedFlow<HashMap<String,DownloadStatus>> = MutableSharedFlow(1)

View File

@ -1,2 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.shabinder.common.ui"/>
<manifest package="com.shabinder.common.list"/>

View File

@ -1,2 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.shabinder.common.ui"/>
<manifest package="com.shabinder.common.main"/>

View File

@ -1,2 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.shabinder.common.ui"/>
<manifest package="com.shabinder.common.root"/>

View File

@ -22,6 +22,7 @@ kotlin {
implementation(project(":common:database"))
implementation(project(":common:dependency-injection"))
implementation(project(":common:compose"))
implementation(project(":common:root"))
implementation(Decompose.decompose)
implementation(Decompose.extensionsCompose)
implementation(MVIKotlin.mvikotlin)