From a802751942b84330bca999d2d223fdc04a1c742c Mon Sep 17 00:00:00 2001 From: shabinder Date: Thu, 30 Jul 2020 12:53:56 +0530 Subject: [PATCH] Latest FFMPEG Async Tasks,Mp3 Art and Album Data,Fetch Downloader Implemented. --- .idea/dictionaries/shabinder.xml | 3 + .idea/jarRepositories.xml | 5 + app/build.gradle | 41 ++- app/src/main/AndroidManifest.xml | 12 +- .../main/java/com/shabinder/spotiflyer/App.kt | 63 ++++ .../com/shabinder/spotiflyer/MainActivity.kt | 96 +++-- .../shabinder/spotiflyer/SharedViewModel.kt | 8 +- .../downloadHelper/DownloadHelper.kt | 165 ++++----- .../spotiflyer/fragments/MainFragment.kt | 127 ++++++- .../spotiflyer/fragments/MainViewModel.kt | 4 +- .../com/shabinder/spotiflyer/models/Album.kt | 8 +- .../com/shabinder/spotiflyer/models/Artist.kt | 7 +- .../shabinder/spotiflyer/models/Copyright.kt | 7 +- .../spotiflyer/models/DownloadObject.kt | 28 ++ .../shabinder/spotiflyer/models/Episodes.kt | 6 +- .../shabinder/spotiflyer/models/Followers.kt | 5 +- .../com/shabinder/spotiflyer/models/Image.kt | 7 +- .../spotiflyer/models/LinkedTrack.kt | 6 +- .../models/PagingObjectPlaylistTrack.kt | 31 ++ .../{PagingObject.kt => PagingObjectTrack.kt} | 10 +- .../shabinder/spotiflyer/models/Playlist.kt | 8 +- .../spotiflyer/models/PlaylistTrack.kt | 6 +- .../com/shabinder/spotiflyer/models/Token.kt | 6 +- .../com/shabinder/spotiflyer/models/Track.kt | 6 +- .../spotiflyer/models/UserPrivate.kt | 6 +- .../shabinder/spotiflyer/models/UserPublic.kt | 6 +- .../recyclerView/TrackListAdapter.kt | 11 +- .../spotiflyer/utils/BindingAdapter.kt | 94 ++++- .../spotiflyer/worker/ForegroundService.kt | 343 ++++++++++++++++++ app/src/main/res/drawable/down_arrowbw.png | Bin 0 -> 12895 bytes app/src/main/res/drawable/ic_github.xml | 21 ++ app/src/main/res/drawable/ic_heart.xml | 25 ++ app/src/main/res/drawable/ic_instagram.xml | 51 +++ app/src/main/res/drawable/ic_mug.xml | 21 +- app/src/main/res/layout/main_fragment.xml | 100 ++++- app/src/main/res/layout/track_list_item.xml | 18 +- app/src/main/res/xml/app_update.xml | 25 ++ build.gradle | 20 +- 38 files changed, 1201 insertions(+), 205 deletions(-) create mode 100644 app/src/main/java/com/shabinder/spotiflyer/App.kt create mode 100644 app/src/main/java/com/shabinder/spotiflyer/models/DownloadObject.kt create mode 100644 app/src/main/java/com/shabinder/spotiflyer/models/PagingObjectPlaylistTrack.kt rename app/src/main/java/com/shabinder/spotiflyer/models/{PagingObject.kt => PagingObjectTrack.kt} (82%) create mode 100644 app/src/main/java/com/shabinder/spotiflyer/worker/ForegroundService.kt create mode 100644 app/src/main/res/drawable/down_arrowbw.png create mode 100644 app/src/main/res/drawable/ic_github.xml create mode 100644 app/src/main/res/drawable/ic_heart.xml create mode 100644 app/src/main/res/drawable/ic_instagram.xml create mode 100644 app/src/main/res/xml/app_update.xml diff --git a/.idea/dictionaries/shabinder.xml b/.idea/dictionaries/shabinder.xml index 16f2713a..599ffd82 100644 --- a/.idea/dictionaries/shabinder.xml +++ b/.idea/dictionaries/shabinder.xml @@ -1,7 +1,10 @@ + ffmpeg flyer + insta + instagram moshi musicforeveryone musicplaceholder diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml index 4dcbce12..bd3984c7 100644 --- a/.idea/jarRepositories.xml +++ b/.idea/jarRepositories.xml @@ -31,5 +31,10 @@ \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 8940d6bc..d71765eb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -20,7 +20,7 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' apply plugin: "androidx.navigation.safeargs.kotlin" - +apply plugin: 'kotlinx-serialization' android { compileSdkVersion 29 @@ -28,16 +28,14 @@ android { buildFeatures{ dataBinding = true - viewBinding = true } defaultConfig { applicationId 'com.shabinder.spotiflyer' minSdkVersion 22 targetSdkVersion 29 - versionCode 1 - versionName "1.0" - + versionCode 2 + versionName "1.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -51,15 +49,20 @@ android { targetCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8 } + lintOptions { + abortOnError false + } kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() } } + + dependencies { implementation fileTree(dir: "libs", include: ["*.jar"]) implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation 'androidx.core:core-ktx:1.3.0' + implementation 'androidx.core:core-ktx:1.3.1' implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.browser:browser:1.2.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' @@ -72,17 +75,21 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7" -// implementation "androidx.room:room-runtime:2.2.5" -// kapt "androidx.room:room-compiler:2.2.5" -// implementation "androidx.room:room-ktx:2.2.5" - implementation "com.github.bumptech.glide:glide:4.11.0" - kapt "com.github.bumptech.glide:compiler:4.11.0" + implementation "androidx.room:room-runtime:2.2.5" + kapt "androidx.room:room-compiler:2.2.5" + implementation "androidx.room:room-ktx:2.2.5" + implementation ("com.github.bumptech.glide:recyclerview-integration:4.11.0") { + transitive = true + } + kapt ("com.github.bumptech.glide:recyclerview-integration:4.11.0") { + transitive = true + } implementation 'androidx.recyclerview:recyclerview:1.1.0' implementation 'com.google.apis:google-api-services-youtube:v3-rev180-1.22.0' implementation 'com.google.oauth-client:google-oauth-client:1.22.0' - implementation 'com.spotify.android:auth:1.1.0' +// implementation 'com.spotify.android:auth:1.1.0' implementation 'com.squareup.okhttp3:okhttp:4.8.0' implementation 'com.squareup.retrofit2:retrofit:2.9.0' @@ -90,8 +97,16 @@ dependencies { implementation "com.squareup.moshi:moshi-kotlin:1.9.3" implementation "com.squareup.retrofit2:converter-moshi:2.9.0" + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" // or "kotlin-stdlib-jdk8" + implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0" // JVM dependency + + implementation 'com.mpatric:mp3agic:0.9.1' + implementation 'com.arthenica:mobile-ffmpeg-audio:4.4.LTS' + implementation 'com.shreyaspatil:EasyUpiPayment:2.2' - implementation 'com.github.sealedtx:java-youtube-downloader:2.2.2' + implementation 'com.github.sealedtx:java-youtube-downloader:2.2.3' + implementation "androidx.tonyodev.fetch2:xfetch2:3.1.4" + implementation 'com.github.javiersantos:AppUpdater:2.7' implementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava' testImplementation 'junit:junit:4.13' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 011f3294..37f088d9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -26,9 +26,12 @@ - + + + - - - +--> + + \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/App.kt b/app/src/main/java/com/shabinder/spotiflyer/App.kt new file mode 100644 index 00000000..82a589b0 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/App.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2020 Shabinder Singh + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.shabinder.spotiflyer + +import android.app.Application +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.os.Build + +/* + * 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 . + */ + +class App:Application() { + private val channelId = "ForegroundServiceChannel" + + override fun onCreate() { + super.onCreate() + createNotificationChannel() + } + + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val serviceChannel = NotificationChannel( + channelId, + "ForeGround Service Channel", + NotificationManager.IMPORTANCE_DEFAULT + ) + val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + manager.createNotificationChannel(serviceChannel) + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/MainActivity.kt b/app/src/main/java/com/shabinder/spotiflyer/MainActivity.kt index 9c675e07..5d1434cf 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/MainActivity.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/MainActivity.kt @@ -18,23 +18,26 @@ package com.shabinder.spotiflyer import android.Manifest -import android.app.DownloadManager import android.content.Context import android.content.Intent import android.content.SharedPreferences import android.net.ConnectivityManager +import android.net.Uri import android.os.Build import android.os.Bundle import android.util.Log import androidx.appcompat.app.AppCompatActivity import androidx.databinding.DataBindingUtil import androidx.lifecycle.ViewModelProvider +import com.github.javiersantos.appupdater.AppUpdater +import com.github.javiersantos.appupdater.enums.UpdateFrom import com.github.kiulian.downloader.YoutubeDownloader import com.shabinder.spotiflyer.databinding.MainActivityBinding import com.shabinder.spotiflyer.downloadHelper.DownloadHelper import com.shabinder.spotiflyer.utils.SpotifyService import com.shabinder.spotiflyer.utils.SpotifyServiceToken import com.shabinder.spotiflyer.utils.YoutubeInterface +import com.shabinder.spotiflyer.utils.createDirectory import com.shreyaspatil.EasyUpiPayment.EasyUpiPayment import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory @@ -48,12 +51,11 @@ import retrofit2.converter.moshi.MoshiConverterFactory @Suppress("DEPRECATION") -class MainActivity : AppCompatActivity() ,DownloadHelper{ +class MainActivity : AppCompatActivity(){ private lateinit var binding: MainActivityBinding private var ytDownloader : YoutubeDownloader? = null private var spotifyService : SpotifyService? = null private var spotifyServiceToken : SpotifyServiceToken? = null - private var downloadManager : DownloadManager? = null // private val redirectUri = "spotiflyer://callback" private val clientId:String = "694d8bf4f6ec420fa66ea7fb4c68f89d" private val clientSecret:String = "02ca2d4021a7452dae2328b47a6e8fe8" @@ -63,20 +65,21 @@ class MainActivity : AppCompatActivity() ,DownloadHelper{ private var token :String ="" private lateinit var sharedViewModel: SharedViewModel + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this,R.layout.main_activity) sharedViewModel = ViewModelProvider(this).get(SharedViewModel::class.java) sharedPref = this.getPreferences(Context.MODE_PRIVATE) -// if(sharedPref?.contains("token")!! && (sharedPref?.getLong("time",System.currentTimeMillis()/1000/60/60)!! < (System.currentTimeMillis()/1000/60/60)) ){ -// val savedToken = sharedPref?.getString("token","error")!! -// sharedViewModel.accessToken.value = savedToken -// Log.i("SharedPrefs Token:",savedToken) -// token = savedToken -// -// implementSpotifyService(savedToken) -// }else{authenticateSpotify()} +/* if(sharedPref?.contains("token")!! && (sharedPref?.getLong("time",System.currentTimeMillis()/1000/60/60)!! < (System.currentTimeMillis()/1000/60/60)) ){ + val savedToken = sharedPref?.getString("token","error")!! + sharedViewModel.accessToken.value = savedToken + Log.i("SharedPrefs Token:",savedToken) + token = savedToken + + implementSpotifyService(savedToken) + }else{authenticateSpotify()}*/ if(sharedViewModel.spotifyService == null){ authenticateSpotify() @@ -85,6 +88,12 @@ class MainActivity : AppCompatActivity() ,DownloadHelper{ } requestPermission() + checkIfLatestVersion() + createDir() + setUpi() + isConnected = isOnline() + sharedViewModel.isConnected.value = isConnected + Log.i("Connection Status",isConnected.toString()) //Object to download From Youtube {"https://github.com/sealedtx/java-youtube-downloader"} ytDownloader = YoutubeDownloader() @@ -92,32 +101,9 @@ class MainActivity : AppCompatActivity() ,DownloadHelper{ //Initialing Communication with Youtube YoutubeInterface.youtubeConnector() - //Getting System Download Manager - downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager - sharedViewModel.downloadManager = downloadManager - - isConnected = isOnline() - sharedViewModel.isConnected.value = isConnected - - Log.i("Connection Status",isConnected.toString()) - - - easyUpiPayment = EasyUpiPayment.Builder() - .with(this) - .setPayeeVpa("technoshab@paytm") - .setPayeeName("Shabinder Singh") - .setTransactionId("UNIQUE_TRANSACTION_ID") - .setTransactionRefId("UNIQUE_TRANSACTION_REF_ID") - .setDescription("Thanks for donating") - .setAmount("39.00") - .build() - - sharedViewModel.easyUpiPayment = easyUpiPayment - handleIntentFromExternalActivity() } - /** * Adding my own new Spotify Web Api Requests! * */ @@ -246,6 +232,48 @@ class MainActivity : AppCompatActivity() ,DownloadHelper{ } } + private fun setUpi() { + easyUpiPayment = EasyUpiPayment.Builder() + .with(this) + .setPayeeVpa("technoshab@paytm") + .setPayeeName("Shabinder Singh") + .setTransactionId("UNIQUE_TRANSACTION_ID") + .setTransactionRefId("UNIQUE_TRANSACTION_REF_ID") + .setDescription("Thanks for donating") + .setAmount("39.00") + .build() + + sharedViewModel.easyUpiPayment = easyUpiPayment + + } + + private fun createDir() { + createDirectory(DownloadHelper.defaultDir) + createDirectory(DownloadHelper.defaultDir+".Images/") + createDirectory(DownloadHelper.defaultDir+"Tracks/") + createDirectory(DownloadHelper.defaultDir+"Albums/") + createDirectory(DownloadHelper.defaultDir+"Playlists/") + } + + private fun checkIfLatestVersion() { + val appUpdater = AppUpdater(this) + .showAppUpdated(false)//true:Show App is Update Dialog + .setUpdateFrom(UpdateFrom.XML) + .setUpdateXML("https://raw.githubusercontent.com/Shabinder/SpotiFlyer/master/app/src/main/res/xml/app_update.xml") + .setCancelable(false) + .setButtonUpdateClickListener { _, _ -> + val uri: Uri = + Uri.parse("http://github.com/Shabinder/SpotiFlyer/releases") + val intent = Intent(Intent.ACTION_VIEW, uri) + startActivity(intent) + } + .setButtonDismissClickListener { dialog, _ -> + dialog.dismiss() + } + appUpdater.start() + } + + /* private fun authenticateSpotify() { val builder = AuthenticationRequest.Builder(clientId,AuthenticationResponse.Type.TOKEN,redirectUri) diff --git a/app/src/main/java/com/shabinder/spotiflyer/SharedViewModel.kt b/app/src/main/java/com/shabinder/spotiflyer/SharedViewModel.kt index 350f72ca..86a3d69d 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/SharedViewModel.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/SharedViewModel.kt @@ -17,9 +17,9 @@ package com.shabinder.spotiflyer -import android.app.DownloadManager import android.content.Context import android.content.res.Resources +import android.os.Environment import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.github.kiulian.downloader.YoutubeDownloader @@ -32,15 +32,16 @@ import com.shreyaspatil.EasyUpiPayment.EasyUpiPayment import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import java.io.File class SharedViewModel : ViewModel() { var intentString = "" var accessToken = MutableLiveData().apply { value = "" } var spotifyService : SpotifyService? = null var ytDownloader : YoutubeDownloader? = null - var downloadManager : DownloadManager? = null var isConnected = MutableLiveData().apply { value = false } var easyUpiPayment: EasyUpiPayment? = null + val defaultDir = Environment.DIRECTORY_MUSIC + File.separator + "SpotiFlyer" + File.separator + ".Images" + File.separator private var viewModelJob = Job() @@ -64,13 +65,12 @@ class SharedViewModel : ViewModel() { } fun showAlertDialog(resources:Resources,context: Context){ - val dialog = MaterialAlertDialogBuilder(context,R.style.AlertDialogTheme) + MaterialAlertDialogBuilder(context,R.style.AlertDialogTheme) .setTitle(resources.getString(R.string.title)) .setMessage(resources.getString(R.string.supporting_text)) .setPositiveButton(resources.getString(R.string.cancel)) { _, _ -> // Respond to neutral button press } - .setBackground(resources.getDrawable(R.drawable.gradient)) .show() } } \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/downloadHelper/DownloadHelper.kt b/app/src/main/java/com/shabinder/spotiflyer/downloadHelper/DownloadHelper.kt index 01e8fa0e..762e8fbf 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/downloadHelper/DownloadHelper.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/downloadHelper/DownloadHelper.kt @@ -17,22 +17,28 @@ package com.shabinder.spotiflyer.downloadHelper -import android.app.DownloadManager -import android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED -import android.net.Uri +import android.content.Context +import android.content.Intent import android.os.Environment import android.util.Log +import androidx.core.content.ContextCompat import com.github.kiulian.downloader.YoutubeDownloader import com.github.kiulian.downloader.model.formats.Format import com.github.kiulian.downloader.model.quality.AudioQuality import com.shabinder.spotiflyer.fragments.MainFragment +import com.shabinder.spotiflyer.models.DownloadObject import com.shabinder.spotiflyer.models.Track import com.shabinder.spotiflyer.utils.YoutubeInterface +import com.shabinder.spotiflyer.worker.ForegroundService import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.io.File -interface DownloadHelper { +object DownloadHelper { + + var context : Context? = null + val defaultDir = Environment.DIRECTORY_MUSIC + File.separator + "SpotiFlyer" + File.separator + private var downloadList = arrayListOf() /** * Function To Download All Tracks Available in a List @@ -40,111 +46,106 @@ interface DownloadHelper { suspend fun downloadAllTracks( type:String, subFolder: String?, - trackList: List, ytDownloader: YoutubeDownloader?, downloadManager: DownloadManager?) { - trackList.forEach { downloadTrack(null,type,subFolder,ytDownloader,downloadManager,"${it.name} ${it.artists?.get(0)?.name ?:""}") } + trackList: List, ytDownloader: YoutubeDownloader?) { + var size = trackList.size + trackList.forEach { + size-- + if(size == 0){ + downloadTrack(null,type,subFolder,ytDownloader,"${it.name} ${it.artists?.get(0)?.name ?:""}", it ,0 ) + }else{ + downloadTrack(null,type,subFolder,ytDownloader,"${it.name} ${it.artists?.get(0)?.name ?:""}", it ) + } + } } - suspend fun downloadTrack( mainFragment: MainFragment?, type:String, subFolder:String?, ytDownloader: YoutubeDownloader?, - downloadManager: DownloadManager?, - searchQuery: String + searchQuery: String, + track: Track, + index: Int? = null ) { withContext(Dispatchers.IO) { - val data = YoutubeInterface.search(searchQuery)?.get(0) - if (data == null) { - Log.i("DownloadHelper", "Youtube Request Failed!") - } else { + val data: YoutubeInterface.VideoItem = YoutubeInterface.search(searchQuery)?.get(0)!! - val video = ytDownloader?.getVideo(data.id) - //Fetching a Video Object. - val details = video?.details() - try{ - val format: Format = - video?.findAudioWithQuality(AudioQuality.medium)?.get(0) as Format - val audioUrl = format.url() - Log.i("DHelper Link Found", audioUrl) - if (audioUrl != null) { - downloadFile(audioUrl, downloadManager, details!!.title(),subFolder,type) - withContext(Dispatchers.Main){ - mainFragment?.showToast("Download Started") - } - } else { - Log.i("YT audio url is null", format.toString()) + //Fetching a Video Object. + try { + val audioUrl = getDownloadLink(AudioQuality.medium, ytDownloader, data) + withContext(Dispatchers.Main) { + mainFragment?.showToast("Starting Download") + } + downloadFile(audioUrl, searchQuery, subFolder, type, track, index,mainFragment) + } catch (e: java.lang.IndexOutOfBoundsException) { + try { + val audioUrl = getDownloadLink(AudioQuality.high, ytDownloader, data) + withContext(Dispatchers.Main) { + mainFragment?.showToast("Starting Download") } - }catch (e:ArrayIndexOutOfBoundsException){ - try{ - val format: Format = - video?.findAudioWithQuality(AudioQuality.high)?.get(0) as Format - val audioUrl = format.url() - Log.i("DHelper Link Found", audioUrl) - if (audioUrl != null) { - downloadFile(audioUrl, downloadManager, details!!.title(),subFolder,type) - withContext(Dispatchers.Main){ - mainFragment?.showToast("Download Started") - } - } else { - Log.i("YT audio url is null", format.toString()) - } - }catch (e:ArrayIndexOutOfBoundsException){ - try{ - val format: Format = - video?.findAudioWithQuality(AudioQuality.high)?.get(0) as Format - val audioUrl = format.url() - Log.i("DHelper Link Found", audioUrl) - if (audioUrl != null) { - downloadFile(audioUrl, downloadManager, details!!.title(),subFolder,type) - withContext(Dispatchers.Main){ - mainFragment?.showToast("Download Started") - } - } else { - Log.i("YT audio url is null", format.toString()) - } - }catch(e:ArrayIndexOutOfBoundsException){ - Log.i("Catch",e.toString()) + downloadFile(audioUrl, searchQuery, subFolder, type, track, index,mainFragment) + } catch (e: java.lang.IndexOutOfBoundsException) { + try { + val audioUrl = getDownloadLink(AudioQuality.low, ytDownloader, data) + withContext(Dispatchers.Main) { + mainFragment?.showToast("Starting Download") } + downloadFile(audioUrl, searchQuery, subFolder, type, track, index,mainFragment) + } catch (e: java.lang.IndexOutOfBoundsException) { + Log.i("Catch", e.toString()) } } - - } + } } - /** - * Downloading Using Android Download Manager - * */ - suspend fun downloadFile(url: String, downloadManager: DownloadManager?, title: String,subFolder: String?,type: String) { + private fun getDownloadLink(quality: AudioQuality ,ytDownloader: YoutubeDownloader?,data:YoutubeInterface.VideoItem): String { + val video = ytDownloader?.getVideo(data.id) + val format: Format = + video?.findAudioWithQuality(quality)?.get(0) as Format + Log.i("Format", video.findAudioWithQuality(AudioQuality.medium)?.get(0)!!.mimeType()) + val audioUrl:String = format.url() + Log.i("DHelper Link Found", audioUrl) + return audioUrl + } + + + private suspend fun downloadFile(url: String, title: String, subFolder: String?, type: String, track:Track, index:Int? = null,mainFragment: MainFragment?) { withContext(Dispatchers.IO) { - val audioUri = Uri.parse(url) - val outputDir:String = - File.separator + "SpotiFlyer" + File.separator + type + File.separator + (if(subFolder == null){""}else{subFolder + File.separator}) + "${removeIllegalChars(title)}.mp3" + val outputFile:String = Environment.getExternalStorageDirectory().toString() + File.separator + + DownloadHelper.defaultDir + removeIllegalChars(type) + File.separator + (if(subFolder == null){""}else{ removeIllegalChars(subFolder) + File.separator} + removeIllegalChars(track.name!!)+".m4a") - val request = DownloadManager.Request(audioUri) - .setAllowedNetworkTypes( - DownloadManager.Request.NETWORK_WIFI or - DownloadManager.Request.NETWORK_MOBILE + if(!File(removeIllegalChars(outputFile.substringBeforeLast('.')) +".mp3").exists()){ + val downloadObject = DownloadObject( + track = track, + url = url, + outputDir = outputFile ) - .setAllowedOverRoaming(false) - .setTitle(title) - .setDescription("Spotify Downloader Working Up here...") - .setDestinationInExternalPublicDir(Environment.DIRECTORY_MUSIC, outputDir) - .setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED) - downloadManager?.enqueue(request) - Log.i("DownloadManager", "Download Request Sent") - + Log.i("DH",outputFile) + if(index==null){ + downloadList.add(downloadObject) + }else{ + downloadList.add(downloadObject) + startService(context!!, downloadList) + downloadList = arrayListOf() + } + }else{withContext(Dispatchers.Main){mainFragment?.showToast("${track.name} is already Downloaded")}} } } + private fun startService(context:Context,list: ArrayList) { + val serviceIntent = Intent(context, ForegroundService::class.java) + serviceIntent.putParcelableArrayListExtra("list",list) + ContextCompat.startForegroundService(context, serviceIntent) + } + /** * Removing Illegal Chars from File Name * **/ - fun removeIllegalChars(fileName: String): String? { + private fun removeIllegalChars(fileName: String): String? { val illegalCharArray = charArrayOf( '/', '\n', @@ -161,12 +162,14 @@ interface DownloadHelper { '|', '\"', '.', - ':' + ':', + '-' ) var name = fileName for (c in illegalCharArray) { name = fileName.replace(c, '_') } + name = name.replace("\\s".toRegex(), "_") return name } } \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/fragments/MainFragment.kt b/app/src/main/java/com/shabinder/spotiflyer/fragments/MainFragment.kt index 27ead4a7..88f6fba3 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/fragments/MainFragment.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/fragments/MainFragment.kt @@ -23,31 +23,42 @@ import android.content.pm.PackageManager import android.net.ConnectivityManager import android.net.Uri import android.os.Bundle +import android.os.Environment import android.text.SpannableStringBuilder import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast +import androidx.core.net.toUri import androidx.databinding.DataBindingUtil import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider +import com.bumptech.glide.Glide +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.Target import com.shabinder.spotiflyer.MainActivity import com.shabinder.spotiflyer.R import com.shabinder.spotiflyer.SharedViewModel import com.shabinder.spotiflyer.databinding.MainFragmentBinding import com.shabinder.spotiflyer.downloadHelper.DownloadHelper +import com.shabinder.spotiflyer.downloadHelper.DownloadHelper.downloadAllTracks import com.shabinder.spotiflyer.models.Track import com.shabinder.spotiflyer.recyclerView.TrackListAdapter import com.shabinder.spotiflyer.utils.SpotifyService import com.shabinder.spotiflyer.utils.bindImage +import com.shabinder.spotiflyer.utils.copyTo import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.io.File +import java.io.IOException @Suppress("DEPRECATION") -class MainFragment : Fragment(),DownloadHelper { +class MainFragment : Fragment() { private lateinit var binding:MainFragmentBinding private lateinit var mainViewModel: MainViewModel private lateinit var sharedViewModel: SharedViewModel @@ -56,12 +67,13 @@ class MainFragment : Fragment(),DownloadHelper { private var type:String = "" private var spotifyLink = "" private var i: Intent? = null + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { binding = DataBindingUtil.inflate(inflater,R.layout.main_fragment,container,false) - + DownloadHelper.context = requireContext() sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java) mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java) spotifyService = sharedViewModel.spotifyService @@ -73,13 +85,14 @@ class MainFragment : Fragment(),DownloadHelper { binding.usage.text = spanStringBuilder openSpotifyButton() + openGithubButton() + openInstaButton() binding.btnDonate.setOnClickListener { sharedViewModel.easyUpiPayment?.startPayment() } binding.btnSearch.setOnClickListener { - sharedViewModel.isConnected.value = isOnline() spotifyLink = binding.linkSearch.text.toString() val link = spotifyLink.substringAfterLast('/', "Error").substringBefore('?') @@ -87,16 +100,15 @@ class MainFragment : Fragment(),DownloadHelper { Log.i("Fragment", "$type : $link") - if(sharedViewModel.spotifyService == null){ + if(sharedViewModel.spotifyService == null && !isOnline()){ (activity as MainActivity).authenticateSpotify() } if (type == "Error" || link == "Error") { showToast("Please Check Your Link!") - } else if(sharedViewModel.isConnected.value == false){ + } else if(!isOnline()){ sharedViewModel.showAlertDialog(resources,requireContext()) - } - else { + } else { adapter = TrackListAdapter() binding.trackList.adapter = adapter adapter.sharedViewModel = sharedViewModel @@ -106,7 +118,9 @@ class MainFragment : Fragment(),DownloadHelper { if(mainViewModel.searchLink == spotifyLink){ //it's a Device Configuration Change adapterConfig(mainViewModel.trackList) - bindImage(binding.imageView,mainViewModel.coverUrl) + sharedViewModel.uiScope.launch { + bindImage(binding.imageView,mainViewModel.coverUrl) + } }else{ when (type) { "track" -> { @@ -121,15 +135,14 @@ class MainFragment : Fragment(),DownloadHelper { adapterConfig(trackList) binding.btnDownloadAll.setOnClickListener { + showToast("Starting Download in Few Seconds") sharedViewModel.uiScope.launch { withContext(Dispatchers.IO) { downloadAllTracks( "Tracks", null, trackList, - sharedViewModel.ytDownloader, - sharedViewModel.downloadManager - ) + sharedViewModel.ytDownloader) } } } @@ -142,22 +155,21 @@ class MainFragment : Fragment(),DownloadHelper { sharedViewModel.uiScope.launch { val albumObject = sharedViewModel.getAlbumDetails(link) val trackList = mutableListOf() - albumObject!!.tracks?.items?.forEach { trackList.add(it!!) } + albumObject!!.tracks?.items?.forEach { trackList.add(it) } mainViewModel.trackList = trackList mainViewModel.coverUrl = albumObject.images?.get(0)!!.url!! bindImage(binding.imageView,mainViewModel.coverUrl) adapter.isAlbum = true adapterConfig(trackList) binding.btnDownloadAll.setOnClickListener { + showToast("Starting Download in Few Seconds") sharedViewModel.uiScope.launch { withContext(Dispatchers.IO) { downloadAllTracks( "Albums", albumObject.name, trackList, - sharedViewModel.ytDownloader, - sharedViewModel.downloadManager - ) + sharedViewModel.ytDownloader) } } } @@ -171,21 +183,21 @@ class MainFragment : Fragment(),DownloadHelper { sharedViewModel.uiScope.launch { val playlistObject = sharedViewModel.getPlaylistDetails(link) val trackList = mutableListOf() - playlistObject!!.tracks?.items!!.forEach { trackList.add(it?.track!!) } + playlistObject!!.tracks?.items!!.forEach { trackList.add(it.track!!) } mainViewModel.trackList = trackList mainViewModel.coverUrl = playlistObject.images?.get(0)!!.url!! bindImage(binding.imageView,mainViewModel.coverUrl) adapterConfig(trackList) binding.btnDownloadAll.setOnClickListener { + showToast("Starting Download in Few Seconds") sharedViewModel.uiScope.launch { withContext(Dispatchers.IO) { + loadAllImages(trackList) downloadAllTracks( "Playlists", playlistObject.name, trackList, - sharedViewModel.ytDownloader, - sharedViewModel.downloadManager - ) + sharedViewModel.ytDownloader) } } } @@ -213,6 +225,59 @@ class MainFragment : Fragment(),DownloadHelper { return binding.root } + /** + * Function to fetch all Images for using in mp3 tag. + **/ + private fun loadAllImages(trackList: List) { + trackList.forEach { + val imgUrl = it.album!!.images?.get(0)?.url + imgUrl?.let { + val imgUri = imgUrl.toUri().buildUpon().scheme("https").build() + Glide + .with(requireContext()) + .asFile() + .load(imgUri) + .listener(object: RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean + ): Boolean { + Log.i("Glide","LoadFailed") + return false + } + + override fun onResourceReady( + resource: File?, + model: Any?, + target: Target?, + dataSource: DataSource?, + isFirstResource: Boolean + ): Boolean { + sharedViewModel.uiScope.launch { + withContext(Dispatchers.IO){ + try { + val file = File( + Environment.getExternalStorageDirectory(), + DownloadHelper.defaultDir+".Images/" + imgUrl.substringAfterLast('/') + ".jpeg" + ) + resource?.copyTo(file) + } catch (e: IOException) { + e.printStackTrace() + } + } + } + return false + } + }).submit() + } + } + } + + /** + * Implementing button to Open Spotify App + **/ private fun openSpotifyButton() { val manager: PackageManager = requireActivity().packageManager try { @@ -232,14 +297,32 @@ class MainFragment : Fragment(),DownloadHelper { } } + private fun openGithubButton() { + val uri: Uri = + Uri.parse("http://github.com/Shabinder/SpotiFlyer") + val intent = Intent(Intent.ACTION_VIEW, uri) + binding.btnGithub.setOnClickListener { + startActivity(intent) + } + } + private fun openInstaButton() { + val uri: Uri = + Uri.parse("http://www.instagram.com/mr.shabinder") + val intent = Intent(Intent.ACTION_VIEW, uri) + binding.developerInsta.setOnClickListener { + startActivity(intent) + } + } + + /** * Configure Recycler View Adapter **/ private fun adapterConfig(trackList: List){ adapter.trackList = trackList.toList() adapter.totalItems = trackList.size + adapter.mainFragment = this adapter.notifyDataSetChanged() - } /** @@ -274,6 +357,10 @@ class MainFragment : Fragment(),DownloadHelper { fun showToast(message:String){ Toast.makeText(context,message,Toast.LENGTH_SHORT).show() } + + /** + * Util. Function To Check Connection Status + **/ private fun isOnline(): Boolean { val cm = requireActivity().getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager diff --git a/app/src/main/java/com/shabinder/spotiflyer/fragments/MainViewModel.kt b/app/src/main/java/com/shabinder/spotiflyer/fragments/MainViewModel.kt index 4a274e1f..53a91ec4 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/fragments/MainViewModel.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/fragments/MainViewModel.kt @@ -21,7 +21,7 @@ import androidx.lifecycle.ViewModel import com.shabinder.spotiflyer.models.Track class MainViewModel: ViewModel() { - var searchLink:String = "" + var searchLink: String = "" var trackList = mutableListOf() - var coverUrl:String = "" + var coverUrl: String = "" } \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/Album.kt b/app/src/main/java/com/shabinder/spotiflyer/models/Album.kt index afb70c8e..025a1e71 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/models/Album.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/Album.kt @@ -17,6 +17,10 @@ package com.shabinder.spotiflyer.models +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +@Parcelize data class Album( var album_type: String? = null, var artists: List? = null, @@ -33,6 +37,6 @@ data class Album( var popularity: Int? = null, var release_date: String? = null, var release_date_precision: String? = null, - var tracks: PagingObject? = null, + var tracks: PagingObjectTrack? = null, var type: String? = null, - var uri: String? = null) \ No newline at end of file + var uri: String? = null):Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/Artist.kt b/app/src/main/java/com/shabinder/spotiflyer/models/Artist.kt index ba70b8a5..59a9ac54 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/models/Artist.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/Artist.kt @@ -16,10 +16,15 @@ */ package com.shabinder.spotiflyer.models + +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +@Parcelize data class Artist( var external_urls: Map? = null, var href: String? = null, var id: String? = null, var name: String? = null, var type: String? = null, - var uri: String? = null) \ No newline at end of file + var uri: String? = null):Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/Copyright.kt b/app/src/main/java/com/shabinder/spotiflyer/models/Copyright.kt index b10f83a5..d16ba35d 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/models/Copyright.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/Copyright.kt @@ -16,6 +16,11 @@ */ package com.shabinder.spotiflyer.models + +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +@Parcelize data class Copyright( var text: String? = null, - var type: String? = null) \ No newline at end of file + var type: String? = null):Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/DownloadObject.kt b/app/src/main/java/com/shabinder/spotiflyer/models/DownloadObject.kt new file mode 100644 index 00000000..adcef155 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/DownloadObject.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2020 Shabinder Singh + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.shabinder.spotiflyer.models + +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +@Parcelize +data class DownloadObject( + var track: Track, + var url:String, + var outputDir:String +):Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/Episodes.kt b/app/src/main/java/com/shabinder/spotiflyer/models/Episodes.kt index 4cf5e834..804d438d 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/models/Episodes.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/Episodes.kt @@ -17,6 +17,10 @@ package com.shabinder.spotiflyer.models +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +@Parcelize data class Episodes( var audio_preview_url:String?, var description:String?, @@ -35,4 +39,4 @@ data class Episodes( var release_date_precision:String?, var type:String?, var uri:String -) \ No newline at end of file +): Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/Followers.kt b/app/src/main/java/com/shabinder/spotiflyer/models/Followers.kt index 45cae186..013dd195 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/models/Followers.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/Followers.kt @@ -17,6 +17,9 @@ package com.shabinder.spotiflyer.models +import kotlinx.serialization.Serializable + +@Serializable data class Followers( var href: String? = null, - var total: Int? = null) \ No newline at end of file + var total: Int? = null):java.io.Serializable \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/Image.kt b/app/src/main/java/com/shabinder/spotiflyer/models/Image.kt index 61f67f17..764f59ad 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/models/Image.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/Image.kt @@ -16,7 +16,12 @@ */ package com.shabinder.spotiflyer.models + +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +@Parcelize data class Image( var width: Int? = null, var height: Int? = null, - var url: String? = null) \ No newline at end of file + var url: String? = null):Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/LinkedTrack.kt b/app/src/main/java/com/shabinder/spotiflyer/models/LinkedTrack.kt index 3efa8bb8..361378d4 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/models/LinkedTrack.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/LinkedTrack.kt @@ -17,9 +17,13 @@ package com.shabinder.spotiflyer.models +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +@Parcelize data class LinkedTrack( var external_urls: Map? = null, var href: String? = null, var id: String? = null, var type: String? = null, - var uri: String? = null) \ No newline at end of file + var uri: String? = null): Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/PagingObjectPlaylistTrack.kt b/app/src/main/java/com/shabinder/spotiflyer/models/PagingObjectPlaylistTrack.kt new file mode 100644 index 00000000..3f298934 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/PagingObjectPlaylistTrack.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2020 Shabinder Singh + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.shabinder.spotiflyer.models + +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +@Parcelize +data class PagingObjectPlaylistTrack( + var href: String? = null, + var items: List? = null, + var limit: Int = 0, + var next: String? = null, + var offset: Int = 0, + var previous: String? = null, + var total: Int = 0): Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/PagingObject.kt b/app/src/main/java/com/shabinder/spotiflyer/models/PagingObjectTrack.kt similarity index 82% rename from app/src/main/java/com/shabinder/spotiflyer/models/PagingObject.kt rename to app/src/main/java/com/shabinder/spotiflyer/models/PagingObjectTrack.kt index 603b4319..004a79ec 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/models/PagingObject.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/PagingObjectTrack.kt @@ -17,11 +17,15 @@ package com.shabinder.spotiflyer.models -data class PagingObject( +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +@Parcelize +data class PagingObjectTrack( var href: String? = null, - var items: List? = null, + var items: List? = null, var limit: Int = 0, var next: String? = null, var offset: Int = 0, var previous: String? = null, - var total: Int = 0) \ No newline at end of file + var total: Int = 0):Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/Playlist.kt b/app/src/main/java/com/shabinder/spotiflyer/models/Playlist.kt index 1cc01e6b..4842ae91 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/models/Playlist.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/Playlist.kt @@ -17,7 +17,11 @@ package com.shabinder.spotiflyer.models +import android.os.Parcelable import com.squareup.moshi.Json +import kotlinx.android.parcel.Parcelize + +@Parcelize data class Playlist( @Json(name = "collaborative")var is_collaborative: Boolean? = null, var description: String? = null, @@ -30,6 +34,6 @@ data class Playlist( var owner: UserPublic? = null, @Json(name = "public")var is_public: Boolean? = null, var snapshot_id: String? = null, - var tracks: PagingObject? = null, + var tracks: PagingObjectPlaylistTrack? = null, var type: String? = null, - var uri: String? = null) \ No newline at end of file + var uri: String? = null): Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/PlaylistTrack.kt b/app/src/main/java/com/shabinder/spotiflyer/models/PlaylistTrack.kt index 0eba9d32..56a5d103 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/models/PlaylistTrack.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/PlaylistTrack.kt @@ -17,8 +17,12 @@ package com.shabinder.spotiflyer.models +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +@Parcelize data class PlaylistTrack( var added_at: String? = null, var added_by: UserPublic? = null, var track: Track? = null, - var is_local: Boolean? = null) \ No newline at end of file + var is_local: Boolean? = null): Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/Token.kt b/app/src/main/java/com/shabinder/spotiflyer/models/Token.kt index 0b69ef32..c2fdb85b 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/models/Token.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/Token.kt @@ -17,8 +17,12 @@ package com.shabinder.spotiflyer.models +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +@Parcelize data class Token( var access_token:String, var token_type:String, var expires_in:Int -) \ No newline at end of file +): Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/Track.kt b/app/src/main/java/com/shabinder/spotiflyer/models/Track.kt index b1ef82d8..e7e1427a 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/models/Track.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/Track.kt @@ -17,6 +17,10 @@ package com.shabinder.spotiflyer.models +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +@Parcelize data class Track( var artists: List? = null, var available_markets: List? = null, @@ -35,4 +39,4 @@ data class Track( var uri: String? = null, var album: Album? = null, var external_ids: Map? = null, - var popularity: Int? = null) \ No newline at end of file + var popularity: Int? = null):Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/UserPrivate.kt b/app/src/main/java/com/shabinder/spotiflyer/models/UserPrivate.kt index ed1db284..178798dd 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/models/UserPrivate.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/UserPrivate.kt @@ -17,6 +17,10 @@ package com.shabinder.spotiflyer.models +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +@Parcelize data class UserPrivate( val country:String, var display_name: String, @@ -28,4 +32,4 @@ data class UserPrivate( var images: List? = null, var product:String, var type: String? = null, - var uri: String? = null) \ No newline at end of file + var uri: String? = null): Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/UserPublic.kt b/app/src/main/java/com/shabinder/spotiflyer/models/UserPublic.kt index 9a6b636b..06df67cd 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/models/UserPublic.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/UserPublic.kt @@ -17,6 +17,10 @@ package com.shabinder.spotiflyer.models +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +@Parcelize data class UserPublic( var display_name: String? = null, var external_urls: Map? = null, @@ -25,4 +29,4 @@ data class UserPublic( var id: String? = null, var images: List? = null, var type: String? = null, - var uri: String? = null) \ No newline at end of file + var uri: String? = null): Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/recyclerView/TrackListAdapter.kt b/app/src/main/java/com/shabinder/spotiflyer/recyclerView/TrackListAdapter.kt index 033435e3..e6be791e 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/recyclerView/TrackListAdapter.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/recyclerView/TrackListAdapter.kt @@ -26,13 +26,14 @@ import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import com.shabinder.spotiflyer.R import com.shabinder.spotiflyer.SharedViewModel -import com.shabinder.spotiflyer.downloadHelper.DownloadHelper +import com.shabinder.spotiflyer.downloadHelper.DownloadHelper.downloadTrack import com.shabinder.spotiflyer.fragments.MainFragment import com.shabinder.spotiflyer.models.Track import com.shabinder.spotiflyer.utils.bindImage import kotlinx.coroutines.launch -class TrackListAdapter:RecyclerView.Adapter(),DownloadHelper { + +class TrackListAdapter:RecyclerView.Adapter() { var trackList = listOf() var totalItems:Int = 0 @@ -52,7 +53,9 @@ class TrackListAdapter:RecyclerView.Adapter(),Downl override fun onBindViewHolder(holder: ViewHolder, position: Int) { val item = trackList[position] if(totalItems == 1 || isAlbum){holder.coverImage.visibility = View.GONE}else{ - bindImage(holder.coverImage, item.album!!.images?.get(0)?.url) + sharedViewModel.uiScope.launch { + bindImage(holder.coverImage, item.album!!.images?.get(0)?.url) + } } holder.trackName.text = "${if(item.name!!.length > 17){"${item.name!!.subSequence(0,16)}..."}else{item.name}}" @@ -60,7 +63,7 @@ class TrackListAdapter:RecyclerView.Adapter(),Downl holder.duration.text = "${item.duration_ms/1000/60} minutes, ${(item.duration_ms/1000)%60} sec" holder.downloadBtn.setOnClickListener{ sharedViewModel.uiScope.launch { - downloadTrack(mainFragment,"Tracks",null,sharedViewModel.ytDownloader,sharedViewModel.downloadManager,"${item.name} ${item.artists?.get(0)!!.name?:""}") + downloadTrack(mainFragment,"Tracks",null,sharedViewModel.ytDownloader,"${item.name} ${item.artists?.get(0)!!.name?:""}",track = item,index = 0) } } diff --git a/app/src/main/java/com/shabinder/spotiflyer/utils/BindingAdapter.kt b/app/src/main/java/com/shabinder/spotiflyer/utils/BindingAdapter.kt index 5052ad65..876bfd18 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/utils/BindingAdapter.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/utils/BindingAdapter.kt @@ -17,22 +17,102 @@ package com.shabinder.spotiflyer.utils +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.os.Environment +import android.util.Log import android.widget.ImageView import androidx.core.net.toUri import androidx.databinding.BindingAdapter import com.bumptech.glide.Glide -import com.bumptech.glide.request.RequestOptions +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.Target import com.shabinder.spotiflyer.R +import com.shabinder.spotiflyer.downloadHelper.DownloadHelper +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.File +import java.io.FileInputStream +import java.io.IOException + @BindingAdapter("imageUrl") fun bindImage(imgView: ImageView, imgUrl: String?) { imgUrl?.let { val imgUri = imgUrl.toUri().buildUpon().scheme("https").build() - Glide.with(imgView.context) + Glide + .with(imgView.context) + .asFile() .load(imgUri) - .apply(RequestOptions() - .placeholder(R.drawable.ic_song_placeholder) - .error(R.drawable.ic_musicplaceholder)) - .into(imgView) + .placeholder(R.drawable.ic_song_placeholder) + .error(R.drawable.ic_musicplaceholder) + .listener(object:RequestListener{ + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean + ): Boolean { + Log.i("Glide","LoadFailed") + return false + } + + override fun onResourceReady( + resource: File?, + model: Any?, + target: Target?, + dataSource: DataSource?, + isFirstResource: Boolean + ): Boolean { + CoroutineScope(Dispatchers.Main).launch { + try { + val file = File( + Environment.getExternalStorageDirectory(), + DownloadHelper.defaultDir+".Images/" + imgUrl.substringAfterLast('/') + ".jpeg" + ) // the File to save , append increasing numeric counter to prevent files from getting overwritten. + val options = BitmapFactory.Options() + options.inPreferredConfig = Bitmap.Config.ARGB_8888 + val bitmap = BitmapFactory.decodeStream(FileInputStream(resource), null, options) + resource?.copyTo(file) + withContext(Dispatchers.Main){ + imgView.setImageBitmap(bitmap) +// Log.i("Glide","imageSaved") + } + } catch (e: IOException) { + e.printStackTrace() + } + } + return false + } + }).submit() + } } -} \ No newline at end of file + +/** + *Extension Function For Copying Files! + **/ +fun File.copyTo(file: File) { + inputStream().use { input -> + file.outputStream().use { output -> + input.copyTo(output) + } + } +} +fun createDirectory(dir:String){ + val yourAppDir = File(Environment.getExternalStorageDirectory(), + dir) + + if(!yourAppDir.exists() && !yourAppDir.isDirectory) + { // create empty directory + if (yourAppDir.mkdirs()) + {Log.i("CreateDir","App dir created")} + else + {Log.w("CreateDir","Unable to create app dir!")} + } + else + {Log.i("CreateDir","App dir already exists")} +} diff --git a/app/src/main/java/com/shabinder/spotiflyer/worker/ForegroundService.kt b/app/src/main/java/com/shabinder/spotiflyer/worker/ForegroundService.kt new file mode 100644 index 00000000..c97f3a71 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/worker/ForegroundService.kt @@ -0,0 +1,343 @@ +/* + * Copyright (C) 2020 Shabinder Singh + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.shabinder.spotiflyer.worker + +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.Context +import android.content.Intent +import android.os.Environment +import android.os.IBinder +import android.util.Log +import androidx.core.app.NotificationCompat +import com.arthenica.mobileffmpeg.Config +import com.arthenica.mobileffmpeg.Config.RETURN_CODE_CANCEL +import com.arthenica.mobileffmpeg.Config.RETURN_CODE_SUCCESS +import com.arthenica.mobileffmpeg.FFmpeg +import com.mpatric.mp3agic.ID3v1Tag +import com.mpatric.mp3agic.ID3v24Tag +import com.mpatric.mp3agic.Mp3File +import com.shabinder.spotiflyer.MainActivity +import com.shabinder.spotiflyer.R +import com.shabinder.spotiflyer.downloadHelper.DownloadHelper +import com.shabinder.spotiflyer.models.DownloadObject +import com.shabinder.spotiflyer.models.Track +import com.tonyodev.fetch2.* +import com.tonyodev.fetch2core.DownloadBlock +import com.tonyodev.fetch2core.Func +import kotlinx.coroutines.* +import java.io.File +import java.io.FileInputStream + +class ForegroundService : Service(){ + private val tag = "Foreground Service" + private val channelId = "SpotiFlyer: Download Service" + private var total = 0 //Total Downloads Requested + private var converted = 0//Total Files Converted + private var fetch:Fetch? = null + private var downloadList = mutableListOf() + private var serviceJob = Job() + private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob) + private val requestMap = mutableMapOf() + private val downloadMap = mutableMapOf() + private var speed :Long = 0 + + override fun onBind(intent: Intent): IBinder? { + return null + } + + override fun onCreate() { + super.onCreate() + val notificationIntent = Intent(this, MainActivity::class.java) + val pendingIntent = PendingIntent.getActivity( + this, + 0, notificationIntent, 0 + ) + val notification = NotificationCompat.Builder(this, channelId) + .setContentTitle("SpotiFlyer: Downloading Your Music") + .setSubText("Speed: $speed KB/s ") + .setNotificationSilent() + .setOnlyAlertOnce(true) + .setContentText("Total: $total Downloaded: ${total - requestMap.keys.size} Converted:$converted ") + .setSmallIcon(R.drawable.down_arrowbw) + .build() + + val fetchConfiguration = + FetchConfiguration.Builder(this) + .setDownloadConcurrentLimit(4) + .build() + + Fetch.Impl.setDefaultInstanceConfiguration(fetchConfiguration) + fetch = Fetch.getDefaultInstance() + fetch?.addListener(fetchListener) + + startForeground(1, notification) + } + + override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + // Send a notification that service is started + Log.i(tag,"Service Started.") + + //do heavy work on a background thread +// val list = intent.getSerializableExtra("list") as List + val list = intent.getParcelableArrayListExtra("list") ?: intent.extras?.getParcelableArrayList("list") + Log.i(tag,"Intent List Size: ${list!!.size}") + total += list.size + list.forEach { downloadList.add(it as DownloadObject) } + + serviceScope.launch { + withContext(Dispatchers.IO){ + for (downloadObject in downloadList) { + val request= Request(downloadObject.url, downloadObject.outputDir) + request.priority = Priority.NORMAL + request.networkType = NetworkType.ALL + + fetch?.enqueue(request, + Func { + Log.i("DownloadManager", "Download Request Sent") + requestMap[it] = downloadObject.track + downloadList.remove(downloadObject) }, + Func { + Log.i("DownloadManager", "Download Request Error:${it.throwable.toString()}")} + ) + + } + + } + } + return START_NOT_STICKY + } + + override fun onDestroy() { + super.onDestroy() + if(downloadMap.isEmpty() && converted == total){ + Log.i(tag,"Service destroyed.") + stopForeground(true) + } + } + + + override fun onTaskRemoved(rootIntent: Intent?) { + super.onTaskRemoved(rootIntent) + if(downloadMap.isEmpty() && converted == total ){ + Log.i(tag,"Service destroyed.") + stopSelf() + } + } + + private var fetchListener: FetchListener = object : FetchListener { + override fun onQueued( + download: Download, + waitingOnNetwork: Boolean + ) { + // TODO("Not yet implemented") + } + + override fun onRemoved(download: Download) { + // TODO("Not yet implemented") + } + + override fun onResumed(download: Download) { + // TODO("Not yet implemented") + } + + override fun onStarted( + download: Download, + downloadBlocks: List, + totalBlocks: Int + ) { + val track = requestMap[download.request] + Log.i(tag,"${track?.name} Download Started") + } + + override fun onWaitingNetwork(download: Download) { + // TODO("Not yet implemented") + } + + override fun onAdded(download: Download) { + // TODO("Not yet implemented") + } + + override fun onCancelled(download: Download) { + // TODO("Not yet implemented") + } + + override fun onCompleted(download: Download) { + val track = requestMap[download.request] + speed = 0 + serviceScope.launch { + convertToMp3(download.file, track!!) + } + Log.i(tag,"${track?.name} Download Completed") + requestMap.remove(download.request) + updateNotification() + } + + override fun onDeleted(download: Download) { + // TODO("Not yet implemented") + } + + override fun onDownloadBlockUpdated( + download: Download, + downloadBlock: DownloadBlock, + totalBlocks: Int + ) { + // TODO("Not yet implemented") + } + + override fun onError(download: Download, error: Error, throwable: Throwable?) { + Log.i(tag,download.error.throwable.toString()) + } + + override fun onPaused(download: Download) { + // TODO("Not yet implemented") + } + + override fun onProgress( + download: Download, + etaInMilliSeconds: Long, + downloadedBytesPerSecond: Long + ) { + val track = requestMap[download.request] + Log.i(tag,"${track?.name} ETA: ${etaInMilliSeconds/1000} sec") + speed = (downloadedBytesPerSecond/1000) + updateNotification() + } + + } + + + fun convertToMp3(filePath: String,track: Track){ + val m4aFile = File(filePath) + + val executionId = FFmpeg.executeAsync( + "-i $filePath -vn ${filePath.substringBeforeLast('.') + ".mp3"}" + ) { _, returnCode -> + when (returnCode) { + RETURN_CODE_SUCCESS -> { + Log.i(Config.TAG, "Async command execution completed successfully.") + m4aFile.delete() + writeMp3Tags(filePath.substringBeforeLast('.')+".mp3",track) + //FFMPEG task Completed + } + RETURN_CODE_CANCEL -> { + Log.i(Config.TAG, "Async command execution cancelled by user.") + } + else -> { + Log.i(Config.TAG, String.format("Async command execution failed with rc=%d.", returnCode)) + } + } + } + } + + private fun writeMp3Tags(filePath:String, track: Track){ + var mp3File = Mp3File(filePath) + mp3File = removeAllTags(mp3File) + mp3File = setId3v1Tags(mp3File,track) + mp3File = setId3v2Tags(mp3File,track) + Log.i("Mp3Tags","saving file") + mp3File.save(filePath.substringBeforeLast('.')+".new.mp3") + val file = File(filePath) + file.delete() + val newFile = File((filePath.substringBeforeLast('.')+".new.mp3")) + newFile.renameTo(file) + converted++ + updateNotification() + //All tasks completed (REST IN PEACE) + if(converted == total){ + stopForeground(false) + stopSelf() + } + + } + /** + * This is the method that can be called to update the Notification + */ + private fun updateNotification() { + val mNotificationManager: NotificationManager = + getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val notification = NotificationCompat.Builder(this, channelId) + .setContentTitle("SpotiFlyer: Downloading Your Music") + .setContentText("Total: $total Downloaded: ${total - requestMap.keys.size} Converted:$converted ") + .setSubText("Speed: $speed KB/s ") + .setNotificationSilent() + .setOnlyAlertOnce(true) + .setSmallIcon(R.drawable.down_arrowbw) + .build() + mNotificationManager.notify(1, notification) + } + + private fun setId3v1Tags(mp3File: Mp3File, track: Track): Mp3File { + val id3v1Tag = ID3v1Tag() + id3v1Tag.track = track.disc_number.toString() + val artistsList = mutableListOf() + track.artists?.forEach { artistsList.add(it!!.name!!) } + id3v1Tag.artist = artistsList.joinToString() + id3v1Tag.title = track.name + id3v1Tag.album = track.album?.name + id3v1Tag.year = track.album?.release_date + id3v1Tag.comment = "Genres:${track.album?.genres?.joinToString()}" + mp3File.id3v1Tag = id3v1Tag + return mp3File + } + private fun setId3v2Tags(mp3file: Mp3File, track: Track): Mp3File { + val id3v2Tag = ID3v24Tag() + id3v2Tag.track = track.disc_number.toString() + val artistsList = mutableListOf() + track.artists?.forEach { artistsList.add(it!!.name!!) } + id3v2Tag.artist = artistsList.joinToString() + id3v2Tag.title = track.name + id3v2Tag.album = track.album?.name + id3v2Tag.year = track.album?.release_date + id3v2Tag.comment = "Genres:${track.album?.genres?.joinToString()}" + id3v2Tag.lyrics = "Gonna Implement Soon" + val copyrights = mutableListOf() + track.album?.copyrights?.forEach { copyrights.add(it!!.type!!) } + id3v2Tag.copyright = copyrights.joinToString() + id3v2Tag.url = track.href + track.let { + val file = File( + Environment.getExternalStorageDirectory(), + DownloadHelper.defaultDir +".Images/" + (it.album!!.images?.get(0)?.url!!).substringAfterLast('/') + ".jpeg") + Log.i("Mp3Tags editing Tags",file.path) + //init array with file length + val bytesArray = ByteArray(file.length().toInt()) + val fis = FileInputStream(file) + fis.read(bytesArray) //read file into bytes[] + fis.close() + id3v2Tag.setAlbumImage(bytesArray,"image/jpeg") + } + id3v2Tag.albumImage + mp3file.id3v2Tag = id3v2Tag + return mp3file + } + private fun removeAllTags(mp3file: Mp3File): Mp3File { + if (mp3file.hasId3v1Tag()) { + mp3file.removeId3v1Tag() + } + if (mp3file.hasId3v2Tag()) { + mp3file.removeId3v2Tag() + } + if (mp3file.hasCustomTag()) { + mp3file.removeCustomTag() + } + return mp3file + } + +} \ No newline at end of file diff --git a/app/src/main/res/drawable/down_arrowbw.png b/app/src/main/res/drawable/down_arrowbw.png new file mode 100644 index 0000000000000000000000000000000000000000..b73462316e698eb8f4800db28256fe22d0c6f2d5 GIT binary patch literal 12895 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 bTVfB+O|`Xk>3|=MK#-Y{wPCS7CGvj&fQg9q literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/ic_github.xml b/app/src/main/res/drawable/ic_github.xml new file mode 100644 index 00000000..050eff17 --- /dev/null +++ b/app/src/main/res/drawable/ic_github.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_heart.xml b/app/src/main/res/drawable/ic_heart.xml new file mode 100644 index 00000000..c94771eb --- /dev/null +++ b/app/src/main/res/drawable/ic_heart.xml @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_instagram.xml b/app/src/main/res/drawable/ic_instagram.xml new file mode 100644 index 00000000..b99f87ce --- /dev/null +++ b/app/src/main/res/drawable/ic_instagram.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_mug.xml b/app/src/main/res/drawable/ic_mug.xml index 52f1237d..7fe934f1 100644 --- a/app/src/main/res/drawable/ic_mug.xml +++ b/app/src/main/res/drawable/ic_mug.xml @@ -1,5 +1,22 @@ - + + + diff --git a/app/src/main/res/layout/main_fragment.xml b/app/src/main/res/layout/main_fragment.xml index 386a17e0..220b838d 100644 --- a/app/src/main/res/layout/main_fragment.xml +++ b/app/src/main/res/layout/main_fragment.xml @@ -134,7 +134,7 @@ + + + + + + + + + + + + - - + android:id="@+id/track_list" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingTop="26dp" + android:visibility="gone" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/open_spotify" /> diff --git a/app/src/main/res/layout/track_list_item.xml b/app/src/main/res/layout/track_list_item.xml index a1197185..5a2e1c88 100644 --- a/app/src/main/res/layout/track_list_item.xml +++ b/app/src/main/res/layout/track_list_item.xml @@ -1,5 +1,22 @@ + + @@ -15,7 +32,6 @@ android:layout_width="100dp" android:layout_height="80dp" android:contentDescription="Track Image" - android:visibility="visible" android:scaleType="centerInside" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@+id/artist" diff --git a/app/src/main/res/xml/app_update.xml b/app/src/main/res/xml/app_update.xml new file mode 100644 index 00000000..28cc79a2 --- /dev/null +++ b/app/src/main/res/xml/app_update.xml @@ -0,0 +1,25 @@ + + + + + + 1.1 + 2 + https://github.com/Shabinder/SpotiFlyer/releases + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 31b62629..3243adf8 100644 --- a/build.gradle +++ b/build.gradle @@ -1,3 +1,20 @@ +/* + * 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 . + */ + // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { ext{ @@ -7,13 +24,14 @@ buildscript { repositories { google() jcenter() - + mavenCentral() } dependencies { classpath "com.android.tools.build:gradle:4.0.1" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" //safe-Args classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigationVersion" + classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files }