- Removed BroadcastReceivers and Bound to Service instead,

- Code Improv and Cleanup
This commit is contained in:
shabinder 2021-06-22 11:43:30 +05:30
parent 9b447c3a9d
commit 979fcc342b
11 changed files with 204 additions and 176 deletions

View File

@ -121,13 +121,14 @@ dependencies {
implementation(MVIKotlin.mvikotlinTimeTravel)
// Extras
Extras.Android.apply {
with(Extras.Android) {
implementation(Acra.notification)
implementation(Acra.http)
implementation(appUpdator)
implementation(matomo)
}
implementation(Extras.kermit)
//implementation("com.jakewharton.timber:timber:4.7.1")
implementation("dev.icerock.moko:parcelize:0.7.0")
implementation("com.github.shabinder:storage-chooser:2.0.4.45")

View File

@ -72,6 +72,6 @@
</intent-filter>
</activity>
<service android:name="com.shabinder.common.di.worker.ForegroundService"/>
<service android:name=".service.ForegroundService"/>
</application>
</manifest>

View File

@ -17,15 +17,16 @@
package com.shabinder.spotiflyer
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import android.os.PowerManager
import android.util.Log
import androidx.activity.ComponentActivity
@ -51,18 +52,17 @@ import com.google.accompanist.insets.navigationBarsPadding
import com.google.accompanist.insets.statusBarsHeight
import com.google.accompanist.insets.statusBarsPadding
import com.shabinder.common.di.*
import com.shabinder.common.di.worker.ForegroundService
import com.shabinder.common.models.Actions
import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.PlatformActions
import com.shabinder.common.models.PlatformActions.Companion.SharedPreferencesKey
import com.shabinder.common.models.Status
import com.shabinder.common.models.TrackDetails
import com.shabinder.common.models.methods
import com.shabinder.common.root.SpotiFlyerRoot
import com.shabinder.common.root.SpotiFlyerRoot.Analytics
import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks
import com.shabinder.common.uikit.*
import com.shabinder.spotiflyer.service.ForegroundService
import com.shabinder.spotiflyer.ui.AnalyticsDialog
import com.shabinder.spotiflyer.ui.NetworkDialog
import com.shabinder.spotiflyer.ui.PermissionDialog
@ -82,12 +82,16 @@ class MainActivity : ComponentActivity() {
private val callBacks: SpotiFlyerRootCallBacks get() = root.callBacks
private val trackStatusFlow = MutableSharedFlow<HashMap<String, DownloadStatus>>(1)
private var permissionGranted = mutableStateOf(true)
private lateinit var updateUIReceiver: BroadcastReceiver
private lateinit var queryReceiver: BroadcastReceiver
private val internetAvailability by lazy { ConnectionLiveData(applicationContext) }
private val tracker get() = (application as App).tracker
private val visibleChild get(): SpotiFlyerRoot.Child = root.routerState.value.activeChild.instance
// Variable for storing instance of our service class
var foregroundService: ForegroundService? = null
// Boolean to check if our activity is bound to service or not
var isServiceBound: Boolean? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// This app draws behind the system bars, so we want to handle fitting system windows
@ -162,8 +166,62 @@ class MainActivity : ComponentActivity() {
TrackHelper.track().download().with(tracker)
}
handleIntentFromExternalActivity()
initForegroundService()
}
/*START: Foreground Service Handlers*/
private fun initForegroundService() {
// Start and then Bind to the Service
ContextCompat.startForegroundService(
this@MainActivity,
Intent(this, ForegroundService::class.java)
)
bindService()
}
/**
* Interface for getting the instance of binder from our service class
* So client can get instance of our service class and can directly communicate with it.
*/
private val serviceConnection = object : ServiceConnection {
val tag = "Service Connection"
override fun onServiceConnected(className: ComponentName, iBinder: IBinder) {
Log.d(tag, "connected to service.")
// We've bound to MyService, cast the IBinder and get MyBinder instance
val binder = iBinder as ForegroundService.DownloadServiceBinder
foregroundService = binder.service
isServiceBound = true
lifecycleScope.launch {
foregroundService?.trackStatusFlowMap?.statusFlow?.let {
trackStatusFlow.emitAll(it.conflate())
}
}
}
override fun onServiceDisconnected(arg0: ComponentName) {
Log.d(tag, "disconnected from service.")
isServiceBound = false
}
}
/*Used to bind to our service class*/
private fun bindService() {
Intent(this, ForegroundService::class.java).also { intent ->
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
}
}
/*Used to unbind from our service class*/
private fun unbindService() {
Intent(this, ForegroundService::class.java).also {
unbindService(serviceConnection)
}
}
/*END: Foreground Service Handlers*/
@Composable
private fun isInternetAvailableState(): State<Boolean?> {
return internetAvailability.observeAsState()
@ -206,12 +264,9 @@ class MainActivity : ComponentActivity() {
)
}
override fun sendTracksToService(array: ArrayList<TrackDetails>) {
for (list in array.chunked(50)) {
val serviceIntent = Intent(this@MainActivity, ForegroundService::class.java)
serviceIntent.putParcelableArrayListExtra("object", list as ArrayList)
ContextCompat.startForegroundService(this@MainActivity, serviceIntent)
}
override fun sendTracksToService(array: List<TrackDetails>) {
if (foregroundService == null) initForegroundService()
foregroundService?.downloadAllTracks(array)
}
}
@ -296,10 +351,16 @@ class MainActivity : ComponentActivity() {
)
private fun queryActiveTracks() {
val serviceIntent = Intent(this@MainActivity, ForegroundService::class.java).apply {
action = "query"
lifecycleScope.launch {
foregroundService?.trackStatusFlowMap?.let { tracksStatus ->
trackStatusFlow.emit(tracksStatus)
}
}
ContextCompat.startForegroundService(this@MainActivity, serviceIntent)
}
override fun onResume() {
super.onResume()
queryActiveTracks()
}
@Suppress("DEPRECATION")
@ -357,80 +418,6 @@ class MainActivity : ComponentActivity() {
}
}
/*
* Broadcast Handlers
* */
private fun initializeBroadcast(){
val intentFilter = IntentFilter().apply {
addAction(Status.QUEUED.name)
addAction(Status.FAILED.name)
addAction(Status.DOWNLOADING.name)
addAction(Status.COMPLETED.name)
addAction("Progress")
addAction("Converting")
}
updateUIReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
//Update Flow with latest details
if (intent != null) {
val trackDetails = intent.getParcelableExtra<TrackDetails?>("track")
trackDetails?.let { track ->
lifecycleScope.launch {
val latestMap = trackStatusFlow.replayCache.getOrElse(0
) { hashMapOf() }.apply {
this[track.title] = when (intent.action) {
Status.QUEUED.name -> DownloadStatus.Queued
Status.FAILED.name -> DownloadStatus.Failed
Status.DOWNLOADING.name -> DownloadStatus.Downloading()
"Progress" -> DownloadStatus.Downloading(intent.getIntExtra("progress", 0))
"Converting" -> DownloadStatus.Converting
Status.COMPLETED.name -> DownloadStatus.Downloaded
else -> DownloadStatus.NotDownloaded
}
}
trackStatusFlow.emit(latestMap)
Log.i("Track Update",track.title + track.downloaded.toString())
}
}
}
}
}
val queryFilter = IntentFilter().apply { addAction("query_result") }
queryReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
//UI update here
if (intent != null){
@Suppress("UNCHECKED_CAST")
val trackList = intent.getSerializableExtra("tracks") as? HashMap<String, DownloadStatus>?
trackList?.let { list ->
Log.i("Service Response", "${list.size} Tracks Active")
lifecycleScope.launch {
trackStatusFlow.emit(list)
}
}
}
}
}
registerReceiver(updateUIReceiver, intentFilter)
registerReceiver(queryReceiver, queryFilter)
}
override fun onResume() {
super.onResume()
initializeBroadcast()
if(visibleChild is SpotiFlyerRoot.Child.List) {
// Update Track List Statuses when Returning to App
queryActiveTracks()
}
}
override fun onPause() {
super.onPause()
unregisterReceiver(updateUIReceiver)
unregisterReceiver(queryReceiver)
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
handleIntentFromExternalActivity(intent)
@ -455,6 +442,11 @@ class MainActivity : ComponentActivity() {
}
}
override fun onDestroy() {
super.onDestroy()
unbindService()
}
companion object {
const val disableDozeCode = 1223
}

View File

@ -14,7 +14,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.common.di.worker
package com.shabinder.spotiflyer.service
import android.annotation.SuppressLint
import android.app.DownloadManager
@ -26,6 +26,7 @@ import android.app.PendingIntent.FLAG_CANCEL_CURRENT
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Binder
import android.os.Build
import android.os.IBinder
import android.os.PowerManager
@ -40,12 +41,13 @@ import com.shabinder.common.di.downloadFile
import com.shabinder.common.di.utils.ParallelExecutor
import com.shabinder.common.models.DownloadResult
import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.Status
import com.shabinder.common.models.TrackDetails
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
@ -68,7 +70,8 @@ class ForegroundService : Service(), CoroutineScope {
override val coroutineContext: CoroutineContext
get() = serviceJob + Dispatchers.IO
private val allTracksStatus = hashMapOf<String, DownloadStatus>()
val trackStatusFlowMap = TrackStatusFlowMap(MutableSharedFlow(replay = 1),this)
private var messageList = mutableListOf("", "", "", "", "")
private var wakeLock: PowerManager.WakeLock? = null
private var isServiceStarted = false
@ -80,7 +83,16 @@ class ForegroundService : Service(), CoroutineScope {
private val logger: Kermit by inject()
private val dir: Dir by inject()
override fun onBind(intent: Intent): IBinder? = null
inner class DownloadServiceBinder : Binder() {
// Return this instance of MyService so clients can call public methods
val service: ForegroundService
get() =// Return this instance of Foreground Service so clients can call public methods
this@ForegroundService
}
private val myBinder: IBinder = DownloadServiceBinder()
override fun onBind(intent: Intent): IBinder = myBinder
@SuppressLint("UnspecifiedImmutableFlag")
override fun onCreate() {
@ -110,31 +122,13 @@ class ForegroundService : Service(), CoroutineScope {
"query" -> {
val response = Intent().apply {
action = "query_result"
synchronized(allTracksStatus) {
putExtra("tracks", allTracksStatus)
synchronized(trackStatusFlowMap) {
putExtra("tracks", trackStatusFlowMap)
}
}
sendBroadcast(response)
}
}
val downloadObjects: ArrayList<TrackDetails>? = (
it.getParcelableArrayListExtra("object") ?: it.extras?.getParcelableArrayList(
"object"
)
)
downloadObjects?.let { list ->
downloadObjects.size.let { size ->
total += size
isSingleDownload = (size == 1)
}
list.forEach { track ->
allTracksStatus[track.title] = DownloadStatus.Queued
}
updateNotification()
downloadAllTracks(list)
}
}
// Wake locks and misc tasks from here :
return if (isServiceStarted) {
@ -156,8 +150,16 @@ class ForegroundService : Service(), CoroutineScope {
/**
* Function To Download All Tracks Available in a List
**/
private fun downloadAllTracks(trackList: List<TrackDetails>) {
fun downloadAllTracks(trackList: List<TrackDetails>) {
trackList.size.also { size ->
total += size
isSingleDownload = (size == 1)
updateNotification()
}
trackList.forEach {
trackStatusFlowMap[it.title] = DownloadStatus.Queued
launch(Dispatchers.IO) {
downloadService.execute {
fetcher.findMp3DownloadLink(it).fold(
@ -165,10 +167,9 @@ class ForegroundService : Service(), CoroutineScope {
enqueueDownload(url, it)
},
failure = { _ ->
sendTrackBroadcast(Status.FAILED.name, it)
failed++
updateNotification()
allTracksStatus[it.title] = DownloadStatus.Failed
trackStatusFlowMap[it.title] = DownloadStatus.Failed
}
)
}
@ -180,24 +181,20 @@ class ForegroundService : Service(), CoroutineScope {
// Initiating Download
addToNotification("Downloading ${track.title}")
logger.d(tag) { "${track.title} Download Started" }
allTracksStatus[track.title] = DownloadStatus.Downloading()
sendTrackBroadcast(Status.DOWNLOADING.name, track)
trackStatusFlowMap[track.title] = DownloadStatus.Downloading()
// Enqueueing Download
downloadFile(url).collect {
when (it) {
is DownloadResult.Error -> {
launch {
logger.d(tag) { it.message }
removeFromNotification("Downloading ${track.title}")
failed++
updateNotification()
sendTrackBroadcast(Status.FAILED.name, track)
}
logger.d(tag) { it.message }
removeFromNotification("Downloading ${track.title}")
failed++
updateNotification()
}
is DownloadResult.Progress -> {
allTracksStatus[track.title] = DownloadStatus.Downloading(it.progress)
trackStatusFlowMap[track.title] = DownloadStatus.Downloading(it.progress)
logger.d(tag) { "${track.title} Progress: ${it.progress} %" }
val intent = Intent().apply {
@ -209,26 +206,31 @@ class ForegroundService : Service(), CoroutineScope {
}
is DownloadResult.Success -> {
try {
// Save File and Embed Metadata
val job = launch(Dispatchers.Default) { dir.saveFileWithMetadata(it.byteArray, track) {} }
allTracksStatus[track.title] = DownloadStatus.Converting
sendTrackBroadcast("Converting", track)
addToNotification("Processing ${track.title}")
job.invokeOnCompletion {
converted++
allTracksStatus[track.title] = DownloadStatus.Downloaded
sendTrackBroadcast(Status.COMPLETED.name, track)
removeFromNotification("Processing ${track.title}")
coroutineScope {
try {
// Save File and Embed Metadata
val job = launch(Dispatchers.Default) { dir.saveFileWithMetadata(it.byteArray, track) {} }
// Send Converting Status
trackStatusFlowMap[track.title] = DownloadStatus.Converting
addToNotification("Processing ${track.title}")
// All Processing Completed for this Track
job.invokeOnCompletion {
converted++
trackStatusFlowMap[track.title] = DownloadStatus.Downloaded
removeFromNotification("Processing ${track.title}")
}
logger.d(tag) { "${track.title} Download Completed" }
downloaded++
} catch (e: Exception) {
e.printStackTrace()
// Download Failed
logger.d(tag) { "${track.title} Download Failed! Error:Fetch!!!!" }
failed++
}
logger.d(tag) { "${track.title} Download Completed" }
downloaded++
} catch (e: Exception) {
// Download Failed
logger.d(tag) { "${track.title} Download Failed! Error:Fetch!!!!" }
failed++
removeFromNotification("Downloading ${track.title}")
}
removeFromNotification("Downloading ${track.title}")
}
}
}
@ -270,7 +272,7 @@ class ForegroundService : Service(), CoroutineScope {
messageList = mutableListOf("Cleaning And Exiting", "", "", "", "")
downloadService.close()
updateNotification()
cleanFiles(File(dir.defaultDir()), logger)
cleanFiles(File(dir.defaultDir()))
// TODO cleanFiles(File(dir.imageCacheDir()))
messageList = mutableListOf("", "", "", "", "")
releaseWakeLock()
@ -336,12 +338,4 @@ class ForegroundService : Service(), CoroutineScope {
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
mNotificationManager.notify(notificationId, getNotification())
}
private fun sendTrackBroadcast(action: String, track: TrackDetails) {
val intent = Intent().apply {
setAction(action)
putExtra("track", track)
}
this@ForegroundService.sendBroadcast(intent)
}
}

View File

@ -0,0 +1,17 @@
package com.shabinder.spotiflyer.service
import com.shabinder.common.models.DownloadStatus
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
class TrackStatusFlowMap(
val statusFlow: MutableSharedFlow<HashMap<String,DownloadStatus>>,
private val scope: CoroutineScope
): HashMap<String,DownloadStatus>() {
override fun put(key: String, value: DownloadStatus): DownloadStatus? {
val res = super.put(key, value)
scope.launch { statusFlow.emit(this@TrackStatusFlowMap) }
return res
}
}

View File

@ -1,22 +1,22 @@
package com.shabinder.common.di.worker
package com.shabinder.spotiflyer.service
import co.touchlab.kermit.Kermit
import android.util.Log
import java.io.File
/**
* Cleaning All Residual Files except Mp3 Files
**/
fun cleanFiles(dir: File, logger: Kermit) {
fun cleanFiles(dir: File) {
try {
logger.d("File Cleaning") { "Starting Cleaning in ${dir.path} " }
Log.d("File Cleaning","Starting Cleaning in ${dir.path} ")
val fList = dir.listFiles()
fList?.let {
for (file in fList) {
if (file.isDirectory) {
cleanFiles(file, logger)
cleanFiles(file)
} else if (file.isFile) {
if (file.path.toString().substringAfterLast(".") != "mp3") {
logger.d("Files Cleaning") { "Cleaning ${file.path}" }
Log.d("Files Cleaning","Cleaning ${file.path}")
file.delete()
}
}
@ -24,3 +24,4 @@ fun cleanFiles(dir: File, logger: Kermit) {
}
} catch (e: Exception) { e.printStackTrace() }
}

View File

@ -17,12 +17,32 @@
package com.shabinder.common.uikit
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ExtendedFloatingActionButton
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -53,6 +73,7 @@ fun SpotiFlyerListContent(
component.onBackPressed()
}
}
Box(modifier = modifier.fillMaxSize()) {
val result = model.queryResult
if (result == null) {

View File

@ -14,7 +14,7 @@ actual interface PlatformActions {
fun addToLibrary(path: String)
fun sendTracksToService(array: ArrayList<TrackDetails>)
fun sendTracksToService(array: List<TrackDetails>)
}
actual val StubPlatformActions = object : PlatformActions {
@ -24,5 +24,5 @@ actual val StubPlatformActions = object : PlatformActions {
override fun addToLibrary(path: String) {}
override fun sendTracksToService(array: ArrayList<TrackDetails>) {}
override fun sendTracksToService(array: List<TrackDetails>) {}
}

View File

@ -23,11 +23,9 @@ import com.shabinder.common.di.utils.removeIllegalChars
import com.shabinder.common.models.DownloadResult
import com.shabinder.common.models.TrackDetails
import com.shabinder.database.Database
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.get
import io.ktor.client.statement.HttpStatement
import io.ktor.http.contentLength
import io.ktor.http.isSuccess
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlin.math.roundToInt
@ -105,7 +103,7 @@ suspend fun downloadFile(url: String): Flow<DownloadResult> {
var offset = 0
do {
// Set Length optimally, after how many kb you want a progress update, now it 0.25mb
val currentRead = response.content.readAvailable(data, offset, 250000)
val currentRead = response.content.readAvailable(data, offset, 2_50_000)
offset += currentRead
val progress = (offset * 100f / data.size).roundToInt()
emit(DownloadResult.Progress(progress))

View File

@ -18,6 +18,7 @@ package com.shabinder.common.list.integration
import co.touchlab.stately.ensureNeverFrozen
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.lifecycle.doOnResume
import com.arkivanov.decompose.value.Value
import com.shabinder.common.caching.Cache
import com.shabinder.common.di.Picture
@ -38,6 +39,9 @@ internal class SpotiFlyerListImpl(
init {
instanceKeeper.ensureNeverFrozen()
lifecycle.doOnResume {
onRefreshTracksStatuses()
}
}
private val store =

View File

@ -60,7 +60,7 @@ internal class SpotiFlyerListStoreProvider(
data class UpdateTrackList(val list: List<TrackDetails>) : Result()
data class UpdateTrackItem(val item: TrackDetails) : Result()
data class ErrorOccurred(val error: Throwable) : Result()
data class AskForDonation(val isAllowed: Boolean) : Result()
data class AskForSupport(val isAllowed: Boolean) : Result()
}
private inner class ExecutorImpl : SuspendExecutor<Intent, Unit, State, Result, Nothing>() {
@ -73,7 +73,7 @@ internal class SpotiFlyerListStoreProvider(
logger.d(message = "Database List Last ID: $it", tag = "Database Last ID")
val offset = dir.getDonationOffset
dispatch(
Result.AskForDonation(
Result.AskForSupport(
// Every 3rd Interval or After some offset
isAllowed = offset < 4 && (it % offset == 0L)
)
@ -81,7 +81,7 @@ internal class SpotiFlyerListStoreProvider(
}
downloadProgressFlow.collect { map ->
logger.d(map.size.toString(), "ListStore: flow Updated")
// logger.d(map.size.toString(), "ListStore: flow Updated")
val updatedTrackList = getState().trackList.updateTracksStatuses(map)
if (updatedTrackList.isNotEmpty()) dispatch(Result.UpdateTrackList(updatedTrackList))
}
@ -131,7 +131,7 @@ internal class SpotiFlyerListStoreProvider(
is Result.UpdateTrackList -> copy(trackList = result.list)
is Result.UpdateTrackItem -> updateTrackItem(result.item)
is Result.ErrorOccurred -> copy(errorOccurred = result.error)
is Result.AskForDonation -> copy(askForDonation = result.isAllowed)
is Result.AskForSupport -> copy(askForDonation = result.isAllowed)
}
private fun State.updateTrackItem(item: TrackDetails): State {