diff --git a/app/build.gradle b/app/build.gradle
index f92aa26b..38466056 100755
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -23,6 +23,8 @@ plugins {
id 'androidx.navigation.safeargs.kotlin'
id 'dagger.hilt.android.plugin'
id 'kotlinx-serialization'
+ id 'com.google.gms.google-services'
+ id 'com.google.firebase.crashlytics'
}
android {
@@ -83,14 +85,17 @@ android {
dependencies {
//Android
- implementation "org.jetbrains.kotlin:kotlin-stdlib:1.4.10"
+ //noinspection DifferentStdlibGradleVersion
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.browser:browser:1.2.0'
implementation 'androidx.webkit:webkit:1.3.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
+ implementation "androidx.fragment:fragment-ktx:1.2.5"
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
+ implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.1'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.1'
@@ -105,7 +110,6 @@ dependencies {
//Room: Local SQL-lite Database
implementation "androidx.room:room-runtime:2.2.5"
- implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'
kapt "androidx.room:room-compiler:2.2.5"
implementation "androidx.room:room-ktx:2.2.5"
@@ -131,11 +135,17 @@ dependencies {
implementation "com.squareup.retrofit2:converter-scalars:2.9.0"
implementation 'com.beust:klaxon:5.4'
+ //Crashlytics & Analytics
+ implementation platform('com.google.firebase:firebase-bom:26.1.0')
+ implementation 'com.google.firebase:firebase-crashlytics-ktx'
+ implementation 'com.google.firebase:firebase-analytics-ktx'
+
//Extras
implementation 'me.xdrop:fuzzywuzzy:1.3.1'
implementation 'com.mpatric:mp3agic:0.9.1'
implementation 'com.shreyaspatil:EasyUpiPayment:3.0.0'
implementation 'com.github.javiersantos:AppUpdater:2.7'
+ implementation 'com.github.lzyzsd:circleprogress:1.2.1'
implementation "androidx.tonyodev.fetch2:xfetch2:3.1.5"
implementation 'com.github.sealedtx:java-youtube-downloader:2.4.4'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 540f8fed..cdaef4ca 100755
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -20,6 +20,12 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.shabinder.spotiflyer">
+
+
+
+
+
+
@@ -28,6 +34,8 @@
+
diff --git a/app/src/main/java/com/shabinder/spotiflyer/MainActivity.kt b/app/src/main/java/com/shabinder/spotiflyer/MainActivity.kt
index 6b377c39..974efb7a 100755
--- a/app/src/main/java/com/shabinder/spotiflyer/MainActivity.kt
+++ b/app/src/main/java/com/shabinder/spotiflyer/MainActivity.kt
@@ -31,6 +31,7 @@ import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
import androidx.navigation.NavController
import androidx.navigation.findNavController
import com.github.javiersantos.appupdater.AppUpdater
@@ -42,7 +43,6 @@ import com.shabinder.spotiflyer.networking.SpotifyServiceTokenRequest
import com.shabinder.spotiflyer.utils.NetworkInterceptor
import com.shabinder.spotiflyer.utils.createDirectories
import com.shabinder.spotiflyer.utils.showMessage
-import com.shabinder.spotiflyer.utils.startService
import com.squareup.moshi.Moshi
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
@@ -76,17 +76,15 @@ class MainActivity : AppCompatActivity(){
navController = findNavController(R.id.navHostFragment)
snackBarAnchor = binding.snackBarPosition
DownloadHelper.youtubeMusicApi = sharedViewModel.youtubeMusicApi
-
- //starting Notification and Downloader Service!
- startService(this)
-
authenticateSpotify()
+ }
+ override fun onStart() {
+ super.onStart()
requestPermission()
disableDozeMode()
checkIfLatestVersion()
createDirectories()
-
handleIntentFromExternalActivity()
}
@@ -154,9 +152,8 @@ class MainActivity : AppCompatActivity(){
sharedViewModel.spotifyService.value = spotifyService
}
-
fun authenticateSpotify() {
- sharedViewModel.uiScope.launch {
+ sharedViewModel.viewModelScope.launch {
Log.i("Spotify Authentication","Started")
val token = spotifyServiceTokenRequest.getToken()
token.value?.let {
@@ -210,7 +207,7 @@ class MainActivity : AppCompatActivity(){
companion object{
private lateinit var instance: MainActivity
- fun getInstance():MainActivity = instance
+ fun getInstance():MainActivity = this.instance
}
init {
diff --git a/app/src/main/java/com/shabinder/spotiflyer/SharedViewModel.kt b/app/src/main/java/com/shabinder/spotiflyer/SharedViewModel.kt
index 0191fee7..9686d8e2 100755
--- a/app/src/main/java/com/shabinder/spotiflyer/SharedViewModel.kt
+++ b/app/src/main/java/com/shabinder/spotiflyer/SharedViewModel.kt
@@ -22,21 +22,10 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.shabinder.spotiflyer.networking.SpotifyService
import com.shabinder.spotiflyer.networking.YoutubeMusicApi
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
class SharedViewModel @ViewModelInject constructor(
val youtubeMusicApi: YoutubeMusicApi
) : ViewModel() {
var intentString = MutableLiveData()
var spotifyService = MutableLiveData()
-
- private var viewModelJob = Job()
- val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
-
- override fun onCleared() {
- super.onCleared()
- viewModelJob.cancel()
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/shabinder/spotiflyer/downloadHelper/DownloadHelper.kt b/app/src/main/java/com/shabinder/spotiflyer/downloadHelper/DownloadHelper.kt
index ed250f45..cfcb33e3 100755
--- a/app/src/main/java/com/shabinder/spotiflyer/downloadHelper/DownloadHelper.kt
+++ b/app/src/main/java/com/shabinder/spotiflyer/downloadHelper/DownloadHelper.kt
@@ -18,14 +18,14 @@
package com.shabinder.spotiflyer.downloadHelper
import android.annotation.SuppressLint
+import android.content.Intent
import android.os.Handler
+import android.os.Looper
import android.util.Log
import android.view.View
import android.view.animation.AlphaAnimation
import android.view.animation.Animation
import android.widget.TextView
-import android.widget.Toast
-import com.shabinder.spotiflyer.SharedViewModel
import com.shabinder.spotiflyer.models.DownloadObject
import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.TrackDetails
@@ -34,6 +34,8 @@ import com.shabinder.spotiflyer.networking.makeJsonBody
import com.shabinder.spotiflyer.utils.*
import com.shabinder.spotiflyer.utils.Provider.defaultDir
import com.shabinder.spotiflyer.utils.Provider.mainActivity
+import com.tonyodev.fetch2.Status
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -46,7 +48,6 @@ object DownloadHelper {
var statusBar:TextView? = null
var youtubeMusicApi: YoutubeMusicApi? = null
- var sharedViewModel: SharedViewModel? = null
private var total = 0
private var processed = 0
@@ -61,7 +62,7 @@ object DownloadHelper {
trackList: List) {
resetStatusBar()// For New Download Request's Status
val downloadList = ArrayList()
- withContext(Dispatchers.Main){
+ withContext(Dispatchers.IO){
total += trackList.size // Adding New Download List Count to StatusBar
trackList.forEachIndexed { index, it ->
if(!isOnline()){
@@ -71,12 +72,10 @@ object DownloadHelper {
if(it.downloaded == DownloadStatus.Downloaded){//Download Already Present!!
processed++
if(index == (trackList.size-1)){//LastElement
- Handler().postDelayed({
+ Handler(Looper.myLooper()!!).postDelayed({
//Delay is Added ,if a request is in processing it may finish
Log.i("Spotify Helper","Download Request Sent")
- sharedViewModel?.uiScope?.launch (Dispatchers.Main){
- showMessage("Download Started, Now You can leave the App!")
- }
+ showMessage("Download Started, Now You can leave the App!")
startService(mainActivity,downloadList)
},3000)
}
@@ -86,47 +85,50 @@ object DownloadHelper {
youtubeMusicApi?.getYoutubeMusicResponse(jsonBody)?.enqueue(
object : Callback{
override fun onResponse(call: Call, response: Response) {
- sharedViewModel?.uiScope?.launch {
- val videoId = sortByBestMatch(
- getYTTracks(response.body().toString()),
- trackName = it.title,
- trackArtists = it.artists,
- trackDurationSec = it.durationSec
- ).keys.firstOrNull()
- Log.i("Spotify Helper Video ID",videoId ?: "Not Found")
+ val videoId = sortByBestMatch(
+ getYTTracks(response.body().toString()),
+ trackName = it.title,
+ trackArtists = it.artists,
+ trackDurationSec = it.durationSec
+ ).keys.firstOrNull()
+ Log.i("Spotify Helper Video ID",videoId ?: "Not Found")
+ if(videoId.isNullOrBlank()) {
+ //Track Not Found
+ notFound++ ; updateStatusBar()
+ val intent = Intent()
+ .setAction(Status.FAILED.name)
+ .putExtra("track",it)
+ statusBar?.context?.sendBroadcast(intent)
+ }
+ else {//Found Youtube Video ID
+ val outputFile: String =
+ defaultDir +
+ removeIllegalChars(type) + File.separator +
+ (if (subFolder == null) { "" }
+ else { removeIllegalChars(subFolder) + File.separator }
+ + removeIllegalChars(it.title) + ".m4a")
- if(videoId.isNullOrBlank()) {notFound++ ; updateStatusBar()}
- else {//Found Youtube Video ID
- val outputFile: String =
- defaultDir +
- removeIllegalChars(type) + File.separator +
- (if (subFolder == null) { "" }
- else { removeIllegalChars(subFolder) + File.separator }
- + removeIllegalChars(it.title) + ".m4a")
-
- val downloadObject = DownloadObject(
- trackDetails = it,
- ytVideoId = videoId,
- outputFile = outputFile
- )
- processed++
- sharedViewModel?.uiScope?.launch(Dispatchers.Main) {
- updateStatusBar()
- }
- downloadList.add(downloadObject)
- if(index == (trackList.size-1)){//LastElement
- Handler().postDelayed({
- //Delay is Added ,if a request is in processing it may finish
- Log.i("Spotify Helper","Download Request Sent")
- sharedViewModel?.uiScope?.launch (Dispatchers.Main){
- Toast.makeText(mainActivity,"Download Started, Now You can leave the App!", Toast.LENGTH_SHORT).show()
- }
- startService(mainActivity,downloadList)
- },5000)
- }
+ val downloadObject = DownloadObject(
+ trackDetails = it,
+ ytVideoId = videoId,
+ outputFile = outputFile
+ )
+ processed++
+ updateStatusBar()
+ downloadList.add(downloadObject)
+ }
+ if(index == (trackList.size-1)){//LastElement
+ statusBar?.clearAnimation()
+ if(downloadList.size > 0) {
+ Handler(Looper.myLooper()!!).postDelayed({
+ //Delay is Added ,if a request is in processing it may finish
+ Log.i("Spotify Helper", "Download Request Sent")
+ showMessage("Download Started, Now You can leave the App!")
+ startService(mainActivity, downloadList)
+ }, 3000)
}
}
- }
+ }
override fun onFailure(call: Call, t: Throwable) {
if(t.message.toString().contains("Failed to connect")) showMessage("Failed, Check Your Internet Connection!")
Log.i("YT API Req. Fail",t.message.toString())
@@ -157,8 +159,10 @@ object DownloadHelper {
}
@SuppressLint("SetTextI18n")
- fun updateStatusBar() {
- statusBar!!.visibility = View.VISIBLE
- statusBar?.text = "Total: $total ${getEmojiByUnicode(0x2705)}: $processed ${getEmojiByUnicode(0x274C)}: $notFound"
+ private fun updateStatusBar() {
+ CoroutineScope(Dispatchers.Main).launch{
+ statusBar!!.visibility = View.VISIBLE
+ statusBar?.text = "Total: $total ${getEmojiByUnicode(0x2705)}: $processed ${getEmojiByUnicode(0x274C)}: $notFound"
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/shabinder/spotiflyer/downloadHelper/YTDownloadHelper.kt b/app/src/main/java/com/shabinder/spotiflyer/downloadHelper/YTDownloadHelper.kt
index 4c075b61..ff0e53d7 100755
--- a/app/src/main/java/com/shabinder/spotiflyer/downloadHelper/YTDownloadHelper.kt
+++ b/app/src/main/java/com/shabinder/spotiflyer/downloadHelper/YTDownloadHelper.kt
@@ -18,20 +18,16 @@
package com.shabinder.spotiflyer.downloadHelper
import android.util.Log
-import android.widget.Toast
import com.shabinder.spotiflyer.models.DownloadObject
import com.shabinder.spotiflyer.models.TrackDetails
+import com.shabinder.spotiflyer.utils.*
import com.shabinder.spotiflyer.utils.Provider.defaultDir
import com.shabinder.spotiflyer.utils.Provider.mainActivity
-import com.shabinder.spotiflyer.utils.isOnline
-import com.shabinder.spotiflyer.utils.removeIllegalChars
-import com.shabinder.spotiflyer.utils.showNoConnectionAlert
-import com.shabinder.spotiflyer.utils.startService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
-object YTDownloadHelper {
+interface YTDownloadHelper {
suspend fun downloadYTTracks(
type:String,
subFolder: String?,
@@ -60,7 +56,7 @@ object YTDownloadHelper {
}
Log.i("YT Downloader Helper","Download Request Sent")
withContext(Dispatchers.Main){
- Toast.makeText(mainActivity,"Download Started, Now You can leave the App!", Toast.LENGTH_SHORT).show()
+ showMessage("Download Started, Now You can leave the App!")
startService(mainActivity,downloadList)
}
}
diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/DownloadObject.kt b/app/src/main/java/com/shabinder/spotiflyer/models/DownloadObject.kt
index f2448cbf..caec1f40 100755
--- a/app/src/main/java/com/shabinder/spotiflyer/models/DownloadObject.kt
+++ b/app/src/main/java/com/shabinder/spotiflyer/models/DownloadObject.kt
@@ -42,11 +42,15 @@ data class TrackDetails(
var albumArt: File,
var albumArtURL: String,
var source: Source,
- var downloaded: DownloadStatus = DownloadStatus.NotDownloaded
+ var downloaded: DownloadStatus = DownloadStatus.NotDownloaded,
+ var progress: Int = 0
):Parcelable
enum class DownloadStatus{
Downloaded,
Downloading,
- NotDownloaded
+ Queued,
+ NotDownloaded,
+ Converting,
+ Failed
}
\ No newline at end of file
diff --git a/app/src/main/java/com/shabinder/spotiflyer/networking/GaanaInterface.kt b/app/src/main/java/com/shabinder/spotiflyer/networking/GaanaInterface.kt
index cc69047b..93d5c714 100644
--- a/app/src/main/java/com/shabinder/spotiflyer/networking/GaanaInterface.kt
+++ b/app/src/main/java/com/shabinder/spotiflyer/networking/GaanaInterface.kt
@@ -19,8 +19,11 @@ package com.shabinder.spotiflyer.networking
import com.shabinder.spotiflyer.models.Optional
import com.shabinder.spotiflyer.models.gaana.*
+import okhttp3.ResponseBody
+import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Query
+import retrofit2.http.Url
const val gaana_token = "b2e6d7fbc136547a940516e9b77e5990"
@@ -98,4 +101,9 @@ interface GaanaInterface {
@Query("limit") limit: Int = 50
): Optional
+ /*
+ * Dynamic Url Requests
+ * */
+ @GET
+ fun getResponse(@Url url:String): Call
}
\ No newline at end of file
diff --git a/app/src/main/java/com/shabinder/spotiflyer/recyclerView/TrackListAdapter.kt b/app/src/main/java/com/shabinder/spotiflyer/recyclerView/TrackListAdapter.kt
index 68b39513..dd85ae03 100755
--- a/app/src/main/java/com/shabinder/spotiflyer/recyclerView/TrackListAdapter.kt
+++ b/app/src/main/java/com/shabinder/spotiflyer/recyclerView/TrackListAdapter.kt
@@ -21,6 +21,7 @@ import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.lifecycle.viewModelScope
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
@@ -31,10 +32,11 @@ import com.shabinder.spotiflyer.downloadHelper.YTDownloadHelper
import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.spotify.Source
+import com.shabinder.spotiflyer.ui.base.tracklistbase.TrackListViewModel
import com.shabinder.spotiflyer.utils.*
import kotlinx.coroutines.launch
-class TrackListAdapter(private val viewModel :TrackListViewModel): ListAdapter(TrackDiffCallback()) {
+class TrackListAdapter(private val viewModel : TrackListViewModel): ListAdapter(TrackDiffCallback()),YTDownloadHelper {
var source:Source =Source.Spotify
@@ -51,53 +53,87 @@ class TrackListAdapter(private val viewModel :TrackListViewModel): ListAdapter {
- holder.binding.btnDownload.setImageResource(R.drawable.ic_tick)
- holder.binding.btnDownload.clearAnimation()
+ holder.binding.btnDownloadProgress.invisible()
+ holder.binding.btnDownload.apply{
+ setImageResource(R.drawable.ic_tick)
+ clearAnimation()
+ visible()
+ }
+ }
+ DownloadStatus.Queued -> {
+ holder.binding.btnDownloadProgress.invisible()
+ holder.binding.btnDownload.apply{
+ setImageResource(R.drawable.ic_refresh)
+ rotate()
+ visible()
+ }
+ }
+ DownloadStatus.Failed -> {
+ holder.binding.btnDownloadProgress.invisible()
+ holder.binding.btnDownload.apply{
+ setImageResource(R.drawable.ic_error)
+ clearAnimation()
+ visible()
+ }
}
DownloadStatus.Downloading -> {
- holder.binding.btnDownload.setImageResource(R.drawable.ic_refresh)
- rotateAnim(holder.binding.btnDownload)
+ holder.binding.btnDownload.invisible()
+ holder.binding.btnDownloadProgress.apply {
+ progress = item.progress
+ bottomText = "Downloading"
+ visible()
+ }
+ }
+ DownloadStatus.Converting -> {
+ holder.binding.btnDownload.invisible()
+ holder.binding.btnDownloadProgress.apply {
+ visible()
+ progress = 100
+ bottomText = "Converting"
+ }
}
DownloadStatus.NotDownloaded -> {
- holder.binding.btnDownload.setImageResource(R.drawable.ic_arrow)
- holder.binding.btnDownload.clearAnimation()
- holder.binding.btnDownload.setOnClickListener{
- if(!isOnline()){
- showNoConnectionAlert()
- return@setOnClickListener
- }
- showMessage("Processing!")
- holder.binding.btnDownload.setImageResource(R.drawable.ic_refresh)
- rotateAnim(it)
- item.downloaded = DownloadStatus.Downloading
- when(source){
- Source.YouTube -> {
- viewModel.uiScope.launch {
- YTDownloadHelper.downloadYTTracks(
- viewModel.folderType,
- viewModel.subFolder,
- listOf(item)
- )
- }
- }
- else -> {
- viewModel.uiScope.launch {
- DownloadHelper.downloadAllTracks(
- viewModel.folderType,
- viewModel.subFolder,
- listOf(item)
- )
+ holder.binding.btnDownloadProgress.invisible()
+ holder.binding.btnDownload.apply{
+ setImageResource(R.drawable.ic_arrow)
+ clearAnimation()
+ visible()
+ setOnClickListener{
+ if(!isOnline()){
+ showNoConnectionAlert()
+ return@setOnClickListener
+ }
+ showMessage("Processing!")
+ item.downloaded = DownloadStatus.Queued
+ when(source){
+ Source.YouTube -> {
+ viewModel.viewModelScope.launch {
+ downloadYTTracks(
+ viewModel.folderType,
+ viewModel.subFolder,
+ listOf(item)
+ )
+ }
+ }
+ else -> {
+ viewModel.viewModelScope.launch {
+ DownloadHelper.downloadAllTracks(
+ viewModel.folderType,
+ viewModel.subFolder,
+ listOf(item)
+ )
+ }
}
}
+ notifyItemChanged(position)//start showing anim!
}
- notifyItemChanged(position)//start showing anim!
}
}
}
diff --git a/app/src/main/java/com/shabinder/spotiflyer/splash/SplashScreen.kt b/app/src/main/java/com/shabinder/spotiflyer/splash/SplashScreen.kt
index 2ba1e77a..ae01668c 100755
--- a/app/src/main/java/com/shabinder/spotiflyer/splash/SplashScreen.kt
+++ b/app/src/main/java/com/shabinder/spotiflyer/splash/SplashScreen.kt
@@ -20,6 +20,7 @@ package com.shabinder.spotiflyer.splash
import android.content.Intent
import android.os.Bundle
import android.os.Handler
+import android.os.Looper
import androidx.appcompat.app.AppCompatActivity
import com.shabinder.spotiflyer.MainActivity
import com.shabinder.spotiflyer.R
@@ -33,7 +34,7 @@ class SplashScreen : AppCompatActivity(){
val splashTimeout = 400
val homeIntent = Intent(this@SplashScreen, MainActivity::class.java)
- Handler().postDelayed({
+ Handler(Looper.myLooper()!!).postDelayed({
//TODO:Bring Initial Setup here
startActivity(homeIntent)
finish()
diff --git a/app/src/main/java/com/shabinder/spotiflyer/ui/base/BaseFragment.kt b/app/src/main/java/com/shabinder/spotiflyer/ui/base/BaseFragment.kt
new file mode 100644
index 00000000..fb82fffb
--- /dev/null
+++ b/app/src/main/java/com/shabinder/spotiflyer/ui/base/BaseFragment.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2020 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 .
+ */
+
+package com.shabinder.spotiflyer.ui.base
+
+import android.content.Context
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import androidx.viewbinding.ViewBinding
+import com.shabinder.spotiflyer.SharedViewModel
+
+abstract class BaseFragment : Fragment() {
+
+ protected val sharedViewModel: SharedViewModel by activityViewModels()
+ protected abstract val binding: VB
+ protected abstract val viewModel: VM
+ protected val viewModelScope by lazy{viewModel.viewModelScope}
+
+ open fun applicationContext(): Context = requireActivity().applicationContext
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/shabinder/spotiflyer/utils/TrackListFragment.kt b/app/src/main/java/com/shabinder/spotiflyer/ui/base/tracklistbase/TrackListFragment.kt
similarity index 66%
rename from app/src/main/java/com/shabinder/spotiflyer/utils/TrackListFragment.kt
rename to app/src/main/java/com/shabinder/spotiflyer/ui/base/tracklistbase/TrackListFragment.kt
index 62fb3197..dfa75523 100644
--- a/app/src/main/java/com/shabinder/spotiflyer/utils/TrackListFragment.kt
+++ b/app/src/main/java/com/shabinder/spotiflyer/ui/base/tracklistbase/TrackListFragment.kt
@@ -15,37 +15,35 @@
* along with this program. If not, see .
*/
-package com.shabinder.spotiflyer.utils
+package com.shabinder.spotiflyer.ui.base.tracklistbase
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
-import android.os.Handler
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import androidx.fragment.app.Fragment
-import androidx.lifecycle.ViewModelProvider
import androidx.navigation.NavArgs
-import androidx.recyclerview.widget.SimpleItemAnimator
import com.shabinder.spotiflyer.R
-import com.shabinder.spotiflyer.SharedViewModel
import com.shabinder.spotiflyer.databinding.TrackListFragmentBinding
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper
import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.recyclerView.TrackListAdapter
+import com.shabinder.spotiflyer.ui.base.BaseFragment
import com.shabinder.spotiflyer.utils.Provider.mainActivity
+import com.shabinder.spotiflyer.utils.bindImage
+import com.shabinder.spotiflyer.utils.isOnline
+import com.shabinder.spotiflyer.utils.showNoConnectionAlert
+import com.tonyodev.fetch2.Status
-abstract class TrackListFragment : Fragment() {
+abstract class TrackListFragment : BaseFragment() {
- protected lateinit var sharedViewModel: SharedViewModel
- protected lateinit var binding: TrackListFragmentBinding
- protected abstract var viewModel: VM
+ override lateinit var binding: TrackListFragmentBinding
protected abstract var adapter: TrackListAdapter
protected abstract var source: Source
private var intentFilter: IntentFilter? = null
@@ -58,8 +56,6 @@ abstract class TrackListFragment : Frag
showNoConnectionAlert()
mainActivity.navController.popBackStack()
}
- Handler()
- sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java)
}
override fun onCreateView(
@@ -74,9 +70,7 @@ abstract class TrackListFragment : Frag
private fun initializeAll() {
DownloadHelper.youtubeMusicApi = sharedViewModel.youtubeMusicApi
- DownloadHelper.sharedViewModel = sharedViewModel
DownloadHelper.statusBar = binding.statusBar
- (binding.trackList.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -98,7 +92,7 @@ abstract class TrackListFragment : Frag
})
viewModel.coverUrl.observe(viewLifecycleOwner, {
- it?.let{bindImage(binding.coverImage,it, source)}
+ it?.let{ bindImage(binding.coverImage,it, source) }
})
viewModel.title.observe(viewLifecycleOwner, {
@@ -108,6 +102,11 @@ abstract class TrackListFragment : Frag
private fun initializeBroadcast() {
intentFilter = IntentFilter()
+ intentFilter?.addAction(Status.QUEUED.name)
+ intentFilter?.addAction(Status.FAILED.name)
+ intentFilter?.addAction(Status.DOWNLOADING.name)
+ intentFilter?.addAction("Progress")
+ intentFilter?.addAction("Converting")
intentFilter?.addAction("track_download_completed")
updateUIReceiver = object : BroadcastReceiver() {
@@ -117,11 +116,33 @@ abstract class TrackListFragment : Frag
val trackDetails = intent.getParcelableExtra("track")
trackDetails?.let {
val position: Int = viewModel.trackList.value?.map { it.title }?.indexOf(trackDetails.title) ?: -1
- Log.i("Track","Download Completed Intent :$position")
+ Log.i("BroadCast Received","$position, ${intent.action} , ${trackDetails.title}")
if(position != -1) {
val track = viewModel.trackList.value?.get(position)
track?.let{
- it.downloaded = DownloadStatus.Downloaded
+ when(intent.action){
+ Status.QUEUED.name -> {
+ it.downloaded = DownloadStatus.Queued
+ }
+ Status.FAILED.name -> {
+ it.downloaded = DownloadStatus.Failed
+ }
+ Status.DOWNLOADING.name -> {
+ it.downloaded = DownloadStatus.Downloading
+ }
+ "Progress" -> {
+ //Progress Update
+ it.progress = intent.getIntExtra("progress",0)
+ it.downloaded = DownloadStatus.Downloading
+ }
+ "Converting" -> {
+ //Progress Update
+ it.downloaded = DownloadStatus.Converting
+ }
+ "track_download_completed" -> {
+ it.downloaded = DownloadStatus.Downloaded
+ }
+ }
viewModel.trackList.value?.set(position, it)
adapter.notifyItemChanged(position)
checkIfAllDownloaded()
@@ -144,7 +165,7 @@ abstract class TrackListFragment : Frag
requireActivity().unregisterReceiver(updateUIReceiver)
}
private fun checkIfAllDownloaded() {
- if(!viewModel.trackList.value!!.any { it.downloaded != DownloadStatus.Downloaded }){
+ if(!viewModel.trackList.value!!.any { it.downloaded == DownloadStatus.NotDownloaded || it.downloaded == DownloadStatus.Queued || it.downloaded == DownloadStatus.Converting }){
//All Tracks Downloaded
binding.btnDownloadAll.visibility = View.GONE
binding.downloadingFab.apply{
@@ -154,5 +175,4 @@ abstract class TrackListFragment : Frag
}
}
}
- open fun applicationContext(): Context = requireActivity().applicationContext
}
\ No newline at end of file
diff --git a/app/src/main/java/com/shabinder/spotiflyer/utils/TrackListViewModel.kt b/app/src/main/java/com/shabinder/spotiflyer/ui/base/tracklistbase/TrackListViewModel.kt
similarity index 74%
rename from app/src/main/java/com/shabinder/spotiflyer/utils/TrackListViewModel.kt
rename to app/src/main/java/com/shabinder/spotiflyer/ui/base/tracklistbase/TrackListViewModel.kt
index 4bc69340..47d2da89 100644
--- a/app/src/main/java/com/shabinder/spotiflyer/utils/TrackListViewModel.kt
+++ b/app/src/main/java/com/shabinder/spotiflyer/ui/base/tracklistbase/TrackListViewModel.kt
@@ -15,30 +15,19 @@
* along with this program. If not, see .
*/
-package com.shabinder.spotiflyer.utils
+package com.shabinder.spotiflyer.ui.base.tracklistbase
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.shabinder.spotiflyer.models.TrackDetails
-import kotlinx.coroutines.CompletableJob
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
abstract class TrackListViewModel:ViewModel() {
abstract var folderType:String
abstract var subFolder:String
open val trackList = MutableLiveData>()
- private val viewModelJob:CompletableJob = Job()
- open val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
-
private val loading = "Loading!"
open var title = MutableLiveData().apply { value = loading }
open var coverUrl = MutableLiveData()
- override fun onCleared() {
- super.onCleared()
- viewModelJob.cancel()
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/shabinder/spotiflyer/ui/gaana/GaanaFragment.kt b/app/src/main/java/com/shabinder/spotiflyer/ui/gaana/GaanaFragment.kt
index 881dd600..699aff5f 100644
--- a/app/src/main/java/com/shabinder/spotiflyer/ui/gaana/GaanaFragment.kt
+++ b/app/src/main/java/com/shabinder/spotiflyer/ui/gaana/GaanaFragment.kt
@@ -22,21 +22,23 @@ import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import androidx.lifecycle.ViewModelProvider
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.viewModelScope
import androidx.navigation.fragment.navArgs
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper
import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.recyclerView.TrackListAdapter
+import com.shabinder.spotiflyer.ui.base.tracklistbase.TrackListFragment
import com.shabinder.spotiflyer.utils.*
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@AndroidEntryPoint
-class GaanaFragment : TrackListFragment() {
+class GaanaFragment : TrackListFragment() {
- override lateinit var viewModel: GaanaViewModel
+ override val viewModel: GaanaViewModel by viewModels()
override lateinit var adapter: TrackListAdapter
override var source: Source = Source.Gaana
override val args: GaanaFragmentArgs by navArgs()
@@ -46,7 +48,6 @@ class GaanaFragment : TrackListFragment() {
savedInstanceState: Bundle?
): View? {
super.onCreateView(inflater, container, savedInstanceState)
- viewModel = ViewModelProvider(this).get(GaanaViewModel::class.java)
adapter = TrackListAdapter(viewModel)
val gaanaLink = GaanaFragmentArgs.fromBundle(requireArguments()).link.substringAfter("gaana.com/")
@@ -70,28 +71,22 @@ class GaanaFragment : TrackListFragment() {
showNoConnectionAlert()
return@setOnClickListener
}
- binding.btnDownloadAll.visibility = View.GONE
- binding.downloadingFab.visibility = View.VISIBLE
-
- rotateAnim(binding.downloadingFab)
+ binding.btnDownloadAll.gone()
+ binding.downloadingFab.apply{
+ visible()
+ rotate()
+ }
for (track in viewModel.trackList.value!!){
if(track.downloaded != DownloadStatus.Downloaded){
- track.downloaded = DownloadStatus.Downloading
+ track.downloaded = DownloadStatus.Queued
adapter.notifyItemChanged(viewModel.trackList.value!!.indexOf(track))
}
}
showMessage("Processing!")
- sharedViewModel.uiScope.launch(Dispatchers.Default){
- val urlList = arrayListOf()
- viewModel.trackList.value?.forEach { urlList.add(it.albumArtURL) }
- //Appending Source
- urlList.add("gaana")
- loadAllImages(
- requireActivity(),
- urlList
- )
+ sharedViewModel.viewModelScope.launch(Dispatchers.Default){
+ loadAllImages(requireActivity(), viewModel.trackList.value?.map{it.albumArtURL}, Source.Gaana)
}
- viewModel.uiScope.launch {
+ viewModel.viewModelScope.launch {
val finalList = viewModel.trackList.value
if(finalList.isNullOrEmpty())showMessage("Not Downloading Any Song")
DownloadHelper.downloadAllTracks(
diff --git a/app/src/main/java/com/shabinder/spotiflyer/ui/gaana/GaanaViewModel.kt b/app/src/main/java/com/shabinder/spotiflyer/ui/gaana/GaanaViewModel.kt
index c7d5f57c..5845260c 100644
--- a/app/src/main/java/com/shabinder/spotiflyer/ui/gaana/GaanaViewModel.kt
+++ b/app/src/main/java/com/shabinder/spotiflyer/ui/gaana/GaanaViewModel.kt
@@ -18,6 +18,7 @@
package com.shabinder.spotiflyer.ui.gaana
import androidx.hilt.lifecycle.ViewModelInject
+import androidx.lifecycle.viewModelScope
import com.shabinder.spotiflyer.database.DatabaseDAO
import com.shabinder.spotiflyer.database.DownloadRecord
import com.shabinder.spotiflyer.models.DownloadStatus
@@ -25,8 +26,8 @@ import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.gaana.GaanaTrack
import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.networking.GaanaInterface
+import com.shabinder.spotiflyer.ui.base.tracklistbase.TrackListViewModel
import com.shabinder.spotiflyer.utils.Provider
-import com.shabinder.spotiflyer.utils.TrackListViewModel
import com.shabinder.spotiflyer.utils.finalOutputDir
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -40,12 +41,13 @@ class GaanaViewModel @ViewModelInject constructor(
override var folderType:String = ""
override var subFolder:String = ""
+
private val gaanaPlaceholderImageUrl = "https://a10.gaanacdn.com/images/social/gaana_social.jpg"
fun gaanaSearch(type:String,link:String){
when(type){
"song" -> {
- uiScope.launch {
+ viewModelScope.launch {
gaanaInterface.getGaanaSong(seokey = link).value?.tracks?.firstOrNull()?.also {
folderType = "Tracks"
if(File(finalOutputDir(it.track_title,folderType,subFolder)).exists()){//Download Already Present!!
@@ -71,7 +73,7 @@ class GaanaViewModel @ViewModelInject constructor(
}
}
"album" -> {
- uiScope.launch {
+ viewModelScope.launch {
gaanaInterface.getGaanaAlbum(seokey = link).value?.also {
folderType = "Albums"
subFolder = link
@@ -98,7 +100,7 @@ class GaanaViewModel @ViewModelInject constructor(
}
}
"playlist" -> {
- uiScope.launch {
+ viewModelScope.launch {
gaanaInterface.getGaanaPlaylist(seokey = link).value?.also {
folderType = "Playlists"
subFolder = link
@@ -126,7 +128,7 @@ class GaanaViewModel @ViewModelInject constructor(
}
}
"artist" -> {
- uiScope.launch {
+ viewModelScope.launch {
folderType = "Artist"
subFolder = link
val artistDetails = gaanaInterface.getGaanaArtistDetails(seokey = link).value?.artist?.firstOrNull()?.also {
@@ -157,7 +159,6 @@ class GaanaViewModel @ViewModelInject constructor(
}
}
-
private fun List.toTrackDetailsList() = this.map {
TrackDetails(
title = it.track_title,
diff --git a/app/src/main/java/com/shabinder/spotiflyer/ui/mainfragment/MainFragment.kt b/app/src/main/java/com/shabinder/spotiflyer/ui/mainfragment/MainFragment.kt
index 7a0033e8..2995093f 100755
--- a/app/src/main/java/com/shabinder/spotiflyer/ui/mainfragment/MainFragment.kt
+++ b/app/src/main/java/com/shabinder/spotiflyer/ui/mainfragment/MainFragment.kt
@@ -23,7 +23,9 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
-import androidx.lifecycle.ViewModelProvider
+import androidx.fragment.app.activityViewModels
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.viewModelScope
import androidx.navigation.fragment.findNavController
import com.shabinder.spotiflyer.MainActivity
import com.shabinder.spotiflyer.R
@@ -33,6 +35,7 @@ import com.shabinder.spotiflyer.utils.*
import com.shreyaspatil.easyupipayment.EasyUpiPayment
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
@@ -40,12 +43,11 @@ import javax.inject.Inject
@AndroidEntryPoint
class MainFragment : Fragment() {
- private lateinit var mainViewModel: MainViewModel
- private lateinit var sharedViewModel: SharedViewModel
+ private val mainViewModel: MainViewModel by viewModels()
+ private val sharedViewModel: SharedViewModel by activityViewModels()
private lateinit var binding: MainFragmentBinding
@Inject lateinit var easyUpiPayment: EasyUpiPayment
-
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
@@ -84,21 +86,27 @@ class MainFragment : Fragment() {
return binding.root
}
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ //starting Notification and Downloader Service!
+ startService(requireContext())
+ }
+
/**
* Handle Intent If there is any!
**/
private fun handleIntent() {
sharedViewModel.intentString.observe(viewLifecycleOwner,{ it?.let {
- sharedViewModel.uiScope.launch(Dispatchers.IO) {
+ sharedViewModel.viewModelScope.launch(Dispatchers.IO) {
//Wait for any Authentication to Finish ,
// this Wait prevents from multiple Authentication Requests
- Thread.sleep(1000)
+ delay(1500)
if(sharedViewModel.spotifyService.value == null){
//Not Authenticated Yet
Provider.mainActivity.authenticateSpotify()
while (sharedViewModel.spotifyService.value == null) {
//Waiting for Authentication to Finish
- Thread.sleep(1000)
+ delay(1000)
}
}
@@ -114,8 +122,6 @@ class MainFragment : Fragment() {
}
private fun initializeAll() {
- mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
- sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java)
binding.apply {
btnGaana.openPlatformOnClick("com.gaana","http://gaana.com")
btnSpotify.openPlatformOnClick("com.spotify.music","http://open.spotify.com")
@@ -139,4 +145,5 @@ class MainFragment : Fragment() {
.append(getText(R.string.d_three)).append("\n")
.append(getText(R.string.d_four)).append("\n")
}
+
}
\ No newline at end of file
diff --git a/app/src/main/java/com/shabinder/spotiflyer/ui/spotify/SpotifyFragment.kt b/app/src/main/java/com/shabinder/spotiflyer/ui/spotify/SpotifyFragment.kt
index 05202a82..a5d0a309 100755
--- a/app/src/main/java/com/shabinder/spotiflyer/ui/spotify/SpotifyFragment.kt
+++ b/app/src/main/java/com/shabinder/spotiflyer/ui/spotify/SpotifyFragment.kt
@@ -23,12 +23,15 @@ import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import androidx.lifecycle.ViewModelProvider
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.viewModelScope
import androidx.navigation.fragment.navArgs
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper
import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.spotify.Source
+import com.shabinder.spotiflyer.networking.SpotifyService
import com.shabinder.spotiflyer.recyclerView.TrackListAdapter
+import com.shabinder.spotiflyer.ui.base.tracklistbase.TrackListFragment
import com.shabinder.spotiflyer.utils.*
import com.shabinder.spotiflyer.utils.Provider.mainActivity
import dagger.hilt.android.AndroidEntryPoint
@@ -36,12 +39,16 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@AndroidEntryPoint
-class SpotifyFragment : TrackListFragment() {
+class SpotifyFragment : TrackListFragment() {
- override lateinit var viewModel: SpotifyViewModel
+ override val viewModel: SpotifyViewModel by viewModels()
+ override val args: SpotifyFragmentArgs by navArgs()
override lateinit var adapter: TrackListAdapter
override var source: Source = Source.Spotify
- override val args: SpotifyFragmentArgs by navArgs()
+ private val spotifyService:SpotifyService?
+ get() = sharedViewModel.spotifyService.value
+ lateinit var link:String
+ lateinit var type:String
@SuppressLint("SetJavaScriptEnabled")
override fun onCreateView(
@@ -51,71 +58,83 @@ class SpotifyFragment : TrackListFragment(
super.onCreateView(inflater, container, savedInstanceState)
initializeAll()
- val spotifyLink = args.link.substringAfter("open.spotify.com/")
+ var spotifyLink = "https://" + args.link.substringAfterLast("https://").substringBefore(" ").trim()
+ Log.i("Spotify Fragment Link", spotifyLink)
+ viewModelScope.launch(Dispatchers.IO) {
- val link = spotifyLink.substringAfterLast('/', "Error").substringBefore('?')
- val type = spotifyLink.substringBeforeLast('/', "Error").substringAfterLast('/')
-
- Log.i("Spotify Fragment", "$type : $link")
-
-
- if(sharedViewModel.spotifyService.value == null){//Authentication pending!!
- if(isOnline()) mainActivity.authenticateSpotify()
- }
-
- when{
- type == "Error" || link == "Error" -> {
- showMessage("Please Check Your Link!")
- mainActivity.onBackPressed()
+ /*
+ * New Link Schema: https://link.tospotify.com/kqTBblrjQbb,
+ * Fetching Standard Link: https://open.spotify.com/playlist/37i9dQZF1DX9RwfGbeGQwP?si=iWz7B1tETiunDntnDo3lSQ&_branch_match_id=862039436205270630
+ * */
+ if (!spotifyLink.contains("open.spotify")) {
+ val resolvedLink = viewModel.resolveLink(spotifyLink)
+ Log.d("Spotify Resolved Link", resolvedLink)
+ spotifyLink = resolvedLink
}
- else -> {
- if(type == "episode" || type == "show"){//TODO Implementation
- showMessage("Implementing Soon, Stay Tuned!")
+ link = spotifyLink.substringAfterLast('/', "Error").substringBefore('?')
+ type = spotifyLink.substringBeforeLast('/', "Error").substringAfterLast('/')
+
+ Log.i("Spotify Fragment", "$type : $link")
+
+ if (sharedViewModel.spotifyService.value == null) {//Authentication pending!!
+ if (isOnline()) mainActivity.authenticateSpotify()
+ }
+
+ when {
+ type == "Error" || link == "Error" -> {
+ showMessage("Please Check Your Link!")
+ mainActivity.onBackPressed()
}
- else{
- this.viewModel.spotifySearch(type,link)
- binding.btnDownloadAll.setOnClickListener {
- if(!isOnline()){
- showNoConnectionAlert()
- return@setOnClickListener
- }
- binding.btnDownloadAll.visibility = View.GONE
- binding.downloadingFab.visibility = View.VISIBLE
+ else -> {
+ if (type == "episode" || type == "show") {//TODO Implementation
+ showMessage("Implementing Soon, Stay Tuned!")
+ } else {
+ viewModel.spotifySearch(type, link)
- rotateAnim(binding.downloadingFab)
- for (track in this.viewModel.trackList.value ?: listOf()){
- if(track.downloaded != DownloadStatus.Downloaded){
- track.downloaded = DownloadStatus.Downloading
- adapter.notifyItemChanged(this.viewModel.trackList.value!!.indexOf(track))
+ binding.btnDownloadAll.setOnClickListener {
+ if (!isOnline()) {
+ showNoConnectionAlert()
+ return@setOnClickListener
+ }
+ binding.btnDownloadAll.gone()
+ binding.downloadingFab.apply {
+ visible()
+ rotate()
+ }
+ for (track in viewModel.trackList.value ?: listOf()) {
+ if (track.downloaded != DownloadStatus.Downloaded) {
+ track.downloaded = DownloadStatus.Queued
+ adapter.notifyItemChanged(
+ viewModel.trackList.value!!.indexOf(
+ track
+ )
+ )
+ }
+ }
+ showMessage("Processing!")
+ sharedViewModel.viewModelScope.launch(Dispatchers.Default) {
+ loadAllImages(
+ requireActivity(),
+ viewModel.trackList.value?.map { it.albumArtURL },
+ Source.Spotify
+ )
+ }
+ viewModelScope.launch {
+ val finalList = viewModel.trackList.value
+ if (finalList.isNullOrEmpty()) showMessage("Not Downloading Any Song")
+ DownloadHelper.downloadAllTracks(
+ viewModel.folderType,
+ viewModel.subFolder,
+ finalList ?: listOf(),
+ )
}
- }
- showMessage("Processing!")
- sharedViewModel.uiScope.launch(Dispatchers.Default){
- val urlList = arrayListOf()
- this@SpotifyFragment.viewModel.trackList.value?.forEach { urlList.add(it.albumArtURL) }
- //Appending Source
- urlList.add("spotify")
- loadAllImages(
- requireActivity(),
- urlList
- )
- }
- this.viewModel.uiScope.launch {
- val finalList = viewModel.trackList.value
- if(finalList.isNullOrEmpty())showMessage("Not Downloading Any Song")
- DownloadHelper.downloadAllTracks(
- viewModel.folderType,
- viewModel.subFolder,
- finalList ?: listOf(),
- )
}
}
}
}
}
-
return binding.root
}
@@ -123,10 +142,10 @@ class SpotifyFragment : TrackListFragment(
* Basic Initialization
**/
private fun initializeAll() {
- this.viewModel = ViewModelProvider(this).get(SpotifyViewModel::class.java)
- adapter = TrackListAdapter(this.viewModel)
sharedViewModel.spotifyService.observe(viewLifecycleOwner, {
this.viewModel.spotifyService = it
})
+ viewModel.spotifyService = spotifyService //Temp Initialisation
+ adapter = TrackListAdapter(this.viewModel)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/shabinder/spotiflyer/ui/spotify/SpotifyViewModel.kt b/app/src/main/java/com/shabinder/spotiflyer/ui/spotify/SpotifyViewModel.kt
index d63a9c6e..39ab9b5d 100755
--- a/app/src/main/java/com/shabinder/spotiflyer/ui/spotify/SpotifyViewModel.kt
+++ b/app/src/main/java/com/shabinder/spotiflyer/ui/spotify/SpotifyViewModel.kt
@@ -19,6 +19,7 @@ package com.shabinder.spotiflyer.ui.spotify
import android.util.Log
import androidx.hilt.lifecycle.ViewModelInject
+import androidx.lifecycle.viewModelScope
import com.shabinder.spotiflyer.database.DatabaseDAO
import com.shabinder.spotiflyer.database.DownloadRecord
import com.shabinder.spotiflyer.models.DownloadStatus
@@ -27,9 +28,10 @@ import com.shabinder.spotiflyer.models.spotify.Album
import com.shabinder.spotiflyer.models.spotify.Image
import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.models.spotify.Track
+import com.shabinder.spotiflyer.networking.GaanaInterface
import com.shabinder.spotiflyer.networking.SpotifyService
+import com.shabinder.spotiflyer.ui.base.tracklistbase.TrackListViewModel
import com.shabinder.spotiflyer.utils.Provider.imageDir
-import com.shabinder.spotiflyer.utils.TrackListViewModel
import com.shabinder.spotiflyer.utils.finalOutputDir
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -38,6 +40,7 @@ import java.io.File
class SpotifyViewModel @ViewModelInject constructor(
val databaseDAO: DatabaseDAO,
+ val gaanaInterface : GaanaInterface
) : TrackListViewModel(){
override var folderType:String = ""
@@ -45,8 +48,14 @@ class SpotifyViewModel @ViewModelInject constructor(
var spotifyService : SpotifyService? = null
+ fun resolveLink(url:String):String {
+ val response = gaanaInterface.getResponse(url).execute().body()?.string().toString()
+ val regex = """https://open\.spotify\.com.+\w""".toRegex()
+ return regex.find(response)?.value.toString()
+ }
+
fun spotifySearch(type:String,link: String){
- uiScope.launch {
+ viewModelScope.launch {
when (type) {
"track" -> {
spotifyService?.getTrack(link)?.value?.also {
@@ -130,6 +139,7 @@ class SpotifyViewModel @ViewModelInject constructor(
}
"playlist" -> {
+ Log.i("Spotify Service",spotifyService.toString())
val playlistObject = spotifyService?.getPlaylist(link)?.value
folderType = "Playlists"
subFolder = playlistObject?.name.toString()
diff --git a/app/src/main/java/com/shabinder/spotiflyer/ui/youtube/YoutubeFragment.kt b/app/src/main/java/com/shabinder/spotiflyer/ui/youtube/YoutubeFragment.kt
index fe71231b..f4b5c021 100755
--- a/app/src/main/java/com/shabinder/spotiflyer/ui/youtube/YoutubeFragment.kt
+++ b/app/src/main/java/com/shabinder/spotiflyer/ui/youtube/YoutubeFragment.kt
@@ -21,12 +21,14 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import androidx.lifecycle.ViewModelProvider
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.viewModelScope
import androidx.navigation.fragment.navArgs
import com.shabinder.spotiflyer.downloadHelper.YTDownloadHelper
import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.recyclerView.TrackListAdapter
+import com.shabinder.spotiflyer.ui.base.tracklistbase.TrackListFragment
import com.shabinder.spotiflyer.utils.*
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
@@ -36,9 +38,9 @@ private const val sampleDomain2 = "youtu.be"
private const val sampleDomain1 = "youtube.com"
@AndroidEntryPoint
-class YoutubeFragment : TrackListFragment() {
+class YoutubeFragment : TrackListFragment() , YTDownloadHelper {
- override lateinit var viewModel: YoutubeViewModel
+ override val viewModel: YoutubeViewModel by viewModels()
override lateinit var adapter : TrackListAdapter
override var source: Source = Source.YouTube
override val args: YoutubeFragmentArgs by navArgs()
@@ -48,8 +50,7 @@ class YoutubeFragment : TrackListFragment(
savedInstanceState: Bundle?
): View? {
super.onCreateView(inflater, container, savedInstanceState)
- this.viewModel = ViewModelProvider(this).get(YoutubeViewModel::class.java)
- adapter = TrackListAdapter(this.viewModel)
+ adapter = TrackListAdapter(viewModel)
val args = YoutubeFragmentArgs.fromBundle(requireArguments())
val link = args.link
@@ -62,7 +63,7 @@ class YoutubeFragment : TrackListFragment(
if(link.contains("playlist",true) || link.contains("list",true)){
// Given Link is of a Playlist
val playlistId = link.substringAfter("?list=").substringAfter("&list=").substringBefore("&")
- this.viewModel.getYTPlaylist(playlistId)
+ viewModel.getYTPlaylist(playlistId)
}else{//Given Link is of a Video
var searchId = "error"
if(link.contains(sampleDomain1,true) ){
@@ -84,31 +85,25 @@ class YoutubeFragment : TrackListFragment(
showNoConnectionAlert()
return@setOnClickListener
}
- binding.btnDownloadAll.visibility = View.GONE
- binding.downloadingFab.visibility = View.VISIBLE
-
- rotateAnim(binding.downloadingFab)
+ binding.btnDownloadAll.gone()
+ binding.downloadingFab.apply{
+ visible()
+ rotate()
+ }
for (track in this.viewModel.trackList.value?: listOf()){
if(track.downloaded != DownloadStatus.Downloaded){
- track.downloaded = DownloadStatus.Downloading
- adapter.notifyItemChanged(this.viewModel.trackList.value!!.indexOf(track))
+ track.downloaded = DownloadStatus.Queued
+ //adapter.notifyItemChanged(this.viewModel.trackList.value!!.indexOf(track))
}
}
+ adapter.notifyDataSetChanged()
showMessage("Processing!")
- sharedViewModel.uiScope.launch(Dispatchers.Default){
- val urlList = arrayListOf()
- viewModel.trackList.value?.forEach { urlList.add("https://i.ytimg.com/vi/${it.albumArt.absolutePath.substringAfterLast("/")
- .substringBeforeLast(".")}/hqdefault.jpg")}
- //Appending Source
- urlList.add("youtube")
- loadAllImages(
- requireActivity(),
- urlList
- )
+ sharedViewModel.viewModelScope.launch(Dispatchers.Default){
+ loadAllImages(requireActivity(), viewModel.trackList.value?.map{it.albumArtURL}, Source.YouTube)
}
- viewModel.uiScope.launch {
- YTDownloadHelper.downloadYTTracks(
+ viewModel.viewModelScope.launch {
+ downloadYTTracks(
type = viewModel.folderType,
subFolder = viewModel.subFolder,
tracks = viewModel.trackList.value ?: listOf()
diff --git a/app/src/main/java/com/shabinder/spotiflyer/ui/youtube/YoutubeViewModel.kt b/app/src/main/java/com/shabinder/spotiflyer/ui/youtube/YoutubeViewModel.kt
index b740ef7d..fa354495 100755
--- a/app/src/main/java/com/shabinder/spotiflyer/ui/youtube/YoutubeViewModel.kt
+++ b/app/src/main/java/com/shabinder/spotiflyer/ui/youtube/YoutubeViewModel.kt
@@ -20,14 +20,19 @@ package com.shabinder.spotiflyer.ui.youtube
import android.annotation.SuppressLint
import android.util.Log
import androidx.hilt.lifecycle.ViewModelInject
+import androidx.lifecycle.viewModelScope
import com.github.kiulian.downloader.YoutubeDownloader
import com.shabinder.spotiflyer.database.DatabaseDAO
import com.shabinder.spotiflyer.database.DownloadRecord
import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.spotify.Source
-import com.shabinder.spotiflyer.utils.*
+import com.shabinder.spotiflyer.ui.base.tracklistbase.TrackListViewModel
import com.shabinder.spotiflyer.utils.Provider.imageDir
+import com.shabinder.spotiflyer.utils.finalOutputDir
+import com.shabinder.spotiflyer.utils.isOnline
+import com.shabinder.spotiflyer.utils.removeIllegalChars
+import com.shabinder.spotiflyer.utils.showMessage
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -49,7 +54,7 @@ class YoutubeViewModel @ViewModelInject constructor(
fun getYTPlaylist(searchId:String){
if(!isOnline())return
try{
- uiScope.launch(Dispatchers.IO) {
+ viewModelScope.launch(Dispatchers.IO) {
Log.i("YT Playlist",searchId)
val playlist = ytDownloader.getPlaylist(searchId)
val playlistDetails = playlist.details()
@@ -106,7 +111,7 @@ class YoutubeViewModel @ViewModelInject constructor(
fun getYTTrack(searchId:String) {
if(!isOnline())return
try{
- uiScope.launch(Dispatchers.IO) {
+ viewModelScope.launch(Dispatchers.IO) {
Log.i("YT Video",searchId)
val video = ytDownloader.getVideo(searchId)
coverUrl.postValue("https://i.ytimg.com/vi/$searchId/hqdefault.jpg")
diff --git a/app/src/main/java/com/shabinder/spotiflyer/utils/Extensions.kt b/app/src/main/java/com/shabinder/spotiflyer/utils/Extensions.kt
index 896657ca..55e7c462 100644
--- a/app/src/main/java/com/shabinder/spotiflyer/utils/Extensions.kt
+++ b/app/src/main/java/com/shabinder/spotiflyer/utils/Extensions.kt
@@ -21,6 +21,9 @@ import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.view.View
+import android.view.animation.Animation
+import android.view.animation.LinearInterpolator
+import android.view.animation.RotateAnimation
import com.shabinder.spotiflyer.utils.Provider.mainActivity
fun View.openPlatformOnClick(packageName:String, websiteAddress:String){
@@ -42,4 +45,26 @@ fun View.openPlatformOnClick(websiteAddress:String){
Uri.parse(websiteAddress)
val intent = Intent(Intent.ACTION_VIEW, uri)
this.setOnClickListener { mainActivity.startActivity(intent) }
+}
+
+fun View.rotate(){
+ val rotate = RotateAnimation(
+ 0F, 360F,
+ Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f
+ )
+ rotate.duration = 2000
+ rotate.repeatCount = Animation.INFINITE
+ rotate.repeatMode = Animation.INFINITE
+ rotate.interpolator = LinearInterpolator()
+ this.animation = rotate
+}
+
+fun View.visible(){
+ this.visibility = View.VISIBLE
+}
+fun View.gone(){
+ this.visibility = View.GONE
+}
+fun View.invisible(){
+ this.visibility = View.INVISIBLE
}
\ No newline at end of file
diff --git a/app/src/main/java/com/shabinder/spotiflyer/utils/Provider.kt b/app/src/main/java/com/shabinder/spotiflyer/utils/Provider.kt
index ba84a06e..fcab0d71 100755
--- a/app/src/main/java/com/shabinder/spotiflyer/utils/Provider.kt
+++ b/app/src/main/java/com/shabinder/spotiflyer/utils/Provider.kt
@@ -49,10 +49,9 @@ import javax.inject.Singleton
@Module
object Provider {
-
// mainActivity Instance to use whereEver Needed , as Its God Activity.
- // (i.e, Active Through out App' Lifecycle )
- val mainActivity: MainActivity = MainActivity.getInstance()
+ // (i.e, Active Throughout App' Lifecycle )
+ val mainActivity: MainActivity by lazy { MainActivity.getInstance() }
//Default Directory to save Media in their Own Categorized Folders
@Suppress("DEPRECATION")// We Do Have Media Access (But Just Media in Media Directory,Not Anything Else)
@@ -61,12 +60,13 @@ object Provider {
"SpotiFlyer"+ File.separator
//Default Cache Directory to save Album Art to use them for writing in Media Later
- val imageDir:String
- get() = mainActivity.externalCacheDir?.absolutePath + File.separator +
- ".Images" + File.separator
+ val imageDir:String by lazy { mainActivity
+ .externalCacheDir?.absolutePath + File.separator +
+ ".Images" + File.separator }
@Provides
+ @Singleton
fun databaseDAO(@ApplicationContext appContext: Context):DatabaseDAO{
return DownloadRecordDatabase.getInstance(appContext).databaseDAO
}
diff --git a/app/src/main/java/com/shabinder/spotiflyer/utils/Utils.kt b/app/src/main/java/com/shabinder/spotiflyer/utils/Utils.kt
index 9240ef3b..b3345f74 100755
--- a/app/src/main/java/com/shabinder/spotiflyer/utils/Utils.kt
+++ b/app/src/main/java/com/shabinder/spotiflyer/utils/Utils.kt
@@ -23,10 +23,6 @@ import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import android.util.Log
-import android.view.View
-import android.view.animation.Animation
-import android.view.animation.LinearInterpolator
-import android.view.animation.RotateAnimation
import android.widget.ImageView
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
@@ -50,9 +46,9 @@ import kotlinx.coroutines.launch
import java.io.File
import java.io.IOException
-fun loadAllImages(context: Context?, images:ArrayList? = null ) {
+fun loadAllImages(context: Context?, images:List? = null,source:Source) {
val serviceIntent = Intent(context, ForegroundService::class.java)
- images?.let { serviceIntent.putStringArrayListExtra("imagesList",it) }
+ images?.let { serviceIntent.putStringArrayListExtra("imagesList",(it + source.name) as ArrayList) }
context?.let { ContextCompat.startForegroundService(it, serviceIntent) }
}
@@ -114,19 +110,6 @@ fun showMessage(message: String, long: Boolean = false,isSuccess:Boolean = false
}
}
-
-fun rotateAnim(view: View){
- val rotate = RotateAnimation(
- 0F, 360F,
- Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f
- )
- rotate.duration = 2000
- rotate.repeatCount = Animation.INFINITE
- rotate.repeatMode = Animation.INFINITE
- rotate.interpolator = LinearInterpolator()
- view.animation = rotate
-}
-
fun showNoConnectionAlert(){
CoroutineScope(Dispatchers.Main).launch {
mainActivity.apply {
diff --git a/app/src/main/java/com/shabinder/spotiflyer/worker/ForegroundService.kt b/app/src/main/java/com/shabinder/spotiflyer/worker/ForegroundService.kt
index db71ffcf..5e7d0ce2 100755
--- a/app/src/main/java/com/shabinder/spotiflyer/worker/ForegroundService.kt
+++ b/app/src/main/java/com/shabinder/spotiflyer/worker/ForegroundService.kt
@@ -25,10 +25,7 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.Uri
-import android.os.Build
-import android.os.Handler
-import android.os.IBinder
-import android.os.PowerManager
+import android.os.*
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
@@ -48,10 +45,10 @@ import com.github.kiulian.downloader.model.quality.AudioQuality
import com.mpatric.mp3agic.ID3v1Tag
import com.mpatric.mp3agic.ID3v24Tag
import com.mpatric.mp3agic.Mp3File
-import com.shabinder.spotiflyer.MainActivity
import com.shabinder.spotiflyer.R
import com.shabinder.spotiflyer.models.DownloadObject
import com.shabinder.spotiflyer.models.TrackDetails
+import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.utils.Provider
import com.shabinder.spotiflyer.utils.Provider.imageDir
import com.shabinder.spotiflyer.utils.copyTo
@@ -80,45 +77,38 @@ class ForegroundService : Service(){
private var wakeLock: PowerManager.WakeLock? = null
private var isServiceStarted = false
var notificationLine = 0
- val messageList = mutableListOf("","","","")
- private var pendingIntent:PendingIntent? = null
+ var messageList = mutableListOf("", "", "", "")
+ private var cancelIntent:PendingIntent? = null
- override fun onBind(intent: Intent): IBinder? {
- return null
- }
+ override fun onBind(intent: Intent): IBinder? = null
override fun onCreate() {
super.onCreate()
- val notificationIntent = Intent(this, MainActivity::class.java)
- pendingIntent = PendingIntent.getActivity(
+ val intent = Intent(
this,
- 0, notificationIntent, 0
- )
+ ForegroundService::class.java
+ ).apply{action = "kill"}
+ cancelIntent = PendingIntent.getService (this, 0 , intent , PendingIntent.FLAG_CANCEL_CURRENT )
downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
ytDownloader = YoutubeDownloader()
- val fetchConfiguration =
- FetchConfiguration.Builder(this)
- .setDownloadConcurrentLimit(4)
- .build()
-
- Fetch.setDefaultInstanceConfiguration(fetchConfiguration)
-
- fetch = Fetch.getDefaultInstance()
- fetch.addListener(fetchListener)
- //clearing all not completed Downloads
- //Starting fresh
- fetch.removeAll()
-
+ initialiseFetch()
startForeground()
}
@SuppressLint("WakelockTimeout")
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
// Send a notification that service is started
- Log.i(tag,"Service Started.")
+ Log.i(tag, "Service Started.")
startForeground()
- val downloadObjects: ArrayList? = (intent.getParcelableArrayListExtra("object") ?: intent.extras?.getParcelableArrayList("object"))
- val imagesList: ArrayList? = (intent.getStringArrayListExtra("imagesList") ?: intent.extras?.getStringArrayList("imagesList"))
+
+ if(intent.action == "kill") killService()
+
+ val downloadObjects: ArrayList? = (intent.getParcelableArrayListExtra("object") ?: intent.extras?.getParcelableArrayList(
+ "object"
+ ))
+ val imagesList: ArrayList? = (intent.getStringArrayListExtra("imagesList") ?: intent.extras?.getStringArrayList(
+ "imagesList"
+ ))
imagesList?.let{
serviceScope.launch {
@@ -137,7 +127,7 @@ class ForegroundService : Service(){
//Service Already Started
START_STICKY
} else{
- Log.i(tag,"Starting the foreground service task")
+ Log.i(tag, "Starting the foreground service task")
isServiceStarted = true
wakeLock =
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
@@ -171,20 +161,19 @@ class ForegroundService : Service(){
format?.let {
val url: String = format.url()
Log.i("DHelper Link Found", url)
- serviceScope.launch {
- val request= Request(url, downloadObj.outputFile)
- request.priority = Priority.NORMAL
- request.networkType = NetworkType.ALL
-
- fetch.enqueue(request,
- {
- requestMap[it] = downloadObj.trackDetails
- Log.i(tag, "Enqueuing Download")
- },
- {
- Log.i(tag, "Enqueuing Error:${it.throwable.toString()}")}
- )
+ val request= Request(url, downloadObj.outputFile).apply{
+ priority = Priority.NORMAL
+ networkType = NetworkType.ALL
}
+ fetch.enqueue(request,
+ {
+ requestMap[it] = downloadObj.trackDetails
+ Log.i(tag, "Enqueuing Download")
+ },
+ {
+ Log.i(tag, "Enqueuing Error:${it.throwable.toString()}")
+ }
+ )
}
}catch (e: com.github.kiulian.downloader.YoutubeException){
Log.i("Service YT Error", e.message.toString())
@@ -196,25 +185,16 @@ class ForegroundService : Service(){
override fun onDestroy() {
super.onDestroy()
if(converted == total){
- Handler().postDelayed({
- Log.i(tag,"Service destroyed.")
- cleanFiles(File(defaultDir))
- releaseWakeLock()
- stopForeground(true)
- },2000)
+ Handler(Looper.myLooper()!!).postDelayed({
+ killService()
+ }, 5000)
}
}
override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent)
if(converted == total ){
- Log.i(tag,"Service Removed.")
- cleanFiles(File(defaultDir))
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- stopForeground(true)
- } else {
- stopSelf()//System will automatically close it
- }
+ killService()
}
}
@@ -227,7 +207,11 @@ class ForegroundService : Service(){
download: Download,
waitingOnNetwork: Boolean
) {
- // TODO("Not yet implemented")
+ //Notify Download Completed
+ val intent = Intent()
+ .setAction(Status.QUEUED.name)
+ .putExtra("track", requestMap[download.request])
+ this@ForegroundService.sendBroadcast(intent)
}
override fun onRemoved(download: Download) {
@@ -253,7 +237,7 @@ class ForegroundService : Service(){
messageList[1] = "Downloading ${track?.title}"
notificationLine = 2
}
- 2-> {
+ 2 -> {
messageList[2] = "Downloading ${track?.title}"
notificationLine = 3
}
@@ -262,8 +246,12 @@ class ForegroundService : Service(){
notificationLine = 0
}
}
- Log.i(tag,"${track?.title} Download Started")
+ Log.i(tag, "${track?.title} Download Started")
updateNotification()
+ val intent = Intent()
+ .setAction(Status.DOWNLOADING.name)
+ .putExtra("track", requestMap[download.request])
+ this@ForegroundService.sendBroadcast(intent)
}
override fun onWaitingNetwork(download: Download) {
@@ -290,12 +278,13 @@ class ForegroundService : Service(){
serviceScope.launch {
try{
track?.let { convertToMp3(download.file, it) }
- Log.i(tag,"${track?.title} Download Completed")
- }catch (e:KotlinNullPointerException
+ Log.i(tag, "${track?.title} Download Completed")
+ }catch (
+ e: KotlinNullPointerException
){
- Log.i(tag,"${track?.title} Download Failed! Error:Fetch!!!!")
- Log.i(tag,"${track?.title} Requesting Download thru Android DM")
- downloadUsingDM(download.request.url,download.request.file, track!!)
+ Log.i(tag, "${track?.title} Download Failed! Error:Fetch!!!!")
+ Log.i(tag, "${track?.title} Requesting Download thru Android DM")
+ downloadUsingDM(download.request.url, download.request.file, track!!)
downloaded++
requestMap.remove(download.request)
}
@@ -318,9 +307,9 @@ class ForegroundService : Service(){
serviceScope.launch {
val track = requestMap[download.request]
downloaded++
- Log.i(tag,download.error.throwable.toString())
- Log.i(tag,"${track?.title} Requesting Download thru Android DM")
- downloadUsingDM(download.request.url,download.request.file, track!!)
+ Log.i(tag, download.error.throwable.toString())
+ Log.i(tag, "${track?.title} Requesting Download thru Android DM")
+ downloadUsingDM(download.request.url, download.request.file, track!!)
requestMap.remove(download.request)
}
updateNotification()
@@ -336,16 +325,20 @@ class ForegroundService : Service(){
downloadedBytesPerSecond: Long
) {
val track = requestMap[download.request]
- Log.i(tag,"${track?.title} ETA: ${etaInMilliSeconds/1000} sec")
+ Log.i(tag, "${track?.title} ETA: ${etaInMilliSeconds / 1000} sec")
+ val intent = Intent()
+ .setAction("Progress")
+ .putExtra("progress", download.progress)
+ .putExtra("track", requestMap[download.request])
+ this@ForegroundService.sendBroadcast(intent)
// updateNotification()
}
-
}
/**
* If fetch Fails , Android Download Manager To RESCUE!!
**/
- fun downloadUsingDM(url:String, outputDir:String, track: TrackDetails){
+ fun downloadUsingDM(url: String, outputDir: String, track: TrackDetails){
val uri = Uri.parse(url)
val request = DownloadManager.Request(uri)
.setAllowedNetworkTypes(
@@ -367,20 +360,25 @@ class ForegroundService : Service(){
val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
//Checking if the received broadcast is for our enqueued download by matching download id
if (downloadID == id) {
- convertToMp3(outputDir,track)
+ convertToMp3(outputDir, track)
converted++
//Unregister this broadcast Receiver
this@ForegroundService.unregisterReceiver(this)
}
}
}
- registerReceiver(onDownloadComplete,IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
+ registerReceiver(onDownloadComplete, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
}
/**
*Converting Downloaded Audio (m4a) to Mp3.( Also Applying Metadata)
**/
fun convertToMp3(filePath: String, track: TrackDetails){
+ val intent = Intent()
+ .setAction("Converting")
+ .putExtra("track", track)
+ this@ForegroundService.sendBroadcast(intent)
+
val m4aFile = File(filePath)
FFmpeg.executeAsync(
@@ -390,29 +388,34 @@ class ForegroundService : Service(){
RETURN_CODE_SUCCESS -> {
Log.i(Config.TAG, "Async command execution completed successfully.")
m4aFile.delete()
- writeMp3Tags(filePath.substringBeforeLast('.')+".mp3",track)
+ writeMp3Tags(filePath.substringBeforeLast('.') + ".mp3", track)
//FFMPEG task Completed
- }
+ }
RETURN_CODE_CANCEL -> {
Log.i(Config.TAG, "Async command execution cancelled by user.")
}
else -> {
- Log.i(Config.TAG, String.format("Async command execution failed with rc=%d.", returnCode))
+ Log.i(
+ Config.TAG, String.format(
+ "Async command execution failed with rc=%d.",
+ returnCode
+ )
+ )
}
}
}
}
- private fun writeMp3Tags(filePath:String, track: TrackDetails){
+ private fun writeMp3Tags(filePath: String, track: TrackDetails){
var mp3File = Mp3File(filePath)
mp3File = removeAllTags(mp3File)
- mp3File = setId3v1Tags(mp3File,track)
- mp3File = setId3v2Tags(mp3File,track)
- Log.i("Mp3Tags","saving file")
- mp3File.save(filePath.substringBeforeLast('.')+".new.mp3")
+ mp3File = setId3v1Tags(mp3File, track)
+ mp3File = setId3v2Tags(mp3File, track)
+ Log.i("Mp3Tags", "saving file")
+ mp3File.save(filePath.substringBeforeLast('.') + ".new.mp3")
val file = File(filePath)
file.delete()
- val newFile = File((filePath.substringBeforeLast('.')+".new.mp3"))
+ val newFile = File((filePath.substringBeforeLast('.') + ".new.mp3"))
newFile.renameTo(file)
converted++
updateNotification()
@@ -420,7 +423,7 @@ class ForegroundService : Service(){
//Notify Download Completed
val intent = Intent()
.setAction("track_download_completed")
- .putExtra("track",track)
+ .putExtra("track", track)
this@ForegroundService.sendBroadcast(intent)
//All tasks completed (REST IN PEACE)
@@ -439,13 +442,15 @@ class ForegroundService : Service(){
.setSmallIcon(R.drawable.down_arrowbw)
.setSubText("Total: $total Completed:$converted")
.setNotificationSilent()
- .setStyle(NotificationCompat.InboxStyle()
+ .setStyle(
+ NotificationCompat.InboxStyle()
// .setBigContentTitle("Speed: $speed KB/s")
- .addLine(messageList[0])
- .addLine(messageList[1])
- .addLine(messageList[2])
- .addLine(messageList[3]))
- .setContentIntent(pendingIntent)
+ .addLine(messageList[0])
+ .addLine(messageList[1])
+ .addLine(messageList[2])
+ .addLine(messageList[3])
+ )
+ .addAction(R.drawable.ic_baseline_cancel_24,"Exit",cancelIntent)
.build()
mNotificationManager.notify(notificationId, notification)
}
@@ -479,9 +484,9 @@ class ForegroundService : Service(){
val fis = FileInputStream(track.albumArt)
fis.read(bytesArray) //read file into bytes[]
fis.close()
- id3v2Tag.setAlbumImage(bytesArray,"image/jpeg")
- }catch (e:java.io.FileNotFoundException){
- Log.i("Error","Couldn't Write Mp3 Album Art")
+ id3v2Tag.setAlbumImage(bytesArray, "image/jpeg")
+ }catch (e: java.io.FileNotFoundException){
+ Log.i("Error", "Couldn't Write Mp3 Album Art")
}
mp3file.id3v2Tag = id3v2Tag
return mp3file
@@ -501,7 +506,7 @@ class ForegroundService : Service(){
}
private fun releaseWakeLock() {
- Log.i(tag,"Releasing Wake Lock")
+ Log.i(tag, "Releasing Wake Lock")
try {
wakeLock?.let {
if (it.isHeld) {
@@ -509,7 +514,7 @@ class ForegroundService : Service(){
}
}
} catch (e: Exception) {
- Log.i(tag,"Service stopped without being started: ${e.message}")
+ Log.i(tag, "Service stopped without being started: ${e.message}")
}
isServiceStarted = false
}
@@ -531,13 +536,15 @@ class ForegroundService : Service(){
.setSmallIcon(R.drawable.down_arrowbw)
.setNotificationSilent()
.setSubText("Total: $total Completed:$converted")
- .setStyle(NotificationCompat.InboxStyle()
+ .setStyle(
+ NotificationCompat.InboxStyle()
// .setBigContentTitle("Speed: $speed KB/s")
- .addLine(messageList[0])
- .addLine(messageList[1])
- .addLine(messageList[2])
- .addLine(messageList[3]))
- .setContentIntent(pendingIntent)
+ .addLine(messageList[0])
+ .addLine(messageList[1])
+ .addLine(messageList[2])
+ .addLine(messageList[3])
+ )
+ .addAction(R.drawable.ic_baseline_cancel_24,"Exit",cancelIntent)
.build()
startForeground(notificationId, notification)
}
@@ -545,8 +552,10 @@ class ForegroundService : Service(){
@Suppress("SameParameterValue")
@RequiresApi(Build.VERSION_CODES.O)
private fun createNotificationChannel(channelId: String, channelName: String): String{
- val chan = NotificationChannel(channelId,
- channelName, NotificationManager.IMPORTANCE_DEFAULT)
+ val chan = NotificationChannel(
+ channelId,
+ channelName, NotificationManager.IMPORTANCE_DEFAULT
+ )
chan.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
service.createNotificationChannel(chan)
@@ -556,8 +565,8 @@ class ForegroundService : Service(){
/**
* Cleaning All Residual Files except Mp3 Files
**/
- private fun cleanFiles(dir:File) {
- Log.i(tag,"Starting Cleaning in ${dir.path} ")
+ private fun cleanFiles(dir: File) {
+ Log.i(tag, "Starting Cleaning in ${dir.path} ")
val fList = dir.listFiles()
fList?.let {
for (file in fList) {
@@ -565,7 +574,7 @@ class ForegroundService : Service(){
cleanFiles(file)
} else if(file.isFile) {
if(file.path.toString().substringAfterLast(".") != "mp3"){
- Log.i(tag,"Cleaning ${file.path}")
+ Log.i(tag, "Cleaning ${file.path}")
file.delete()
}
}
@@ -581,20 +590,20 @@ class ForegroundService : Service(){
* Last Element of this List defines Its Source
* */
val source = urlList.last()
- for (url in urlList.subList(0,urlList.size-2)) {
+ for (url in urlList.subList(0, urlList.size - 2)) {
val imgUri = url.toUri().buildUpon().scheme("https").build()
Glide
.with(this)
.asFile()
.load(imgUri)
- .listener(object: RequestListener {
+ .listener(object : RequestListener {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target?,
isFirstResource: Boolean
): Boolean {
- Log.i("Glide","LoadFailed")
+ Log.i("Glide", "LoadFailed")
return false
}
@@ -606,20 +615,35 @@ class ForegroundService : Service(){
isFirstResource: Boolean
): Boolean {
serviceScope.launch {
- withContext(Dispatchers.IO){
+ withContext(Dispatchers.IO) {
try {
- val file = when(source){
- "spotify" ->{
+ val file = when (source) {
+ Source.Spotify.name -> {
File(imageDir, url.substringAfterLast('/') + ".jpeg")
}
- "youtube" ->{
- File(imageDir, url.substringBeforeLast('/',url).substringAfterLast('/',url) + ".jpeg")
+ Source.YouTube.name -> {
+ File(
+ imageDir,
+ url.substringBeforeLast('/', url)
+ .substringAfterLast(
+ '/',
+ url
+ ) + ".jpeg"
+ )
}
- "gaana" -> {
- File(imageDir, (url.substringBeforeLast('/').substringAfterLast('/')) + ".jpeg")
+ Source.Gaana.name -> {
+ File(
+ imageDir,
+ (url.substringBeforeLast('/').substringAfterLast(
+ '/'
+ )) + ".jpeg"
+ )
}
- else -> File(imageDir, url.substringAfterLast('/') + ".jpeg")
+ else -> File(
+ imageDir,
+ url.substringAfterLast('/') + ".jpeg"
+ )
}
resource?.copyTo(file)
} catch (e: IOException) {
@@ -633,4 +657,36 @@ class ForegroundService : Service(){
}
}
+ private fun killService() {
+ serviceScope.launch{
+ messageList = mutableListOf("Cleaning And Exiting","","","")
+ fetch.cancelAll()
+ fetch.removeAll()
+ updateNotification()
+ cleanFiles(File(defaultDir))
+ messageList = mutableListOf("","","","")
+ releaseWakeLock()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ stopForeground(true)
+ } else {
+ stopSelf()//System will automatically close it
+ }
+ }
+ }
+
+ private fun initialiseFetch() {
+ val fetchConfiguration =
+ FetchConfiguration.Builder(this)
+ .setNamespace(channelId)
+ .setDownloadConcurrentLimit(4)
+ .build()
+
+ Fetch.setDefaultInstanceConfiguration(fetchConfiguration)
+
+ fetch = Fetch.getDefaultInstance()
+ fetch.addListener(fetchListener)
+ //clearing all not completed Downloads
+ //Starting fresh
+ fetch.removeAll()
+ }
}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_baseline_cancel_24.xml b/app/src/main/res/drawable/ic_baseline_cancel_24.xml
new file mode 100644
index 00000000..40aa0509
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_cancel_24.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_error.xml b/app/src/main/res/drawable/ic_error.xml
new file mode 100644
index 00000000..09d82627
--- /dev/null
+++ b/app/src/main/res/drawable/ic_error.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/progress_bar.xml b/app/src/main/res/drawable/progress_bar.xml
new file mode 100644
index 00000000..1ea0761d
--- /dev/null
+++ b/app/src/main/res/drawable/progress_bar.xml
@@ -0,0 +1,61 @@
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/download_song_item.xml b/app/src/main/res/layout/download_song_item.xml
new file mode 100644
index 00000000..2db3a56a
--- /dev/null
+++ b/app/src/main/res/layout/download_song_item.xml
@@ -0,0 +1,133 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/track_list_fragment.xml b/app/src/main/res/layout/track_list_fragment.xml
index 9f6800eb..8b6c6665 100755
--- a/app/src/main/res/layout/track_list_fragment.xml
+++ b/app/src/main/res/layout/track_list_fragment.xml
@@ -54,7 +54,7 @@
app:layout_anchor="@+id/appbar"
app:layout_anchorGravity="bottom|center"
app:maxImageSize="38dp"
- app:rippleColor="@color/colorPrimaryDark"
+ android:clickable="false"
app:srcCompat="@drawable/ic_refresh"
app:tint="@null" />
diff --git a/app/src/main/res/layout/track_list_item.xml b/app/src/main/res/layout/track_list_item.xml
index 96e3c72c..9acd2ca7 100755
--- a/app/src/main/res/layout/track_list_item.xml
+++ b/app/src/main/res/layout/track_list_item.xml
@@ -19,7 +19,7 @@
@@ -50,7 +50,7 @@
android:textAppearance="@style/TextAppearance.AppTheme.Headline4"
android:textColor="#9AB3FF"
android:textSize="18sp"
- app:layout_constraintEnd_toStartOf="@+id/btn_download"
+ app:layout_constraintEnd_toStartOf="@+id/btn_download_progress"
app:layout_constraintStart_toEndOf="@+id/imageUrl"
app:layout_constraintTop_toTopOf="parent" />
@@ -75,11 +75,11 @@
style="@style/TextAppearance.AppCompat.Body2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginEnd="8dp"
+ android:layout_marginEnd="16dp"
android:text="4 minutes, 20 sec"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="@+id/artist"
- app:layout_constraintEnd_toStartOf="@+id/btn_download"
+ app:layout_constraintEnd_toStartOf="@+id/btn_download_progress"
app:layout_constraintStart_toEndOf="@+id/artist"
app:layout_constraintTop_toTopOf="@+id/artist" />
@@ -87,6 +87,7 @@
android:id="@+id/btn_download"
android:layout_width="60dp"
android:layout_height="0dp"
+ android:layout_marginEnd="8dp"
android:background="@drawable/circular_background"
android:backgroundTint="@color/black"
android:scaleType="centerInside"
@@ -95,5 +96,27 @@
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_arrow" />
+
diff --git a/build.gradle b/build.gradle
index 4f7e54a4..f8a61458 100755
--- a/build.gradle
+++ b/build.gradle
@@ -36,6 +36,9 @@ buildscript {
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
//Kotlinx-Serialization
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
+ //Crashlytics & Analytics
+ classpath 'com.google.gms:google-services:4.3.4'
+ classpath 'com.google.firebase:firebase-crashlytics-gradle:2.4.1'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
diff --git a/mobile-ffmpeg/build.gradle b/mobile-ffmpeg/build.gradle
deleted file mode 100755
index 34434740..00000000
--- a/mobile-ffmpeg/build.gradle
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * Copyright (C) 2020 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 .
- */
-
-configurations.maybeCreate("default")
-artifacts.add("default", file('mobile-ffmpeg.aar'))
\ No newline at end of file
diff --git a/mobile-ffmpeg/mobile-ffmpeg.aar b/mobile-ffmpeg/mobile-ffmpeg.aar
deleted file mode 100755
index ba7575f6..00000000
Binary files a/mobile-ffmpeg/mobile-ffmpeg.aar and /dev/null differ