Spotify New Link Schema support,Progress Visibility, Failure Icons and more.

This commit is contained in:
Shabinder 2020-12-01 02:33:09 +05:30
parent d80821f759
commit 9a9b25db27
33 changed files with 888 additions and 434 deletions

View File

@ -23,6 +23,8 @@ plugins {
id 'androidx.navigation.safeargs.kotlin' id 'androidx.navigation.safeargs.kotlin'
id 'dagger.hilt.android.plugin' id 'dagger.hilt.android.plugin'
id 'kotlinx-serialization' id 'kotlinx-serialization'
id 'com.google.gms.google-services'
id 'com.google.firebase.crashlytics'
} }
android { android {
@ -83,14 +85,17 @@ android {
dependencies { dependencies {
//Android //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.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.browser:browser:1.2.0' implementation 'androidx.browser:browser:1.2.0'
implementation 'androidx.webkit:webkit:1.3.0' implementation 'androidx.webkit:webkit:1.3.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.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.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' 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.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.1' implementation 'androidx.navigation:navigation-fragment-ktx:2.3.1'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.1' implementation 'androidx.navigation:navigation-ui-ktx:2.3.1'
@ -105,7 +110,6 @@ dependencies {
//Room: Local SQL-lite Database //Room: Local SQL-lite Database
implementation "androidx.room:room-runtime:2.2.5" 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" kapt "androidx.room:room-compiler:2.2.5"
implementation "androidx.room:room-ktx: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.squareup.retrofit2:converter-scalars:2.9.0"
implementation 'com.beust:klaxon:5.4' 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 //Extras
implementation 'me.xdrop:fuzzywuzzy:1.3.1' implementation 'me.xdrop:fuzzywuzzy:1.3.1'
implementation 'com.mpatric:mp3agic:0.9.1' implementation 'com.mpatric:mp3agic:0.9.1'
implementation 'com.shreyaspatil:EasyUpiPayment:3.0.0' implementation 'com.shreyaspatil:EasyUpiPayment:3.0.0'
implementation 'com.github.javiersantos:AppUpdater:2.7' implementation 'com.github.javiersantos:AppUpdater:2.7'
implementation 'com.github.lzyzsd:circleprogress:1.2.1'
implementation "androidx.tonyodev.fetch2:xfetch2:3.1.5" implementation "androidx.tonyodev.fetch2:xfetch2:3.1.5"
implementation 'com.github.sealedtx:java-youtube-downloader:2.4.4' implementation 'com.github.sealedtx:java-youtube-downloader:2.4.4'

View File

@ -20,6 +20,12 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="com.shabinder.spotiflyer"> package="com.shabinder.spotiflyer">
<queries>
<package android:name="com.gaana" />
<package android:name="com.spotify.music" />
<package android:name="com.google.android.youtube" />
</queries>
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.GET_ACCOUNTS" /> <uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@ -28,6 +34,8 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" /> android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.READ_STORAGE_PERMISSION" /> <uses-permission android:name="android.permission.READ_STORAGE_PERMISSION" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" /><!--For UPI Apps-->
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />

View File

@ -31,6 +31,7 @@ import android.view.View
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.findNavController import androidx.navigation.findNavController
import com.github.javiersantos.appupdater.AppUpdater 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.NetworkInterceptor
import com.shabinder.spotiflyer.utils.createDirectories import com.shabinder.spotiflyer.utils.createDirectories
import com.shabinder.spotiflyer.utils.showMessage import com.shabinder.spotiflyer.utils.showMessage
import com.shabinder.spotiflyer.utils.startService
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -76,17 +76,15 @@ class MainActivity : AppCompatActivity(){
navController = findNavController(R.id.navHostFragment) navController = findNavController(R.id.navHostFragment)
snackBarAnchor = binding.snackBarPosition snackBarAnchor = binding.snackBarPosition
DownloadHelper.youtubeMusicApi = sharedViewModel.youtubeMusicApi DownloadHelper.youtubeMusicApi = sharedViewModel.youtubeMusicApi
//starting Notification and Downloader Service!
startService(this)
authenticateSpotify() authenticateSpotify()
}
override fun onStart() {
super.onStart()
requestPermission() requestPermission()
disableDozeMode() disableDozeMode()
checkIfLatestVersion() checkIfLatestVersion()
createDirectories() createDirectories()
handleIntentFromExternalActivity() handleIntentFromExternalActivity()
} }
@ -154,9 +152,8 @@ class MainActivity : AppCompatActivity(){
sharedViewModel.spotifyService.value = spotifyService sharedViewModel.spotifyService.value = spotifyService
} }
fun authenticateSpotify() { fun authenticateSpotify() {
sharedViewModel.uiScope.launch { sharedViewModel.viewModelScope.launch {
Log.i("Spotify Authentication","Started") Log.i("Spotify Authentication","Started")
val token = spotifyServiceTokenRequest.getToken() val token = spotifyServiceTokenRequest.getToken()
token.value?.let { token.value?.let {
@ -210,7 +207,7 @@ class MainActivity : AppCompatActivity(){
companion object{ companion object{
private lateinit var instance: MainActivity private lateinit var instance: MainActivity
fun getInstance():MainActivity = instance fun getInstance():MainActivity = this.instance
} }
init { init {

View File

@ -22,21 +22,10 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.shabinder.spotiflyer.networking.SpotifyService import com.shabinder.spotiflyer.networking.SpotifyService
import com.shabinder.spotiflyer.networking.YoutubeMusicApi import com.shabinder.spotiflyer.networking.YoutubeMusicApi
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
class SharedViewModel @ViewModelInject constructor( class SharedViewModel @ViewModelInject constructor(
val youtubeMusicApi: YoutubeMusicApi val youtubeMusicApi: YoutubeMusicApi
) : ViewModel() { ) : ViewModel() {
var intentString = MutableLiveData<String>() var intentString = MutableLiveData<String>()
var spotifyService = MutableLiveData<SpotifyService>() var spotifyService = MutableLiveData<SpotifyService>()
private var viewModelJob = Job()
val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
} }

View File

@ -18,14 +18,14 @@
package com.shabinder.spotiflyer.downloadHelper package com.shabinder.spotiflyer.downloadHelper
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent
import android.os.Handler import android.os.Handler
import android.os.Looper
import android.util.Log import android.util.Log
import android.view.View import android.view.View
import android.view.animation.AlphaAnimation import android.view.animation.AlphaAnimation
import android.view.animation.Animation import android.view.animation.Animation
import android.widget.TextView import android.widget.TextView
import android.widget.Toast
import com.shabinder.spotiflyer.SharedViewModel
import com.shabinder.spotiflyer.models.DownloadObject import com.shabinder.spotiflyer.models.DownloadObject
import com.shabinder.spotiflyer.models.DownloadStatus import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.TrackDetails 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.*
import com.shabinder.spotiflyer.utils.Provider.defaultDir import com.shabinder.spotiflyer.utils.Provider.defaultDir
import com.shabinder.spotiflyer.utils.Provider.mainActivity import com.shabinder.spotiflyer.utils.Provider.mainActivity
import com.tonyodev.fetch2.Status
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -46,7 +48,6 @@ object DownloadHelper {
var statusBar:TextView? = null var statusBar:TextView? = null
var youtubeMusicApi: YoutubeMusicApi? = null var youtubeMusicApi: YoutubeMusicApi? = null
var sharedViewModel: SharedViewModel? = null
private var total = 0 private var total = 0
private var processed = 0 private var processed = 0
@ -61,7 +62,7 @@ object DownloadHelper {
trackList: List<TrackDetails>) { trackList: List<TrackDetails>) {
resetStatusBar()// For New Download Request's Status resetStatusBar()// For New Download Request's Status
val downloadList = ArrayList<DownloadObject>() val downloadList = ArrayList<DownloadObject>()
withContext(Dispatchers.Main){ withContext(Dispatchers.IO){
total += trackList.size // Adding New Download List Count to StatusBar total += trackList.size // Adding New Download List Count to StatusBar
trackList.forEachIndexed { index, it -> trackList.forEachIndexed { index, it ->
if(!isOnline()){ if(!isOnline()){
@ -71,12 +72,10 @@ object DownloadHelper {
if(it.downloaded == DownloadStatus.Downloaded){//Download Already Present!! if(it.downloaded == DownloadStatus.Downloaded){//Download Already Present!!
processed++ processed++
if(index == (trackList.size-1)){//LastElement if(index == (trackList.size-1)){//LastElement
Handler().postDelayed({ Handler(Looper.myLooper()!!).postDelayed({
//Delay is Added ,if a request is in processing it may finish //Delay is Added ,if a request is in processing it may finish
Log.i("Spotify Helper","Download Request Sent") 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) startService(mainActivity,downloadList)
},3000) },3000)
} }
@ -86,7 +85,6 @@ object DownloadHelper {
youtubeMusicApi?.getYoutubeMusicResponse(jsonBody)?.enqueue( youtubeMusicApi?.getYoutubeMusicResponse(jsonBody)?.enqueue(
object : Callback<String>{ object : Callback<String>{
override fun onResponse(call: Call<String>, response: Response<String>) { override fun onResponse(call: Call<String>, response: Response<String>) {
sharedViewModel?.uiScope?.launch {
val videoId = sortByBestMatch( val videoId = sortByBestMatch(
getYTTracks(response.body().toString()), getYTTracks(response.body().toString()),
trackName = it.title, trackName = it.title,
@ -94,8 +92,14 @@ object DownloadHelper {
trackDurationSec = it.durationSec trackDurationSec = it.durationSec
).keys.firstOrNull() ).keys.firstOrNull()
Log.i("Spotify Helper Video ID",videoId ?: "Not Found") Log.i("Spotify Helper Video ID",videoId ?: "Not Found")
if(videoId.isNullOrBlank()) {
if(videoId.isNullOrBlank()) {notFound++ ; updateStatusBar()} //Track Not Found
notFound++ ; updateStatusBar()
val intent = Intent()
.setAction(Status.FAILED.name)
.putExtra("track",it)
statusBar?.context?.sendBroadcast(intent)
}
else {//Found Youtube Video ID else {//Found Youtube Video ID
val outputFile: String = val outputFile: String =
defaultDir + defaultDir +
@ -110,20 +114,18 @@ object DownloadHelper {
outputFile = outputFile outputFile = outputFile
) )
processed++ processed++
sharedViewModel?.uiScope?.launch(Dispatchers.Main) {
updateStatusBar() updateStatusBar()
}
downloadList.add(downloadObject) downloadList.add(downloadObject)
}
if(index == (trackList.size-1)){//LastElement if(index == (trackList.size-1)){//LastElement
Handler().postDelayed({ statusBar?.clearAnimation()
if(downloadList.size > 0) {
Handler(Looper.myLooper()!!).postDelayed({
//Delay is Added ,if a request is in processing it may finish //Delay is Added ,if a request is in processing it may finish
Log.i("Spotify Helper","Download Request Sent") Log.i("Spotify Helper", "Download Request Sent")
sharedViewModel?.uiScope?.launch (Dispatchers.Main){ showMessage("Download Started, Now You can leave the App!")
Toast.makeText(mainActivity,"Download Started, Now You can leave the App!", Toast.LENGTH_SHORT).show() startService(mainActivity, downloadList)
} }, 3000)
startService(mainActivity,downloadList)
},5000)
}
} }
} }
} }
@ -157,8 +159,10 @@ object DownloadHelper {
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
fun updateStatusBar() { private fun updateStatusBar() {
CoroutineScope(Dispatchers.Main).launch{
statusBar!!.visibility = View.VISIBLE statusBar!!.visibility = View.VISIBLE
statusBar?.text = "Total: $total ${getEmojiByUnicode(0x2705)}: $processed ${getEmojiByUnicode(0x274C)}: $notFound" statusBar?.text = "Total: $total ${getEmojiByUnicode(0x2705)}: $processed ${getEmojiByUnicode(0x274C)}: $notFound"
} }
}
} }

View File

@ -18,20 +18,16 @@
package com.shabinder.spotiflyer.downloadHelper package com.shabinder.spotiflyer.downloadHelper
import android.util.Log import android.util.Log
import android.widget.Toast
import com.shabinder.spotiflyer.models.DownloadObject import com.shabinder.spotiflyer.models.DownloadObject
import com.shabinder.spotiflyer.models.TrackDetails import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.utils.*
import com.shabinder.spotiflyer.utils.Provider.defaultDir import com.shabinder.spotiflyer.utils.Provider.defaultDir
import com.shabinder.spotiflyer.utils.Provider.mainActivity 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.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
object YTDownloadHelper { interface YTDownloadHelper {
suspend fun downloadYTTracks( suspend fun downloadYTTracks(
type:String, type:String,
subFolder: String?, subFolder: String?,
@ -60,7 +56,7 @@ object YTDownloadHelper {
} }
Log.i("YT Downloader Helper","Download Request Sent") Log.i("YT Downloader Helper","Download Request Sent")
withContext(Dispatchers.Main){ 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) startService(mainActivity,downloadList)
} }
} }

View File

@ -42,11 +42,15 @@ data class TrackDetails(
var albumArt: File, var albumArt: File,
var albumArtURL: String, var albumArtURL: String,
var source: Source, var source: Source,
var downloaded: DownloadStatus = DownloadStatus.NotDownloaded var downloaded: DownloadStatus = DownloadStatus.NotDownloaded,
var progress: Int = 0
):Parcelable ):Parcelable
enum class DownloadStatus{ enum class DownloadStatus{
Downloaded, Downloaded,
Downloading, Downloading,
NotDownloaded Queued,
NotDownloaded,
Converting,
Failed
} }

View File

@ -19,8 +19,11 @@ package com.shabinder.spotiflyer.networking
import com.shabinder.spotiflyer.models.Optional import com.shabinder.spotiflyer.models.Optional
import com.shabinder.spotiflyer.models.gaana.* import com.shabinder.spotiflyer.models.gaana.*
import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Query import retrofit2.http.Query
import retrofit2.http.Url
const val gaana_token = "b2e6d7fbc136547a940516e9b77e5990" const val gaana_token = "b2e6d7fbc136547a940516e9b77e5990"
@ -98,4 +101,9 @@ interface GaanaInterface {
@Query("limit") limit: Int = 50 @Query("limit") limit: Int = 50
): Optional<GaanaArtistTracks> ): Optional<GaanaArtistTracks>
/*
* Dynamic Url Requests
* */
@GET
fun getResponse(@Url url:String): Call<ResponseBody>
} }

View File

@ -21,6 +21,7 @@ import android.annotation.SuppressLint
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.lifecycle.viewModelScope
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView 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.DownloadStatus
import com.shabinder.spotiflyer.models.TrackDetails import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.spotify.Source import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.ui.base.tracklistbase.TrackListViewModel
import com.shabinder.spotiflyer.utils.* import com.shabinder.spotiflyer.utils.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class TrackListAdapter(private val viewModel :TrackListViewModel): ListAdapter<TrackDetails, TrackListAdapter.ViewHolder>(TrackDiffCallback()) { class TrackListAdapter(private val viewModel : TrackListViewModel): ListAdapter<TrackDetails, TrackListAdapter.ViewHolder>(TrackDiffCallback()),YTDownloadHelper {
var source:Source =Source.Spotify var source:Source =Source.Spotify
@ -51,36 +53,69 @@ class TrackListAdapter(private val viewModel :TrackListViewModel): ListAdapter<T
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position) val item = getItem(position)
if(itemCount == 1){ holder.binding.imageUrl.visibility = View.GONE}else{ if(itemCount == 1){ holder.binding.imageUrl.visibility = View.GONE}else{
viewModel.uiScope.launch { viewModel.viewModelScope.launch {
bindImage(holder.binding.imageUrl,item.albumArtURL, source) bindImage(holder.binding.imageUrl,item.albumArtURL, source)
} }
} }
when (item.downloaded) { when (item.downloaded) {
DownloadStatus.Downloaded -> { DownloadStatus.Downloaded -> {
holder.binding.btnDownload.setImageResource(R.drawable.ic_tick) holder.binding.btnDownloadProgress.invisible()
holder.binding.btnDownload.clearAnimation() 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 -> { DownloadStatus.Downloading -> {
holder.binding.btnDownload.setImageResource(R.drawable.ic_refresh) holder.binding.btnDownload.invisible()
rotateAnim(holder.binding.btnDownload) 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 -> { DownloadStatus.NotDownloaded -> {
holder.binding.btnDownload.setImageResource(R.drawable.ic_arrow) holder.binding.btnDownloadProgress.invisible()
holder.binding.btnDownload.clearAnimation() holder.binding.btnDownload.apply{
holder.binding.btnDownload.setOnClickListener{ setImageResource(R.drawable.ic_arrow)
clearAnimation()
visible()
setOnClickListener{
if(!isOnline()){ if(!isOnline()){
showNoConnectionAlert() showNoConnectionAlert()
return@setOnClickListener return@setOnClickListener
} }
showMessage("Processing!") showMessage("Processing!")
holder.binding.btnDownload.setImageResource(R.drawable.ic_refresh) item.downloaded = DownloadStatus.Queued
rotateAnim(it)
item.downloaded = DownloadStatus.Downloading
when(source){ when(source){
Source.YouTube -> { Source.YouTube -> {
viewModel.uiScope.launch { viewModel.viewModelScope.launch {
YTDownloadHelper.downloadYTTracks( downloadYTTracks(
viewModel.folderType, viewModel.folderType,
viewModel.subFolder, viewModel.subFolder,
listOf(item) listOf(item)
@ -88,7 +123,7 @@ class TrackListAdapter(private val viewModel :TrackListViewModel): ListAdapter<T
} }
} }
else -> { else -> {
viewModel.uiScope.launch { viewModel.viewModelScope.launch {
DownloadHelper.downloadAllTracks( DownloadHelper.downloadAllTracks(
viewModel.folderType, viewModel.folderType,
viewModel.subFolder, viewModel.subFolder,
@ -101,6 +136,7 @@ class TrackListAdapter(private val viewModel :TrackListViewModel): ListAdapter<T
} }
} }
} }
}
holder.binding.trackName.text = if(item.title.length > 20){"${item.title.subSequence(0,18)}..."}else{item.title} holder.binding.trackName.text = if(item.title.length > 20){"${item.title.subSequence(0,18)}..."}else{item.title}
holder.binding.artist.text = "${item.artists.firstOrNull()}..." holder.binding.artist.text = "${item.artists.firstOrNull()}..."

View File

@ -20,6 +20,7 @@ package com.shabinder.spotiflyer.splash
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.shabinder.spotiflyer.MainActivity import com.shabinder.spotiflyer.MainActivity
import com.shabinder.spotiflyer.R import com.shabinder.spotiflyer.R
@ -33,7 +34,7 @@ class SplashScreen : AppCompatActivity(){
val splashTimeout = 400 val splashTimeout = 400
val homeIntent = Intent(this@SplashScreen, MainActivity::class.java) val homeIntent = Intent(this@SplashScreen, MainActivity::class.java)
Handler().postDelayed({ Handler(Looper.myLooper()!!).postDelayed({
//TODO:Bring Initial Setup here //TODO:Bring Initial Setup here
startActivity(homeIntent) startActivity(homeIntent)
finish() finish()

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<VB:ViewBinding,VM : ViewModel> : 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
}

View File

@ -15,37 +15,35 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.shabinder.spotiflyer.utils package com.shabinder.spotiflyer.ui.base.tracklistbase
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.NavArgs import androidx.navigation.NavArgs
import androidx.recyclerview.widget.SimpleItemAnimator
import com.shabinder.spotiflyer.R import com.shabinder.spotiflyer.R
import com.shabinder.spotiflyer.SharedViewModel
import com.shabinder.spotiflyer.databinding.TrackListFragmentBinding import com.shabinder.spotiflyer.databinding.TrackListFragmentBinding
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper import com.shabinder.spotiflyer.downloadHelper.DownloadHelper
import com.shabinder.spotiflyer.models.DownloadStatus import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.TrackDetails import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.spotify.Source import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.recyclerView.TrackListAdapter 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.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<VM : TrackListViewModel , args: NavArgs> : Fragment() { abstract class TrackListFragment<VM : TrackListViewModel, args: NavArgs> : BaseFragment<TrackListFragmentBinding,VM>() {
protected lateinit var sharedViewModel: SharedViewModel override lateinit var binding: TrackListFragmentBinding
protected lateinit var binding: TrackListFragmentBinding
protected abstract var viewModel: VM
protected abstract var adapter: TrackListAdapter protected abstract var adapter: TrackListAdapter
protected abstract var source: Source protected abstract var source: Source
private var intentFilter: IntentFilter? = null private var intentFilter: IntentFilter? = null
@ -58,8 +56,6 @@ abstract class TrackListFragment<VM : TrackListViewModel , args: NavArgs> : Frag
showNoConnectionAlert() showNoConnectionAlert()
mainActivity.navController.popBackStack() mainActivity.navController.popBackStack()
} }
Handler()
sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java)
} }
override fun onCreateView( override fun onCreateView(
@ -74,9 +70,7 @@ abstract class TrackListFragment<VM : TrackListViewModel , args: NavArgs> : Frag
private fun initializeAll() { private fun initializeAll() {
DownloadHelper.youtubeMusicApi = sharedViewModel.youtubeMusicApi DownloadHelper.youtubeMusicApi = sharedViewModel.youtubeMusicApi
DownloadHelper.sharedViewModel = sharedViewModel
DownloadHelper.statusBar = binding.statusBar DownloadHelper.statusBar = binding.statusBar
(binding.trackList.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -98,7 +92,7 @@ abstract class TrackListFragment<VM : TrackListViewModel , args: NavArgs> : Frag
}) })
viewModel.coverUrl.observe(viewLifecycleOwner, { viewModel.coverUrl.observe(viewLifecycleOwner, {
it?.let{bindImage(binding.coverImage,it, source)} it?.let{ bindImage(binding.coverImage,it, source) }
}) })
viewModel.title.observe(viewLifecycleOwner, { viewModel.title.observe(viewLifecycleOwner, {
@ -108,6 +102,11 @@ abstract class TrackListFragment<VM : TrackListViewModel , args: NavArgs> : Frag
private fun initializeBroadcast() { private fun initializeBroadcast() {
intentFilter = IntentFilter() 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") intentFilter?.addAction("track_download_completed")
updateUIReceiver = object : BroadcastReceiver() { updateUIReceiver = object : BroadcastReceiver() {
@ -117,11 +116,33 @@ abstract class TrackListFragment<VM : TrackListViewModel , args: NavArgs> : Frag
val trackDetails = intent.getParcelableExtra<TrackDetails?>("track") val trackDetails = intent.getParcelableExtra<TrackDetails?>("track")
trackDetails?.let { trackDetails?.let {
val position: Int = viewModel.trackList.value?.map { it.title }?.indexOf(trackDetails.title) ?: -1 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) { if(position != -1) {
val track = viewModel.trackList.value?.get(position) val track = viewModel.trackList.value?.get(position)
track?.let{ track?.let{
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 it.downloaded = DownloadStatus.Downloaded
}
}
viewModel.trackList.value?.set(position, it) viewModel.trackList.value?.set(position, it)
adapter.notifyItemChanged(position) adapter.notifyItemChanged(position)
checkIfAllDownloaded() checkIfAllDownloaded()
@ -144,7 +165,7 @@ abstract class TrackListFragment<VM : TrackListViewModel , args: NavArgs> : Frag
requireActivity().unregisterReceiver(updateUIReceiver) requireActivity().unregisterReceiver(updateUIReceiver)
} }
private fun checkIfAllDownloaded() { 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 //All Tracks Downloaded
binding.btnDownloadAll.visibility = View.GONE binding.btnDownloadAll.visibility = View.GONE
binding.downloadingFab.apply{ binding.downloadingFab.apply{
@ -154,5 +175,4 @@ abstract class TrackListFragment<VM : TrackListViewModel , args: NavArgs> : Frag
} }
} }
} }
open fun applicationContext(): Context = requireActivity().applicationContext
} }

View File

@ -15,30 +15,19 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.shabinder.spotiflyer.utils package com.shabinder.spotiflyer.ui.base.tracklistbase
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.shabinder.spotiflyer.models.TrackDetails 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 class TrackListViewModel:ViewModel() {
abstract var folderType:String abstract var folderType:String
abstract var subFolder:String abstract var subFolder:String
open val trackList = MutableLiveData<MutableList<TrackDetails>>() open val trackList = MutableLiveData<MutableList<TrackDetails>>()
private val viewModelJob:CompletableJob = Job()
open val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
private val loading = "Loading!" private val loading = "Loading!"
open var title = MutableLiveData<String>().apply { value = loading } open var title = MutableLiveData<String>().apply { value = loading }
open var coverUrl = MutableLiveData<String>() open var coverUrl = MutableLiveData<String>()
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
} }

View File

@ -22,21 +22,23 @@ import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.lifecycle.ViewModelProvider import androidx.fragment.app.viewModels
import androidx.lifecycle.viewModelScope
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper import com.shabinder.spotiflyer.downloadHelper.DownloadHelper
import com.shabinder.spotiflyer.models.DownloadStatus import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.spotify.Source import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.recyclerView.TrackListAdapter import com.shabinder.spotiflyer.recyclerView.TrackListAdapter
import com.shabinder.spotiflyer.ui.base.tracklistbase.TrackListFragment
import com.shabinder.spotiflyer.utils.* import com.shabinder.spotiflyer.utils.*
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@AndroidEntryPoint @AndroidEntryPoint
class GaanaFragment : TrackListFragment<GaanaViewModel,GaanaFragmentArgs>() { class GaanaFragment : TrackListFragment<GaanaViewModel, GaanaFragmentArgs>() {
override lateinit var viewModel: GaanaViewModel override val viewModel: GaanaViewModel by viewModels()
override lateinit var adapter: TrackListAdapter override lateinit var adapter: TrackListAdapter
override var source: Source = Source.Gaana override var source: Source = Source.Gaana
override val args: GaanaFragmentArgs by navArgs() override val args: GaanaFragmentArgs by navArgs()
@ -46,7 +48,6 @@ class GaanaFragment : TrackListFragment<GaanaViewModel,GaanaFragmentArgs>() {
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View? {
super.onCreateView(inflater, container, savedInstanceState) super.onCreateView(inflater, container, savedInstanceState)
viewModel = ViewModelProvider(this).get(GaanaViewModel::class.java)
adapter = TrackListAdapter(viewModel) adapter = TrackListAdapter(viewModel)
val gaanaLink = GaanaFragmentArgs.fromBundle(requireArguments()).link.substringAfter("gaana.com/") val gaanaLink = GaanaFragmentArgs.fromBundle(requireArguments()).link.substringAfter("gaana.com/")
@ -70,28 +71,22 @@ class GaanaFragment : TrackListFragment<GaanaViewModel,GaanaFragmentArgs>() {
showNoConnectionAlert() showNoConnectionAlert()
return@setOnClickListener return@setOnClickListener
} }
binding.btnDownloadAll.visibility = View.GONE binding.btnDownloadAll.gone()
binding.downloadingFab.visibility = View.VISIBLE binding.downloadingFab.apply{
visible()
rotateAnim(binding.downloadingFab) rotate()
}
for (track in viewModel.trackList.value!!){ for (track in viewModel.trackList.value!!){
if(track.downloaded != DownloadStatus.Downloaded){ if(track.downloaded != DownloadStatus.Downloaded){
track.downloaded = DownloadStatus.Downloading track.downloaded = DownloadStatus.Queued
adapter.notifyItemChanged(viewModel.trackList.value!!.indexOf(track)) adapter.notifyItemChanged(viewModel.trackList.value!!.indexOf(track))
} }
} }
showMessage("Processing!") showMessage("Processing!")
sharedViewModel.uiScope.launch(Dispatchers.Default){ sharedViewModel.viewModelScope.launch(Dispatchers.Default){
val urlList = arrayListOf<String>() loadAllImages(requireActivity(), viewModel.trackList.value?.map{it.albumArtURL}, Source.Gaana)
viewModel.trackList.value?.forEach { urlList.add(it.albumArtURL) }
//Appending Source
urlList.add("gaana")
loadAllImages(
requireActivity(),
urlList
)
} }
viewModel.uiScope.launch { viewModel.viewModelScope.launch {
val finalList = viewModel.trackList.value val finalList = viewModel.trackList.value
if(finalList.isNullOrEmpty())showMessage("Not Downloading Any Song") if(finalList.isNullOrEmpty())showMessage("Not Downloading Any Song")
DownloadHelper.downloadAllTracks( DownloadHelper.downloadAllTracks(

View File

@ -18,6 +18,7 @@
package com.shabinder.spotiflyer.ui.gaana package com.shabinder.spotiflyer.ui.gaana
import androidx.hilt.lifecycle.ViewModelInject import androidx.hilt.lifecycle.ViewModelInject
import androidx.lifecycle.viewModelScope
import com.shabinder.spotiflyer.database.DatabaseDAO import com.shabinder.spotiflyer.database.DatabaseDAO
import com.shabinder.spotiflyer.database.DownloadRecord import com.shabinder.spotiflyer.database.DownloadRecord
import com.shabinder.spotiflyer.models.DownloadStatus 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.gaana.GaanaTrack
import com.shabinder.spotiflyer.models.spotify.Source import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.networking.GaanaInterface 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.Provider
import com.shabinder.spotiflyer.utils.TrackListViewModel
import com.shabinder.spotiflyer.utils.finalOutputDir import com.shabinder.spotiflyer.utils.finalOutputDir
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -40,12 +41,13 @@ class GaanaViewModel @ViewModelInject constructor(
override var folderType:String = "" override var folderType:String = ""
override var subFolder:String = "" override var subFolder:String = ""
private val gaanaPlaceholderImageUrl = "https://a10.gaanacdn.com/images/social/gaana_social.jpg" private val gaanaPlaceholderImageUrl = "https://a10.gaanacdn.com/images/social/gaana_social.jpg"
fun gaanaSearch(type:String,link:String){ fun gaanaSearch(type:String,link:String){
when(type){ when(type){
"song" -> { "song" -> {
uiScope.launch { viewModelScope.launch {
gaanaInterface.getGaanaSong(seokey = link).value?.tracks?.firstOrNull()?.also { gaanaInterface.getGaanaSong(seokey = link).value?.tracks?.firstOrNull()?.also {
folderType = "Tracks" folderType = "Tracks"
if(File(finalOutputDir(it.track_title,folderType,subFolder)).exists()){//Download Already Present!! if(File(finalOutputDir(it.track_title,folderType,subFolder)).exists()){//Download Already Present!!
@ -71,7 +73,7 @@ class GaanaViewModel @ViewModelInject constructor(
} }
} }
"album" -> { "album" -> {
uiScope.launch { viewModelScope.launch {
gaanaInterface.getGaanaAlbum(seokey = link).value?.also { gaanaInterface.getGaanaAlbum(seokey = link).value?.also {
folderType = "Albums" folderType = "Albums"
subFolder = link subFolder = link
@ -98,7 +100,7 @@ class GaanaViewModel @ViewModelInject constructor(
} }
} }
"playlist" -> { "playlist" -> {
uiScope.launch { viewModelScope.launch {
gaanaInterface.getGaanaPlaylist(seokey = link).value?.also { gaanaInterface.getGaanaPlaylist(seokey = link).value?.also {
folderType = "Playlists" folderType = "Playlists"
subFolder = link subFolder = link
@ -126,7 +128,7 @@ class GaanaViewModel @ViewModelInject constructor(
} }
} }
"artist" -> { "artist" -> {
uiScope.launch { viewModelScope.launch {
folderType = "Artist" folderType = "Artist"
subFolder = link subFolder = link
val artistDetails = gaanaInterface.getGaanaArtistDetails(seokey = link).value?.artist?.firstOrNull()?.also { val artistDetails = gaanaInterface.getGaanaArtistDetails(seokey = link).value?.artist?.firstOrNull()?.also {
@ -157,7 +159,6 @@ class GaanaViewModel @ViewModelInject constructor(
} }
} }
private fun List<GaanaTrack>.toTrackDetailsList() = this.map { private fun List<GaanaTrack>.toTrackDetailsList() = this.map {
TrackDetails( TrackDetails(
title = it.track_title, title = it.track_title,

View File

@ -23,7 +23,9 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment 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 androidx.navigation.fragment.findNavController
import com.shabinder.spotiflyer.MainActivity import com.shabinder.spotiflyer.MainActivity
import com.shabinder.spotiflyer.R import com.shabinder.spotiflyer.R
@ -33,6 +35,7 @@ import com.shabinder.spotiflyer.utils.*
import com.shreyaspatil.easyupipayment.EasyUpiPayment import com.shreyaspatil.easyupipayment.EasyUpiPayment
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
@ -40,12 +43,11 @@ import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class MainFragment : Fragment() { class MainFragment : Fragment() {
private lateinit var mainViewModel: MainViewModel private val mainViewModel: MainViewModel by viewModels()
private lateinit var sharedViewModel: SharedViewModel private val sharedViewModel: SharedViewModel by activityViewModels()
private lateinit var binding: MainFragmentBinding private lateinit var binding: MainFragmentBinding
@Inject lateinit var easyUpiPayment: EasyUpiPayment @Inject lateinit var easyUpiPayment: EasyUpiPayment
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
@ -84,21 +86,27 @@ class MainFragment : Fragment() {
return binding.root 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! * Handle Intent If there is any!
**/ **/
private fun handleIntent() { private fun handleIntent() {
sharedViewModel.intentString.observe(viewLifecycleOwner,{ it?.let { sharedViewModel.intentString.observe(viewLifecycleOwner,{ it?.let {
sharedViewModel.uiScope.launch(Dispatchers.IO) { sharedViewModel.viewModelScope.launch(Dispatchers.IO) {
//Wait for any Authentication to Finish , //Wait for any Authentication to Finish ,
// this Wait prevents from multiple Authentication Requests // this Wait prevents from multiple Authentication Requests
Thread.sleep(1000) delay(1500)
if(sharedViewModel.spotifyService.value == null){ if(sharedViewModel.spotifyService.value == null){
//Not Authenticated Yet //Not Authenticated Yet
Provider.mainActivity.authenticateSpotify() Provider.mainActivity.authenticateSpotify()
while (sharedViewModel.spotifyService.value == null) { while (sharedViewModel.spotifyService.value == null) {
//Waiting for Authentication to Finish //Waiting for Authentication to Finish
Thread.sleep(1000) delay(1000)
} }
} }
@ -114,8 +122,6 @@ class MainFragment : Fragment() {
} }
private fun initializeAll() { private fun initializeAll() {
mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java)
binding.apply { binding.apply {
btnGaana.openPlatformOnClick("com.gaana","http://gaana.com") btnGaana.openPlatformOnClick("com.gaana","http://gaana.com")
btnSpotify.openPlatformOnClick("com.spotify.music","http://open.spotify.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_three)).append("\n")
.append(getText(R.string.d_four)).append("\n") .append(getText(R.string.d_four)).append("\n")
} }
} }

View File

@ -23,12 +23,15 @@ import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.lifecycle.ViewModelProvider import androidx.fragment.app.viewModels
import androidx.lifecycle.viewModelScope
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper import com.shabinder.spotiflyer.downloadHelper.DownloadHelper
import com.shabinder.spotiflyer.models.DownloadStatus import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.spotify.Source import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.networking.SpotifyService
import com.shabinder.spotiflyer.recyclerView.TrackListAdapter import com.shabinder.spotiflyer.recyclerView.TrackListAdapter
import com.shabinder.spotiflyer.ui.base.tracklistbase.TrackListFragment
import com.shabinder.spotiflyer.utils.* import com.shabinder.spotiflyer.utils.*
import com.shabinder.spotiflyer.utils.Provider.mainActivity import com.shabinder.spotiflyer.utils.Provider.mainActivity
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@ -36,12 +39,16 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@AndroidEntryPoint @AndroidEntryPoint
class SpotifyFragment : TrackListFragment<SpotifyViewModel,SpotifyFragmentArgs>() { class SpotifyFragment : TrackListFragment<SpotifyViewModel, SpotifyFragmentArgs>() {
override lateinit var viewModel: SpotifyViewModel override val viewModel: SpotifyViewModel by viewModels()
override val args: SpotifyFragmentArgs by navArgs()
override lateinit var adapter: TrackListAdapter override lateinit var adapter: TrackListAdapter
override var source: Source = Source.Spotify 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") @SuppressLint("SetJavaScriptEnabled")
override fun onCreateView( override fun onCreateView(
@ -51,60 +58,72 @@ class SpotifyFragment : TrackListFragment<SpotifyViewModel,SpotifyFragmentArgs>(
super.onCreateView(inflater, container, savedInstanceState) super.onCreateView(inflater, container, savedInstanceState)
initializeAll() 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('/') * New Link Schema: https://link.tospotify.com/kqTBblrjQbb,
* Fetching Standard Link: https://open.spotify.com/playlist/37i9dQZF1DX9RwfGbeGQwP?si=iWz7B1tETiunDntnDo3lSQ&amp;_branch_match_id=862039436205270630
* */
if (!spotifyLink.contains("open.spotify")) {
val resolvedLink = viewModel.resolveLink(spotifyLink)
Log.d("Spotify Resolved Link", resolvedLink)
spotifyLink = resolvedLink
}
link = spotifyLink.substringAfterLast('/', "Error").substringBefore('?')
type = spotifyLink.substringBeforeLast('/', "Error").substringAfterLast('/')
Log.i("Spotify Fragment", "$type : $link") Log.i("Spotify Fragment", "$type : $link")
if (sharedViewModel.spotifyService.value == null) {//Authentication pending!!
if(sharedViewModel.spotifyService.value == null){//Authentication pending!! if (isOnline()) mainActivity.authenticateSpotify()
if(isOnline()) mainActivity.authenticateSpotify()
} }
when{ when {
type == "Error" || link == "Error" -> { type == "Error" || link == "Error" -> {
showMessage("Please Check Your Link!") showMessage("Please Check Your Link!")
mainActivity.onBackPressed() mainActivity.onBackPressed()
} }
else -> { else -> {
if(type == "episode" || type == "show"){//TODO Implementation if (type == "episode" || type == "show") {//TODO Implementation
showMessage("Implementing Soon, Stay Tuned!") showMessage("Implementing Soon, Stay Tuned!")
} } else {
else{ viewModel.spotifySearch(type, link)
this.viewModel.spotifySearch(type,link)
binding.btnDownloadAll.setOnClickListener { binding.btnDownloadAll.setOnClickListener {
if(!isOnline()){ if (!isOnline()) {
showNoConnectionAlert() showNoConnectionAlert()
return@setOnClickListener return@setOnClickListener
} }
binding.btnDownloadAll.visibility = View.GONE binding.btnDownloadAll.gone()
binding.downloadingFab.visibility = View.VISIBLE binding.downloadingFab.apply {
visible()
rotateAnim(binding.downloadingFab) rotate()
for (track in this.viewModel.trackList.value ?: listOf()){ }
if(track.downloaded != DownloadStatus.Downloaded){ for (track in viewModel.trackList.value ?: listOf()) {
track.downloaded = DownloadStatus.Downloading if (track.downloaded != DownloadStatus.Downloaded) {
adapter.notifyItemChanged(this.viewModel.trackList.value!!.indexOf(track)) track.downloaded = DownloadStatus.Queued
adapter.notifyItemChanged(
viewModel.trackList.value!!.indexOf(
track
)
)
} }
} }
showMessage("Processing!") showMessage("Processing!")
sharedViewModel.uiScope.launch(Dispatchers.Default){ sharedViewModel.viewModelScope.launch(Dispatchers.Default) {
val urlList = arrayListOf<String>()
this@SpotifyFragment.viewModel.trackList.value?.forEach { urlList.add(it.albumArtURL) }
//Appending Source
urlList.add("spotify")
loadAllImages( loadAllImages(
requireActivity(), requireActivity(),
urlList viewModel.trackList.value?.map { it.albumArtURL },
Source.Spotify
) )
} }
this.viewModel.uiScope.launch { viewModelScope.launch {
val finalList = viewModel.trackList.value val finalList = viewModel.trackList.value
if(finalList.isNullOrEmpty())showMessage("Not Downloading Any Song") if (finalList.isNullOrEmpty()) showMessage("Not Downloading Any Song")
DownloadHelper.downloadAllTracks( DownloadHelper.downloadAllTracks(
viewModel.folderType, viewModel.folderType,
viewModel.subFolder, viewModel.subFolder,
@ -115,7 +134,7 @@ class SpotifyFragment : TrackListFragment<SpotifyViewModel,SpotifyFragmentArgs>(
} }
} }
} }
}
return binding.root return binding.root
} }
@ -123,10 +142,10 @@ class SpotifyFragment : TrackListFragment<SpotifyViewModel,SpotifyFragmentArgs>(
* Basic Initialization * Basic Initialization
**/ **/
private fun initializeAll() { private fun initializeAll() {
this.viewModel = ViewModelProvider(this).get(SpotifyViewModel::class.java)
adapter = TrackListAdapter(this.viewModel)
sharedViewModel.spotifyService.observe(viewLifecycleOwner, { sharedViewModel.spotifyService.observe(viewLifecycleOwner, {
this.viewModel.spotifyService = it this.viewModel.spotifyService = it
}) })
viewModel.spotifyService = spotifyService //Temp Initialisation
adapter = TrackListAdapter(this.viewModel)
} }
} }

View File

@ -19,6 +19,7 @@ package com.shabinder.spotiflyer.ui.spotify
import android.util.Log import android.util.Log
import androidx.hilt.lifecycle.ViewModelInject import androidx.hilt.lifecycle.ViewModelInject
import androidx.lifecycle.viewModelScope
import com.shabinder.spotiflyer.database.DatabaseDAO import com.shabinder.spotiflyer.database.DatabaseDAO
import com.shabinder.spotiflyer.database.DownloadRecord import com.shabinder.spotiflyer.database.DownloadRecord
import com.shabinder.spotiflyer.models.DownloadStatus 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.Image
import com.shabinder.spotiflyer.models.spotify.Source import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.models.spotify.Track import com.shabinder.spotiflyer.models.spotify.Track
import com.shabinder.spotiflyer.networking.GaanaInterface
import com.shabinder.spotiflyer.networking.SpotifyService 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.Provider.imageDir
import com.shabinder.spotiflyer.utils.TrackListViewModel
import com.shabinder.spotiflyer.utils.finalOutputDir import com.shabinder.spotiflyer.utils.finalOutputDir
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -38,6 +40,7 @@ import java.io.File
class SpotifyViewModel @ViewModelInject constructor( class SpotifyViewModel @ViewModelInject constructor(
val databaseDAO: DatabaseDAO, val databaseDAO: DatabaseDAO,
val gaanaInterface : GaanaInterface
) : TrackListViewModel(){ ) : TrackListViewModel(){
override var folderType:String = "" override var folderType:String = ""
@ -45,8 +48,14 @@ class SpotifyViewModel @ViewModelInject constructor(
var spotifyService : SpotifyService? = null 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){ fun spotifySearch(type:String,link: String){
uiScope.launch { viewModelScope.launch {
when (type) { when (type) {
"track" -> { "track" -> {
spotifyService?.getTrack(link)?.value?.also { spotifyService?.getTrack(link)?.value?.also {
@ -130,6 +139,7 @@ class SpotifyViewModel @ViewModelInject constructor(
} }
"playlist" -> { "playlist" -> {
Log.i("Spotify Service",spotifyService.toString())
val playlistObject = spotifyService?.getPlaylist(link)?.value val playlistObject = spotifyService?.getPlaylist(link)?.value
folderType = "Playlists" folderType = "Playlists"
subFolder = playlistObject?.name.toString() subFolder = playlistObject?.name.toString()

View File

@ -21,12 +21,14 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.lifecycle.ViewModelProvider import androidx.fragment.app.viewModels
import androidx.lifecycle.viewModelScope
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import com.shabinder.spotiflyer.downloadHelper.YTDownloadHelper import com.shabinder.spotiflyer.downloadHelper.YTDownloadHelper
import com.shabinder.spotiflyer.models.DownloadStatus import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.spotify.Source import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.recyclerView.TrackListAdapter import com.shabinder.spotiflyer.recyclerView.TrackListAdapter
import com.shabinder.spotiflyer.ui.base.tracklistbase.TrackListFragment
import com.shabinder.spotiflyer.utils.* import com.shabinder.spotiflyer.utils.*
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -36,9 +38,9 @@ private const val sampleDomain2 = "youtu.be"
private const val sampleDomain1 = "youtube.com" private const val sampleDomain1 = "youtube.com"
@AndroidEntryPoint @AndroidEntryPoint
class YoutubeFragment : TrackListFragment<YoutubeViewModel,YoutubeFragmentArgs>() { class YoutubeFragment : TrackListFragment<YoutubeViewModel, YoutubeFragmentArgs>() , YTDownloadHelper {
override lateinit var viewModel: YoutubeViewModel override val viewModel: YoutubeViewModel by viewModels()
override lateinit var adapter : TrackListAdapter override lateinit var adapter : TrackListAdapter
override var source: Source = Source.YouTube override var source: Source = Source.YouTube
override val args: YoutubeFragmentArgs by navArgs() override val args: YoutubeFragmentArgs by navArgs()
@ -48,8 +50,7 @@ class YoutubeFragment : TrackListFragment<YoutubeViewModel,YoutubeFragmentArgs>(
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View? {
super.onCreateView(inflater, container, savedInstanceState) super.onCreateView(inflater, container, savedInstanceState)
this.viewModel = ViewModelProvider(this).get(YoutubeViewModel::class.java) adapter = TrackListAdapter(viewModel)
adapter = TrackListAdapter(this.viewModel)
val args = YoutubeFragmentArgs.fromBundle(requireArguments()) val args = YoutubeFragmentArgs.fromBundle(requireArguments())
val link = args.link val link = args.link
@ -62,7 +63,7 @@ class YoutubeFragment : TrackListFragment<YoutubeViewModel,YoutubeFragmentArgs>(
if(link.contains("playlist",true) || link.contains("list",true)){ if(link.contains("playlist",true) || link.contains("list",true)){
// Given Link is of a Playlist // Given Link is of a Playlist
val playlistId = link.substringAfter("?list=").substringAfter("&list=").substringBefore("&") val playlistId = link.substringAfter("?list=").substringAfter("&list=").substringBefore("&")
this.viewModel.getYTPlaylist(playlistId) viewModel.getYTPlaylist(playlistId)
}else{//Given Link is of a Video }else{//Given Link is of a Video
var searchId = "error" var searchId = "error"
if(link.contains(sampleDomain1,true) ){ if(link.contains(sampleDomain1,true) ){
@ -84,31 +85,25 @@ class YoutubeFragment : TrackListFragment<YoutubeViewModel,YoutubeFragmentArgs>(
showNoConnectionAlert() showNoConnectionAlert()
return@setOnClickListener return@setOnClickListener
} }
binding.btnDownloadAll.visibility = View.GONE binding.btnDownloadAll.gone()
binding.downloadingFab.visibility = View.VISIBLE binding.downloadingFab.apply{
visible()
rotateAnim(binding.downloadingFab) rotate()
}
for (track in this.viewModel.trackList.value?: listOf()){ for (track in this.viewModel.trackList.value?: listOf()){
if(track.downloaded != DownloadStatus.Downloaded){ if(track.downloaded != DownloadStatus.Downloaded){
track.downloaded = DownloadStatus.Downloading track.downloaded = DownloadStatus.Queued
adapter.notifyItemChanged(this.viewModel.trackList.value!!.indexOf(track)) //adapter.notifyItemChanged(this.viewModel.trackList.value!!.indexOf(track))
} }
} }
adapter.notifyDataSetChanged()
showMessage("Processing!") showMessage("Processing!")
sharedViewModel.uiScope.launch(Dispatchers.Default){ sharedViewModel.viewModelScope.launch(Dispatchers.Default){
val urlList = arrayListOf<String>() loadAllImages(requireActivity(), viewModel.trackList.value?.map{it.albumArtURL}, Source.YouTube)
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
)
} }
viewModel.uiScope.launch { viewModel.viewModelScope.launch {
YTDownloadHelper.downloadYTTracks( downloadYTTracks(
type = viewModel.folderType, type = viewModel.folderType,
subFolder = viewModel.subFolder, subFolder = viewModel.subFolder,
tracks = viewModel.trackList.value ?: listOf() tracks = viewModel.trackList.value ?: listOf()

View File

@ -20,14 +20,19 @@ package com.shabinder.spotiflyer.ui.youtube
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.util.Log import android.util.Log
import androidx.hilt.lifecycle.ViewModelInject import androidx.hilt.lifecycle.ViewModelInject
import androidx.lifecycle.viewModelScope
import com.github.kiulian.downloader.YoutubeDownloader import com.github.kiulian.downloader.YoutubeDownloader
import com.shabinder.spotiflyer.database.DatabaseDAO import com.shabinder.spotiflyer.database.DatabaseDAO
import com.shabinder.spotiflyer.database.DownloadRecord import com.shabinder.spotiflyer.database.DownloadRecord
import com.shabinder.spotiflyer.models.DownloadStatus import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.TrackDetails import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.spotify.Source 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.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.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -49,7 +54,7 @@ class YoutubeViewModel @ViewModelInject constructor(
fun getYTPlaylist(searchId:String){ fun getYTPlaylist(searchId:String){
if(!isOnline())return if(!isOnline())return
try{ try{
uiScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
Log.i("YT Playlist",searchId) Log.i("YT Playlist",searchId)
val playlist = ytDownloader.getPlaylist(searchId) val playlist = ytDownloader.getPlaylist(searchId)
val playlistDetails = playlist.details() val playlistDetails = playlist.details()
@ -106,7 +111,7 @@ class YoutubeViewModel @ViewModelInject constructor(
fun getYTTrack(searchId:String) { fun getYTTrack(searchId:String) {
if(!isOnline())return if(!isOnline())return
try{ try{
uiScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
Log.i("YT Video",searchId) Log.i("YT Video",searchId)
val video = ytDownloader.getVideo(searchId) val video = ytDownloader.getVideo(searchId)
coverUrl.postValue("https://i.ytimg.com/vi/$searchId/hqdefault.jpg") coverUrl.postValue("https://i.ytimg.com/vi/$searchId/hqdefault.jpg")

View File

@ -21,6 +21,9 @@ import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.view.View 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 import com.shabinder.spotiflyer.utils.Provider.mainActivity
fun View.openPlatformOnClick(packageName:String, websiteAddress:String){ fun View.openPlatformOnClick(packageName:String, websiteAddress:String){
@ -43,3 +46,25 @@ fun View.openPlatformOnClick(websiteAddress:String){
val intent = Intent(Intent.ACTION_VIEW, uri) val intent = Intent(Intent.ACTION_VIEW, uri)
this.setOnClickListener { mainActivity.startActivity(intent) } 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
}

View File

@ -49,10 +49,9 @@ import javax.inject.Singleton
@Module @Module
object Provider { object Provider {
// mainActivity Instance to use whereEver Needed , as Its God Activity. // mainActivity Instance to use whereEver Needed , as Its God Activity.
// (i.e, Active Through out App' Lifecycle ) // (i.e, Active Throughout App' Lifecycle )
val mainActivity: MainActivity = MainActivity.getInstance() val mainActivity: MainActivity by lazy { MainActivity.getInstance() }
//Default Directory to save Media in their Own Categorized Folders //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) @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 "SpotiFlyer"+ File.separator
//Default Cache Directory to save Album Art to use them for writing in Media Later //Default Cache Directory to save Album Art to use them for writing in Media Later
val imageDir:String val imageDir:String by lazy { mainActivity
get() = mainActivity.externalCacheDir?.absolutePath + File.separator + .externalCacheDir?.absolutePath + File.separator +
".Images" + File.separator ".Images" + File.separator }
@Provides @Provides
@Singleton
fun databaseDAO(@ApplicationContext appContext: Context):DatabaseDAO{ fun databaseDAO(@ApplicationContext appContext: Context):DatabaseDAO{
return DownloadRecordDatabase.getInstance(appContext).databaseDAO return DownloadRecordDatabase.getInstance(appContext).databaseDAO
} }

View File

@ -23,10 +23,6 @@ import android.net.ConnectivityManager
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.os.Build import android.os.Build
import android.util.Log 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 android.widget.ImageView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.net.toUri import androidx.core.net.toUri
@ -50,9 +46,9 @@ import kotlinx.coroutines.launch
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
fun loadAllImages(context: Context?, images:ArrayList<String>? = null ) { fun loadAllImages(context: Context?, images:List<String>? = null,source:Source) {
val serviceIntent = Intent(context, ForegroundService::class.java) val serviceIntent = Intent(context, ForegroundService::class.java)
images?.let { serviceIntent.putStringArrayListExtra("imagesList",it) } images?.let { serviceIntent.putStringArrayListExtra("imagesList",(it + source.name) as ArrayList<String>) }
context?.let { ContextCompat.startForegroundService(it, serviceIntent) } 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(){ fun showNoConnectionAlert(){
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
mainActivity.apply { mainActivity.apply {

View File

@ -25,10 +25,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.*
import android.os.Handler
import android.os.IBinder
import android.os.PowerManager
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat 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.ID3v1Tag
import com.mpatric.mp3agic.ID3v24Tag import com.mpatric.mp3agic.ID3v24Tag
import com.mpatric.mp3agic.Mp3File import com.mpatric.mp3agic.Mp3File
import com.shabinder.spotiflyer.MainActivity
import com.shabinder.spotiflyer.R import com.shabinder.spotiflyer.R
import com.shabinder.spotiflyer.models.DownloadObject import com.shabinder.spotiflyer.models.DownloadObject
import com.shabinder.spotiflyer.models.TrackDetails 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
import com.shabinder.spotiflyer.utils.Provider.imageDir import com.shabinder.spotiflyer.utils.Provider.imageDir
import com.shabinder.spotiflyer.utils.copyTo import com.shabinder.spotiflyer.utils.copyTo
@ -80,45 +77,38 @@ class ForegroundService : Service(){
private var wakeLock: PowerManager.WakeLock? = null private var wakeLock: PowerManager.WakeLock? = null
private var isServiceStarted = false private var isServiceStarted = false
var notificationLine = 0 var notificationLine = 0
val messageList = mutableListOf("","","","") var messageList = mutableListOf("", "", "", "")
private var pendingIntent:PendingIntent? = null private var cancelIntent:PendingIntent? = null
override fun onBind(intent: Intent): IBinder? { override fun onBind(intent: Intent): IBinder? = null
return null
}
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
val notificationIntent = Intent(this, MainActivity::class.java) val intent = Intent(
pendingIntent = PendingIntent.getActivity(
this, 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 downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
ytDownloader = YoutubeDownloader() ytDownloader = YoutubeDownloader()
val fetchConfiguration = initialiseFetch()
FetchConfiguration.Builder(this)
.setDownloadConcurrentLimit(4)
.build()
Fetch.setDefaultInstanceConfiguration(fetchConfiguration)
fetch = Fetch.getDefaultInstance()
fetch.addListener(fetchListener)
//clearing all not completed Downloads
//Starting fresh
fetch.removeAll()
startForeground() startForeground()
} }
@SuppressLint("WakelockTimeout") @SuppressLint("WakelockTimeout")
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
// Send a notification that service is started // Send a notification that service is started
Log.i(tag,"Service Started.") Log.i(tag, "Service Started.")
startForeground() startForeground()
val downloadObjects: ArrayList<DownloadObject>? = (intent.getParcelableArrayListExtra("object") ?: intent.extras?.getParcelableArrayList("object"))
val imagesList: ArrayList<String>? = (intent.getStringArrayListExtra("imagesList") ?: intent.extras?.getStringArrayList("imagesList")) if(intent.action == "kill") killService()
val downloadObjects: ArrayList<DownloadObject>? = (intent.getParcelableArrayListExtra("object") ?: intent.extras?.getParcelableArrayList(
"object"
))
val imagesList: ArrayList<String>? = (intent.getStringArrayListExtra("imagesList") ?: intent.extras?.getStringArrayList(
"imagesList"
))
imagesList?.let{ imagesList?.let{
serviceScope.launch { serviceScope.launch {
@ -137,7 +127,7 @@ class ForegroundService : Service(){
//Service Already Started //Service Already Started
START_STICKY START_STICKY
} else{ } else{
Log.i(tag,"Starting the foreground service task") Log.i(tag, "Starting the foreground service task")
isServiceStarted = true isServiceStarted = true
wakeLock = wakeLock =
(getSystemService(Context.POWER_SERVICE) as PowerManager).run { (getSystemService(Context.POWER_SERVICE) as PowerManager).run {
@ -171,20 +161,19 @@ class ForegroundService : Service(){
format?.let { format?.let {
val url: String = format.url() val url: String = format.url()
Log.i("DHelper Link Found", url) Log.i("DHelper Link Found", url)
serviceScope.launch { val request= Request(url, downloadObj.outputFile).apply{
val request= Request(url, downloadObj.outputFile) priority = Priority.NORMAL
request.priority = Priority.NORMAL networkType = NetworkType.ALL
request.networkType = NetworkType.ALL }
fetch.enqueue(request, fetch.enqueue(request,
{ {
requestMap[it] = downloadObj.trackDetails requestMap[it] = downloadObj.trackDetails
Log.i(tag, "Enqueuing Download") Log.i(tag, "Enqueuing Download")
}, },
{ {
Log.i(tag, "Enqueuing Error:${it.throwable.toString()}")} Log.i(tag, "Enqueuing Error:${it.throwable.toString()}")
)
} }
)
} }
}catch (e: com.github.kiulian.downloader.YoutubeException){ }catch (e: com.github.kiulian.downloader.YoutubeException){
Log.i("Service YT Error", e.message.toString()) Log.i("Service YT Error", e.message.toString())
@ -196,25 +185,16 @@ class ForegroundService : Service(){
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
if(converted == total){ if(converted == total){
Handler().postDelayed({ Handler(Looper.myLooper()!!).postDelayed({
Log.i(tag,"Service destroyed.") killService()
cleanFiles(File(defaultDir)) }, 5000)
releaseWakeLock()
stopForeground(true)
},2000)
} }
} }
override fun onTaskRemoved(rootIntent: Intent?) { override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent) super.onTaskRemoved(rootIntent)
if(converted == total ){ if(converted == total ){
Log.i(tag,"Service Removed.") killService()
cleanFiles(File(defaultDir))
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
stopForeground(true)
} else {
stopSelf()//System will automatically close it
}
} }
} }
@ -227,7 +207,11 @@ class ForegroundService : Service(){
download: Download, download: Download,
waitingOnNetwork: Boolean 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) { override fun onRemoved(download: Download) {
@ -253,7 +237,7 @@ class ForegroundService : Service(){
messageList[1] = "Downloading ${track?.title}" messageList[1] = "Downloading ${track?.title}"
notificationLine = 2 notificationLine = 2
} }
2-> { 2 -> {
messageList[2] = "Downloading ${track?.title}" messageList[2] = "Downloading ${track?.title}"
notificationLine = 3 notificationLine = 3
} }
@ -262,8 +246,12 @@ class ForegroundService : Service(){
notificationLine = 0 notificationLine = 0
} }
} }
Log.i(tag,"${track?.title} Download Started") Log.i(tag, "${track?.title} Download Started")
updateNotification() updateNotification()
val intent = Intent()
.setAction(Status.DOWNLOADING.name)
.putExtra("track", requestMap[download.request])
this@ForegroundService.sendBroadcast(intent)
} }
override fun onWaitingNetwork(download: Download) { override fun onWaitingNetwork(download: Download) {
@ -290,12 +278,13 @@ class ForegroundService : Service(){
serviceScope.launch { serviceScope.launch {
try{ try{
track?.let { convertToMp3(download.file, it) } track?.let { convertToMp3(download.file, it) }
Log.i(tag,"${track?.title} Download Completed") Log.i(tag, "${track?.title} Download Completed")
}catch (e:KotlinNullPointerException }catch (
e: KotlinNullPointerException
){ ){
Log.i(tag,"${track?.title} Download Failed! Error:Fetch!!!!") Log.i(tag, "${track?.title} Download Failed! Error:Fetch!!!!")
Log.i(tag,"${track?.title} Requesting Download thru Android DM") Log.i(tag, "${track?.title} Requesting Download thru Android DM")
downloadUsingDM(download.request.url,download.request.file, track!!) downloadUsingDM(download.request.url, download.request.file, track!!)
downloaded++ downloaded++
requestMap.remove(download.request) requestMap.remove(download.request)
} }
@ -318,9 +307,9 @@ class ForegroundService : Service(){
serviceScope.launch { serviceScope.launch {
val track = requestMap[download.request] val track = requestMap[download.request]
downloaded++ downloaded++
Log.i(tag,download.error.throwable.toString()) Log.i(tag, download.error.throwable.toString())
Log.i(tag,"${track?.title} Requesting Download thru Android DM") Log.i(tag, "${track?.title} Requesting Download thru Android DM")
downloadUsingDM(download.request.url,download.request.file, track!!) downloadUsingDM(download.request.url, download.request.file, track!!)
requestMap.remove(download.request) requestMap.remove(download.request)
} }
updateNotification() updateNotification()
@ -336,16 +325,20 @@ class ForegroundService : Service(){
downloadedBytesPerSecond: Long downloadedBytesPerSecond: Long
) { ) {
val track = requestMap[download.request] 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() // updateNotification()
} }
} }
/** /**
* If fetch Fails , Android Download Manager To RESCUE!! * 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 uri = Uri.parse(url)
val request = DownloadManager.Request(uri) val request = DownloadManager.Request(uri)
.setAllowedNetworkTypes( .setAllowedNetworkTypes(
@ -367,20 +360,25 @@ class ForegroundService : Service(){
val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1) val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
//Checking if the received broadcast is for our enqueued download by matching download id //Checking if the received broadcast is for our enqueued download by matching download id
if (downloadID == id) { if (downloadID == id) {
convertToMp3(outputDir,track) convertToMp3(outputDir, track)
converted++ converted++
//Unregister this broadcast Receiver //Unregister this broadcast Receiver
this@ForegroundService.unregisterReceiver(this) 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) *Converting Downloaded Audio (m4a) to Mp3.( Also Applying Metadata)
**/ **/
fun convertToMp3(filePath: String, track: TrackDetails){ fun convertToMp3(filePath: String, track: TrackDetails){
val intent = Intent()
.setAction("Converting")
.putExtra("track", track)
this@ForegroundService.sendBroadcast(intent)
val m4aFile = File(filePath) val m4aFile = File(filePath)
FFmpeg.executeAsync( FFmpeg.executeAsync(
@ -390,29 +388,34 @@ class ForegroundService : Service(){
RETURN_CODE_SUCCESS -> { RETURN_CODE_SUCCESS -> {
Log.i(Config.TAG, "Async command execution completed successfully.") Log.i(Config.TAG, "Async command execution completed successfully.")
m4aFile.delete() m4aFile.delete()
writeMp3Tags(filePath.substringBeforeLast('.')+".mp3",track) writeMp3Tags(filePath.substringBeforeLast('.') + ".mp3", track)
//FFMPEG task Completed //FFMPEG task Completed
} }
RETURN_CODE_CANCEL -> { RETURN_CODE_CANCEL -> {
Log.i(Config.TAG, "Async command execution cancelled by user.") Log.i(Config.TAG, "Async command execution cancelled by user.")
} }
else -> { 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) var mp3File = Mp3File(filePath)
mp3File = removeAllTags(mp3File) mp3File = removeAllTags(mp3File)
mp3File = setId3v1Tags(mp3File,track) mp3File = setId3v1Tags(mp3File, track)
mp3File = setId3v2Tags(mp3File,track) mp3File = setId3v2Tags(mp3File, track)
Log.i("Mp3Tags","saving file") Log.i("Mp3Tags", "saving file")
mp3File.save(filePath.substringBeforeLast('.')+".new.mp3") mp3File.save(filePath.substringBeforeLast('.') + ".new.mp3")
val file = File(filePath) val file = File(filePath)
file.delete() file.delete()
val newFile = File((filePath.substringBeforeLast('.')+".new.mp3")) val newFile = File((filePath.substringBeforeLast('.') + ".new.mp3"))
newFile.renameTo(file) newFile.renameTo(file)
converted++ converted++
updateNotification() updateNotification()
@ -420,7 +423,7 @@ class ForegroundService : Service(){
//Notify Download Completed //Notify Download Completed
val intent = Intent() val intent = Intent()
.setAction("track_download_completed") .setAction("track_download_completed")
.putExtra("track",track) .putExtra("track", track)
this@ForegroundService.sendBroadcast(intent) this@ForegroundService.sendBroadcast(intent)
//All tasks completed (REST IN PEACE) //All tasks completed (REST IN PEACE)
@ -439,13 +442,15 @@ class ForegroundService : Service(){
.setSmallIcon(R.drawable.down_arrowbw) .setSmallIcon(R.drawable.down_arrowbw)
.setSubText("Total: $total Completed:$converted") .setSubText("Total: $total Completed:$converted")
.setNotificationSilent() .setNotificationSilent()
.setStyle(NotificationCompat.InboxStyle() .setStyle(
NotificationCompat.InboxStyle()
// .setBigContentTitle("Speed: $speed KB/s") // .setBigContentTitle("Speed: $speed KB/s")
.addLine(messageList[0]) .addLine(messageList[0])
.addLine(messageList[1]) .addLine(messageList[1])
.addLine(messageList[2]) .addLine(messageList[2])
.addLine(messageList[3])) .addLine(messageList[3])
.setContentIntent(pendingIntent) )
.addAction(R.drawable.ic_baseline_cancel_24,"Exit",cancelIntent)
.build() .build()
mNotificationManager.notify(notificationId, notification) mNotificationManager.notify(notificationId, notification)
} }
@ -479,9 +484,9 @@ class ForegroundService : Service(){
val fis = FileInputStream(track.albumArt) val fis = FileInputStream(track.albumArt)
fis.read(bytesArray) //read file into bytes[] fis.read(bytesArray) //read file into bytes[]
fis.close() fis.close()
id3v2Tag.setAlbumImage(bytesArray,"image/jpeg") id3v2Tag.setAlbumImage(bytesArray, "image/jpeg")
}catch (e:java.io.FileNotFoundException){ }catch (e: java.io.FileNotFoundException){
Log.i("Error","Couldn't Write Mp3 Album Art") Log.i("Error", "Couldn't Write Mp3 Album Art")
} }
mp3file.id3v2Tag = id3v2Tag mp3file.id3v2Tag = id3v2Tag
return mp3file return mp3file
@ -501,7 +506,7 @@ class ForegroundService : Service(){
} }
private fun releaseWakeLock() { private fun releaseWakeLock() {
Log.i(tag,"Releasing Wake Lock") Log.i(tag, "Releasing Wake Lock")
try { try {
wakeLock?.let { wakeLock?.let {
if (it.isHeld) { if (it.isHeld) {
@ -509,7 +514,7 @@ class ForegroundService : Service(){
} }
} }
} catch (e: Exception) { } 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 isServiceStarted = false
} }
@ -531,13 +536,15 @@ class ForegroundService : Service(){
.setSmallIcon(R.drawable.down_arrowbw) .setSmallIcon(R.drawable.down_arrowbw)
.setNotificationSilent() .setNotificationSilent()
.setSubText("Total: $total Completed:$converted") .setSubText("Total: $total Completed:$converted")
.setStyle(NotificationCompat.InboxStyle() .setStyle(
NotificationCompat.InboxStyle()
// .setBigContentTitle("Speed: $speed KB/s") // .setBigContentTitle("Speed: $speed KB/s")
.addLine(messageList[0]) .addLine(messageList[0])
.addLine(messageList[1]) .addLine(messageList[1])
.addLine(messageList[2]) .addLine(messageList[2])
.addLine(messageList[3])) .addLine(messageList[3])
.setContentIntent(pendingIntent) )
.addAction(R.drawable.ic_baseline_cancel_24,"Exit",cancelIntent)
.build() .build()
startForeground(notificationId, notification) startForeground(notificationId, notification)
} }
@ -545,8 +552,10 @@ class ForegroundService : Service(){
@Suppress("SameParameterValue") @Suppress("SameParameterValue")
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
private fun createNotificationChannel(channelId: String, channelName: String): String{ private fun createNotificationChannel(channelId: String, channelName: String): String{
val chan = NotificationChannel(channelId, val chan = NotificationChannel(
channelName, NotificationManager.IMPORTANCE_DEFAULT) channelId,
channelName, NotificationManager.IMPORTANCE_DEFAULT
)
chan.lockscreenVisibility = Notification.VISIBILITY_PUBLIC chan.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
service.createNotificationChannel(chan) service.createNotificationChannel(chan)
@ -556,8 +565,8 @@ class ForegroundService : Service(){
/** /**
* Cleaning All Residual Files except Mp3 Files * Cleaning All Residual Files except Mp3 Files
**/ **/
private fun cleanFiles(dir:File) { private fun cleanFiles(dir: File) {
Log.i(tag,"Starting Cleaning in ${dir.path} ") Log.i(tag, "Starting Cleaning in ${dir.path} ")
val fList = dir.listFiles() val fList = dir.listFiles()
fList?.let { fList?.let {
for (file in fList) { for (file in fList) {
@ -565,7 +574,7 @@ class ForegroundService : Service(){
cleanFiles(file) cleanFiles(file)
} else if(file.isFile) { } else if(file.isFile) {
if(file.path.toString().substringAfterLast(".") != "mp3"){ if(file.path.toString().substringAfterLast(".") != "mp3"){
Log.i(tag,"Cleaning ${file.path}") Log.i(tag, "Cleaning ${file.path}")
file.delete() file.delete()
} }
} }
@ -581,20 +590,20 @@ class ForegroundService : Service(){
* Last Element of this List defines Its Source * Last Element of this List defines Its Source
* */ * */
val source = urlList.last() 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() val imgUri = url.toUri().buildUpon().scheme("https").build()
Glide Glide
.with(this) .with(this)
.asFile() .asFile()
.load(imgUri) .load(imgUri)
.listener(object: RequestListener<File> { .listener(object : RequestListener<File> {
override fun onLoadFailed( override fun onLoadFailed(
e: GlideException?, e: GlideException?,
model: Any?, model: Any?,
target: Target<File>?, target: Target<File>?,
isFirstResource: Boolean isFirstResource: Boolean
): Boolean { ): Boolean {
Log.i("Glide","LoadFailed") Log.i("Glide", "LoadFailed")
return false return false
} }
@ -606,20 +615,35 @@ class ForegroundService : Service(){
isFirstResource: Boolean isFirstResource: Boolean
): Boolean { ): Boolean {
serviceScope.launch { serviceScope.launch {
withContext(Dispatchers.IO){ withContext(Dispatchers.IO) {
try { try {
val file = when(source){ val file = when (source) {
"spotify" ->{ Source.Spotify.name -> {
File(imageDir, url.substringAfterLast('/') + ".jpeg") File(imageDir, url.substringAfterLast('/') + ".jpeg")
} }
"youtube" ->{ Source.YouTube.name -> {
File(imageDir, url.substringBeforeLast('/',url).substringAfterLast('/',url) + ".jpeg") File(
imageDir,
url.substringBeforeLast('/', url)
.substringAfterLast(
'/',
url
) + ".jpeg"
)
} }
"gaana" -> { Source.Gaana.name -> {
File(imageDir, (url.substringBeforeLast('/').substringAfterLast('/')) + ".jpeg") File(
imageDir,
(url.substringBeforeLast('/').substringAfterLast(
'/'
)) + ".jpeg"
)
} }
else -> File(imageDir, url.substringAfterLast('/') + ".jpeg") else -> File(
imageDir,
url.substringAfterLast('/') + ".jpeg"
)
} }
resource?.copyTo(file) resource?.copyTo(file)
} catch (e: IOException) { } 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()
}
} }

View File

@ -0,0 +1,27 @@
<!--
~ 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 <https://www.gnu.org/licenses/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2zM17,15.59L15.59,17 12,13.41 8.41,17 7,15.59 10.59,12 7,8.41 8.41,7 12,10.59 15.59,7 17,8.41 13.41,12 17,15.59z"/>
</vector>

View File

@ -0,0 +1,32 @@
<!--
~ 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 <https://www.gnu.org/licenses/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
android:width="38dp" android:height="38dp"
android:viewportWidth="512" android:viewportHeight="512">
<path android:pathData="m512,256c0,141.387 -114.613,256 -256,256s-256,-114.613 -256,-256 114.613,-256 256,-256 256,114.613 256,256zM512,256">
<aapt:attr name="android:fillColor">
<gradient android:endX="512" android:endY="256"
android:startX="0" android:startY="256" android:type="linear">
<item android:color="#748AFF" android:offset="0"/>
<item android:color="#FF3C64" android:offset="1"/>
</gradient>
</aapt:attr>
</path>
<path android:fillColor="#000" android:pathData="m256,56c-110.281,0 -200,89.719 -200,200s89.719,200 200,200 200,-89.719 200,-200 -89.719,-200 -200,-200zM256,426c-93.738,0 -170,-76.262 -170,-170s76.262,-170 170,-170 170,76.262 170,170 -76.262,170 -170,170zM256,426"/>
<path android:fillColor="#000" android:pathData="m324.18,187.82c-5.859,-5.855 -15.355,-5.855 -21.215,0l-46.965,46.965 -46.965,-46.965c-5.859,-5.855 -15.355,-5.855 -21.215,0 -5.855,5.859 -5.855,15.355 0,21.215l46.965,46.965 -46.965,46.965c-5.855,5.859 -5.855,15.355 0,21.215 2.93,2.93 6.77,4.395 10.605,4.395 3.84,0 7.68,-1.465 10.605,-4.395l46.969,-46.965 46.965,46.965c2.93,2.93 6.77,4.395 10.605,4.395 3.84,0 7.68,-1.465 10.609,-4.395 5.855,-5.859 5.855,-15.355 0,-21.215l-46.965,-46.965 46.965,-46.965c5.855,-5.859 5.855,-15.355 0,-21.215zM324.18,187.82"/>
</vector>

View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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 <https://www.gnu.org/licenses/>.
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape>
<corners android:radius="5dip" />
<gradient
android:angle="270"
android:centerColor="#ff9d9e9d"
android:centerY="0.75"
android:endColor="#ff9d9e9d"
android:startColor="#ff9d9e9d"
/>
</shape>
</item>
<item android:id="@android:id/secondaryProgress">
<clip>
<shape>
<corners android:radius="5dip" />
<gradient
android:angle="270"
android:centerColor="#80ffb600"
android:centerY="0.75"
android:endColor="#80ffb600"
android:startColor="#80ffb600"
/>
</shape>
</clip>
</item>
<item
android:id="@android:id/progress"
>
<clip>
<shape>
<corners
android:radius="5dip" />
<gradient
android:angle="270"
android:endColor="#2196f3"
android:startColor="#2196f3" />
</shape>
</clip>
</item>
</layer-list>

View File

@ -0,0 +1,133 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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 <https://www.gnu.org/licenses/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/titleTextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:textSize="17sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Title" />
<ProgressBar
android:id="@+id/progressBar"
style="@android:style/Widget.Material.ProgressBar.Horizontal"
android:layout_width="0dp"
android:layout_height="5dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:max="100"
android:progress="0"
android:progressDrawable="@drawable/progress_bar"
app:layout_constraintBottom_toBottomOf="@+id/actionButton"
app:layout_constraintEnd_toStartOf="@+id/actionButton"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/actionButton" />
<Button
android:id="@+id/actionButton"
style="?android:attr/borderlessButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:text="Retry"
android:textColor="@color/colorAccent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/titleTextView"
tools:ignore="HardcodedText" />
<TextView
android:id="@+id/progress_TextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toTopOf="@+id/status_TextView"
app:layout_constraintEnd_toStartOf="@+id/actionButton"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/progressBar"
app:layout_constraintTop_toBottomOf="@+id/progressBar"
tools:text="10%" />
<TextView
android:id="@+id/downloadSpeedTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toTopOf="@+id/status_TextView"
app:layout_constraintEnd_toEndOf="@+id/progressBar"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toEndOf="@+id/progress_TextView"
app:layout_constraintTop_toBottomOf="@+id/progressBar"
tools:text="204 MB/s" />
<TextView
android:id="@+id/remaining_TextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/status_TextView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/progress_TextView"
tools:text="10s" />
<TextView
android:id="@+id/status_TextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:textStyle="italic|bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/remaining_TextView"
app:layout_constraintTop_toBottomOf="@+id/actionButton"
tools:text="Status" />
<View
android:layout_width="0dp"
android:layout_height="1dp"
android:background="@android:color/darker_gray"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -54,7 +54,7 @@
app:layout_anchor="@+id/appbar" app:layout_anchor="@+id/appbar"
app:layout_anchorGravity="bottom|center" app:layout_anchorGravity="bottom|center"
app:maxImageSize="38dp" app:maxImageSize="38dp"
app:rippleColor="@color/colorPrimaryDark" android:clickable="false"
app:srcCompat="@drawable/ic_refresh" app:srcCompat="@drawable/ic_refresh"
app:tint="@null" /> app:tint="@null" />

View File

@ -19,7 +19,7 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="70dp" android:layout_height="wrap_content"
android:layout_marginBottom="12dp" android:layout_marginBottom="12dp"
android:background="#000000"> android:background="#000000">
@ -50,7 +50,7 @@
android:textAppearance="@style/TextAppearance.AppTheme.Headline4" android:textAppearance="@style/TextAppearance.AppTheme.Headline4"
android:textColor="#9AB3FF" android:textColor="#9AB3FF"
android:textSize="18sp" 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_constraintStart_toEndOf="@+id/imageUrl"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
@ -75,11 +75,11 @@
style="@style/TextAppearance.AppCompat.Body2" style="@style/TextAppearance.AppCompat.Body2"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="8dp" android:layout_marginEnd="16dp"
android:text="4 minutes, 20 sec" android:text="4 minutes, 20 sec"
android:textSize="12sp" android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="@+id/artist" 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_constraintStart_toEndOf="@+id/artist"
app:layout_constraintTop_toTopOf="@+id/artist" /> app:layout_constraintTop_toTopOf="@+id/artist" />
@ -87,6 +87,7 @@
android:id="@+id/btn_download" android:id="@+id/btn_download"
android:layout_width="60dp" android:layout_width="60dp"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginEnd="8dp"
android:background="@drawable/circular_background" android:background="@drawable/circular_background"
android:backgroundTint="@color/black" android:backgroundTint="@color/black"
android:scaleType="centerInside" android:scaleType="centerInside"
@ -95,5 +96,27 @@
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_arrow" /> app:srcCompat="@drawable/ic_arrow" />
<com.github.lzyzsd.circleprogress.ArcProgress
android:id="@+id/btn_download_progress"
android:layout_width="55dp"
android:layout_height="0dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="12dp"
android:padding="1dp"
android:visibility="invisible"
app:arc_angle="260"
app:arc_bottom_text="Waiting"
app:arc_bottom_text_size="9sp"
app:arc_finished_color="@color/colorPrimary"
app:arc_progress="0"
app:arc_stroke_width="2dp"
app:arc_suffix_text_padding="0dp"
app:arc_suffix_text_size="11sp"
app:arc_text_color="@color/colorPrimary"
app:arc_text_size="20sp"
app:arc_unfinished_color="@color/colorAccent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -36,6 +36,9 @@ buildscript {
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
//Kotlinx-Serialization //Kotlinx-Serialization
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" 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 // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files
} }

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
configurations.maybeCreate("default")
artifacts.add("default", file('mobile-ffmpeg.aar'))

Binary file not shown.