SpotiFlyer/android/src/main/java/com/shabinder/spotiflyer/MainActivity.kt

448 lines
19 KiB
Kotlin
Raw Normal View History

2021-03-18 19:11:45 +01:00
/*
* * Copyright (c) 2021 Shabinder Singh
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
2021-02-25 14:28:33 +01:00
package com.shabinder.spotiflyer
2021-01-26 13:54:28 +01:00
2021-02-10 18:25:36 +01:00
import android.annotation.SuppressLint
2021-02-22 18:38:33 +01:00
import android.content.BroadcastReceiver
2021-02-10 18:25:36 +01:00
import android.content.Context
import android.content.Intent
2021-02-22 18:38:33 +01:00
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.media.MediaScannerConnection
import android.net.Uri
2021-02-10 18:25:36 +01:00
import android.os.Build
2021-01-26 13:54:28 +01:00
import android.os.Bundle
2021-02-10 18:25:36 +01:00
import android.os.PowerManager
2021-02-22 18:38:33 +01:00
import android.util.Log
2021-02-10 18:25:36 +01:00
import androidx.activity.ComponentActivity
2021-02-11 21:51:11 +01:00
import androidx.activity.compose.setContent
2021-02-26 11:29:10 +01:00
import androidx.compose.animation.*
2021-02-25 16:23:10 +01:00
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
2021-02-25 14:28:33 +01:00
import androidx.compose.runtime.*
2021-02-25 16:23:10 +01:00
import androidx.compose.ui.Modifier
2021-02-24 18:02:01 +01:00
import androidx.compose.ui.platform.LocalView
import androidx.core.content.ContextCompat
2021-02-24 18:02:01 +01:00
import androidx.core.view.WindowCompat
2021-02-22 18:38:33 +01:00
import androidx.lifecycle.lifecycleScope
2021-02-07 17:19:48 +01:00
import com.arkivanov.decompose.ComponentContext
2021-02-25 14:28:33 +01:00
import com.arkivanov.decompose.extensions.compose.jetbrains.rememberRootComponent
2021-02-07 17:19:48 +01:00
import com.arkivanov.mvikotlin.logging.store.LoggingStoreFactory
import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory
2021-04-19 20:24:48 +02:00
import com.codekidlabs.storagechooser.R
import com.codekidlabs.storagechooser.StorageChooser
import com.google.accompanist.insets.ProvideWindowInsets
import com.google.accompanist.insets.navigationBarsPadding
import com.google.accompanist.insets.statusBarsHeight
import com.google.accompanist.insets.statusBarsPadding
2021-02-25 16:23:10 +01:00
import com.shabinder.common.di.*
import com.shabinder.common.di.worker.ForegroundService
import com.shabinder.common.models.Actions
2021-02-21 15:21:43 +01:00
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
2021-02-22 18:38:33 +01:00
import com.shabinder.common.models.TrackDetails
import com.shabinder.common.models.methods
2021-02-07 17:19:48 +01:00
import com.shabinder.common.root.SpotiFlyerRoot
import com.shabinder.common.root.SpotiFlyerRoot.Analytics
2021-02-10 18:25:36 +01:00
import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks
2021-02-25 16:23:10 +01:00
import com.shabinder.common.uikit.*
2021-05-16 20:10:02 +02:00
import com.shabinder.spotiflyer.ui.AnalyticsDialog
import com.shabinder.spotiflyer.ui.NetworkDialog
import com.shabinder.spotiflyer.ui.PermissionDialog
import com.shabinder.spotiflyer.utils.*
2021-02-10 18:25:36 +01:00
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
2021-02-07 17:19:48 +01:00
import org.koin.android.ext.android.inject
import org.matomo.sdk.extra.TrackHelper
2021-04-19 20:24:48 +02:00
import java.io.File
2021-01-26 13:54:28 +01:00
2021-02-26 10:41:25 +01:00
@ExperimentalAnimationApi
class MainActivity : ComponentActivity() {
2021-02-10 18:25:36 +01:00
private val fetcher: FetchPlatformQueryResult by inject()
private val dir: Dir by inject()
private lateinit var root: SpotiFlyerRoot
private val callBacks: SpotiFlyerRootCallBacks
get() = root.callBacks
private val trackStatusFlow = MutableSharedFlow<HashMap<String, DownloadStatus>>(1)
2021-02-25 16:23:10 +01:00
private var permissionGranted = mutableStateOf(true)
2021-02-22 18:38:33 +01:00
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
2021-02-07 17:19:48 +01:00
2021-01-26 13:54:28 +01:00
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
2021-02-24 18:02:01 +01:00
// This app draws behind the system bars, so we want to handle fitting system windows
WindowCompat.setDecorFitsSystemWindows(window, false)
2021-01-26 13:54:28 +01:00
setContent {
2021-02-07 17:19:48 +01:00
SpotiFlyerTheme {
Surface(contentColor = colorOffWhite) {
ProvideWindowInsets {
permissionGranted = remember { mutableStateOf(true) }
val view = LocalView.current
2021-02-24 18:02:01 +01:00
Box {
root = SpotiFlyerRootContent(
rememberRootComponent(::spotiFlyerRoot),
Modifier.statusBarsPadding().navigationBarsPadding()
)
Spacer(
Modifier
.statusBarsHeight()
.fillMaxWidth()
.background(MaterialTheme.colors.background.copy(alpha = 0.65f))
)
2021-02-24 18:02:01 +01:00
}
2021-02-25 16:23:10 +01:00
NetworkDialog(isInternetAvailableState())
2021-05-16 20:10:02 +02:00
PermissionDialog(
permissionGranted.value,
{ requestStoragePermission() },
{ disableDozeMode(disableDozeCode) },
)
2021-05-16 20:10:02 +02:00
var askForAnalyticsPermission by remember { mutableStateOf(false) }
AnalyticsDialog(
askForAnalyticsPermission,
dir::enableAnalytics,
dismissDialog = {
askForAnalyticsPermission = false
}
)
LaunchedEffect(view) {
permissionGranted.value = checkPermissions()
if(dir.isFirstLaunch) {
delay(2500)
// Ask For Analytics Permission on first Dialog
askForAnalyticsPermission = true
dir.firstLaunchDone()
}
}
}
}
2021-02-07 17:19:48 +01:00
}
2021-01-26 13:54:28 +01:00
}
2021-02-10 18:25:36 +01:00
initialise()
2021-01-26 13:54:28 +01:00
}
2021-02-07 17:19:48 +01:00
2021-02-10 18:25:36 +01:00
private fun initialise() {
checkIfLatestVersion()
2021-02-26 11:29:10 +01:00
handleIntentFromExternalActivity()
if(dir.isAnalyticsEnabled){
// Download/App Install Event
TrackHelper.track().download().with(tracker)
}
}
@Composable
private fun isInternetAvailableState(): State<Boolean?> {
return internetAvailability.observeAsState()
2021-04-19 20:24:48 +02:00
}
@Suppress("DEPRECATION")
private fun setUpOnPrefClickListener() {
// Initialize Builder
val chooser = StorageChooser.Builder()
.withActivity(this)
.withFragmentManager(fragmentManager)
.withMemoryBar(true)
.setTheme(StorageChooser.Theme(applicationContext).apply {
2021-04-19 20:24:48 +02:00
scheme = applicationContext.resources.getIntArray(R.array.default_dark)
})
.setDialogTitle("Set Download Directory")
.allowCustomPath(true)
.setType(StorageChooser.DIRECTORY_CHOOSER)
.build()
// get path that the user has chosen
chooser.setOnSelectListener { path ->
Log.d("Setting Base Path", path)
val f = File(path)
if (f.canWrite()) {
// hell yeah :)
dir.setDownloadDirectory(path)
showPopUpMessage(
2021-04-19 20:24:48 +02:00
"Download Directory Set to:\n${dir.defaultDir()} "
)
}else{
showPopUpMessage(
2021-04-19 20:24:48 +02:00
"NO WRITE ACCESS on \n$path ,\nReverting Back to Previous"
)
}
}
// Show dialog whenever you want by
chooser.show()
2021-02-10 18:25:36 +01:00
}
2021-02-07 17:19:48 +01:00
private fun showPopUpMessage(string: String, long: Boolean = false) {
android.widget.Toast.makeText(
applicationContext,
string,
if(long) android.widget.Toast.LENGTH_LONG else android.widget.Toast.LENGTH_SHORT
).show()
}
2021-02-26 10:41:25 +01:00
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
permissionGranted.value = checkPermissions()
}
2021-02-07 17:19:48 +01:00
private fun spotiFlyerRoot(componentContext: ComponentContext): SpotiFlyerRoot =
SpotiFlyerRoot(
componentContext,
dependencies = object : SpotiFlyerRoot.Dependencies{
override val storeFactory = LoggingStoreFactory(DefaultStoreFactory)
override val database = this@MainActivity.dir.db
2021-02-07 17:19:48 +01:00
override val fetchPlatformQueryResult = this@MainActivity.fetcher
override val directories: Dir = this@MainActivity.dir
override val downloadProgressReport: MutableSharedFlow<HashMap<String, DownloadStatus>> = trackStatusFlow
override val actions = object: Actions {
override val platformActions = object : PlatformActions {
override val imageCacheDir: String = applicationContext.cacheDir.absolutePath + File.separator
override val sharedPreferences = applicationContext.getSharedPreferences(SharedPreferencesKey,
MODE_PRIVATE
)
override fun addToLibrary(path: String) {
MediaScannerConnection.scanFile (
applicationContext,
listOf(path).toTypedArray(), null, null
)
}
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 showPopUpMessage(string: String, long: Boolean) = this@MainActivity.showPopUpMessage(string,long)
override fun setDownloadDirectoryAction() = setUpOnPrefClickListener()
override fun queryActiveTracks() {
val serviceIntent = Intent(this@MainActivity, ForegroundService::class.java).apply {
action = "query"
}
ContextCompat.startForegroundService(this@MainActivity, serviceIntent)
}
2021-05-08 17:12:09 +02:00
override fun giveDonation() {
openPlatform("",platformLink = "https://razorpay.com/payment-button/pl_GnKuuDBdBu0ank/view/?utm_source=payment_button&utm_medium=button&utm_campaign=payment_button")
2021-05-08 17:12:09 +02:00
}
override fun shareApp() {
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, "Hey, checkout this excellent Music Downloader http://github.com/Shabinder/SpotiFlyer")
type = "text/plain"
}
val shareIntent = Intent.createChooser(sendIntent, null)
startActivity(shareIntent)
}
override fun openPlatform(packageID: String, platformLink: String) {
val manager: PackageManager = applicationContext.packageManager
try {
val intent = manager.getLaunchIntentForPackage(packageID)
?: throw PackageManager.NameNotFoundException()
intent.addCategory(Intent.CATEGORY_LAUNCHER)
startActivity(intent)
} catch (e: PackageManager.NameNotFoundException) {
val uri: Uri =
Uri.parse(platformLink)
val intent = Intent(Intent.ACTION_VIEW, uri)
startActivity(intent)
}
}
override fun writeMp3Tags(trackDetails: TrackDetails) {/*IMPLEMENTED*/}
override val isInternetAvailable get() = internetAvailability.value ?: true
}
/*
* Analytics Will Only Be Sent if User Granted us the Permission
* */
override val analytics = object: Analytics {
override fun appLaunchEvent() {
if(dir.isAnalyticsEnabled){
TrackHelper.track()
.event("events","App_Launch")
.name("App Launch").with(tracker)
}
}
override fun homeScreenVisit() {
if(dir.isAnalyticsEnabled){
// HomeScreen Visit Event
TrackHelper.track().screen("/main_activity/home_screen")
.title("HomeScreen").with(tracker)
}
}
override fun listScreenVisit() {
if(dir.isAnalyticsEnabled){
// ListScreen Visit Event
TrackHelper.track().screen("/main_activity/list_screen")
.title("ListScreen").with(tracker)
}
}
override fun donationDialogVisit() {
if (dir.isAnalyticsEnabled) {
// Donation Dialog Open Event
TrackHelper.track().screen("/main_activity/donation_dialog")
.title("DonationDialog").with(tracker)
}
}
}
2021-02-07 17:19:48 +01:00
}
)
2021-02-10 18:25:36 +01:00
@SuppressLint("ObsoleteSdkInt")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == disableDozeCode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val pm =
getSystemService(Context.POWER_SERVICE) as PowerManager
val isIgnoringBatteryOptimizations =
pm.isIgnoringBatteryOptimizations(packageName)
if (isIgnoringBatteryOptimizations) {
// Ignoring battery optimization
2021-02-25 16:23:10 +01:00
permissionGranted.value = true
2021-02-10 18:25:36 +01:00
} else {
disableDozeMode(disableDozeCode)//Again Ask For Permission!!
}
}
}
}
/*
* Broadcast Handlers
* */
2021-02-22 18:38:33 +01:00
private fun initializeBroadcast(){
val intentFilter = IntentFilter().apply {
addAction(Status.QUEUED.name)
addAction(Status.FAILED.name)
addAction(Status.DOWNLOADING.name)
addAction(Status.COMPLETED.name)
2021-02-22 18:38:33 +01:00
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 {
2021-02-22 18:38:33 +01:00
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
2021-02-22 18:38:33 +01:00
else -> DownloadStatus.NotDownloaded
}
}
trackStatusFlow.emit(latestMap)
Log.i("Track Update",track.title + track.downloaded.toString())
2021-02-22 18:38:33 +01:00
}
}
}
}
}
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)
2021-02-22 18:38:33 +01:00
}
}
}
}
}
registerReceiver(updateUIReceiver, intentFilter)
registerReceiver(queryReceiver, queryFilter)
}
override fun onResume() {
super.onResume()
initializeBroadcast()
}
override fun onPause() {
super.onPause()
unregisterReceiver(updateUIReceiver)
unregisterReceiver(queryReceiver)
}
2021-02-10 18:25:36 +01:00
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
handleIntentFromExternalActivity(intent)
}
private fun handleIntentFromExternalActivity(intent: Intent? = getIntent()) {
if (intent?.action == Intent.ACTION_SEND) {
if ("text/plain" == intent.type) {
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
val filterLinkRegex = """http.+\w""".toRegex()
val string = it.replace("\n".toRegex(), " ")
val link = filterLinkRegex.find(string)?.value.toString()
2021-02-26 11:29:10 +01:00
Log.i("Intent",link)
lifecycleScope.launch {
while(!this@MainActivity::root.isInitialized){
delay(100)
}
if(methods.value.isInternetAvailable)callBacks.searchLink(link)
2021-02-26 11:29:10 +01:00
}
2021-02-10 18:25:36 +01:00
}
}
}
}
companion object {
const val disableDozeCode = 1223
}
2021-02-07 17:19:48 +01:00
}