├── .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 | ![Music List](/screens/2.MusicList.png?raw=true "Music List") 7 | ![Player Full Screen](/screens/3.PlayerFullScreen.png?raw=true "Player Full Screen") 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 | 4 | 10 | 16 | 17 | 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 | --------------------------------------------------------------------------------