├── .gitignore
├── README.md
├── _config.yml
├── app
├── .gitignore
├── build.gradle
├── dependencies.gradle
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── ic_launcher-web.png
│ ├── java
│ └── com
│ │ └── olacabs
│ │ └── olaplaystudio
│ │ ├── OlaApplication.kt
│ │ ├── data
│ │ ├── DataManager.kt
│ │ ├── DataManagerImpl.kt
│ │ ├── local
│ │ │ └── PreferencesHelper.kt
│ │ ├── model
│ │ │ └── Models.kt
│ │ └── remote
│ │ │ ├── MvpService.kt
│ │ │ └── MvpServiceFactory.kt
│ │ ├── di
│ │ ├── ActivityContext.kt
│ │ ├── ApplicationContext.kt
│ │ ├── ConfigPersistent.kt
│ │ ├── PerActivity.kt
│ │ ├── PerFragment.kt
│ │ ├── component
│ │ │ ├── ActivityComponent.kt
│ │ │ ├── ApplicationComponent.kt
│ │ │ ├── ConfigPersistentComponent.kt
│ │ │ └── FragmentComponent.kt
│ │ └── module
│ │ │ ├── ActivityModule.kt
│ │ │ ├── ApplicationModule.kt
│ │ │ ├── Bindings.kt
│ │ │ └── FragmentModule.kt
│ │ ├── playback
│ │ ├── BaseMediaActivity.kt
│ │ ├── LocalPlayback.java
│ │ ├── MediaNotificationManager.kt
│ │ ├── MusicService.kt
│ │ ├── Playback.kt
│ │ ├── PlaybackManager.kt
│ │ └── utils
│ │ │ └── ResourceHelper.java
│ │ ├── ui
│ │ ├── base
│ │ │ ├── BaseFragment.kt
│ │ │ ├── BasePresenter.kt
│ │ │ ├── MvpBaseActivity.kt
│ │ │ ├── MvpView.kt
│ │ │ └── Presenter.kt
│ │ ├── library
│ │ │ ├── LibraryActivity.kt
│ │ │ ├── LibraryAdapter.kt
│ │ │ ├── LibraryPresenter.kt
│ │ │ └── LibraryView.kt
│ │ └── welcome
│ │ │ └── WelcomeActivity.kt
│ │ └── utils
│ │ ├── Common.kt
│ │ ├── NetworkUtil.kt
│ │ └── ViewUtil.kt
│ └── res
│ ├── drawable-hdpi
│ └── ic_notification_icon.png
│ ├── drawable-mdpi
│ └── ic_notification_icon.png
│ ├── drawable-xhdpi
│ └── ic_notification_icon.png
│ ├── drawable-xxhdpi
│ └── ic_notification_icon.png
│ ├── drawable-xxxhdpi
│ ├── album_placeholder.png
│ └── media_sample.jpg
│ ├── drawable
│ ├── actionbar_bg_gradient_light.xml
│ ├── actionbarbackgrounds.xml
│ ├── fullscreen_bg_gradient.xml
│ ├── ic_action_back.xml
│ ├── ic_action_v_next.xml
│ ├── ic_action_v_pause.xml
│ ├── ic_action_v_play.xml
│ ├── ic_action_v_previous.xml
│ ├── ic_equalizer1.xml
│ ├── ic_equalizer2.xml
│ ├── ic_equalizer3.xml
│ ├── ic_equalizer_anim.xml
│ ├── ic_launcher_background.xml
│ ├── ic_launcher_foreground.xml
│ ├── ic_media_download.xml
│ ├── ic_media_favorite_border.xml
│ ├── ic_media_favorite_fill.xml
│ ├── ic_media_pause.xml
│ ├── ic_media_play.xml
│ └── ic_search.xml
│ ├── font
│ └── audiowide.ttf
│ ├── layout
│ ├── activity_library.xml
│ ├── activity_welcome.xml
│ ├── content_fullscreen_player.xml
│ ├── controls_panel.xml
│ └── item_media.xml
│ ├── menu
│ └── music_toolbar.xml
│ ├── mipmap-hdpi
│ └── ic_launcher.png
│ ├── mipmap-mdpi
│ └── ic_launcher.png
│ ├── mipmap-xhdpi
│ └── ic_launcher.png
│ ├── mipmap-xxhdpi
│ └── ic_launcher.png
│ ├── mipmap-xxxhdpi
│ └── ic_launcher.png
│ ├── values-v21
│ └── styles.xml
│ └── values
│ ├── colors.xml
│ ├── strings.xml
│ └── styles.xml
├── build.gradle
├── gradle.properties
├── sample_apk
└── play_studio.apk
├── screens
├── 1.Splash.png
├── 2.MusicList.png
├── 3.PlayerFullScreen.png
├── 4.Favs.png
├── 5.Search.png
├── 6.MediaNotification.png
└── 7.LockScreen.png
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | /local.properties
3 | /.idea/workspace.xml
4 | .DS_Store
5 | /build
6 | .idea/
7 | *iml
8 | *.iml
9 | */build
10 | fastlane
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Android-MusicPlayer-MVP
2 |
3 | I wasted 12 hours straight thinking I will get a job for this project but unfortunately, it didn't happen and I don't want my effort to get wasted so the project is here. It might help someone.
4 |
5 |
6 | 
7 | 
8 |
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-cayman
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 | apply from: 'dependencies.gradle'
5 | apply plugin: 'kotlin-kapt'
6 |
7 |
8 | android {
9 | compileSdkVersion 26
10 | defaultConfig {
11 | applicationId "com.olacabs.olaplaystudio"
12 | minSdkVersion 19
13 | targetSdkVersion 26
14 | versionCode 1
15 | versionName "1.0"
16 | buildConfigField("String", "OLA_MEDIA_BASE_URL", "\"${OlaMediaBaseUrl}\"")
17 | }
18 | buildTypes {
19 | release {
20 | minifyEnabled false
21 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
22 | }
23 | }
24 | }
25 |
26 | dependencies {
27 | implementation fileTree(dir: 'libs', include: ['*.jar'])
28 |
29 | implementation supportLibs
30 | implementation networkLibs
31 | implementation rxJavaLibs
32 | implementation kotpref
33 |
34 | //Dagger
35 | implementation "com.google.dagger:dagger:$versions.dagger"
36 | compileOnly 'org.glassfish:javax.annotation:10.0-b28'
37 | kapt daggerCompiler
38 |
39 | //Kotlin
40 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
41 |
42 | //Joda Time
43 | implementation 'net.danlew:android.joda:2.9.9.1'
44 |
45 | //EventBus
46 | implementation 'org.greenrobot:eventbus:3.1.1'
47 |
48 | //Timber
49 | implementation "com.jakewharton.timber:timber:4.6.0"
50 |
51 | //Picasso
52 | implementation 'com.squareup.picasso:picasso:2.5.2'
53 | implementation 'com.jakewharton.picasso:picasso2-okhttp3-downloader:1.0.2'
54 |
55 | //Android Sliding Up Panel
56 | implementation 'com.sothree.slidinguppanel:library:3.4.0'
57 |
58 | //KenBurnsView
59 | implementation 'com.flaviofaria:kenburnsview:1.0.7'
60 |
61 | //Exo Player
62 | implementation 'com.google.android.exoplayer:exoplayer:r2.5.0'
63 |
64 | //Permissions Dispatcher
65 | implementation('com.github.hotchemi:permissionsdispatcher:3.0.1') {
66 | exclude module: "support-v13"
67 | }
68 | kapt 'com.github.hotchemi:permissionsdispatcher-processor:3.0.1'
69 |
70 | }
71 |
--------------------------------------------------------------------------------
/app/dependencies.gradle:
--------------------------------------------------------------------------------
1 | ext {
2 | versions = [
3 | support : "26.1.0",
4 | okHttp : "3.8.1",
5 | retrofit: '2.3.0',
6 | dagger : '2.11',
7 | kotpref : '2.2.0'
8 | ]
9 |
10 | supportDeps = [
11 | cardView : "com.android.support:cardview-v7:$versions.support",
12 | appcompatV7 : "com.android.support:appcompat-v7:$versions.support",
13 | design : "com.android.support:design:$versions.support",
14 | recyclerView: "com.android.support:recyclerview-v7:$versions.support",
15 | ]
16 |
17 | rxJava = [
18 | rxKotlin : 'io.reactivex.rxjava2:rxkotlin:2.1.0',
19 | rxAndroid: "io.reactivex.rxjava2:rxandroid:2.0.1"
20 | ]
21 |
22 | retrofit = [
23 | retrofit : "com.squareup.retrofit2:retrofit:$versions.retrofit",
24 | rxAdapter : "com.squareup.retrofit2:adapter-rxjava2:$versions.retrofit",
25 | gsonConverter: "com.squareup.retrofit2:converter-gson:$versions.retrofit",
26 | ]
27 |
28 | okHttp = [
29 | logger: "com.squareup.okhttp3:logging-interceptor:$versions.okHttp",
30 | okhttp: "com.squareup.okhttp3:okhttp:$versions.okHttp"
31 | ]
32 |
33 | kotpref = [
34 | kotpref: "com.chibatching.kotpref:kotpref:2.2.0",
35 | ]
36 |
37 | supportLibs = supportDeps.values()
38 | networkLibs = retrofit.values() + okHttp.values()
39 | rxJavaLibs = rxJava.values()
40 | kotpref = kotpref.values()
41 |
42 | daggerCompiler = "com.google.dagger:dagger-compiler:$versions.dagger"
43 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
20 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-web.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saisoftdev/Android-MusicPlayer-MVP/fcb453ca39e821e557eb78cd2161184b11940432/app/src/main/ic_launcher-web.png
--------------------------------------------------------------------------------
/app/src/main/java/com/olacabs/olaplaystudio/OlaApplication.kt:
--------------------------------------------------------------------------------
1 | package com.olacabs.olaplaystudio
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import android.widget.Toast
6 | import com.chibatching.kotpref.Kotpref
7 | import com.olacabs.olaplaystudio.di.component.ApplicationComponent
8 | import com.olacabs.olaplaystudio.di.component.DaggerApplicationComponent
9 | import com.olacabs.olaplaystudio.di.module.ApplicationModule
10 | import com.olacabs.olaplaystudio.utils.regOnce
11 | import net.danlew.android.joda.JodaTimeAndroid
12 | import org.greenrobot.eventbus.EventBus
13 | import org.greenrobot.eventbus.Subscribe
14 | import timber.log.Timber
15 |
16 | /**
17 | * Created by sai on 16/12/17.
18 | */
19 |
20 | open class OlaApplication : Application() {
21 |
22 | private var toast: Toast? = null
23 | private var mApplicationComponent: ApplicationComponent? = null
24 |
25 | var component: ApplicationComponent
26 | get() {
27 | if (mApplicationComponent == null) {
28 | mApplicationComponent = DaggerApplicationComponent.builder()
29 | .applicationModule(ApplicationModule(this))
30 | .build()
31 | }
32 | return mApplicationComponent as ApplicationComponent
33 | }
34 | set(applicationComponent) {
35 | mApplicationComponent = applicationComponent
36 | }
37 |
38 | override fun onCreate() {
39 | super.onCreate()
40 | JodaTimeAndroid.init(this);
41 | Kotpref.init(this)
42 | if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
43 | EventBus.getDefault().regOnce(this)
44 | }
45 |
46 | @Subscribe
47 | fun onShowToastEvent(event: ShowToastEvent) {
48 | toast?.cancel()
49 | toast = Toast.makeText(this, event.message, Toast.LENGTH_SHORT)
50 | .apply { show() }
51 | }
52 |
53 | companion object {
54 | class ShowToastEvent(val message: String)
55 |
56 | operator fun get(context: Context): OlaApplication {
57 | return context.applicationContext as OlaApplication
58 | }
59 | }
60 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/olacabs/olaplaystudio/data/DataManager.kt:
--------------------------------------------------------------------------------
1 | package com.olacabs.olaplaystudio.data
2 |
3 | import io.reactivex.disposables.Disposable
4 | import java.util.*
5 |
6 | interface DataManager {
7 | fun getMediaList(callBack: DataManagerImpl.MediaListCallBack): Disposable
8 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/olacabs/olaplaystudio/data/DataManagerImpl.kt:
--------------------------------------------------------------------------------
1 | package com.olacabs.olaplaystudio.data
2 |
3 | import com.olacabs.olaplaystudio.BuildConfig
4 | import com.olacabs.olaplaystudio.data.model.MediaDetail
5 | import com.olacabs.olaplaystudio.data.remote.MvpService
6 | import io.reactivex.android.schedulers.AndroidSchedulers
7 | import io.reactivex.disposables.Disposable
8 | import io.reactivex.schedulers.Schedulers
9 | import java.util.*
10 | import javax.inject.Inject
11 | import javax.inject.Singleton
12 |
13 | @Singleton
14 | class DataManagerImpl @Inject constructor(private val mMvpService: MvpService) : DataManager {
15 |
16 | override fun getMediaList(callBack: MediaListCallBack):Disposable {
17 | return mMvpService.getMediaList().subscribeOn(Schedulers.io())
18 | .observeOn(AndroidSchedulers.mainThread())
19 | .subscribe({ response ->
20 | if (response.isSuccessful) {
21 | if (response.body() != null)
22 | callBack.onSuccess(response.body()!!)
23 | else
24 | callBack.onError("GetMediaList response body is null")
25 | } else
26 | callBack.onError(response.errorBody().toString())
27 | }, { error ->
28 | if (BuildConfig.DEBUG) error.printStackTrace()
29 | callBack.onError(error.message ?: "GetMediaList error body is null")
30 | })
31 | }
32 |
33 |
34 | interface MediaListCallBack {
35 | fun onSuccess(listOfMedia: List)
36 | fun onError(message: String)
37 | }
38 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/olacabs/olaplaystudio/data/local/PreferencesHelper.kt:
--------------------------------------------------------------------------------
1 | package com.olacabs.olaplaystudio.data.local
2 |
3 | import android.content.Context
4 | import android.preference.PreferenceManager
5 | import com.chibatching.kotpref.KotprefModel
6 |
7 |
8 | object UserPrefs : KotprefModel() {
9 | }
10 |
11 | class AppPrefs(context: Context) {
12 | private val preferences = PreferenceManager.getDefaultSharedPreferences(context)
13 | val favList: MutableSet get() = preferences.getStringSet(PREF_FAV_LIST, emptySet())
14 |
15 | fun addToFav(item: String) {
16 | val oldList = mutableSetOf().apply {
17 | addAll(preferences.getStringSet(PREF_FAV_LIST, emptySet()))
18 | add(item)
19 | }
20 | preferences.edit().putStringSet(PREF_FAV_LIST, oldList).apply()
21 | }
22 |
23 | fun removeFromFav(item: String) {
24 | val oldList = mutableSetOf().apply {
25 | addAll(preferences.getStringSet(PREF_FAV_LIST, emptySet()))
26 | remove(item)
27 | }
28 | preferences.edit().putStringSet(PREF_FAV_LIST, oldList).apply()
29 | }
30 |
31 | companion object {
32 | private val PREF_FAV_LIST = "favList"
33 | }
34 | }
35 |
36 |
--------------------------------------------------------------------------------
/app/src/main/java/com/olacabs/olaplaystudio/data/model/Models.kt:
--------------------------------------------------------------------------------
1 | package com.olacabs.olaplaystudio.data.model
2 |
3 | import android.support.v4.media.session.PlaybackStateCompat
4 |
5 | /**
6 | * Created by sai on 22/11/17.
7 | */
8 | data class MediaDetail(val song: String?, val url: String?, val artists: String?,
9 | val cover_image: String?,
10 | var index: Int = 0,
11 | var playingIndex: Int = 0,
12 | var state: Int = PlaybackStateCompat.STATE_NONE,
13 | var fav: Boolean = false,
14 | var isDownloaded: Boolean = false)
--------------------------------------------------------------------------------
/app/src/main/java/com/olacabs/olaplaystudio/data/remote/MvpService.kt:
--------------------------------------------------------------------------------
1 | package com.olacabs.olaplaystudio.data.remote
2 |
3 |
4 | import com.olacabs.olaplaystudio.data.model.MediaDetail
5 | import io.reactivex.Observable
6 | import retrofit2.Response
7 | import retrofit2.http.GET
8 |
9 | interface MvpService {
10 |
11 | @GET("/studio")
12 | fun getMediaList(): Observable>>
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/java/com/olacabs/olaplaystudio/data/remote/MvpServiceFactory.kt:
--------------------------------------------------------------------------------
1 | package com.olacabs.olaplaystudio.data.remote
2 |
3 | import com.google.gson.FieldNamingPolicy
4 | import com.google.gson.Gson
5 | import com.google.gson.GsonBuilder
6 | import com.olacabs.olaplaystudio.BuildConfig
7 | import okhttp3.OkHttpClient
8 | import okhttp3.logging.HttpLoggingInterceptor
9 | import retrofit2.Retrofit
10 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
11 | import retrofit2.converter.gson.GsonConverterFactory
12 | import timber.log.Timber
13 |
14 | /**
15 | * Provide "make" methods to create instances of [MvpService]
16 | * and its related dependencies, such as OkHttpClient, Gson, etc.
17 | */
18 | object MvpServiceFactory {
19 |
20 | fun makeStarterService(): MvpService {
21 | return makeMvpStarterService(makeGson())
22 | }
23 |
24 | private fun makeMvpStarterService(gson: Gson): MvpService {
25 | val retrofit = Retrofit.Builder()
26 | .baseUrl(BuildConfig.OLA_MEDIA_BASE_URL)
27 | .client(makeOkHttpClient())
28 | .addConverterFactory(GsonConverterFactory.create(gson))
29 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
30 | .build()
31 | return retrofit.create(MvpService::class.java)
32 | }
33 |
34 | private fun makeOkHttpClient(): OkHttpClient {
35 |
36 | val httpClientBuilder = OkHttpClient.Builder()
37 |
38 | if (BuildConfig.DEBUG) {
39 | val loggingInterceptor = HttpLoggingInterceptor { message -> Timber.d(message) }
40 | loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
41 | httpClientBuilder.addInterceptor(loggingInterceptor)
42 | }
43 |
44 | return httpClientBuilder.build()
45 | }
46 |
47 | private fun makeGson(): Gson {
48 | return GsonBuilder()
49 | .setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
50 | .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
51 | .create()
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/app/src/main/java/com/olacabs/olaplaystudio/di/ActivityContext.kt:
--------------------------------------------------------------------------------
1 | package com.olacabs.olaplaystudio.di
2 |
3 |
4 | import javax.inject.Qualifier
5 |
6 | @Qualifier @Retention annotation class ActivityContext
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/olacabs/olaplaystudio/di/ApplicationContext.kt:
--------------------------------------------------------------------------------
1 | package com.olacabs.olaplaystudio.di
2 |
3 |
4 | import javax.inject.Qualifier
5 |
6 | @Qualifier @Retention annotation class ApplicationContext
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/olacabs/olaplaystudio/di/ConfigPersistent.kt:
--------------------------------------------------------------------------------
1 | package com.olacabs.olaplaystudio.di
2 |
3 |
4 | import javax.inject.Scope
5 |
6 | /**
7 | * A scoping annotation to permit dependencies conform to the life of the
8 | * [ConfigPersistentComponent]
9 | */
10 | @Scope @Retention annotation class ConfigPersistent
--------------------------------------------------------------------------------
/app/src/main/java/com/olacabs/olaplaystudio/di/PerActivity.kt:
--------------------------------------------------------------------------------
1 | package com.olacabs.olaplaystudio.di
2 |
3 | import javax.inject.Scope
4 |
5 | /**
6 | * A scoping annotation to permit objects whose lifetime should
7 | * conform to the life of the Activity to be memorised in the
8 | * correct component.
9 | */
10 | @Scope @Retention annotation class PerActivity
11 |
--------------------------------------------------------------------------------
/app/src/main/java/com/olacabs/olaplaystudio/di/PerFragment.kt:
--------------------------------------------------------------------------------
1 | package com.olacabs.olaplaystudio.di
2 |
3 | import javax.inject.Scope
4 |
5 | /**
6 | * A scoping annotation to permit objects whose lifetime should
7 | * conform to the life of the Fragment to be memorised in the
8 | * correct component.
9 | */
10 | @Scope @Retention annotation class PerFragment
--------------------------------------------------------------------------------
/app/src/main/java/com/olacabs/olaplaystudio/di/component/ActivityComponent.kt:
--------------------------------------------------------------------------------
1 | package com.olacabs.olaplaystudio.di.component
2 |
3 | import com.olacabs.olaplaystudio.di.PerActivity
4 | import com.olacabs.olaplaystudio.di.module.ActivityModule
5 | import com.olacabs.olaplaystudio.ui.base.MvpBaseActivity
6 | import com.olacabs.olaplaystudio.ui.library.LibraryActivity
7 | import dagger.Subcomponent
8 |
9 | @PerActivity
10 | @Subcomponent(modules = [(ActivityModule::class)])
11 | interface ActivityComponent {
12 | fun inject(baseActivity: MvpBaseActivity)
13 | fun inject(libraryActivity: LibraryActivity)
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/olacabs/olaplaystudio/di/component/ApplicationComponent.kt:
--------------------------------------------------------------------------------
1 | package com.olacabs.olaplaystudio.di.component
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import com.olacabs.olaplaystudio.data.DataManager
6 | import com.olacabs.olaplaystudio.data.local.AppPrefs
7 | import com.olacabs.olaplaystudio.data.remote.MvpService
8 | import com.olacabs.olaplaystudio.di.ApplicationContext
9 | import com.olacabs.olaplaystudio.di.module.ApplicationModule
10 | import com.olacabs.olaplaystudio.di.module.Bindings
11 | import com.squareup.picasso.Picasso
12 | import dagger.Component
13 | import javax.inject.Singleton
14 |
15 | @Singleton
16 | @Component(modules = [ApplicationModule::class, Bindings::class])
17 | interface ApplicationComponent {
18 |
19 | @ApplicationContext
20 | fun context(): Context
21 |
22 | fun application(): Application
23 |
24 | fun dataManager(): DataManager
25 |
26 | fun mvpService(): MvpService
27 |
28 | fun providePicasso(): Picasso
29 |
30 | fun provideAppPrefs(): AppPrefs
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/main/java/com/olacabs/olaplaystudio/di/component/ConfigPersistentComponent.kt:
--------------------------------------------------------------------------------
1 | package com.olacabs.olaplaystudio.di.component
2 |
3 | import com.olacabs.olaplaystudio.di.module.FragmentModule
4 | import com.olacabs.olaplaystudio.di.ConfigPersistent
5 | import com.olacabs.olaplaystudio.di.module.ActivityModule
6 | import dagger.Component
7 |
8 | /**
9 | * A dagger component that will live during the lifecycle of an Activity or Fragment but it won't
10 | * be destroy during configuration changes. Check [MvpBaseActivity] and [BaseFragment] to
11 | * see how this components survives configuration changes.
12 | * Use the [ConfigPersistent] scope to annotate dependencies that need to survive
13 | * configuration changes (for example Presenters).
14 | */
15 | @ConfigPersistent
16 | @Component(dependencies = [(ApplicationComponent::class)])
17 | interface ConfigPersistentComponent {
18 |
19 | fun activityComponent(activityModule: ActivityModule): ActivityComponent
20 |
21 | fun fragmentComponent(fragmentModule: FragmentModule): FragmentComponent
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/app/src/main/java/com/olacabs/olaplaystudio/di/component/FragmentComponent.kt:
--------------------------------------------------------------------------------
1 | package com.olacabs.olaplaystudio.di.component
2 |
3 | import com.olacabs.olaplaystudio.di.PerFragment
4 | import com.olacabs.olaplaystudio.di.module.FragmentModule
5 | import dagger.Subcomponent
6 |
7 | /**
8 | * This component inject dependencies to all Fragments across the application
9 | */
10 | @PerFragment
11 | @Subcomponent(modules = [(FragmentModule::class)])
12 | interface FragmentComponent
--------------------------------------------------------------------------------
/app/src/main/java/com/olacabs/olaplaystudio/di/module/ActivityModule.kt:
--------------------------------------------------------------------------------
1 | package com.olacabs.olaplaystudio.di.module
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import com.olacabs.olaplaystudio.di.ActivityContext
6 | import dagger.Module
7 | import dagger.Provides
8 |
9 | @Module
10 | class ActivityModule(private val mActivity: Activity){
11 |
12 | @Provides
13 | internal fun provideActivity(): Activity {
14 | return mActivity
15 | }
16 |
17 | @Provides
18 | @ActivityContext
19 | internal fun providesContext(): Context {
20 | return mActivity
21 | }
22 |
23 | }
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/java/com/olacabs/olaplaystudio/di/module/ApplicationModule.kt:
--------------------------------------------------------------------------------
1 | package com.olacabs.olaplaystudio.di.module
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import com.jakewharton.picasso.OkHttp3Downloader
6 | import com.olacabs.olaplaystudio.data.local.AppPrefs
7 | import com.olacabs.olaplaystudio.data.remote.MvpService
8 | import com.olacabs.olaplaystudio.data.remote.MvpServiceFactory
9 | import com.olacabs.olaplaystudio.di.ApplicationContext
10 | import com.squareup.picasso.Picasso
11 | import dagger.Module
12 | import dagger.Provides
13 | import javax.inject.Singleton
14 |
15 | @Module
16 | class ApplicationModule(private val mApplication: Application) {
17 |
18 | @Provides
19 | internal fun provideApplication(): Application {
20 | return mApplication
21 | }
22 |
23 | @Provides
24 | @ApplicationContext
25 | internal fun provideContext(): Context {
26 | return mApplication
27 | }
28 |
29 | @Provides
30 | @Singleton
31 | internal fun provideMvpStarterService(): MvpService {
32 | return MvpServiceFactory.makeStarterService()
33 | }
34 |
35 | @Provides
36 | @Singleton
37 | internal fun providePicasso(): Picasso {
38 | val downloader = OkHttp3Downloader(mApplication)
39 | return Picasso.Builder(mApplication).downloader(downloader).build()
40 | }
41 |
42 | @Provides
43 | @Singleton
44 | internal fun provideAppPrefs(): AppPrefs {
45 | return AppPrefs(mApplication)
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/app/src/main/java/com/olacabs/olaplaystudio/di/module/Bindings.kt:
--------------------------------------------------------------------------------
1 | package com.olacabs.olaplaystudio.di.module
2 |
3 | import com.olacabs.olaplaystudio.data.DataManager
4 | import com.olacabs.olaplaystudio.data.DataManagerImpl
5 | import dagger.Binds
6 | import dagger.Module
7 |
8 | @Module
9 | abstract class Bindings {
10 |
11 | @Binds
12 | internal abstract fun bindDataManger(manager: DataManagerImpl): DataManager
13 |
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/olacabs/olaplaystudio/di/module/FragmentModule.kt:
--------------------------------------------------------------------------------
1 | package com.olacabs.olaplaystudio.di.module
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import android.support.v4.app.Fragment
6 |
7 | import dagger.Module
8 | import dagger.Provides
9 | import com.olacabs.olaplaystudio.di.ActivityContext
10 |
11 | @Module
12 | class FragmentModule(private val mFragment: Fragment) {
13 |
14 | @Provides
15 | internal fun providesFragment(): Fragment {
16 | return mFragment
17 | }
18 |
19 | @Provides
20 | internal fun provideActivity(): Activity {
21 | return mFragment.activity
22 | }
23 |
24 | @Provides
25 | @ActivityContext
26 | internal fun providesContext(): Context {
27 | return mFragment.activity
28 | }
29 |
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/olacabs/olaplaystudio/playback/BaseMediaActivity.kt:
--------------------------------------------------------------------------------
1 | package com.olacabs.olaplaystudio.playback
2 |
3 | import android.content.ComponentName
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.content.ServiceConnection
7 | import android.os.IBinder
8 | import android.support.v4.media.session.MediaSessionCompat
9 | import com.olacabs.olaplaystudio.ui.base.MvpBaseActivity
10 | import timber.log.Timber
11 |
12 | /**
13 | * Created by saiki on 27-02-2016.
14 | */
15 |
16 | abstract class BaseMediaActivity : MvpBaseActivity() {
17 | var service: MusicService? = null
18 | private var mBound = false
19 |
20 | protected abstract fun connectToSession(token: MediaSessionCompat.Token)
21 | protected abstract fun onMusicServiceConnected(service: MusicService)
22 | protected abstract fun onMusicServiceDisconnected()
23 |
24 |
25 | override fun onStart() {
26 | super.onStart()
27 | boundService()
28 | }
29 |
30 | override fun onStop() {
31 | super.onStop()
32 | unBoundService()
33 | }
34 |
35 | private val mConnection = object : ServiceConnection {
36 |
37 | override fun onServiceConnected(className: ComponentName,
38 | service: IBinder) {
39 | val binder = service as MusicService.LocalBinder
40 | this@BaseMediaActivity.service = binder.service
41 | mBound = true
42 | Timber.d("Service connected")
43 | connectToSession(this@BaseMediaActivity.service!!.mSessionToken)
44 | onMusicServiceConnected(this@BaseMediaActivity.service!!)
45 | }
46 |
47 | override fun onServiceDisconnected(arg0: ComponentName) {
48 | mBound = false
49 | }
50 | }
51 |
52 | private fun unBoundService() {
53 | if (mBound) {
54 | unbindService(mConnection)
55 | mBound = false
56 | onMusicServiceDisconnected()
57 | }
58 | }
59 |
60 | private fun boundService() {
61 | val intent = Intent(this, MusicService::class.java)
62 | startService(intent)
63 | bindService(intent, mConnection, Context.BIND_AUTO_CREATE)
64 | }
65 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/olacabs/olaplaystudio/playback/LocalPlayback.java:
--------------------------------------------------------------------------------
1 | package com.olacabs.olaplaystudio.playback;
2 |
3 | import android.content.BroadcastReceiver;
4 | import android.content.Context;
5 | import android.content.Intent;
6 | import android.content.IntentFilter;
7 | import android.media.AudioManager;
8 | import android.net.Uri;
9 | import android.net.wifi.WifiManager;
10 | import android.support.v4.media.session.PlaybackStateCompat;
11 | import android.text.TextUtils;
12 |
13 | import com.google.android.exoplayer2.ExoPlaybackException;
14 | import com.google.android.exoplayer2.ExoPlayer;
15 | import com.google.android.exoplayer2.ExoPlayerFactory;
16 | import com.google.android.exoplayer2.PlaybackParameters;
17 | import com.google.android.exoplayer2.SimpleExoPlayer;
18 | import com.google.android.exoplayer2.Timeline;
19 | import com.google.android.exoplayer2.audio.AudioAttributes;
20 | import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
21 | import com.google.android.exoplayer2.extractor.ExtractorsFactory;
22 | import com.google.android.exoplayer2.source.ExtractorMediaSource;
23 | import com.google.android.exoplayer2.source.MediaSource;
24 | import com.google.android.exoplayer2.source.TrackGroupArray;
25 | import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
26 | import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
27 | import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
28 | import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
29 | import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
30 | import com.google.android.exoplayer2.util.Util;
31 |
32 | import timber.log.Timber;
33 |
34 | import static com.google.android.exoplayer2.C.CONTENT_TYPE_MUSIC;
35 | import static com.google.android.exoplayer2.C.USAGE_MEDIA;
36 |
37 | /**
38 | * A class that implements local media playback using {@link
39 | * com.google.android.exoplayer2.ExoPlayer}
40 | */
41 | public final class LocalPlayback implements Playback {
42 |
43 | // The volume we set the media player to when we lose audio focus, but are
44 | // allowed to reduce the volume instead of stopping playback.
45 | public static final float VOLUME_DUCK = 0.2f;
46 | // The volume we set the media player when we have audio focus.
47 | public static final float VOLUME_NORMAL = 1.0f;
48 |
49 | // we don't have audio focus, and can't duck (play at a low volume)
50 | private static final int AUDIO_NO_FOCUS_NO_DUCK = 0;
51 | // we don't have focus, but can duck (play at a low volume)
52 | private static final int AUDIO_NO_FOCUS_CAN_DUCK = 1;
53 | // we have full audio focus
54 | private static final int AUDIO_FOCUSED = 2;
55 |
56 | private final Context mContext;
57 | private final WifiManager.WifiLock mWifiLock;
58 | private boolean mPlayOnFocusGain;
59 | private Callback mCallback;
60 | private boolean mAudioNoisyReceiverRegistered;
61 | private String mCurrentMediaId;
62 |
63 | private int mCurrentAudioFocusState = AUDIO_NO_FOCUS_NO_DUCK;
64 | private final AudioManager mAudioManager;
65 | private SimpleExoPlayer mExoPlayer;
66 | private final ExoPlayerEventListener mEventListener = new ExoPlayerEventListener();
67 |
68 | // Whether to return STATE_NONE or STATE_STOPPED when mExoPlayer is null;
69 | private boolean mExoPlayerNullIsStopped = false;
70 |
71 | private final IntentFilter mAudioNoisyIntentFilter =
72 | new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
73 |
74 | private final BroadcastReceiver mAudioNoisyReceiver =
75 | new BroadcastReceiver() {
76 | @Override
77 | public void onReceive(Context context, Intent intent) {
78 | if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) {
79 | Timber.d("Headphones disconnected.");
80 | if (isPlaying()) {
81 | Intent i = new Intent(context, MusicService.class);
82 | i.setAction(MusicService.Companion.getACTION_CMD());
83 | i.putExtra(MusicService.Companion.getCMD_NAME(), MusicService.Companion.getCMD_PAUSE());
84 | mContext.startService(i);
85 | }
86 | }
87 | }
88 | };
89 |
90 | public LocalPlayback(Context context) {
91 | Context applicationContext = context.getApplicationContext();
92 | this.mContext = applicationContext;
93 |
94 | this.mAudioManager =
95 | (AudioManager) applicationContext.getSystemService(Context.AUDIO_SERVICE);
96 | // Create the Wifi lock (this does not acquire the lock, this just creates it)
97 | this.mWifiLock =
98 | ((WifiManager) applicationContext.getSystemService(Context.WIFI_SERVICE))
99 | .createWifiLock(WifiManager.WIFI_MODE_FULL, "uAmp_lock");
100 | }
101 |
102 | @Override
103 | public void start() {
104 | // Nothing to do
105 | }
106 |
107 | @Override
108 | public void stop(boolean notifyListeners) {
109 | giveUpAudioFocus();
110 | unregisterAudioNoisyReceiver();
111 | releaseResources(true);
112 | }
113 |
114 | @Override
115 | public void setState(int state) {
116 | // Nothing to do (mExoPlayer holds its own state).
117 | }
118 |
119 | @Override
120 | public int getState() {
121 | if (mExoPlayer == null) {
122 | return mExoPlayerNullIsStopped
123 | ? PlaybackStateCompat.STATE_STOPPED
124 | : PlaybackStateCompat.STATE_NONE;
125 | }
126 | switch (mExoPlayer.getPlaybackState()) {
127 | case ExoPlayer.STATE_IDLE:
128 | return PlaybackStateCompat.STATE_PAUSED;
129 | case ExoPlayer.STATE_BUFFERING:
130 | return PlaybackStateCompat.STATE_BUFFERING;
131 | case ExoPlayer.STATE_READY:
132 | return mExoPlayer.getPlayWhenReady()
133 | ? PlaybackStateCompat.STATE_PLAYING
134 | : PlaybackStateCompat.STATE_PAUSED;
135 | case ExoPlayer.STATE_ENDED:
136 | return PlaybackStateCompat.STATE_PAUSED;
137 | default:
138 | return PlaybackStateCompat.STATE_NONE;
139 | }
140 | }
141 |
142 | @Override
143 | public boolean isConnected() {
144 | return true;
145 | }
146 |
147 | @Override
148 | public boolean isPlaying() {
149 | return mPlayOnFocusGain || (mExoPlayer != null && mExoPlayer.getPlayWhenReady());
150 | }
151 |
152 | @Override
153 | public long getCurrentStreamPosition() {
154 | return mExoPlayer != null ? mExoPlayer.getCurrentPosition() : 0;
155 | }
156 |
157 | @Override
158 | public void updateLastKnownStreamPosition() {
159 | // Nothing to do. Position maintained by ExoPlayer.
160 | }
161 |
162 | @Override
163 | public void play(String item) {
164 | mPlayOnFocusGain = true;
165 | tryToGetAudioFocus();
166 | registerAudioNoisyReceiver();
167 | boolean mediaHasChanged = !TextUtils.equals(item, mCurrentMediaId);
168 | if (mediaHasChanged) {
169 | mCurrentMediaId = item;
170 | }
171 |
172 | if (mediaHasChanged || mExoPlayer == null) {
173 | releaseResources(false); // release everything except the player
174 | String source = item;
175 | if (source != null) {
176 | source = source.replaceAll(" ", "%20"); // Escape spaces for URLs
177 | }
178 |
179 | if (mExoPlayer == null) {
180 | mExoPlayer =
181 | ExoPlayerFactory.newSimpleInstance(mContext, new DefaultTrackSelector());
182 | mExoPlayer.addListener(mEventListener);
183 | }
184 |
185 | // Android "O" makes much greater use of AudioAttributes, especially
186 | // with regards to AudioFocus. All of UAMP's tracks are music, but
187 | // if your content includes spoken word such as audiobooks or podcasts
188 | // then the content type should be set to CONTENT_TYPE_SPEECH for those
189 | // tracks.
190 | final AudioAttributes audioAttributes = new AudioAttributes.Builder()
191 | .setContentType(CONTENT_TYPE_MUSIC)
192 | .setUsage(USAGE_MEDIA)
193 | .build();
194 | mExoPlayer.setAudioAttributes(audioAttributes);
195 |
196 |
197 | //For Url Redirection
198 | String userAgent = Util.getUserAgent(mContext, "OLa Music Player");
199 | // Default parameters, except allowCrossProtocolRedirects is true
200 | DefaultHttpDataSourceFactory httpDataSourceFactory = new DefaultHttpDataSourceFactory(
201 | userAgent,
202 | null /* listener */,
203 | DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS,
204 | DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS,
205 | true /* allowCrossProtocolRedirects */
206 | );
207 |
208 |
209 | // Produces DataSource instances through which media data is loaded.
210 | DefaultDataSourceFactory dataSourceFactory = new DefaultDataSourceFactory(
211 | mContext,
212 | null /* listener */,
213 | httpDataSourceFactory
214 | );
215 |
216 |
217 | // Produces Extractor instances for parsing the media data.
218 | ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory();
219 | // The MediaSource represents the media to be played.
220 | MediaSource mediaSource =
221 | new ExtractorMediaSource(
222 | Uri.parse(source), dataSourceFactory, extractorsFactory, null, null);
223 |
224 | // Prepares media to play (happens on background thread) and triggers
225 | // {@code onPlayerStateChanged} callback when the stream is ready to play.
226 | mExoPlayer.prepare(mediaSource);
227 |
228 | // If we are streaming from the internet, we want to hold a
229 | // Wifi lock, which prevents the Wifi radio from going to
230 | // sleep while the song is playing.
231 | mWifiLock.acquire();
232 | }
233 |
234 | configurePlayerState();
235 | }
236 |
237 | @Override
238 | public void pause() {
239 | // Pause player and cancel the 'foreground service' state.
240 | if (mExoPlayer != null) {
241 | mExoPlayer.setPlayWhenReady(false);
242 | }
243 | // While paused, retain the player instance, but give up audio focus.
244 | releaseResources(false);
245 | unregisterAudioNoisyReceiver();
246 | }
247 |
248 | @Override
249 | public void seekTo(long position) {
250 | Timber.d("seekTo called with %s", position);
251 | if (mExoPlayer != null) {
252 | registerAudioNoisyReceiver();
253 | mExoPlayer.seekTo(position);
254 | }
255 | }
256 |
257 | @Override
258 | public void setCallback(Callback callback) {
259 | this.mCallback = callback;
260 | }
261 |
262 | @Override
263 | public void setCurrentMediaId(String mediaId) {
264 | this.mCurrentMediaId = mediaId;
265 | }
266 |
267 | @Override
268 | public String getCurrentMediaId() {
269 | return mCurrentMediaId;
270 | }
271 |
272 | private void tryToGetAudioFocus() {
273 | Timber.d("tryToGetAudioFocus");
274 | int result =
275 | mAudioManager.requestAudioFocus(
276 | mOnAudioFocusChangeListener,
277 | AudioManager.STREAM_MUSIC,
278 | AudioManager.AUDIOFOCUS_GAIN);
279 | if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
280 | mCurrentAudioFocusState = AUDIO_FOCUSED;
281 | } else {
282 | mCurrentAudioFocusState = AUDIO_NO_FOCUS_NO_DUCK;
283 | }
284 | }
285 |
286 | private void giveUpAudioFocus() {
287 | Timber.d("giveUpAudioFocus");
288 | if (mAudioManager.abandonAudioFocus(mOnAudioFocusChangeListener)
289 | == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
290 | mCurrentAudioFocusState = AUDIO_NO_FOCUS_NO_DUCK;
291 | }
292 | }
293 |
294 | /**
295 | * Reconfigures the player according to audio focus settings and starts/restarts it. This method
296 | * starts/restarts the ExoPlayer instance respecting the current audio focus state. So if we
297 | * have focus, it will play normally; if we don't have focus, it will either leave the player
298 | * paused or set it to a low volume, depending on what is permitted by the current focus
299 | * settings.
300 | */
301 | private void configurePlayerState() {
302 | Timber.d("configurePlayerState. mCurrentAudioFocusState= %s", mCurrentAudioFocusState);
303 | if (mCurrentAudioFocusState == AUDIO_NO_FOCUS_NO_DUCK) {
304 | // We don't have audio focus and can't duck, so we have to pause
305 | pause();
306 | } else {
307 | registerAudioNoisyReceiver();
308 |
309 | if (mCurrentAudioFocusState == AUDIO_NO_FOCUS_CAN_DUCK) {
310 | // We're permitted to play, but only if we 'duck', ie: play softly
311 | mExoPlayer.setVolume(VOLUME_DUCK);
312 | } else {
313 | mExoPlayer.setVolume(VOLUME_NORMAL);
314 | }
315 |
316 | // If we were playing when we lost focus, we need to resume playing.
317 | if (mPlayOnFocusGain) {
318 | mExoPlayer.setPlayWhenReady(true);
319 | mPlayOnFocusGain = false;
320 | }
321 | }
322 | }
323 |
324 | private final AudioManager.OnAudioFocusChangeListener mOnAudioFocusChangeListener =
325 | new AudioManager.OnAudioFocusChangeListener() {
326 | @Override
327 | public void onAudioFocusChange(int focusChange) {
328 | Timber.d("onAudioFocusChange. focusChange=%s", focusChange);
329 | switch (focusChange) {
330 | case AudioManager.AUDIOFOCUS_GAIN:
331 | mCurrentAudioFocusState = AUDIO_FOCUSED;
332 | break;
333 | case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
334 | // Audio focus was lost, but it's possible to duck (i.e.: play quietly)
335 | mCurrentAudioFocusState = AUDIO_NO_FOCUS_CAN_DUCK;
336 | break;
337 | case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
338 | // Lost audio focus, but will gain it back (shortly), so note whether
339 | // playback should resume
340 | mCurrentAudioFocusState = AUDIO_NO_FOCUS_NO_DUCK;
341 | mPlayOnFocusGain = mExoPlayer != null && mExoPlayer.getPlayWhenReady();
342 | break;
343 | case AudioManager.AUDIOFOCUS_LOSS:
344 | // Lost audio focus, probably "permanently"
345 | mCurrentAudioFocusState = AUDIO_NO_FOCUS_NO_DUCK;
346 | break;
347 | }
348 |
349 | if (mExoPlayer != null) {
350 | // Update the player state based on the change
351 | configurePlayerState();
352 | }
353 | }
354 | };
355 |
356 | /**
357 | * Releases resources used by the service for playback, which is mostly just the WiFi lock for
358 | * local playback. If requested, the ExoPlayer instance is also released.
359 | *
360 | * @param releasePlayer Indicates whether the player should also be released
361 | */
362 | private void releaseResources(boolean releasePlayer) {
363 | Timber.d("releaseResources. releasePlayer=%s", releasePlayer);
364 |
365 | // Stops and releases player (if requested and available).
366 | if (releasePlayer && mExoPlayer != null) {
367 | mExoPlayer.release();
368 | mExoPlayer.removeListener(mEventListener);
369 | mExoPlayer = null;
370 | mExoPlayerNullIsStopped = true;
371 | mPlayOnFocusGain = false;
372 | }
373 |
374 | if (mWifiLock.isHeld()) {
375 | mWifiLock.release();
376 | }
377 | }
378 |
379 | private void registerAudioNoisyReceiver() {
380 | if (!mAudioNoisyReceiverRegistered) {
381 | mContext.registerReceiver(mAudioNoisyReceiver, mAudioNoisyIntentFilter);
382 | mAudioNoisyReceiverRegistered = true;
383 | }
384 | }
385 |
386 | private void unregisterAudioNoisyReceiver() {
387 | if (mAudioNoisyReceiverRegistered) {
388 | mContext.unregisterReceiver(mAudioNoisyReceiver);
389 | mAudioNoisyReceiverRegistered = false;
390 | }
391 | }
392 |
393 | private final class ExoPlayerEventListener implements ExoPlayer.EventListener {
394 | @Override
395 | public void onTimelineChanged(Timeline timeline, Object manifest) {
396 | // Nothing to do.
397 | }
398 |
399 | @Override
400 | public void onTracksChanged(
401 | TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
402 | // Nothing to do.
403 | }
404 |
405 | @Override
406 | public void onLoadingChanged(boolean isLoading) {
407 | // Nothing to do.
408 | }
409 |
410 | @Override
411 | public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
412 | switch (playbackState) {
413 | case ExoPlayer.STATE_IDLE:
414 | case ExoPlayer.STATE_BUFFERING:
415 | case ExoPlayer.STATE_READY:
416 | if (mCallback != null) {
417 | mCallback.onPlaybackStatusChanged(getState());
418 | }
419 | break;
420 | case ExoPlayer.STATE_ENDED:
421 | // The media player finished playing the current song.
422 | if (mCallback != null) {
423 | mCallback.onCompletion();
424 | }
425 | break;
426 | }
427 | }
428 |
429 | @Override
430 | public void onPlayerError(ExoPlaybackException error) {
431 | final String what;
432 | switch (error.type) {
433 | case ExoPlaybackException.TYPE_SOURCE:
434 | what = error.getSourceException().getMessage();
435 | break;
436 | case ExoPlaybackException.TYPE_RENDERER:
437 | what = error.getRendererException().getMessage();
438 | break;
439 | case ExoPlaybackException.TYPE_UNEXPECTED:
440 | what = error.getUnexpectedException().getMessage();
441 | break;
442 | default:
443 | what = "Unknown: " + error;
444 | }
445 |
446 | Timber.e("ExoPlayer error: what=%s", what);
447 | if (mCallback != null) {
448 | mCallback.onError("ExoPlayer error " + what);
449 | }
450 | }
451 |
452 | @Override
453 | public void onPositionDiscontinuity() {
454 | // Nothing to do.
455 | }
456 |
457 | @Override
458 | public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
459 | // Nothing to do.
460 | }
461 |
462 | @Override
463 | public void onRepeatModeChanged(int repeatMode) {
464 | // Nothing to do.
465 | }
466 | }
467 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/olacabs/olaplaystudio/playback/MediaNotificationManager.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2014 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.olacabs.olaplaystudio.playback
18 |
19 | import android.app.Notification
20 | import android.app.NotificationChannel
21 | import android.app.NotificationManager
22 | import android.app.PendingIntent
23 | import android.content.BroadcastReceiver
24 | import android.content.Context
25 | import android.content.Intent
26 | import android.content.IntentFilter
27 | import android.graphics.Bitmap
28 | import android.graphics.BitmapFactory
29 | import android.graphics.Color
30 | import android.graphics.drawable.Drawable
31 | import android.os.Build
32 | import android.os.RemoteException
33 | import android.support.annotation.RequiresApi
34 | import android.support.v4.app.NotificationCompat
35 | import android.support.v4.media.MediaDescriptionCompat
36 | import android.support.v4.media.MediaMetadataCompat
37 | import android.support.v4.media.app.NotificationCompat.MediaStyle
38 | import android.support.v4.media.session.MediaControllerCompat
39 | import android.support.v4.media.session.MediaSessionCompat
40 | import android.support.v4.media.session.PlaybackStateCompat
41 | import com.jakewharton.picasso.OkHttp3Downloader
42 | import com.olacabs.olaplaystudio.R
43 | import com.olacabs.olaplaystudio.playback.utils.ResourceHelper
44 | import com.olacabs.olaplaystudio.ui.library.LibraryActivity
45 | import com.squareup.picasso.Picasso
46 | import com.squareup.picasso.Target
47 | import timber.log.Timber
48 | import javax.inject.Inject
49 |
50 |
51 | /**
52 | * Keeps track of a notification and updates it automatically for a given
53 | * MediaSession. Maintaining a visible notification (usually) guarantees that the music service
54 | * won't be killed during playback.
55 | */
56 | class MediaNotificationManager(private val mService: MusicService) : BroadcastReceiver() {
57 | private var mSessionToken: MediaSessionCompat.Token? = null
58 | private var mController: MediaControllerCompat? = null
59 | private var mTransportControls: MediaControllerCompat.TransportControls? = null
60 |
61 | private var mPlaybackState: PlaybackStateCompat? = null
62 | private var mMetadata: MediaMetadataCompat? = null
63 |
64 | private var mNotificationManager: NotificationManager? = null
65 |
66 | private val mPlayIntent: PendingIntent
67 | private val mPauseIntent: PendingIntent
68 | private val mPreviousIntent: PendingIntent
69 | private val mNextIntent: PendingIntent
70 | private val mStopIntent: PendingIntent
71 |
72 | private val mNotificationColor: Int
73 |
74 | private var mStarted = false
75 |
76 | private val mCb = object : MediaControllerCompat.Callback() {
77 | override fun onPlaybackStateChanged(state: PlaybackStateCompat) {
78 | mPlaybackState = state
79 | Timber.d("Received new playback state %s", state)
80 | if (state.state == PlaybackStateCompat.STATE_STOPPED || state.state == PlaybackStateCompat.STATE_NONE) {
81 | stopNotification()
82 | } else {
83 | val notification = createNotification()
84 | if (notification != null) {
85 | mNotificationManager?.notify(NOTIFICATION_ID, notification)
86 | }
87 | }
88 | }
89 |
90 | override fun onMetadataChanged(metadata: MediaMetadataCompat?) {
91 | mMetadata = metadata
92 | Timber.d("Received new metadata %s", metadata)
93 | val notification = createNotification()
94 | if (notification != null) {
95 | mNotificationManager?.notify(NOTIFICATION_ID, notification)
96 | }
97 | }
98 |
99 | override fun onSessionDestroyed() {
100 | super.onSessionDestroyed()
101 | Timber.d("Session was destroyed, resetting to the new session token")
102 | try {
103 | updateSessionToken()
104 | } catch (e: RemoteException) {
105 | Timber.e(e, "could not connect media controller")
106 | }
107 | }
108 | }
109 |
110 | private var loadtarget: Target? = null
111 |
112 | init {
113 | updateSessionToken()
114 |
115 | mNotificationColor = ResourceHelper.getThemeColor(mService, R.attr.colorPrimary,
116 | Color.DKGRAY)
117 |
118 | mNotificationManager = mService.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
119 |
120 | val pkg = mService.packageName
121 | mPauseIntent = PendingIntent.getBroadcast(mService, REQUEST_CODE,
122 | Intent(ACTION_PAUSE).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT)
123 | mPlayIntent = PendingIntent.getBroadcast(mService, REQUEST_CODE,
124 | Intent(ACTION_PLAY).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT)
125 | mPreviousIntent = PendingIntent.getBroadcast(mService, REQUEST_CODE,
126 | Intent(ACTION_PREV).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT)
127 | mNextIntent = PendingIntent.getBroadcast(mService, REQUEST_CODE,
128 | Intent(ACTION_NEXT).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT)
129 | mStopIntent = PendingIntent.getBroadcast(mService, REQUEST_CODE,
130 | Intent(ACTION_STOP).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT)
131 |
132 | // Cancel all notifications to handle the case where the Service was killed and
133 | // restarted by the system.
134 | mNotificationManager?.cancelAll()
135 | }
136 |
137 | /**
138 | * Posts the notification and starts tracking the session to keep it
139 | * updated. The notification will automatically be removed if the session is
140 | * destroyed before [.stopNotification] is called.
141 | */
142 | fun startNotification() {
143 | if (!mStarted) {
144 | mMetadata = mController!!.metadata
145 | mPlaybackState = mController!!.playbackState
146 |
147 | // The notification must be updated after setting started to true
148 | val notification = createNotification()
149 | if (notification != null) {
150 | mController!!.registerCallback(mCb)
151 | val filter = IntentFilter()
152 | filter.addAction(ACTION_NEXT)
153 | filter.addAction(ACTION_PAUSE)
154 | filter.addAction(ACTION_PLAY)
155 | filter.addAction(ACTION_PREV)
156 | mService.registerReceiver(this, filter)
157 |
158 | mService.startForeground(NOTIFICATION_ID, notification)
159 | mStarted = true
160 | }
161 | }
162 | }
163 |
164 | /**
165 | * Removes the notification and stops tracking the session. If the session
166 | * was destroyed this has no effect.
167 | */
168 | fun stopNotification() {
169 | if (mStarted) {
170 | mStarted = false
171 | mController!!.unregisterCallback(mCb)
172 | try {
173 | mNotificationManager?.cancel(NOTIFICATION_ID)
174 | mService.unregisterReceiver(this)
175 | } catch (ex: IllegalArgumentException) {
176 | // ignore if the receiver is not registered.
177 | }
178 |
179 | mService.stopForeground(true)
180 | }
181 | }
182 |
183 | override fun onReceive(context: Context, intent: Intent) {
184 | val action = intent.action
185 | when (action) {
186 | ACTION_PAUSE -> mTransportControls!!.pause()
187 | ACTION_PLAY -> mTransportControls!!.play()
188 | ACTION_NEXT -> mTransportControls!!.skipToNext()
189 | ACTION_PREV -> mTransportControls!!.skipToPrevious()
190 | else -> Timber.w("Unknown intent ignored. Action=%s", action)
191 | }
192 | }
193 |
194 | /**
195 | * Update the state based on a change on the session token. Called either when
196 | * we are running for the first time or when the media session owner has destroyed the session
197 | * (see [android.media.session.MediaController.Callback.onSessionDestroyed])
198 | */
199 | @Throws(RemoteException::class)
200 | private fun updateSessionToken() {
201 | val freshToken = mService.mSessionToken
202 | if (mSessionToken == null) {
203 | if (mController != null) {
204 | mController!!.unregisterCallback(mCb)
205 | }
206 | mSessionToken = freshToken
207 | mController = MediaControllerCompat(mService, mSessionToken!!)
208 | mTransportControls = mController!!.transportControls
209 | if (mStarted) {
210 | mController!!.registerCallback(mCb)
211 | }
212 |
213 | }
214 | }
215 |
216 | private fun createContentIntent(description: MediaDescriptionCompat): PendingIntent {
217 | Timber.d("PendingIntent")
218 | val openUI = Intent(mService, LibraryActivity::class.java)
219 | openUI.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
220 | openUI.action = Intent.ACTION_MAIN
221 | openUI.addCategory(Intent.CATEGORY_LAUNCHER)
222 | return PendingIntent.getActivity(mService, REQUEST_CODE, openUI,
223 | PendingIntent.FLAG_CANCEL_CURRENT)
224 | }
225 |
226 | private fun createNotification(): Notification? {
227 | Timber.d("updateNotificationMetadata. mMetadata=%s", mMetadata.toString())
228 | if (mMetadata == null || mPlaybackState == null) {
229 | return null
230 | }
231 |
232 | val description = mMetadata!!.description
233 |
234 | var fetchArtUrl: String? = null
235 | var art: Bitmap? = null
236 |
237 |
238 | if (description.iconUri != null) {
239 | // This sample assumes the iconUri will be a valid URL formatted String, but
240 | // it can actually be any valid Android Uri formatted String.
241 | // async fetch the album art icon
242 | val artUrl = description.iconUri!!.toString()
243 | if (art == null) {
244 | fetchArtUrl = artUrl
245 | // use a placeholder art while the remote art is being downloaded
246 | art = BitmapFactory.decodeResource(mService.resources,
247 | R.mipmap.ic_launcher)
248 | }
249 | }
250 |
251 | // Notification channels are only supported on Android O+.
252 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
253 | createNotificationChannel()
254 | }
255 |
256 | val notificationBuilder = NotificationCompat.Builder(mService, CHANNEL_ID)
257 |
258 | notificationBuilder.addAction(R.drawable.ic_action_v_previous,
259 | mService.getString(R.string.label_previous), mPreviousIntent)
260 | addPlayPauseAction(notificationBuilder)
261 | notificationBuilder.addAction(R.drawable.ic_action_v_next,
262 | mService.getString(R.string.label_next), mNextIntent)
263 |
264 |
265 | notificationBuilder
266 | .setStyle(MediaStyle()
267 | // show only play/pause in compact view
268 | .setShowActionsInCompactView(0, 1, 2)
269 | .setShowCancelButton(true)
270 | .setCancelButtonIntent(mStopIntent)
271 | .setMediaSession(mSessionToken))
272 | .setDeleteIntent(mStopIntent)
273 | .setColor(mNotificationColor)
274 | .setSmallIcon(R.drawable.ic_notification_icon)
275 | .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
276 | .setOnlyAlertOnce(true)
277 | .setContentIntent(createContentIntent(description))
278 | .setContentTitle(description.title)
279 | .setContentText(description.subtitle)
280 | .setLargeIcon(art)
281 |
282 |
283 | setNotificationPlaybackState(notificationBuilder)
284 | // if (fetchArtUrl != null) {
285 | // fetchBitmapFromURLAsync(fetchArtUrl, notificationBuilder)
286 | // }
287 |
288 | return notificationBuilder.build()
289 | }
290 |
291 | private fun addPlayPauseAction(notificationBuilder: NotificationCompat.Builder) {
292 | val label: String
293 | val icon: Int
294 | val intent: PendingIntent
295 | if (mPlaybackState?.state == PlaybackStateCompat.STATE_PLAYING || mPlaybackState?.state == PlaybackStateCompat.STATE_BUFFERING) {
296 | label = mService.getString(R.string.label_pause)
297 | icon = R.drawable.ic_action_v_pause
298 | intent = mPauseIntent
299 | } else {
300 | label = mService.getString(R.string.label_play)
301 | icon = R.drawable.ic_action_v_play
302 | intent = mPlayIntent
303 | }
304 | notificationBuilder.addAction(NotificationCompat.Action(icon, label, intent))
305 | }
306 |
307 |
308 | private fun setNotificationPlaybackState(builder: NotificationCompat.Builder) {
309 | Timber.d("updateNotificationPlaybackState. mPlaybackState=%s", mPlaybackState)
310 | if (mPlaybackState == null || !mStarted) {
311 | Timber.d("updateNotificationPlaybackState. cancelling notification!")
312 | mService.stopForeground(true)
313 | return
314 | }
315 |
316 | // Make sure that the notification can be dismissed by the user when we are not playing:
317 | builder.setOngoing(mPlaybackState!!.state == PlaybackStateCompat.STATE_PLAYING)
318 | }
319 |
320 | private fun fetchBitmapFromURLAsync(bitmapUrl: String,
321 | builder: NotificationCompat.Builder) {
322 | loadBitmap(bitmapUrl, builder)
323 | }
324 |
325 | private fun loadBitmap(url: String, builder: NotificationCompat.Builder) {
326 | if (loadtarget == null)
327 | loadtarget = object : Target {
328 | override fun onBitmapLoaded(bitmap: Bitmap, from: Picasso.LoadedFrom) {
329 | Timber.d("fetchBitmapFromURLAsync: set bitmap to %s", url)
330 | builder.setLargeIcon(bitmap)
331 | addPlayPauseAction(builder)
332 | mNotificationManager?.notify(NOTIFICATION_ID, builder.build())
333 | }
334 |
335 | override fun onBitmapFailed(errorDrawable: Drawable?) {
336 |
337 | }
338 |
339 | override fun onPrepareLoad(placeHolderDrawable: Drawable?) {
340 |
341 | }
342 | }
343 | val downloader = OkHttp3Downloader(mService)
344 | Picasso.Builder(mService).downloader(downloader).build()
345 | .load(url).into(loadtarget!!)
346 | }
347 |
348 | /**
349 | * Creates Notification Channel. This is required in Android O+ to display notifications.
350 | */
351 | @RequiresApi(Build.VERSION_CODES.O)
352 | private fun createNotificationChannel() {
353 | if (mNotificationManager?.getNotificationChannel(CHANNEL_ID) == null) {
354 | val notificationChannel = NotificationChannel(CHANNEL_ID,
355 | mService.getString(R.string.notification_channel),
356 | NotificationManager.IMPORTANCE_LOW)
357 |
358 | notificationChannel.description = mService.getString(R.string.notification_channel_description)
359 |
360 | mNotificationManager?.createNotificationChannel(notificationChannel)
361 | }
362 | }
363 |
364 | companion object {
365 |
366 | private val CHANNEL_ID = "com.olacabs.olaplaystudio.MUSIC_CHANNEL_ID"
367 |
368 | private val NOTIFICATION_ID = 412
369 | private val REQUEST_CODE = 100
370 |
371 | val ACTION_PAUSE = "com.olacabs.olaplaystudio.pause"
372 | val ACTION_PLAY = "com.olacabs.olaplaystudio.play"
373 | val ACTION_PREV = "com.olacabs.olaplaystudio.prev"
374 | val ACTION_NEXT = "com.olacabs.olaplaystudio.next"
375 | val ACTION_STOP = "com.olacabs.olaplaystudio.stop"
376 | }
377 | }
378 |
--------------------------------------------------------------------------------
/app/src/main/java/com/olacabs/olaplaystudio/playback/MusicService.kt:
--------------------------------------------------------------------------------
1 | package com.olacabs.olaplaystudio.playback
2 |
3 | import android.app.Service
4 | import android.content.Intent
5 | import android.os.*
6 | import android.support.v4.media.MediaMetadataCompat
7 | import android.support.v4.media.session.MediaButtonReceiver
8 | import android.support.v4.media.session.MediaControllerCompat
9 | import android.support.v4.media.session.MediaSessionCompat
10 | import android.support.v4.media.session.PlaybackStateCompat
11 | import com.olacabs.olaplaystudio.data.model.MediaDetail
12 | import com.olacabs.olaplaystudio.utils.clearAndAddAll
13 | import com.olacabs.olaplaystudio.utils.regOnce
14 | import com.olacabs.olaplaystudio.utils.unRegOnce
15 | import org.greenrobot.eventbus.EventBus
16 | import org.greenrobot.eventbus.Subscribe
17 | import timber.log.Timber
18 | import java.lang.ref.WeakReference
19 |
20 | /**
21 | * Created by sai on 16/12/17.
22 | */
23 | class MusicService : Service(), PlaybackManager.PlaybackServiceCallback {
24 | //NotNull
25 | private val mDelayedStopHandler = DelayedStopHandler(this)
26 | private val eventBus = EventBus.getDefault()
27 |
28 | //Lazy
29 | private val mSession: MediaSessionCompat by lazy {
30 | MediaSessionCompat(this, "MusicService")
31 | }
32 | val mSessionToken: MediaSessionCompat.Token by lazy {
33 | mSession.sessionToken
34 | }
35 | private val mTransportControls: MediaControllerCompat.TransportControls by lazy {
36 | MediaControllerCompat(this, mSession).transportControls
37 | }
38 | private val mMediaNotificationManager: MediaNotificationManager by lazy {
39 | try {
40 | MediaNotificationManager(this)
41 | } catch (e: RemoteException) {
42 | throw IllegalStateException("Could not create a MediaNotificationManager", e)
43 | }
44 | }
45 | val mPlaybackManager: PlaybackManager by lazy {
46 | PlaybackManager(mPlayback = LocalPlayback(this), mServiceCallback = this)
47 | }
48 |
49 | override fun onCreate() {
50 | Timber.d("onCreate")
51 | super.onCreate()
52 |
53 | //Init MediaSessionCompat and TransportControls
54 | mSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS)
55 | mSession.setCallback(mPlaybackManager.mMediaSessionCallback)
56 |
57 | //Reg This call to EventBus
58 | eventBus.regOnce(this)
59 | }
60 |
61 | @Subscribe(sticky = true)
62 | fun onGetAllMediaEventResponse(event: PublishMediaLoadEvent) {
63 | mPlaybackManager.listOfMediaDetail.clearAndAddAll(event.list)
64 | eventBus.removeStickyEvent(event)
65 | }
66 |
67 | override fun onPlaybackStart() {
68 | Timber.d("onPlaybackStart ")
69 | mSession.isActive = true
70 | mDelayedStopHandler.removeCallbacksAndMessages(null)
71 | // The service needs to continue running even after the bound client (usually a
72 | // MediaController) disconnects, otherwise the music playback will stop.
73 | // Calling startService(Intent) will keep the service running until it is explicitly killed.
74 | startService(Intent(applicationContext, MusicService::class.java))
75 | }
76 |
77 | override fun onNotificationRequired() {
78 | Timber.d("onNotificationRequired")
79 | mMediaNotificationManager.startNotification()
80 | }
81 |
82 | override fun onPlaybackStop() {
83 | Timber.d("onPlaybackStop ")
84 | mSession.isActive = false
85 | resetDelayHandler()
86 | stopForeground(true)
87 | }
88 |
89 | override fun onPlaybackStateUpdated(newState: PlaybackStateCompat) {
90 | Timber.d("onPlaybackStateUpdated newState = %s ", newState.state)
91 | mSession.setPlaybackState(newState)
92 | }
93 |
94 | override fun updateMetaData(media: MediaDetail) {
95 | Timber.d("updateMetaData")
96 | mSession.setMetadata(MediaMetadataCompat.Builder()
97 | .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, media.artists)
98 | .putString(MediaMetadataCompat.METADATA_KEY_TITLE, media.song)
99 | .putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, media.cover_image)
100 | .putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, media.index.toLong())
101 | .build())
102 | }
103 |
104 | override fun onDestroy() {
105 | Timber.d("onDestroy")
106 | // Service is being killed, so make sure we release our resources
107 | mPlaybackManager.handleStopRequest(null)
108 | mMediaNotificationManager.stopNotification()
109 |
110 | mDelayedStopHandler.removeCallbacksAndMessages(null)
111 | mSession.release()
112 |
113 | eventBus.unRegOnce(this)
114 | }
115 |
116 | override fun onStartCommand(startIntent: Intent?, flags: Int, startId: Int): Int {
117 | Timber.d("onStartCommand ")
118 | startIntent?.let { intent ->
119 | val action = intent.action
120 | val command = intent.getStringExtra(CMD_NAME)
121 |
122 | if (ACTION_CMD == action)
123 | when (command) {
124 | CMD_STOP -> mPlaybackManager.handleStopRequest(null)
125 | else -> ""
126 |
127 | }
128 | else
129 | MediaButtonReceiver.handleIntent(mSession, startIntent)
130 |
131 |
132 | }
133 | resetDelayHandler()
134 | return Service.START_STICKY
135 | }
136 |
137 | private fun resetDelayHandler() {
138 | // Reset the delay handler to enqueue a message to stop the service if
139 | // nothing is playing.
140 | mDelayedStopHandler.removeCallbacksAndMessages(null)
141 | mDelayedStopHandler.sendEmptyMessageDelayed(0, STOP_DELAY.toLong())
142 | }
143 |
144 | override fun onPlaybackPause() {
145 | Timber.d("onPlaybackPause ")
146 | resetDelayHandler()
147 | stopForeground(true)
148 | }
149 |
150 | private class DelayedStopHandler internal constructor(service: MusicService) : Handler() {
151 | private val mWeakReference: WeakReference = WeakReference(service)
152 |
153 | override fun handleMessage(msg: Message) {
154 | val service = mWeakReference.get()
155 | if (service != null) {
156 | if (service.mPlaybackManager.mPlayback.isPlaying) {
157 | Timber.d("Ignoring delayed stop since the media player is in use.")
158 | return
159 | }
160 | Timber.d("Stopping service with delay handler.")
161 | // service.saveLastPlayedInfo();
162 | service.stopSelf()
163 | }
164 | }
165 | }
166 |
167 | companion object {
168 | val ACTION_CMD = "com.olacabs.olaplaystudio.ACTION_CMD"
169 | val CMD_PAUSE = "CMD_PAUSE"
170 | val CMD_NAME = "CMD_NAME"
171 | val CMD_STOP = "CMD_STOP"
172 | private val STOP_DELAY = 80000//1.30min
173 | }
174 |
175 | private val mBinder = LocalBinder()
176 |
177 | override fun onBind(intent: Intent): IBinder? {
178 | return mBinder
179 | }
180 |
181 | inner class LocalBinder : Binder() {
182 | val service: MusicService
183 | get() = this@MusicService
184 | }
185 | }
186 |
187 | data class PublishMediaLoadEvent(val list: List)
--------------------------------------------------------------------------------
/app/src/main/java/com/olacabs/olaplaystudio/playback/Playback.kt:
--------------------------------------------------------------------------------
1 | package com.olacabs.olaplaystudio.playback
2 |
3 |
4 | /**
5 | * Interface representing either Local or Remote Playback. The [MusicService] works
6 | * directly with an instance of the Playback object to make the various calls such as
7 | * play, pause etc.
8 | */
9 | interface Playback {
10 |
11 | /**
12 | * Get the current [android.media.session.PlaybackState.getState]
13 | */
14 | /**
15 | * Set the latest playback state as determined by the caller.
16 | */
17 | var state: Int
18 |
19 | /**
20 | * @return boolean that indicates that this is ready to be used.
21 | */
22 | val isConnected: Boolean
23 |
24 | /**
25 | * @return boolean indicating whether the player is playing or is supposed to be
26 | * playing when we gain audio focus.
27 | */
28 | val isPlaying: Boolean
29 |
30 | /**
31 | * @return pos if currently playing an item
32 | */
33 | val currentStreamPosition: Long
34 |
35 | var currentMediaId: String
36 | /**
37 | * Start/setup the playback.
38 | * Resources/listeners would be allocated by implementations.
39 | */
40 | fun start()
41 |
42 | /**
43 | * Stop the playback. All resources can be de-allocated by implementations here.
44 | *
45 | * @param notifyListeners if true and a callback has been set by setCallback,
46 | * callback.onPlaybackStatusChanged will be called after changing
47 | * the state.
48 | */
49 | fun stop(notifyListeners: Boolean)
50 |
51 | /**
52 | * Queries the underlying stream and update the internal last known stream position.
53 | */
54 | fun updateLastKnownStreamPosition()
55 |
56 | fun play(item: String)
57 |
58 | fun pause()
59 |
60 | fun seekTo(position: Long)
61 |
62 | interface Callback {
63 | /**
64 | * On current music completed.
65 | */
66 | fun onCompletion()
67 |
68 | /**
69 | * on Playback status changed
70 | * Implementations can use this callback to update
71 | * playback state on the media sessions.
72 | */
73 | fun onPlaybackStatusChanged(state: Int)
74 |
75 | /**
76 | * @param error to be added to the PlaybackState
77 | */
78 | fun onError(error: String)
79 |
80 | }
81 |
82 | fun setCallback(callback: Callback)
83 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/olacabs/olaplaystudio/playback/PlaybackManager.kt:
--------------------------------------------------------------------------------
1 | package com.olacabs.olaplaystudio.playback
2 |
3 | import android.media.session.PlaybackState
4 | import android.os.Bundle
5 | import android.os.SystemClock
6 | import android.support.v4.media.session.MediaSessionCompat
7 | import android.support.v4.media.session.PlaybackStateCompat
8 | import com.olacabs.olaplaystudio.data.model.MediaDetail
9 | import timber.log.Timber
10 |
11 | /**
12 | * Created by sai on 5/18/17.
13 | *
14 | * Useful Links:
15 | * Reefer http://beust.com/weblog/2015/10/30/exploring-the-kotlin-standard-library/
16 | */
17 |
18 | class PlaybackManager(val mPlayback: Playback,
19 | val mServiceCallback: PlaybackServiceCallback) : Playback.Callback {
20 |
21 | val mMediaSessionCallback = MediaSessionCallback()
22 | val listOfMediaDetail = mutableListOf()
23 | private var currentPosition = 0
24 |
25 |
26 | init {
27 | mPlayback.state = PlaybackStateCompat.STATE_NONE
28 | mPlayback.setCallback(this)
29 | }
30 |
31 |
32 | private fun playMedia(id: Long) {
33 | currentPosition = id.toInt()
34 | playMedia()
35 | }
36 |
37 | private fun playMedia() {
38 | if (listOfMediaDetail.isNotEmpty()) {
39 | listOfMediaDetail.forEach { it.state = PlaybackStateCompat.STATE_NONE }
40 | listOfMediaDetail[currentPosition].state = PlaybackStateCompat.STATE_PLAYING
41 | listOfMediaDetail[currentPosition].apply { playingIndex = currentPosition }.url?.run { mPlayback.play(this) }
42 | } else
43 | Timber.d("listOfMediaDetail is empty")
44 | }
45 |
46 | private fun pauseMedia() {
47 | listOfMediaDetail[currentPosition].state = PlaybackStateCompat.STATE_PAUSED
48 | mServiceCallback.onPlaybackPause()
49 | mPlayback.pause()
50 | }
51 |
52 | private fun playPreviousMedia() {
53 | if (currentPosition == 0)
54 | currentPosition = listOfMediaDetail.size - 1
55 | else
56 | currentPosition -= 1
57 | playMedia()
58 | }
59 |
60 | private fun playNextMedia() {
61 | if (currentPosition == listOfMediaDetail.size - 1)
62 | currentPosition = 0
63 | else
64 | currentPosition += 1
65 | playMedia()
66 | }
67 |
68 | private fun stopMedia(focus: Boolean) {
69 | if (focus) handleStopRequest(null)
70 | }
71 |
72 | override fun onCompletion() {
73 | playNextMedia()
74 | }
75 |
76 | override fun onPlaybackStatusChanged(state: Int) {
77 | updatePlaybackState(null)
78 | }
79 |
80 | override fun onError(error: String) {
81 | updatePlaybackState(error)
82 | }
83 |
84 |
85 | fun handleStopRequest(withError: String?) {
86 | Timber.d("handleStopRequest: mState= %s error=%s", mPlayback.state, withError)
87 | mPlayback.stop(true)
88 | mServiceCallback.onPlaybackStop()
89 | updatePlaybackState(withError)
90 | }
91 |
92 | private fun updatePlaybackState(error: String?) {
93 | Timber.d("updatePlaybackState, playback state=%s", mPlayback.state)
94 | if (listOfMediaDetail.isNotEmpty())
95 | listOfMediaDetail.let {
96 | mServiceCallback.updateMetaData(listOfMediaDetail[currentPosition])//TODO send the selected item
97 | var position = PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN
98 | if (mPlayback.isConnected) {
99 | position = mPlayback.currentStreamPosition.toLong()
100 | }
101 |
102 | val stateBuilder = PlaybackStateCompat.Builder()
103 | .setActions(availableActions)
104 | var state = mPlayback.state
105 |
106 | // If there is an error message, send it to the playback state:
107 | if (error != null) {
108 | // Error states are really only supposed to be used for errors that cause playback to
109 | // stop unexpectedly and persist until the user takes action to fix it.
110 | stateBuilder.setErrorMessage(PlaybackStateCompat.ERROR_CODE_UNKNOWN_ERROR, error)
111 | state = PlaybackStateCompat.STATE_ERROR
112 | }
113 |
114 | stateBuilder.setState(state, position, 1.0f, SystemClock.elapsedRealtime())
115 |
116 | // Set the activeQueueItemId if the current index is valid.
117 | mServiceCallback.onPlaybackStateUpdated(stateBuilder.build())
118 |
119 | if (state == PlaybackState.STATE_PLAYING || state == PlaybackState.STATE_PAUSED) {
120 | mServiceCallback.onNotificationRequired()
121 | }
122 | }
123 | }
124 |
125 |
126 | private val availableActions: Long
127 | get() {
128 | var actions = PlaybackStateCompat.ACTION_PLAY or
129 | PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID or
130 | PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH
131 | if (mPlayback.isPlaying) {
132 | actions = actions or PlaybackStateCompat.ACTION_PAUSE
133 | }
134 | actions = actions or PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
135 | actions = actions or PlaybackStateCompat.ACTION_SKIP_TO_NEXT
136 | return actions
137 | }
138 |
139 |
140 | interface PlaybackServiceCallback {
141 | fun onPlaybackStart()
142 |
143 | fun onNotificationRequired()
144 |
145 | fun onPlaybackStop()
146 |
147 | fun onPlaybackPause()
148 |
149 | fun onPlaybackStateUpdated(newState: PlaybackStateCompat)
150 |
151 | fun updateMetaData(media: MediaDetail)
152 | }
153 |
154 | inner class MediaSessionCallback : MediaSessionCompat.Callback() {
155 |
156 | override fun onSkipToQueueItem(id: Long) {
157 | super.onSkipToQueueItem(id)
158 | playMedia(id)
159 | }
160 |
161 | override fun onPlay() {
162 | super.onPlay()
163 | playMedia()
164 | }
165 |
166 | override fun onPause() {
167 | super.onPause()
168 | pauseMedia()
169 | }
170 |
171 | override fun onSkipToNext() {
172 | super.onSkipToNext()
173 | playNextMedia()
174 | }
175 |
176 | override fun onSkipToPrevious() {
177 | super.onSkipToPrevious()
178 | playPreviousMedia()
179 | }
180 |
181 |
182 | override fun onStop() {
183 | super.onStop()
184 | stopMedia(true)
185 | }
186 |
187 | override fun onCustomAction(action: String?, extras: Bundle?) {
188 | super.onCustomAction(action, extras)
189 | }
190 | }
191 |
192 |
193 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/olacabs/olaplaystudio/playback/utils/ResourceHelper.java:
--------------------------------------------------------------------------------
1 | package com.olacabs.olaplaystudio.playback.utils;
2 |
3 | import android.content.Context;
4 | import android.content.pm.ApplicationInfo;
5 | import android.content.pm.PackageManager;
6 | import android.content.res.Resources;
7 | import android.content.res.TypedArray;
8 |
9 | /**
10 | * Generic reusable methods to handle resources.
11 | */
12 | public class ResourceHelper {
13 | /**
14 | * Get a color timeInMin from a theme attribute.
15 | * @param context used for getting the color.
16 | * @param attribute theme attribute.
17 | * @param defaultColor default to use.
18 | * @return color timeInMin
19 | */
20 | public static int getThemeColor(Context context, int attribute, int defaultColor) {
21 | int themeColor = 0;
22 | String packageName = context.getPackageName();
23 | try {
24 | Context packageContext = context.createPackageContext(packageName, 0);
25 | ApplicationInfo applicationInfo =
26 | context.getPackageManager().getApplicationInfo(packageName, 0);
27 | packageContext.setTheme(applicationInfo.theme);
28 | Resources.Theme theme = packageContext.getTheme();
29 | TypedArray ta = theme.obtainStyledAttributes(new int[] {attribute});
30 | themeColor = ta.getColor(0, defaultColor);
31 | ta.recycle();
32 | } catch (PackageManager.NameNotFoundException e) {
33 | e.printStackTrace();
34 | }
35 | return themeColor;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/app/src/main/java/com/olacabs/olaplaystudio/ui/base/BaseFragment.kt:
--------------------------------------------------------------------------------
1 | package com.olacabs.olaplaystudio.ui.base
2 |
3 | import android.os.Bundle
4 | import android.support.v4.app.Fragment
5 | import android.support.v4.util.LongSparseArray
6 | import android.view.LayoutInflater
7 | import android.view.View
8 | import android.view.ViewGroup
9 | import com.olacabs.olaplaystudio.OlaApplication
10 | import com.olacabs.olaplaystudio.di.component.ConfigPersistentComponent
11 | import com.olacabs.olaplaystudio.di.component.DaggerConfigPersistentComponent
12 | import com.olacabs.olaplaystudio.di.component.FragmentComponent
13 | import com.olacabs.olaplaystudio.di.module.FragmentModule
14 | import timber.log.Timber
15 | import java.util.concurrent.atomic.AtomicLong
16 |
17 | /**
18 | * Abstract Fragment that every other Fragment in this application must implement. It handles
19 | * creation of Dagger components and makes sure that instances of ConfigPersistentComponent are kept
20 | * across configuration changes.
21 | */
22 | abstract class BaseFragment : Fragment() {
23 |
24 | private var mFragmentComponent: FragmentComponent? = null
25 | private var mFragmentId: Long = 0
26 |
27 | override fun onCreate(savedInstanceState: Bundle?) {
28 | super.onCreate(savedInstanceState)
29 |
30 | // Create the FragmentComponent and reuses cached ConfigPersistentComponent if this is
31 | // being called after a configuration change.
32 | mFragmentId = savedInstanceState?.getLong(KEY_FRAGMENT_ID) ?: NEXT_ID.getAndIncrement()
33 | val configPersistentComponent: ConfigPersistentComponent
34 | if (sComponentsArray.get(mFragmentId) == null) {
35 | Timber.i("Creating new ConfigPersistentComponent id=%d", mFragmentId)
36 | configPersistentComponent = DaggerConfigPersistentComponent.builder()
37 | .applicationComponent(OlaApplication[activity].component)
38 | .build()
39 | sComponentsArray.put(mFragmentId, configPersistentComponent)
40 | } else {
41 | Timber.i("Reusing ConfigPersistentComponent id=%d", mFragmentId)
42 | configPersistentComponent = sComponentsArray.get(mFragmentId)
43 | }
44 | mFragmentComponent = configPersistentComponent.fragmentComponent(FragmentModule(this))
45 | }
46 |
47 | override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?,
48 | savedInstanceState: Bundle?): View? {
49 | val view: View? = inflater?.inflate(layout, container, false)
50 | return view
51 | }
52 |
53 | abstract val layout: Int
54 |
55 | override fun onSaveInstanceState(outState: Bundle?) {
56 | super.onSaveInstanceState(outState)
57 | outState?.putLong(KEY_FRAGMENT_ID, mFragmentId)
58 | }
59 |
60 | override fun onDestroy() {
61 | if (!activity.isChangingConfigurations) {
62 | Timber.i("Clearing ConfigPersistentComponent id=%d", mFragmentId)
63 | sComponentsArray.remove(mFragmentId)
64 | }
65 | super.onDestroy()
66 | }
67 |
68 | fun fragmentComponent(): FragmentComponent {
69 | return mFragmentComponent as FragmentComponent
70 | }
71 |
72 | companion object {
73 |
74 | private val KEY_FRAGMENT_ID = "KEY_FRAGMENT_ID"
75 | private val sComponentsArray = LongSparseArray()
76 | private val NEXT_ID = AtomicLong(0)
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/app/src/main/java/com/olacabs/olaplaystudio/ui/base/BasePresenter.kt:
--------------------------------------------------------------------------------
1 | package com.olacabs.olaplaystudio.ui.base
2 |
3 | import io.reactivex.disposables.CompositeDisposable
4 | import io.reactivex.disposables.Disposable
5 |
6 |
7 | /**
8 | * Base class that implements the Presenter interface and provides a base implementation for
9 | * attachView() and detachView(). It also handles keeping a reference to the mvpView that
10 | * can be accessed from the children classes by calling getMvpView().
11 | */
12 | open class BasePresenter : Presenter {
13 |
14 | var mvpView: T? = null
15 | private set
16 | private val mCompositeDisposable = CompositeDisposable()
17 |
18 | override fun attachView(mvpView: T) {
19 | this.mvpView = mvpView
20 | }
21 |
22 | override fun detachView() {
23 | mvpView = null
24 | if (!mCompositeDisposable.isDisposed) {
25 | mCompositeDisposable.clear()
26 | }
27 | }
28 |
29 | val isViewAttached: Boolean
30 | get() = mvpView != null
31 |
32 | fun checkViewAttached() {
33 | if (!isViewAttached) throw MvpViewNotAttachedException()
34 | }
35 |
36 | fun addDisposable(subs: Disposable) {
37 | mCompositeDisposable.add(subs)
38 | }
39 |
40 | private class MvpViewNotAttachedException internal constructor() : RuntimeException("Please call Presenter.attachView(MvpView) before" + " requesting data to the Presenter")
41 |
42 | }
43 |
44 |
--------------------------------------------------------------------------------
/app/src/main/java/com/olacabs/olaplaystudio/ui/base/MvpBaseActivity.kt:
--------------------------------------------------------------------------------
1 | package com.olacabs.olaplaystudio.ui.base
2 |
3 | import android.os.Bundle
4 | import android.support.v4.util.LongSparseArray
5 | import android.support.v7.app.AppCompatActivity
6 | import android.view.MenuItem
7 | import com.olacabs.olaplaystudio.OlaApplication
8 | import com.olacabs.olaplaystudio.di.component.ActivityComponent
9 | import com.olacabs.olaplaystudio.di.component.ConfigPersistentComponent
10 | import com.olacabs.olaplaystudio.di.component.DaggerConfigPersistentComponent
11 | import com.olacabs.olaplaystudio.di.module.ActivityModule
12 | import timber.log.Timber
13 | import java.util.concurrent.atomic.AtomicLong
14 |
15 | /**
16 | * Abstract activity that every other Activity in this application must implement. It provides the
17 | * following functionality:
18 | * - Handles creation of Dagger components and makes sure that instances of
19 | * ConfigPersistentComponent are kept across configuration changes.
20 | * - Set up and handles a GoogleApiClient instance that can be used to access the Google sign in
21 | * api.
22 | * - Handles signing out when an authentication error event is received.
23 | */
24 | abstract class MvpBaseActivity : AppCompatActivity() {
25 |
26 | private var mActivityComponent: ActivityComponent? = null
27 | private var mActivityId: Long = 0
28 |
29 | override fun onCreate(savedInstanceState: Bundle?) {
30 | super.onCreate(savedInstanceState)
31 | setContentView(layout)
32 | // Create the ActivityComponent and reuses cached ConfigPersistentComponent if this is
33 | // being called after a configuration change.
34 | mActivityId = savedInstanceState?.getLong(KEY_ACTIVITY_ID) ?: NEXT_ID.getAndIncrement()
35 | val configPersistentComponent: ConfigPersistentComponent
36 | if (sComponentsArray.get(mActivityId) == null) {
37 | Timber.i("Creating new ConfigPersistentComponent id=%d", mActivityId)
38 | configPersistentComponent = DaggerConfigPersistentComponent.builder()
39 | .applicationComponent(OlaApplication[this].component)
40 | .build()
41 | sComponentsArray.put(mActivityId, configPersistentComponent)
42 | } else {
43 | Timber.i("Reusing ConfigPersistentComponent id=%d", mActivityId)
44 | configPersistentComponent = sComponentsArray.get(mActivityId)
45 | }
46 | mActivityComponent = configPersistentComponent.activityComponent(ActivityModule(this))
47 | mActivityComponent?.inject(this)
48 | }
49 |
50 | abstract val layout: Int
51 |
52 | override fun onSaveInstanceState(outState: Bundle) {
53 | super.onSaveInstanceState(outState)
54 | outState.putLong(KEY_ACTIVITY_ID, mActivityId)
55 | }
56 |
57 | override fun onDestroy() {
58 | if (!isChangingConfigurations) {
59 | Timber.i("Clearing ConfigPersistentComponent id=%d", mActivityId)
60 | sComponentsArray.remove(mActivityId)
61 | }
62 | super.onDestroy()
63 | }
64 |
65 | override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
66 | android.R.id.home -> {
67 | finish()
68 | true
69 | }
70 | else -> super.onOptionsItemSelected(item)
71 | }
72 |
73 | fun activityComponent(): ActivityComponent = mActivityComponent as ActivityComponent
74 |
75 | companion object {
76 |
77 | private val KEY_ACTIVITY_ID = "KEY_ACTIVITY_ID"
78 | private val NEXT_ID = AtomicLong(0)
79 | private val sComponentsArray = LongSparseArray()
80 | }
81 |
82 | }
83 |
--------------------------------------------------------------------------------
/app/src/main/java/com/olacabs/olaplaystudio/ui/base/MvpView.kt:
--------------------------------------------------------------------------------
1 | package com.olacabs.olaplaystudio.ui.base
2 |
3 |
4 | /**
5 | * Base interface that any class that wants to act as a View in the MVP (Model View Presenter)
6 | * pattern must implement. Generally this interface will be extended by a more specific interface
7 | * that then usually will be implemented by an Activity or Fragment.
8 | */
9 | interface MvpView
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/olacabs/olaplaystudio/ui/base/Presenter.kt:
--------------------------------------------------------------------------------
1 | package com.olacabs.olaplaystudio.ui.base
2 |
3 | /**
4 | * Every presenter in the app must either implement this interface or extend BasePresenter
5 | * indicating the MvpView type that wants to be attached with.
6 | */
7 | interface Presenter {
8 |
9 | fun attachView(mvpView: V)
10 |
11 | fun detachView()
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/java/com/olacabs/olaplaystudio/ui/library/LibraryActivity.kt:
--------------------------------------------------------------------------------
1 | package com.olacabs.olaplaystudio.ui.library
2 |
3 | import android.Manifest
4 | import android.annotation.SuppressLint
5 | import android.app.SearchManager
6 | import android.graphics.drawable.TransitionDrawable
7 | import android.os.Bundle
8 | import android.os.RemoteException
9 | import android.support.v4.media.MediaMetadataCompat
10 | import android.support.v4.media.session.MediaControllerCompat
11 | import android.support.v4.media.session.MediaSessionCompat
12 | import android.support.v4.media.session.PlaybackStateCompat
13 | import android.support.v7.app.AlertDialog
14 | import android.support.v7.app.AppCompatActivity
15 | import android.support.v7.widget.SearchView
16 | import android.view.Menu
17 | import android.view.MenuItem
18 | import android.view.View
19 | import com.olacabs.olaplaystudio.R
20 | import com.olacabs.olaplaystudio.data.model.MediaDetail
21 | import com.olacabs.olaplaystudio.playback.BaseMediaActivity
22 | import com.olacabs.olaplaystudio.playback.MusicService
23 | import com.olacabs.olaplaystudio.utils.hide
24 | import com.olacabs.olaplaystudio.utils.show
25 | import com.olacabs.olaplaystudio.utils.showAsToast
26 | import com.olacabs.olaplaystudio.utils.visible
27 | import com.sothree.slidinguppanel.SlidingUpPanelLayout
28 | import com.squareup.picasso.Picasso
29 | import kotlinx.android.synthetic.main.activity_library.*
30 | import kotlinx.android.synthetic.main.content_fullscreen_player.*
31 | import kotlinx.android.synthetic.main.controls_panel.*
32 | import permissions.dispatcher.*
33 | import timber.log.Timber
34 | import javax.inject.Inject
35 |
36 |
37 | /**
38 | * Created by sai on 16/12/17.
39 | */
40 | @RuntimePermissions
41 | class LibraryActivity(override val layout: Int = R.layout.activity_library) : BaseMediaActivity(), LibraryView {
42 |
43 |
44 | @Inject lateinit var mPicasso: Picasso
45 | @Inject lateinit var presenter: LibraryPresenter
46 | private val transition: TransitionDrawable by lazy { controls_sub_parent.background as TransitionDrawable }
47 | private var mMediaController: MediaControllerCompat? = null
48 |
49 | override fun showError(message: String) = message.showAsToast()
50 |
51 | override fun onCreate(savedInstanceState: Bundle?) {
52 | super.onCreate(savedInstanceState)
53 | activityComponent().inject(this)
54 | presenter.attachView(this)
55 |
56 | //Fetch media from server
57 | presenter.fetchMediaList()
58 |
59 | //Initialize views
60 | initViews()
61 | }
62 |
63 | private fun initViews() {
64 | media_list.adapter = presenter.mLibraryAdapter
65 | swipe_refresh_layout.setOnRefreshListener {
66 | presenter.fetchMediaList()
67 | invalidateOptionsMenu()
68 | }
69 | setSupportActionBar(toolbar)
70 | slide_back_btn.hide()
71 |
72 | transition.isCrossFadeEnabled = true
73 | sliding_layout.setDragView(R.id.controls_sub_parent)
74 | sliding_layout.addPanelSlideListener(object : SlidingUpPanelLayout.PanelSlideListener {
75 |
76 | override fun onPanelSlide(panel: View, slideOffset: Float) {
77 | //setActionBarTranslation(slidingUpPanelLayout.getCurrentParallaxOffset());
78 | }
79 |
80 | override fun onPanelStateChanged(panel: View, previousState: SlidingUpPanelLayout.PanelState,
81 | newState: SlidingUpPanelLayout.PanelState) {
82 |
83 | if (newState == SlidingUpPanelLayout.PanelState.EXPANDED) {
84 | slidingHidePlay(true)
85 | }
86 | if (newState == SlidingUpPanelLayout.PanelState.COLLAPSED) {
87 | slidingHidePlay(false)
88 | }
89 | }
90 | })
91 |
92 |
93 | arrayListOf(play_btn, slide_play_ib).forEach { it.setOnClickListener { handlePlayButtonClick() } }
94 | previous_btn.setOnClickListener {
95 | mMediaController?.transportControls?.skipToPrevious()
96 | }
97 | next_btn.setOnClickListener {
98 | mMediaController?.transportControls?.skipToNext()
99 | }
100 | }
101 |
102 | private fun slidingHidePlay(b: Boolean) {
103 | if (b) {
104 | if (!slide_back_btn.isShown) {
105 | transition.startTransition(200)
106 | slide_back_btn.show()
107 | detail_layout.hide()
108 | play_pause_layout.hide()
109 | }
110 | } else {
111 | if (slide_back_btn.isShown) {
112 | transition.reverseTransition(200)
113 | detail_layout.show()
114 | play_pause_layout.show()
115 | slide_back_btn.hide()
116 | }
117 | }
118 | }
119 |
120 | override fun showProgress(show: Boolean) {
121 | progress_bar.visible(show)
122 | if (!show) swipe_refresh_layout.isRefreshing = show
123 | }
124 |
125 | override fun onDestroy() {
126 | super.onDestroy()
127 | presenter.detachView()
128 | }
129 |
130 | override fun updateMediaUi(mediaDetail: MediaDetail) {
131 | if (mMediaController?.metadata == null)
132 | mediaDetail.run {
133 | slide_title.text = song ?: ""
134 | title_tv.text = song ?: ""
135 |
136 | slide_artist.text = artists ?: ""
137 | artist_tv.text = artists ?: ""
138 |
139 | cover_image?.let {
140 | mPicasso.load(it)
141 | .placeholder(R.drawable.album_placeholder)
142 | .into(background_image)
143 | }
144 | }
145 |
146 | }
147 |
148 | override fun onBackPressed() {
149 | if (sliding_layout.panelState == SlidingUpPanelLayout.PanelState.EXPANDED) {
150 | sliding_layout.panelState = SlidingUpPanelLayout.PanelState.COLLAPSED
151 | } else {
152 | super.onBackPressed()
153 | }
154 | }
155 |
156 | override fun connectToSession(token: MediaSessionCompat.Token) {
157 | try {
158 | mMediaController = MediaControllerCompat(this, token)
159 | mMediaController?.registerCallback(mMediaControllerCallback)
160 | } catch (e: RemoteException) {
161 | e.printStackTrace()
162 | }
163 | }
164 |
165 | private val mMediaControllerCallback = object : MediaControllerCompat.Callback() {
166 |
167 | override fun onPlaybackStateChanged(state: PlaybackStateCompat?) {
168 | Timber.d("mediaControllerCallback onPlaybackStateChanged: state = %s", state?.state)
169 | state?.let { playStateChange(it.state) }
170 | }
171 |
172 | override fun onMetadataChanged(metadata: MediaMetadataCompat?) {
173 | Timber.d("mediaControllerCallback onMetadataChanged %s",
174 | metadata?.getLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER))
175 | metadata?.run {
176 | metadata.getString(MediaMetadataCompat.METADATA_KEY_TITLE).run {
177 | slide_title.text = this
178 | title_tv.text = this
179 | }
180 | metadata.getString(MediaMetadataCompat.METADATA_KEY_ARTIST).run {
181 | slide_artist.text = this
182 | artist_tv.text = this
183 | }
184 | metadata.getString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI)?.let {
185 | mPicasso.load(it)
186 | .placeholder(R.drawable.album_placeholder)
187 | .into(background_image)
188 | }
189 | presenter.notifyAdapter()
190 | }
191 |
192 | }
193 | }
194 |
195 | private fun playStateChange(state: Int) {
196 | Timber.d("playStateChange %s", state)
197 | when (state) {
198 | PlaybackStateCompat.STATE_BUFFERING -> showMediaProgress(true)
199 | PlaybackStateCompat.STATE_PLAYING -> {
200 | updatePlayButton(true)
201 | }
202 | else -> updatePlayButton(false)
203 | }
204 | }
205 |
206 | private fun showMediaProgress(visibility: Boolean) {
207 | media_progress_bar.visible(visibility)
208 | }
209 |
210 | private fun updatePlayButton(isPlaying: Boolean) {
211 | showMediaProgress(false)
212 | if (isPlaying) {
213 | slide_play_ib?.setImageResource(R.drawable.ic_action_v_pause)
214 | play_btn?.setImageResource(R.drawable.ic_action_v_pause)
215 | } else {
216 | slide_play_ib?.setImageResource(R.drawable.ic_action_v_play)
217 | play_btn?.setImageResource(R.drawable.ic_action_v_play)
218 | }
219 | }
220 |
221 |
222 | override fun onMusicServiceConnected(service: MusicService) {
223 | mMediaController?.metadata?.run {
224 | mMediaController?.playbackState?.let { playStateChange(it.state) }
225 | getString(MediaMetadataCompat.METADATA_KEY_TITLE).run {
226 | slide_title.text = this
227 | title_tv.text = this
228 | }
229 | getString(MediaMetadataCompat.METADATA_KEY_ARTIST).run {
230 | slide_artist.text = this
231 | artist_tv.text = this
232 | }
233 | getString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI)?.let {
234 | mPicasso.load(it)
235 | .placeholder(R.drawable.album_placeholder)
236 | .into(background_image)
237 | }
238 | }
239 | }
240 |
241 | override fun onMusicServiceDisconnected() {
242 |
243 | }
244 |
245 | private fun handlePlayButtonClick() {
246 | val state = mMediaController?.playbackState
247 | val controls = mMediaController?.transportControls
248 | if (state != null) {
249 | when (state.state) {
250 | PlaybackStateCompat.STATE_PLAYING // fall through
251 | , PlaybackStateCompat.STATE_BUFFERING -> controls?.pause()
252 | PlaybackStateCompat.STATE_PAUSED, PlaybackStateCompat.STATE_STOPPED -> {
253 | controls?.play()
254 | }
255 | else -> Timber.d("onClick with state %s", state.state)
256 | }
257 | } else {
258 | controls?.play()
259 | }
260 | }
261 |
262 | override fun playSelected(mediaDetail: MediaDetail) {
263 | val controls = mMediaController?.transportControls
264 | controls?.skipToQueueItem(mediaDetail.index.toLong())
265 | }
266 |
267 | override fun onCreateOptionsMenu(menu: Menu?): Boolean {
268 | menuInflater.inflate(R.menu.music_toolbar, menu)
269 | initSearchView(menu)
270 | return super.onCreateOptionsMenu(menu)
271 | }
272 |
273 | private fun initSearchView(menu: Menu?) {
274 | val searchView = menu?.findItem(R.id.menu_search)?.actionView as SearchView
275 | val searchManager = getSystemService(AppCompatActivity.SEARCH_SERVICE) as SearchManager
276 | searchView.setSearchableInfo(searchManager.getSearchableInfo(componentName))
277 | searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
278 | override fun onQueryTextSubmit(query: String?): Boolean = true
279 |
280 | override fun onQueryTextChange(newText: String?): Boolean {
281 | newText?.let { presenter.filterList(it) }
282 | return true
283 | }
284 | })
285 | }
286 |
287 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
288 | when (item.itemId) {
289 | R.id.action_fav -> presenter.toggleFav()
290 | }
291 | return super.onOptionsItemSelected(item)
292 | }
293 |
294 | override fun updateFavMenuIcon(showFavorites: Boolean) {
295 | invalidateOptionsMenu()
296 | }
297 |
298 | override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
299 | menu?.findItem(R.id.action_fav)?.setIcon(
300 | if (presenter.showFavorites)
301 | R.drawable.ic_media_favorite_fill
302 | else
303 | R.drawable.ic_media_favorite_border)
304 | return super.onPrepareOptionsMenu(menu)
305 |
306 | }
307 |
308 |
309 | override fun downloadFile(mediaDetail: MediaDetail) {
310 | downloadFileRequestWithPermissionCheck(mediaDetail)
311 | }
312 |
313 | @NeedsPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
314 | fun downloadFileRequest(mediaDetail: MediaDetail) {
315 | presenter.downloadFile(mediaDetail)
316 | "Download started, check your downloads folder".showAsToast()
317 | }
318 |
319 | @OnShowRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)
320 | fun showRationaleForCamera(request: PermissionRequest) {
321 | AlertDialog.Builder(this)
322 | .setMessage("Need storage permission to download the file")
323 | .setPositiveButton("Allow", { _, _ -> request.proceed() })
324 | .setNegativeButton("Deny", { _, _ -> request.cancel() })
325 | .show()
326 | }
327 |
328 | @OnPermissionDenied(Manifest.permission.WRITE_EXTERNAL_STORAGE)
329 | fun showDeniedForCamera() {
330 | "Storage permissions were denied. Please consider granting it in order to access the storage!".showAsToast()
331 | }
332 |
333 | @OnNeverAskAgain(Manifest.permission.WRITE_EXTERNAL_STORAGE)
334 | fun showNeverAskForCamera() {
335 | "Storage permission was denied with never ask again.".showAsToast()
336 | }
337 |
338 | @SuppressLint("NeedOnRequestPermissionsResult")
339 | override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
340 | super.onRequestPermissionsResult(requestCode, permissions, grantResults)
341 | onRequestPermissionsResult(requestCode, grantResults)
342 | }
343 | override fun onPause() {
344 | super.onPause()
345 | mMediaController?.unregisterCallback(mMediaControllerCallback)
346 | }
347 | }
348 |
--------------------------------------------------------------------------------
/app/src/main/java/com/olacabs/olaplaystudio/ui/library/LibraryAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.olacabs.olaplaystudio.ui.library
2 |
3 | import android.content.Context
4 | import android.graphics.PorterDuff
5 | import android.graphics.drawable.AnimationDrawable
6 | import android.graphics.drawable.Drawable
7 | import android.os.Build
8 | import android.support.v4.content.ContextCompat
9 | import android.support.v4.graphics.drawable.DrawableCompat
10 | import android.support.v4.media.session.PlaybackStateCompat
11 | import android.support.v7.widget.RecyclerView
12 | import android.view.LayoutInflater
13 | import android.view.View
14 | import android.view.ViewGroup
15 | import com.olacabs.olaplaystudio.R
16 | import com.olacabs.olaplaystudio.data.model.MediaDetail
17 | import com.olacabs.olaplaystudio.utils.visible
18 | import com.squareup.picasso.Picasso
19 | import kotlinx.android.synthetic.main.item_media.view.*
20 | import javax.inject.Inject
21 |
22 | class LibraryAdapter @Inject
23 | constructor() : RecyclerView.Adapter() {
24 |
25 | @Inject lateinit var mPicasso: Picasso
26 |
27 | val mMediaList: ArrayList = arrayListOf()
28 | var mClickListener: ClickListener? = null
29 |
30 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LibraryViewHolder {
31 | val view = LayoutInflater
32 | .from(parent.context)
33 | .inflate(R.layout.item_media, parent, false)
34 | return LibraryViewHolder(view)
35 | }
36 |
37 | override fun onBindViewHolder(holder: LibraryViewHolder, position: Int) {
38 | val media = mMediaList[position]
39 | val view = holder.itemView
40 |
41 | view.media_title.text = media.song ?: ""
42 | view.media_artist.text = media.artists ?: ""
43 |
44 | media.cover_image?.let {
45 | mPicasso.load(it).placeholder(R.drawable.album_placeholder).into(view.media_image)
46 | }
47 |
48 | view.play_button.setOnClickListener {
49 | mClickListener?.onMediaClick(mediaDetail = media)
50 | }
51 |
52 | val drawable = getDrawableByState(view.context.applicationContext, media.state)
53 | if (drawable != null) {
54 | view.play_button?.let {
55 | it.setImageDrawable(drawable)
56 | (it.drawable as? AnimationDrawable)?.start()
57 | }
58 | } else {
59 | view.play_button?.setImageResource(R.drawable.ic_media_play)
60 | }
61 |
62 | view.fav_iv.setOnClickListener { mClickListener?.onFavClick(holder.adapterPosition, media) }
63 | view.download_iv.setOnClickListener { mClickListener?.onDownloadClick(media) }
64 | view.fav_iv.setImageResource(
65 | if (media.fav) R.drawable.ic_media_favorite_fill
66 | else R.drawable.ic_media_favorite_border)
67 |
68 | view?.download_iv?.visible(!media.isDownloaded)
69 |
70 | }
71 |
72 | fun setClickListener(clickListener: ClickListener) {
73 | mClickListener = clickListener
74 | }
75 |
76 | override fun getItemCount(): Int {
77 | return mMediaList.size
78 | }
79 |
80 | interface ClickListener {
81 | fun onMediaClick(mediaDetail: MediaDetail)
82 | fun onFavClick(position: Int, mediaDetail: MediaDetail)
83 | fun onDownloadClick(mediaDetail: MediaDetail)
84 | }
85 |
86 | class LibraryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
87 |
88 | companion object {
89 |
90 | private var sColorStatePlaying: Int? = null
91 | private var sColorStateNotPlaying: Int? = null
92 |
93 | fun getDrawableByState(context: Context?, state: Int): Drawable? {
94 | if (context == null)
95 | return null
96 |
97 | if (sColorStateNotPlaying == null || sColorStatePlaying == null)
98 | initializeColorStateLists(context)
99 |
100 | when (state) {
101 | PlaybackStateCompat.STATE_NONE -> {
102 | val pauseDrawable = ContextCompat.getDrawable(context,
103 | R.drawable.ic_media_play)
104 | setDrawableTint(pauseDrawable, sColorStateNotPlaying!!)
105 | return pauseDrawable
106 | }
107 | PlaybackStateCompat.STATE_PLAYING -> {
108 | val animation = ContextCompat.getDrawable(context, R.drawable.ic_equalizer_anim)
109 | as AnimationDrawable
110 | setDrawableTint(animation, sColorStatePlaying!!)
111 | //Starting animation here was not working in some device
112 | return animation
113 | }
114 | PlaybackStateCompat.STATE_BUFFERING, PlaybackStateCompat.STATE_PAUSED -> {
115 | val playDrawable = ContextCompat.getDrawable(context,
116 | R.drawable.ic_equalizer1)
117 | setDrawableTint(playDrawable, sColorStatePlaying!!)
118 | return playDrawable
119 | }
120 | else -> return null
121 | }
122 | }
123 |
124 | private fun setDrawableTint(drawable: Drawable, color: Int) {
125 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
126 | DrawableCompat.setTint(drawable, color)
127 | else
128 | drawable.mutate().setColorFilter(color, PorterDuff.Mode.SRC_IN)
129 | }
130 |
131 | private fun initializeColorStateLists(ctx: Context) {
132 | sColorStateNotPlaying = ContextCompat.getColor(ctx,
133 | R.color.media_item_icon_not_playing)
134 | sColorStatePlaying = ContextCompat.getColor(ctx,
135 | R.color.media_item_icon_playing)
136 | }
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/app/src/main/java/com/olacabs/olaplaystudio/ui/library/LibraryPresenter.kt:
--------------------------------------------------------------------------------
1 | package com.olacabs.olaplaystudio.ui.library
2 |
3 | import android.app.Application
4 | import android.app.DownloadManager
5 | import android.content.Context.DOWNLOAD_SERVICE
6 | import android.net.Uri
7 | import android.os.Environment
8 | import com.olacabs.olaplaystudio.data.DataManager
9 | import com.olacabs.olaplaystudio.data.DataManagerImpl
10 | import com.olacabs.olaplaystudio.data.local.AppPrefs
11 | import com.olacabs.olaplaystudio.data.model.MediaDetail
12 | import com.olacabs.olaplaystudio.di.ConfigPersistent
13 | import com.olacabs.olaplaystudio.playback.PublishMediaLoadEvent
14 | import com.olacabs.olaplaystudio.ui.base.BasePresenter
15 | import com.olacabs.olaplaystudio.utils.clearAndAddAll
16 | import org.greenrobot.eventbus.EventBus
17 | import timber.log.Timber
18 | import javax.inject.Inject
19 |
20 |
21 | /**
22 | * Created by sai on 16/12/17.
23 | */
24 |
25 | @ConfigPersistent
26 | class LibraryPresenter @Inject
27 | constructor(private val mDataManager: DataManager) : BasePresenter() {
28 | @Inject lateinit var mLibraryAdapter: LibraryAdapter
29 | @Inject lateinit var mAppPrefs: AppPrefs
30 | @Inject lateinit var mApplication: Application
31 | val mMediaListOriginal: ArrayList = arrayListOf()
32 | var showFavorites = false
33 |
34 | override fun attachView(mvpView: LibraryView) {
35 | super.attachView(mvpView)
36 | mLibraryAdapter.setClickListener(object : LibraryAdapter.ClickListener {
37 | override fun onMediaClick(mediaDetail: MediaDetail) {
38 | Timber.i("Play request for %s", mediaDetail.song)
39 | mvpView.playSelected(mediaDetail)
40 | }
41 |
42 | override fun onFavClick(position: Int, mediaDetail: MediaDetail) {
43 | favClicked(mediaDetail, position)
44 | }
45 |
46 | override fun onDownloadClick(mediaDetail: MediaDetail) {
47 | onDownloadClicked(mediaDetail)
48 | }
49 | })
50 | }
51 |
52 | private fun onDownloadClicked(mediaDetail: MediaDetail) {
53 | mvpView?.downloadFile(mediaDetail)
54 | }
55 |
56 | private fun favClicked(mediaDetail: MediaDetail, position: Int) {
57 | if (mAppPrefs.favList.contains(mediaDetail.song)) {
58 | mAppPrefs.removeFromFav(mediaDetail.song ?: "")
59 | mLibraryAdapter.mMediaList[position].fav = false
60 | } else {
61 | mediaDetail.song?.let { mAppPrefs.addToFav(it.trim()) }
62 | mLibraryAdapter.mMediaList[position].fav = true
63 | }
64 | mLibraryAdapter.notifyItemChanged(position)
65 | }
66 |
67 | private val fetchMediaCallBack = object : DataManagerImpl.MediaListCallBack {
68 | override fun onSuccess(listOfMedia: List) {
69 | mvpView?.showProgress(false)
70 | listOfMedia.forEachIndexed { index, mediaDetail ->
71 | mediaDetail.index = index
72 | mediaDetail.fav = mAppPrefs.favList.find { it == mediaDetail.song } !== null
73 | }
74 | mMediaListOriginal.clearAndAddAll(listOfMedia)
75 | mLibraryAdapter.mMediaList.clearAndAddAll(mMediaListOriginal)
76 | updateMediaUI(mediaDetail = mMediaListOriginal.first())
77 | EventBus.getDefault().post(PublishMediaLoadEvent(mMediaListOriginal))
78 | mLibraryAdapter.notifyDataSetChanged()
79 | }
80 |
81 | override fun onError(message: String) {
82 | mvpView?.showProgress(false)
83 | mvpView?.showError(message)
84 | }
85 |
86 | }
87 |
88 | private fun updateMediaUI(mediaDetail: MediaDetail) {
89 | mvpView?.updateMediaUi(mediaDetail)
90 | }
91 |
92 | fun fetchMediaList() {
93 | checkViewAttached()
94 | showFavorites = false
95 | mvpView?.showProgress(true)
96 | addDisposable(mDataManager.getMediaList(fetchMediaCallBack))
97 | }
98 |
99 | fun notifyAdapter() {
100 | mLibraryAdapter.notifyDataSetChanged()
101 | }
102 |
103 | fun toggleFav(): Boolean {
104 | showFavorites = !showFavorites
105 | if (showFavorites) {
106 | val favorites = mMediaListOriginal.filter { it.fav }
107 | if (favorites.isNotEmpty()) {
108 | mLibraryAdapter.mMediaList.clearAndAddAll(favorites)
109 | } else {
110 | mvpView?.showError("No favorites found")
111 | return true
112 | }
113 | } else {
114 | mLibraryAdapter.mMediaList.clearAndAddAll(mMediaListOriginal)
115 | }
116 | mLibraryAdapter.notifyDataSetChanged()
117 | mvpView?.updateFavMenuIcon(showFavorites)
118 | return true
119 | }
120 |
121 | fun downloadFile(mediaDetail: MediaDetail) {
122 | mediaDetail.url?.let { mediaUrl ->
123 | val r = DownloadManager.Request(Uri.parse(mediaUrl))
124 | r.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, mediaDetail.song)
125 | r.allowScanningByMediaScanner()
126 | r.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
127 | val dm = mApplication.getSystemService(DOWNLOAD_SERVICE) as DownloadManager
128 | dm.enqueue(r)
129 | }
130 | mLibraryAdapter.mMediaList.find { it == mediaDetail }?.isDownloaded = true
131 | mLibraryAdapter.notifyDataSetChanged()
132 | }
133 |
134 | fun filterList(text: String) {
135 | mLibraryAdapter.run {
136 | mLibraryAdapter.mMediaList.clearAndAddAll(mMediaListOriginal.filter { it.song!!.contains(text,true)})
137 | notifyDataSetChanged()
138 | }
139 | }
140 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/olacabs/olaplaystudio/ui/library/LibraryView.kt:
--------------------------------------------------------------------------------
1 | package com.olacabs.olaplaystudio.ui.library
2 |
3 | import com.olacabs.olaplaystudio.data.model.MediaDetail
4 | import com.olacabs.olaplaystudio.ui.base.MvpView
5 | import permissions.dispatcher.NeedsPermission
6 |
7 | /**
8 | * Created by sai on 16/12/17.
9 | */
10 | interface LibraryView : MvpView {
11 | fun showProgress(show: Boolean)
12 | fun showError(message: String)
13 | fun updateMediaUi(mediaDetail: MediaDetail)
14 | fun playSelected(mediaDetail: MediaDetail)
15 | fun updateFavMenuIcon(showFavorites: Boolean)
16 | fun downloadFile(mediaDetail: MediaDetail)
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/olacabs/olaplaystudio/ui/welcome/WelcomeActivity.kt:
--------------------------------------------------------------------------------
1 | package com.olacabs.olaplaystudio.ui.welcome
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import android.os.Handler
6 | import android.support.v7.app.AppCompatActivity
7 | import com.olacabs.olaplaystudio.R
8 | import com.olacabs.olaplaystudio.ui.library.LibraryActivity
9 |
10 | class WelcomeActivity : AppCompatActivity() {
11 |
12 | override fun onCreate(savedInstanceState: Bundle?) {
13 | super.onCreate(savedInstanceState)
14 | setContentView(R.layout.activity_welcome)
15 |
16 | Handler().postDelayed({
17 | startActivity(Intent(applicationContext, LibraryActivity::class.java))
18 | finish()
19 | }, 1000)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/src/main/java/com/olacabs/olaplaystudio/utils/Common.kt:
--------------------------------------------------------------------------------
1 | package com.olacabs.olaplaystudio.utils
2 |
3 | import com.olacabs.olaplaystudio.OlaApplication
4 | import org.greenrobot.eventbus.EventBus
5 | import java.text.SimpleDateFormat
6 | import java.util.*
7 |
8 |
9 | fun Date.formatDate(): String {
10 | val dateFormat = SimpleDateFormat("MMM dd, yyyy", Locale.ENGLISH)
11 | return dateFormat.format(this)
12 | }
13 |
14 | fun Date.getSimpleTime(): String {
15 | val dateFormat = SimpleDateFormat("hh:mm a", Locale.ENGLISH)
16 | return dateFormat.format(this)
17 | }
18 |
19 | fun MutableCollection.clearAndAddAll(replace: Collection) {
20 | clear()
21 | addAll(replace)
22 | }
23 |
24 | fun Any.showAsToast() {
25 | EventBus.getDefault().post(OlaApplication.Companion.ShowToastEvent(this.toString()))
26 | }
27 |
28 | fun EventBus.regOnce(subscriber: Any) {
29 | if (!isRegistered(subscriber)) register(subscriber)
30 | }
31 |
32 | fun EventBus.unRegOnce(subscriber: Any) {
33 | if (isRegistered(subscriber)) unregister(subscriber)
34 | }
35 |
--------------------------------------------------------------------------------
/app/src/main/java/com/olacabs/olaplaystudio/utils/NetworkUtil.kt:
--------------------------------------------------------------------------------
1 | package com.olacabs.olaplaystudio.utils
2 |
3 | import android.content.Context
4 | import android.net.ConnectivityManager
5 | import retrofit2.HttpException
6 |
7 |
8 | object NetworkUtil {
9 |
10 | /**
11 | * Returns true if the Throwable is an instance of RetrofitError with an
12 | * http status code equals to the given one.
13 | */
14 | fun isHttpStatusCode(throwable: Throwable, statusCode: Int): Boolean {
15 | return throwable is HttpException && throwable.code() == statusCode
16 | }
17 |
18 | fun isNetworkConnected(context: Context): Boolean {
19 | val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
20 | val activeNetwork = cm.activeNetworkInfo
21 | return activeNetwork != null && activeNetwork.isConnectedOrConnecting
22 | }
23 |
24 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/olacabs/olaplaystudio/utils/ViewUtil.kt:
--------------------------------------------------------------------------------
1 | package com.olacabs.olaplaystudio.utils
2 |
3 | import android.content.Context
4 | import android.content.res.Resources
5 | import android.view.View
6 | import android.view.inputmethod.InputMethodManager
7 |
8 |
9 | fun Float.pxToDp(): Float {
10 | val densityDpi = Resources.getSystem().displayMetrics.densityDpi.toFloat()
11 | return this / (densityDpi / 160f)
12 | }
13 |
14 | fun Int.dpToPx(): Int {
15 | val density = Resources.getSystem().displayMetrics.density
16 | return Math.round(this * density)
17 | }
18 |
19 | fun View.hideKeyboard() {
20 | val input = this.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
21 | input.hideSoftInputFromWindow(this.applicationWindowToken, InputMethodManager.HIDE_NOT_ALWAYS)
22 | }
23 |
24 | fun View.showKeyboard() {
25 | val imm = this.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
26 | imm.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT)
27 | }
28 |
29 | fun View.visible(b: Boolean = true) {
30 | if (b) show() else hide()
31 | }
32 |
33 | fun View.show() {
34 | this.visibility = View.VISIBLE
35 | }
36 |
37 | fun View.hide() {
38 | this.visibility = View.GONE
39 | }
40 |
41 | fun View.invisible() {
42 | this.visibility = View.INVISIBLE
43 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_notification_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saisoftdev/Android-MusicPlayer-MVP/fcb453ca39e821e557eb78cd2161184b11940432/app/src/main/res/drawable-hdpi/ic_notification_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_notification_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saisoftdev/Android-MusicPlayer-MVP/fcb453ca39e821e557eb78cd2161184b11940432/app/src/main/res/drawable-mdpi/ic_notification_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_notification_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saisoftdev/Android-MusicPlayer-MVP/fcb453ca39e821e557eb78cd2161184b11940432/app/src/main/res/drawable-xhdpi/ic_notification_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_notification_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saisoftdev/Android-MusicPlayer-MVP/fcb453ca39e821e557eb78cd2161184b11940432/app/src/main/res/drawable-xxhdpi/ic_notification_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/album_placeholder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saisoftdev/Android-MusicPlayer-MVP/fcb453ca39e821e557eb78cd2161184b11940432/app/src/main/res/drawable-xxxhdpi/album_placeholder.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/media_sample.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saisoftdev/Android-MusicPlayer-MVP/fcb453ca39e821e557eb78cd2161184b11940432/app/src/main/res/drawable-xxxhdpi/media_sample.jpg
--------------------------------------------------------------------------------
/app/src/main/res/drawable/actionbar_bg_gradient_light.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/actionbarbackgrounds.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/fullscreen_bg_gradient.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
24 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_action_back.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_action_v_next.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_action_v_pause.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_action_v_play.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_action_v_previous.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_equalizer1.xml:
--------------------------------------------------------------------------------
1 |
3 |
5 |
7 |
9 |
11 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_equalizer2.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
14 |
18 |
22 |
26 |
27 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_equalizer3.xml:
--------------------------------------------------------------------------------
1 |
3 |
5 |
7 |
9 |
11 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_equalizer_anim.xml:
--------------------------------------------------------------------------------
1 |
16 |
18 |
21 |
24 |
27 |
28 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_media_download.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_media_favorite_border.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_media_favorite_fill.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_media_pause.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_media_play.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_search.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/font/audiowide.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saisoftdev/Android-MusicPlayer-MVP/fcb453ca39e821e557eb78cd2161184b11940432/app/src/main/res/font/audiowide.ttf
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_library.xml:
--------------------------------------------------------------------------------
1 |
12 |
13 |
16 |
17 |
20 |
21 |
28 |
29 |
30 |
31 |
36 |
37 |
43 |
44 |
51 |
52 |
53 |
54 |
58 |
59 |
64 |
65 |
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_welcome.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/content_fullscreen_player.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
16 |
17 |
24 |
25 |
31 |
32 |
44 |
45 |
57 |
58 |
62 |
63 |
71 |
72 |
80 |
81 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/controls_panel.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
16 |
17 |
25 |
26 |
31 |
32 |
33 |
34 |
42 |
43 |
53 |
54 |
64 |
65 |
66 |
74 |
75 |
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_media.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
14 |
15 |
19 |
20 |
29 |
30 |
35 |
36 |
47 |
48 |
59 |
60 |
69 |
70 |
84 |
85 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
116 |
117 |
118 |
119 |
120 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/music_toolbar.xml:
--------------------------------------------------------------------------------
1 |
2 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saisoftdev/Android-MusicPlayer-MVP/fcb453ca39e821e557eb78cd2161184b11940432/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saisoftdev/Android-MusicPlayer-MVP/fcb453ca39e821e557eb78cd2161184b11940432/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saisoftdev/Android-MusicPlayer-MVP/fcb453ca39e821e557eb78cd2161184b11940432/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saisoftdev/Android-MusicPlayer-MVP/fcb453ca39e821e557eb78cd2161184b11940432/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saisoftdev/Android-MusicPlayer-MVP/fcb453ca39e821e557eb78cd2161184b11940432/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/values-v21/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #4CAF50
4 | #388E3C
5 | #C8E6C9
6 | #FF5722
7 | #212121
8 | #757575
9 | #FFFFFF
10 | #BDBDBD
11 | #FFF
12 | #000
13 | #000
14 |
15 |
16 | @color/icons
17 | @color/icons
18 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Ola Play Studio
3 | OLA_Channel_ID
4 | Channel ID for OLA
5 | Pause
6 | Play
7 | Previous
8 | Next
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
13 |
14 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | ext.kotlin_version = '1.2.0'
5 | repositories {
6 | google()
7 | jcenter()
8 | }
9 | dependencies {
10 | classpath 'com.android.tools.build:gradle:3.0.1'
11 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
12 |
13 | // NOTE: Do not place your application dependencies here; they belong
14 | // in the individual module build.gradle files
15 | }
16 | }
17 |
18 | allprojects {
19 | repositories {
20 | google()
21 | jcenter()
22 | }
23 | }
24 |
25 | task clean(type: Delete) {
26 | delete rootProject.buildDir
27 | }
28 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | OlaMediaBaseUrl=http://starlord.hackerearth.com
--------------------------------------------------------------------------------
/sample_apk/play_studio.apk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saisoftdev/Android-MusicPlayer-MVP/fcb453ca39e821e557eb78cd2161184b11940432/sample_apk/play_studio.apk
--------------------------------------------------------------------------------
/screens/1.Splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saisoftdev/Android-MusicPlayer-MVP/fcb453ca39e821e557eb78cd2161184b11940432/screens/1.Splash.png
--------------------------------------------------------------------------------
/screens/2.MusicList.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saisoftdev/Android-MusicPlayer-MVP/fcb453ca39e821e557eb78cd2161184b11940432/screens/2.MusicList.png
--------------------------------------------------------------------------------
/screens/3.PlayerFullScreen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saisoftdev/Android-MusicPlayer-MVP/fcb453ca39e821e557eb78cd2161184b11940432/screens/3.PlayerFullScreen.png
--------------------------------------------------------------------------------
/screens/4.Favs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saisoftdev/Android-MusicPlayer-MVP/fcb453ca39e821e557eb78cd2161184b11940432/screens/4.Favs.png
--------------------------------------------------------------------------------
/screens/5.Search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saisoftdev/Android-MusicPlayer-MVP/fcb453ca39e821e557eb78cd2161184b11940432/screens/5.Search.png
--------------------------------------------------------------------------------
/screens/6.MediaNotification.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saisoftdev/Android-MusicPlayer-MVP/fcb453ca39e821e557eb78cd2161184b11940432/screens/6.MediaNotification.png
--------------------------------------------------------------------------------
/screens/7.LockScreen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saisoftdev/Android-MusicPlayer-MVP/fcb453ca39e821e557eb78cd2161184b11940432/screens/7.LockScreen.png
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------