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 @@ + + + + + + + + +