├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── values-night │ │ │ │ └── colors.xml │ │ │ ├── drawable │ │ │ │ ├── ic_play_black_48dp.xml │ │ │ │ ├── ic_pause_black_48dp.xml │ │ │ │ ├── ic_skip_next_black_48dp.xml │ │ │ │ ├── ic_stop_black_24dp.xml │ │ │ │ ├── ic_home_black_24dp.xml │ │ │ │ ├── ic_skip_next_black_24dp.xml │ │ │ │ ├── ic_skip_previous_black_24dp.xml │ │ │ │ ├── ic_playlist_black_24dp.xml │ │ │ │ ├── ic_music_note_black_12dp.xml │ │ │ │ ├── ic_now_playing_black_24dp.xml │ │ │ │ ├── ic_music_note_black_24dp.xml │ │ │ │ ├── ic_genres_black_24dp.xml │ │ │ │ ├── ic_album_black_24dp.xml │ │ │ │ ├── ic_tracks_black_24dp.xml │ │ │ │ ├── ic_artist_black_24dp.xml │ │ │ │ ├── bg_no_art.xml │ │ │ │ ├── now_playing_anim.xml │ │ │ │ └── ic_launcher_background.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── values │ │ │ │ ├── themes.xml │ │ │ │ ├── styles.xml │ │ │ │ ├── dimens.xml │ │ │ │ ├── colors.xml │ │ │ │ └── strings.xml │ │ │ ├── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ │ └── layout │ │ │ │ ├── item_track.xml │ │ │ │ └── activity_dashboard.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── github │ │ │ │ └── odaridavid │ │ │ │ └── zikk │ │ │ │ ├── models │ │ │ │ ├── Genre.kt │ │ │ │ ├── PlaybackStatus.kt │ │ │ │ ├── MediaId.kt │ │ │ │ ├── Playlist.kt │ │ │ │ ├── Artist.kt │ │ │ │ ├── Album.kt │ │ │ │ ├── PlayableTrack.kt │ │ │ │ ├── Track.kt │ │ │ │ └── MediaCategoryInfo.kt │ │ │ │ ├── utils │ │ │ │ ├── Constants.kt │ │ │ │ ├── SdkUtils.kt │ │ │ │ ├── InjectorUtils.kt │ │ │ │ ├── MediaControllerUtils.kt │ │ │ │ ├── PermissionUtils.kt │ │ │ │ ├── TimeUtils.kt │ │ │ │ ├── DebugUtils.kt │ │ │ │ ├── MediaItemUtils.kt │ │ │ │ ├── ViewUtils.kt │ │ │ │ └── MediaMetadataUtils.kt │ │ │ │ ├── di │ │ │ │ ├── GenreModule.kt │ │ │ │ ├── ArtistModule.kt │ │ │ │ ├── DataModule.kt │ │ │ │ ├── PlaylistModule.kt │ │ │ │ ├── AlbumModule.kt │ │ │ │ ├── TrackModule.kt │ │ │ │ ├── AppComponent.kt │ │ │ │ ├── PlaybackModule.kt │ │ │ │ └── AppModule.kt │ │ │ │ ├── mappers │ │ │ │ ├── MediaItemToPlayableTrack.kt │ │ │ │ └── TrackToPlayableTrack.kt │ │ │ │ ├── ZikkApp.kt │ │ │ │ ├── data │ │ │ │ └── LastPlayedTrackPreference.kt │ │ │ │ ├── playback │ │ │ │ ├── BecomingNoisyReceiver.kt │ │ │ │ ├── player │ │ │ │ │ └── TrackPlayer.kt │ │ │ │ └── session │ │ │ │ │ ├── ZikkMediaService.kt │ │ │ │ │ ├── MediaLoader.kt │ │ │ │ │ └── MediaSessionCallback.kt │ │ │ │ ├── base │ │ │ │ └── BaseActivity.kt │ │ │ │ ├── ui │ │ │ │ ├── DashboardViewModel.kt │ │ │ │ ├── TracksAdapter.kt │ │ │ │ └── DashboardActivity.kt │ │ │ │ ├── notification │ │ │ │ ├── NotificationsChannelManager.kt │ │ │ │ └── PlaybackNotificationBuilder.kt │ │ │ │ └── repositories │ │ │ │ ├── GenreRepository.kt │ │ │ │ ├── PlaylistRepository.kt │ │ │ │ ├── ArtistRepository.kt │ │ │ │ ├── AlbumRepository.kt │ │ │ │ └── TrackRepository.kt │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── github │ │ │ └── odaridavid │ │ │ └── zikk │ │ │ ├── SongRepositoryTest.kt │ │ │ ├── DashboardViewModelTest.kt │ │ │ └── RecentsRepositoryTest.kt │ └── androidTest │ │ └── java │ │ └── com │ │ └── github │ │ └── odaridavid │ │ └── zikk │ │ └── DashboardActivityTest.kt ├── proguard-rules.pro ├── google-services.json └── build.gradle ├── settings.gradle ├── art └── app.gif ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── README.md ├── gradle.properties ├── gradlew.bat ├── gradlew └── LICENSE /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name='Zikk' 2 | include ':app' 3 | -------------------------------------------------------------------------------- /art/app.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odaridavid/Zikk-Music-App/HEAD/art/app.gif -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odaridavid/Zikk-Music-App/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odaridavid/Zikk-Music-App/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odaridavid/Zikk-Music-App/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odaridavid/Zikk-Music-App/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odaridavid/Zikk-Music-App/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odaridavid/Zikk-Music-App/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odaridavid/Zikk-Music-App/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odaridavid/Zikk-Music-App/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odaridavid/Zikk-Music-App/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odaridavid/Zikk-Music-App/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odaridavid/Zikk-Music-App/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | @color/grey800 4 | @color/grey800Dark 5 | @color/yellowA700 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Apr 09 01:31:24 EAT 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_play_black_48dp.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_pause_black_48dp.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_skip_next_black_48dp.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_stop_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_home_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_skip_next_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_skip_previous_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_playlist_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_music_note_black_12dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_now_playing_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_music_note_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_genres_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_album_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_tracks_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 4dp 5 | 4dp 6 | 120dp 7 | 80dp 8 | 72dp 9 | 72dp 10 | 8dp 11 | 48dp 12 | 48dp 13 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | @color/grey100 4 | @color/grey100Dark 5 | @color/yellowA700 6 | 7 | 8 | #ffff52 9 | #ffd600 10 | #f5f5f5 11 | #c2c2c2 12 | #424242 13 | #1b1b1b 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_artist_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/test/java/com/github/odaridavid/zikk/SongRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ -------------------------------------------------------------------------------- /app/src/test/java/com/github/odaridavid/zikk/DashboardViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | class DashboardViewModelTest -------------------------------------------------------------------------------- /app/src/test/java/com/github/odaridavid/zikk/RecentsRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | class RecentsRepositoryTest -------------------------------------------------------------------------------- /app/src/androidTest/java/com/github/odaridavid/zikk/DashboardActivityTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | class DashboardActivityTest -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/models/Genre.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.models 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | internal data class Genre(val id: Long, val name: String) -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/models/PlaybackStatus.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.models 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | class PlaybackStatus(val prevTrackIndex: Int, val currentTrackIndex: Int) -------------------------------------------------------------------------------- /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/java/com/github/odaridavid/zikk/models/MediaId.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.models 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | enum class MediaId { 17 | ARTIST, 18 | GENRE, 19 | ALBUM, 20 | TRACK, 21 | PLAYLIST 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/models/Playlist.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.models 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | internal data class Playlist( 17 | val id: Long, 18 | val name: String, 19 | val modified: String 20 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/utils/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.utils 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | object Constants { 17 | const val PLAYBACK_NOTIFICATION_ID = 1000 18 | const val PREFERENCE_NAME = "zikk_preferences" 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/models/Artist.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.models 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | internal data class Artist( 17 | val id: Long, 18 | val name: String, 19 | val noOfAlbums: Int, 20 | val noOfTracks: Int 21 | ) -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_no_art.xml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/models/Album.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.models 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | internal data class Album( 17 | val id: Long, 18 | val title: String, 19 | val artist: String, 20 | val noOfSongs: Int, 21 | val albumArt: String 22 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/models/PlayableTrack.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.models 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | import android.net.Uri 17 | 18 | /** 19 | * Track displayed on the UI 20 | */ 21 | data class PlayableTrack( 22 | val mediaId: String?, 23 | val title: String?, 24 | val artist: String, 25 | val icon: Uri?, 26 | val isPlaying: Boolean = false 27 | ) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Zikk 2 | 3 | A sample media player application making use of android MediaPlayer Apis 4 | The sample is still under development and may contain a number of bugs.Feel 5 | free to file an issue if you spot one or send a PR😎 6 | 7 | ### Demo/Screenshots 8 | 9 | 10 | 11 | ### Prerequisites 12 | 13 | Setup a firebase project and hook it up. 14 | 15 | ### License 16 | ``` 17 | Copyright 2020 David Odari 18 | 19 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 20 | in compliance with the License. You may obtain a copy of the License at 21 | http://www.apache.org/licenses/LICENSE-2.0 22 | Unless required by applicable law or agreed to in writing, software distributed under the License 23 | is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 24 | or implied. See the License for the specific language governing permissions and limitations under 25 | the License. 26 | ``` 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/models/Track.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.models 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | internal data class Track( 17 | val id: Long, 18 | val artistId: Long, 19 | val title: String, 20 | val album: String, 21 | val artist: String, 22 | val displayName: String, 23 | val track: String, 24 | val duration: String, 25 | val filePath: String, 26 | val albumArt: String 27 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/models/MediaCategoryInfo.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.models 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | import android.net.Uri 17 | import android.support.v4.media.MediaBrowserCompat 18 | 19 | 20 | data class MediaCategoryInfo( 21 | val id: MediaId, 22 | val title: String, 23 | val subtitle: String, 24 | val iconUri: Uri, 25 | val description: String, 26 | @MediaBrowserCompat.MediaItem.Flags val mediaFlags: Int 27 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/utils/SdkUtils.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.utils 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | import android.os.Build 17 | import androidx.annotation.IntRange 18 | 19 | fun versionFrom(@IntRange(from = 1, to = 29) versionCode: Int): Boolean { 20 | return Build.VERSION.SDK_INT >= versionCode 21 | } 22 | 23 | fun versionUntil(@IntRange(from = 1, to = 29) versionCode: Int): Boolean { 24 | return Build.VERSION.SDK_INT <= versionCode 25 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/utils/InjectorUtils.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.utils 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | import android.app.Activity 17 | import android.app.Service 18 | import com.github.odaridavid.zikk.ZikkApp 19 | import com.github.odaridavid.zikk.di.AppComponent 20 | 21 | internal val Activity.injector: AppComponent 22 | get() = (applicationContext as ZikkApp).appComponent 23 | 24 | internal val Service.injector: AppComponent 25 | get() = (applicationContext as ZikkApp).appComponent -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/di/GenreModule.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.di 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | import android.content.Context 17 | import com.github.odaridavid.zikk.repositories.GenreRepository 18 | import dagger.Module 19 | import dagger.Provides 20 | 21 | @Module 22 | internal class GenreModule { 23 | 24 | @Provides 25 | fun providesGenreProvider(applicationContext: Context): GenreRepository { 26 | return GenreRepository( 27 | applicationContext 28 | ) 29 | } 30 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/di/ArtistModule.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.di 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | import android.content.Context 17 | import com.github.odaridavid.zikk.repositories.ArtistRepository 18 | import dagger.Module 19 | import dagger.Provides 20 | 21 | 22 | @Module 23 | internal class ArtistModule { 24 | 25 | @Provides 26 | fun providesArtistsProvider(applicationContext: Context): ArtistRepository { 27 | return ArtistRepository( 28 | applicationContext 29 | ) 30 | } 31 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/mappers/MediaItemToPlayableTrack.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.mappers 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | import android.support.v4.media.MediaBrowserCompat 17 | import com.github.odaridavid.zikk.models.PlayableTrack 18 | 19 | 20 | fun MediaBrowserCompat.MediaItem.toTrack(): PlayableTrack { 21 | return PlayableTrack( 22 | this.mediaId, 23 | this.description.title.toString(), 24 | this.description.subtitle.toString(), 25 | this.description.iconUri 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/di/DataModule.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.di 2 | /** 3 | * 4 | * Copyright 2020 David Odari 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | * 14 | **/ 15 | import android.content.SharedPreferences 16 | import com.github.odaridavid.zikk.data.LastPlayedTrackPreference 17 | import dagger.Module 18 | import dagger.Provides 19 | 20 | 21 | @Module 22 | internal class DataModule { 23 | 24 | @Provides 25 | fun providesLastPlayedTrackPreference(sharedPreferences: SharedPreferences):LastPlayedTrackPreference{ 26 | return LastPlayedTrackPreference(sharedPreferences) 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/di/PlaylistModule.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.di 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | import android.content.Context 17 | import com.github.odaridavid.zikk.repositories.PlaylistRepository 18 | import dagger.Module 19 | import dagger.Provides 20 | 21 | 22 | @Module 23 | internal class PlaylistModule { 24 | 25 | @Provides 26 | fun providesPlaylistProvider(applicationContext: Context): PlaylistRepository { 27 | return PlaylistRepository( 28 | applicationContext 29 | ) 30 | } 31 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/di/AlbumModule.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.di 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | import android.content.Context 17 | import com.github.odaridavid.zikk.repositories.AlbumRepository 18 | import dagger.Module 19 | import dagger.Provides 20 | 21 | /** 22 | * Creats Album Module Dependencies 23 | */ 24 | @Module 25 | internal class AlbumModule { 26 | 27 | @Provides 28 | fun providesAlbumProvider(applicationContext: Context): AlbumRepository { 29 | return AlbumRepository( 30 | applicationContext 31 | ) 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/mappers/TrackToPlayableTrack.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.mappers 2 | 3 | import androidx.core.net.toUri 4 | import com.github.odaridavid.zikk.models.MediaId 5 | import com.github.odaridavid.zikk.models.PlayableTrack 6 | import com.github.odaridavid.zikk.models.Track 7 | 8 | /** 9 | * 10 | * Copyright 2020 David Odari 11 | * 12 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 13 | * in compliance with the License. You may obtain a copy of the License at 14 | * http://www.apache.org/licenses/LICENSE-2.0 15 | * Unless required by applicable law or agreed to in writing, software distributed under the License 16 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 17 | * or implied. See the License for the specific language governing permissions and limitations under 18 | * the License. 19 | * 20 | **/ 21 | 22 | internal fun Track.toPlayableTrack(): PlayableTrack { 23 | val mediaId = "${MediaId.TRACK}-${id}" 24 | 25 | return PlayableTrack( 26 | mediaId, 27 | title, 28 | artist, 29 | albumArt.toUri() 30 | ) 31 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/utils/MediaControllerUtils.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.utils 2 | 3 | import android.support.v4.media.session.MediaControllerCompat 4 | import com.github.odaridavid.zikk.ui.DashboardActivity 5 | 6 | /** 7 | * 8 | * Copyright 2020 David Odari 9 | * 10 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 11 | * in compliance with the License. You may obtain a copy of the License at 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * Unless required by applicable law or agreed to in writing, software distributed under the License 14 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 15 | * or implied. See the License for the specific language governing permissions and limitations under 16 | * the License. 17 | * 18 | **/ 19 | internal val DashboardActivity.mediaTranspotControls: MediaControllerCompat.TransportControls? 20 | get() = MediaControllerCompat.getMediaController(this).transportControls 21 | 22 | internal val DashboardActivity.mediaControllerCompat: MediaControllerCompat? 23 | get() = MediaControllerCompat.getMediaController(this) 24 | 25 | -------------------------------------------------------------------------------- /app/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "636424474576", 4 | "firebase_url": "https://zikki-cad03.firebaseio.com", 5 | "project_id": "zikki-cad03", 6 | "storage_bucket": "zikki-cad03.appspot.com" 7 | }, 8 | "client": [ 9 | { 10 | "client_info": { 11 | "mobilesdk_app_id": "1:636424474576:android:8283c4217c46117b87f3d5", 12 | "android_client_info": { 13 | "package_name": "com.github.odaridavid.zikk" 14 | } 15 | }, 16 | "oauth_client": [ 17 | { 18 | "client_id": "636424474576-73p72pplpnp3njov7tufafhb2h0lvge1.apps.googleusercontent.com", 19 | "client_type": 3 20 | } 21 | ], 22 | "api_key": [ 23 | { 24 | "current_key": "AIzaSyBXnRIrGnP22nZ-gvbTVm_LFdvgp_hfkEk" 25 | } 26 | ], 27 | "services": { 28 | "appinvite_service": { 29 | "other_platform_oauth_client": [ 30 | { 31 | "client_id": "636424474576-73p72pplpnp3njov7tufafhb2h0lvge1.apps.googleusercontent.com", 32 | "client_type": 3 33 | } 34 | ] 35 | } 36 | } 37 | } 38 | ], 39 | "configuration_version": "1" 40 | } -------------------------------------------------------------------------------- /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 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/utils/PermissionUtils.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.utils 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | import android.Manifest 17 | import android.content.Context 18 | import android.content.pm.PackageManager 19 | import androidx.core.content.ContextCompat 20 | 21 | object PermissionUtils { 22 | 23 | val STORAGE_PERMISSIONS = arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE) 24 | 25 | fun checkAllPermissionsGranted(context: Context, permissions: Array) = permissions.all { 26 | ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/di/TrackModule.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.di 2 | 3 | import android.content.Context 4 | import com.github.odaridavid.zikk.repositories.AlbumRepository 5 | import com.github.odaridavid.zikk.repositories.TrackRepository 6 | import dagger.Module 7 | import dagger.Provides 8 | 9 | /** 10 | * 11 | * Copyright 2020 David Odari 12 | * 13 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 14 | * in compliance with the License. You may obtain a copy of the License at 15 | * http://www.apache.org/licenses/LICENSE-2.0 16 | * Unless required by applicable law or agreed to in writing, software distributed under the License 17 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 18 | * or implied. See the License for the specific language governing permissions and limitations under 19 | * the License. 20 | * 21 | **/ 22 | @Module 23 | internal class TrackModule { 24 | 25 | @Provides 26 | fun providesTrackProvider(context: Context, albumRepository: AlbumRepository): TrackRepository { 27 | return TrackRepository( 28 | context, 29 | albumRepository 30 | ) 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/ZikkApp.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | import android.app.Application 17 | import com.github.odaridavid.zikk.di.AppComponent 18 | import com.github.odaridavid.zikk.di.DaggerAppComponent 19 | import timber.log.Timber 20 | 21 | internal class ZikkApp : Application() { 22 | 23 | lateinit var appComponent: AppComponent 24 | 25 | override fun onCreate() { 26 | super.onCreate() 27 | 28 | if (BuildConfig.DEBUG) 29 | Timber.plant(Timber.DebugTree()) 30 | 31 | appComponent = DaggerAppComponent.factory().create(applicationContext) 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/utils/TimeUtils.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.utils 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | import java.util.concurrent.TimeUnit 17 | 18 | /** 19 | * Converts a long to a duration representatioin e.g 3:19,01:00:00 20 | */ 21 | fun convertMillisecondsToDuration(millis: Long): String { 22 | val hrs = TimeUnit.MILLISECONDS.toHours(millis) 23 | val minutes = TimeUnit.MILLISECONDS.toMinutes(millis) % TimeUnit.HOURS.toMinutes(1) 24 | val seconds = TimeUnit.MILLISECONDS.toSeconds(millis) % TimeUnit.MINUTES.toSeconds(1) 25 | return if (hrs > 0L) 26 | String.format("%02d:%02d:%02d", hrs, minutes, seconds) 27 | else 28 | String.format("%02d:%02d", minutes, seconds) 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/data/LastPlayedTrackPreference.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.data 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | import android.content.SharedPreferences 17 | import javax.inject.Inject 18 | 19 | /** 20 | * Used to show or hide the player 21 | */ 22 | internal class LastPlayedTrackPreference @Inject constructor(private val sharedPreferences: SharedPreferences) { 23 | 24 | var lastPlayedTrackId: Long 25 | set(value) { 26 | val editor = sharedPreferences.edit() 27 | editor.putLong(KEY_LAST_PLAYED_TRACK_ID, value) 28 | editor.apply() 29 | } 30 | get() { 31 | return sharedPreferences.getLong(KEY_LAST_PLAYED_TRACK_ID, DEFAULT_VALUE) 32 | } 33 | 34 | companion object { 35 | private const val KEY_LAST_PLAYED_TRACK_ID = "last_played_id" 36 | private const val DEFAULT_VALUE = -1L 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/playback/BecomingNoisyReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.playback 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | * 16 | **/ 17 | import android.content.BroadcastReceiver 18 | import android.content.Context 19 | import android.content.Intent 20 | import android.media.AudioManager 21 | import android.support.v4.media.session.MediaControllerCompat 22 | import javax.inject.Inject 23 | 24 | /** 25 | * @see Don't be noisy 26 | */ 27 | class BecomingNoisyReceiver @Inject constructor(private val mediaControllerCompat: MediaControllerCompat) : 28 | BroadcastReceiver() { 29 | override fun onReceive(context: Context, intent: Intent) { 30 | if (intent.action == AudioManager.ACTION_AUDIO_BECOMING_NOISY) { 31 | mediaControllerCompat.transportControls.pause() 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/utils/DebugUtils.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.utils 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | import android.support.v4.media.session.PlaybackStateCompat 17 | 18 | //TODO Move to a debug source set 19 | internal object DebugUtils { 20 | fun getPlaybackState(state: Int): String { 21 | return when (state) { 22 | PlaybackStateCompat.STATE_NONE -> "Playback None" 23 | PlaybackStateCompat.STATE_STOPPED -> "Playback Stopped" 24 | PlaybackStateCompat.STATE_ERROR -> "Playback Error" 25 | PlaybackStateCompat.STATE_PLAYING -> "Playback Playing" 26 | PlaybackStateCompat.STATE_PAUSED -> "Playback Paused" 27 | PlaybackStateCompat.STATE_SKIPPING_TO_NEXT -> "Playback Skipping To Next" 28 | PlaybackStateCompat.STATE_BUFFERING -> "Playback Buffering" 29 | else -> "Unknown Playback State" 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/base/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.base 2 | 3 | import android.os.Build 4 | import android.os.Bundle 5 | import android.view.View 6 | import androidx.appcompat.app.AppCompatActivity 7 | import com.github.odaridavid.zikk.notification.NotificationsChannelManager 8 | import com.github.odaridavid.zikk.notification.NotificationsChannelManager.Companion.PLAYBACK_CHANNEL_ID 9 | import com.github.odaridavid.zikk.utils.injector 10 | import com.github.odaridavid.zikk.utils.versionFrom 11 | import javax.inject.Inject 12 | 13 | internal abstract class BaseActivity : AppCompatActivity() { 14 | @Inject 15 | lateinit var notificationsChannelManager: NotificationsChannelManager 16 | 17 | 18 | override fun onCreate(savedInstanceState: Bundle?) { 19 | matchStatusBarWithBackground() 20 | injector.inject(this) 21 | super.onCreate(savedInstanceState) 22 | initNotificationChannel() 23 | } 24 | 25 | private fun matchStatusBarWithBackground() { 26 | if (versionFrom(Build.VERSION_CODES.M)) { 27 | window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR 28 | window.statusBarColor = getColor(android.R.color.background_light) 29 | } 30 | } 31 | 32 | private fun initNotificationChannel() { 33 | with(notificationsChannelManager) { 34 | if (versionFrom(Build.VERSION_CODES.O) && !hasChannel(PLAYBACK_CHANNEL_ID)) 35 | createNotificationChannel(PLAYBACK_CHANNEL_ID) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/di/AppComponent.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.di 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | import android.content.Context 17 | import com.github.odaridavid.zikk.base.BaseActivity 18 | import com.github.odaridavid.zikk.playback.session.ZikkMediaService 19 | import com.github.odaridavid.zikk.ui.DashboardActivity 20 | import dagger.BindsInstance 21 | import dagger.Component 22 | import javax.inject.Singleton 23 | 24 | /** 25 | * Application Dependency Graph 26 | */ 27 | @Singleton 28 | @Component( 29 | modules = [ 30 | AppModule::class, 31 | 32 | PlaybackModule::class, 33 | DataModule::class, 34 | 35 | //Media Categories 36 | AlbumModule::class, 37 | ArtistModule::class, 38 | PlaylistModule::class, 39 | TrackModule::class, 40 | GenreModule::class 41 | ] 42 | ) 43 | internal interface AppComponent { 44 | 45 | fun inject(baseActivity: BaseActivity) 46 | 47 | fun inject(zikkMediaService: ZikkMediaService) 48 | 49 | fun inject(dashboardActivity: DashboardActivity) 50 | 51 | @Component.Factory 52 | interface Factory { 53 | 54 | fun create(@BindsInstance context: Context): AppComponent 55 | 56 | } 57 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/di/PlaybackModule.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.di 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | import android.content.Context 17 | import com.github.odaridavid.zikk.playback.session.MediaLoader 18 | import com.github.odaridavid.zikk.playback.player.TrackPlayer 19 | import com.github.odaridavid.zikk.repositories.* 20 | import dagger.Module 21 | import dagger.Provides 22 | 23 | @Module 24 | internal class PlaybackModule { 25 | 26 | @Provides 27 | fun providesMediaLoader( 28 | context: Context, 29 | albumRepository: AlbumRepository, 30 | artistRepository: ArtistRepository, 31 | genreRepository: GenreRepository, 32 | trackRepository: TrackRepository, 33 | playlistRepository: PlaylistRepository 34 | ): MediaLoader { 35 | return MediaLoader( 36 | context, 37 | albumRepository, 38 | artistRepository, 39 | genreRepository, 40 | trackRepository, 41 | playlistRepository 42 | ) 43 | } 44 | 45 | @Provides 46 | fun providesTrackPlayer(context: Context, trackRepository: TrackRepository): TrackPlayer { 47 | return TrackPlayer( 48 | context, 49 | trackRepository 50 | ) 51 | } 52 | 53 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/utils/MediaItemUtils.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.utils 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | import android.content.Context 17 | import android.net.Uri 18 | import android.support.v4.media.MediaBrowserCompat 19 | import android.support.v4.media.MediaDescriptionCompat 20 | 21 | fun createMediaItem( 22 | title: String, 23 | subtitle: String, 24 | mediaItemId: String, 25 | iconUri: Uri?, 26 | description: String, 27 | @MediaBrowserCompat.MediaItem.Flags mediaItemFlags: Int 28 | ): MediaBrowserCompat.MediaItem { 29 | return MediaBrowserCompat.MediaItem( 30 | MediaDescriptionCompat.Builder() 31 | .setMediaId(mediaItemId) 32 | .setTitle(title) 33 | .setSubtitle(subtitle) 34 | .setIconUri(iconUri) 35 | .setMediaUri(null) 36 | .setDescription(description) 37 | .build(), 38 | mediaItemFlags 39 | ) 40 | } 41 | 42 | fun getDrawableUri(context: Context, drawableName: String): Uri { 43 | val appPackageName = context.packageName 44 | return Uri.parse("android.resource://$appPackageName/drawable/$drawableName") 45 | } 46 | 47 | fun convertMediaIdToTrackId(mediaId: String): Long { 48 | val spIndex = mediaId.indexOf('-') 49 | return mediaId.substring(spIndex + 1).toLong() 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/di/AppModule.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.di 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | import android.app.NotificationManager 17 | import android.content.Context 18 | import android.content.SharedPreferences 19 | import android.media.AudioManager 20 | import com.github.odaridavid.zikk.notification.NotificationsChannelManager 21 | import com.github.odaridavid.zikk.utils.Constants 22 | import dagger.Module 23 | import dagger.Provides 24 | 25 | /** 26 | * Application wide dependencies 27 | */ 28 | @Module 29 | class AppModule { 30 | 31 | @Provides 32 | fun providesNotificationManager(context: Context): NotificationManager { 33 | return context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 34 | } 35 | 36 | @Provides 37 | fun providesAudioManager(context: Context): AudioManager { 38 | return context.getSystemService(Context.AUDIO_SERVICE) as AudioManager 39 | } 40 | 41 | @Provides 42 | fun providesSharedPreference(context: Context): SharedPreferences { 43 | return context.getSharedPreferences(Constants.PREFERENCE_NAME, Context.MODE_PRIVATE) 44 | } 45 | 46 | @Provides 47 | fun providesNotificationChannelsManager( 48 | context: Context, 49 | notificationManager: NotificationManager 50 | ): NotificationsChannelManager { 51 | return NotificationsChannelManager(context, notificationManager) 52 | } 53 | } -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Zikk 3 | 4 | 5 | Songs 6 | Recent 7 | Tracks 8 | Playlists 9 | Albums 10 | Artists 11 | Genres 12 | 13 | 14 | Storage Permissions Not Granted 15 | 16 | 17 | 18 | %d song 19 | %d songs 20 | 21 | 22 | %d album 23 | %d albums 24 | 25 | 26 | 27 | Playback 28 | Controls Media Playback 29 | 30 | 31 | Pause 32 | Play 33 | Stop 34 | Skip 35 | 36 | 37 | All Tracks 38 | All Albums 39 | All Artists 40 | All Playlists 41 | All Genres 42 | 43 | 44 | Needs Storage Permission 45 | This permission is required to access music stored in the phones storage. 46 | 47 | 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/utils/ViewUtils.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.utils 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | import android.content.Context 17 | import android.graphics.Color 18 | import android.view.View 19 | import android.widget.Toast 20 | import androidx.annotation.StringRes 21 | import androidx.appcompat.app.AlertDialog 22 | 23 | fun View.show() { 24 | visibility = View.VISIBLE 25 | } 26 | 27 | fun View.invisible() { 28 | visibility = View.INVISIBLE 29 | } 30 | 31 | fun View.hide() { 32 | visibility = View.GONE 33 | } 34 | 35 | fun Context.showToast(msg: String) { 36 | Toast.makeText(this, msg, Toast.LENGTH_LONG).show() 37 | } 38 | 39 | fun Context.showDialog( 40 | @StringRes title: Int, 41 | @StringRes message: Int, 42 | positiveBlock: () -> (Unit), 43 | negativeBlock: () -> (Unit) 44 | ) { 45 | AlertDialog.Builder(this) 46 | .setTitle(getString(title)) 47 | .setMessage(getString(message)) 48 | .setPositiveButton(android.R.string.yes) { dialog, _ -> 49 | positiveBlock() 50 | dialog.dismiss() 51 | } 52 | .setNegativeButton(android.R.string.no) { dialog, _ -> 53 | negativeBlock() 54 | dialog.dismiss() 55 | } 56 | .setIcon(android.R.drawable.ic_dialog_alert) 57 | .show().apply { 58 | getButton(AlertDialog.BUTTON_NEGATIVE).setTextColor(Color.BLACK) 59 | getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(Color.BLACK) 60 | } 61 | 62 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/ui/DashboardViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.ui 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | import androidx.lifecycle.LiveData 17 | import androidx.lifecycle.MutableLiveData 18 | import androidx.lifecycle.ViewModel 19 | import androidx.lifecycle.ViewModelProvider 20 | import com.github.odaridavid.zikk.data.LastPlayedTrackPreference 21 | import com.github.odaridavid.zikk.models.PlaybackStatus 22 | 23 | internal class DashboardViewModel(private val lastPlayedTrackPreference: LastPlayedTrackPreference) : 24 | ViewModel() { 25 | 26 | private val _isMediaBrowserConnected = MutableLiveData() 27 | val isMediaBrowserConnected: LiveData 28 | get() = _isMediaBrowserConnected 29 | 30 | private val _playbackStatus = MutableLiveData() 31 | val playbackStatus: LiveData 32 | get() = _playbackStatus 33 | 34 | fun setIsConnected(value: Boolean) { 35 | _isMediaBrowserConnected.value = value 36 | } 37 | 38 | fun setNowPlayingStatus(playbackStatus: PlaybackStatus) { 39 | _playbackStatus.value = playbackStatus 40 | } 41 | 42 | fun setCurrentlyPlayingTrackId(trackId: Long) { 43 | lastPlayedTrackPreference.lastPlayedTrackId = trackId 44 | } 45 | 46 | class Factory( 47 | private val lastPlayedTrackPreference: LastPlayedTrackPreference 48 | ) : ViewModelProvider.NewInstanceFactory() { 49 | 50 | @Suppress("unchecked_cast") 51 | override fun create(modelClass: Class): T { 52 | return DashboardViewModel(lastPlayedTrackPreference) as T 53 | } 54 | } 55 | } 56 | 57 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/notification/NotificationsChannelManager.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.notification 2 | 3 | import android.app.NotificationChannel 4 | import android.app.NotificationManager 5 | import android.content.Context 6 | import android.os.Build 7 | import androidx.annotation.RequiresApi 8 | import com.github.odaridavid.zikk.R 9 | import javax.inject.Inject 10 | 11 | /** 12 | * 13 | * Copyright 2020 David Odari 14 | * 15 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 16 | * in compliance with the License. You may obtain a copy of the License at 17 | * http://www.apache.org/licenses/LICENSE-2.0 18 | * Unless required by applicable law or agreed to in writing, software distributed under the License 19 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 20 | * or implied. See the License for the specific language governing permissions and limitations under 21 | * the License. 22 | * 23 | **/ 24 | class NotificationsChannelManager @Inject constructor( 25 | val context: Context, 26 | private val notificationManager: NotificationManager 27 | ) { 28 | 29 | @RequiresApi(Build.VERSION_CODES.O) 30 | fun createNotificationChannel(channelId: String) { 31 | val channel = when (channelId) { 32 | PLAYBACK_CHANNEL_ID -> { 33 | val name = context.getString(R.string.notification_playback_channel_name) 34 | val descriptionText = 35 | context.getString(R.string.notification_playback_channel_description) 36 | val importance = NotificationManager.IMPORTANCE_DEFAULT 37 | NotificationChannel(channelId, name, importance).apply { 38 | description = descriptionText 39 | } 40 | } 41 | else -> throw IllegalArgumentException("Unknown Channel Id Received") 42 | } 43 | notificationManager.createNotificationChannel(channel) 44 | } 45 | 46 | @RequiresApi(Build.VERSION_CODES.O) 47 | fun hasChannel(channelId: String): Boolean { 48 | return notificationManager.getNotificationChannel(channelId) != null 49 | } 50 | 51 | companion object { 52 | const val PLAYBACK_CHANNEL_ID = "playback" 53 | } 54 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/repositories/GenreRepository.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.repositories 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | import android.content.Context 17 | import android.database.Cursor 18 | import android.provider.MediaStore 19 | import com.github.odaridavid.zikk.models.Genre 20 | 21 | 22 | internal class GenreRepository(val applicationContext: Context) { 23 | 24 | /** 25 | * Returns a list of genres after extracting from a cursor 26 | */ 27 | fun loadAllGenres(): List { 28 | val genres = mutableListOf() 29 | val cursor = getGenreCursor() ?: throw IllegalStateException("Genres cursor is null") 30 | while (cursor.moveToNext()) { 31 | genres.add(cursor.mapToGenreEntity()) 32 | } 33 | cursor.close() 34 | return genres 35 | } 36 | 37 | private fun Cursor.mapToGenreEntity(): Genre { 38 | val genreName = getColumnIndexOrThrow(MediaStore.Audio.Genres.NAME) 39 | val genreId = getColumnIndexOrThrow(MediaStore.Audio.Genres._ID) 40 | return Genre( 41 | id = getLong(genreId), 42 | name = getString(genreName) 43 | ) 44 | } 45 | 46 | private fun getGenreCursor(): Cursor? { 47 | val cr = applicationContext.contentResolver 48 | val uri = MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI 49 | val projection: Array = getGenreColumns() 50 | val sortOrder = "${MediaStore.Audio.Genres.NAME} DESC" 51 | return cr.query(uri, projection, null, null, sortOrder) 52 | } 53 | 54 | private fun getGenreColumns(): Array { 55 | return arrayOf( 56 | MediaStore.Audio.Genres._ID, 57 | MediaStore.Audio.Genres.NAME 58 | ) 59 | } 60 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/utils/MediaMetadataUtils.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.utils 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | import android.graphics.BitmapFactory 6 | import android.graphics.ImageDecoder 7 | import android.net.Uri 8 | import android.os.Build 9 | import android.provider.MediaStore 10 | import android.support.v4.media.MediaMetadataCompat 11 | import androidx.core.net.toUri 12 | import com.github.odaridavid.zikk.R 13 | import java.io.FileNotFoundException 14 | 15 | /** 16 | * 17 | * Copyright 2020 David Odari 18 | * 19 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 20 | * in compliance with the License. You may obtain a copy of the License at 21 | * http://www.apache.org/licenses/LICENSE-2.0 22 | * Unless required by applicable law or agreed to in writing, software distributed under the License 23 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 24 | * or implied. See the License for the specific language governing permissions and limitations under 25 | * the License. 26 | * 27 | **/ 28 | inline val MediaMetadataCompat.id: String 29 | get() = getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID) 30 | 31 | inline val MediaMetadataCompat.title: String? 32 | get() = getString(MediaMetadataCompat.METADATA_KEY_TITLE) 33 | 34 | inline val MediaMetadataCompat.artist: String? 35 | get() = getString(MediaMetadataCompat.METADATA_KEY_ARTIST) 36 | 37 | inline val MediaMetadataCompat.duration 38 | get() = getLong(MediaMetadataCompat.METADATA_KEY_DURATION) 39 | 40 | inline val MediaMetadataCompat.album: String? 41 | get() = getString(MediaMetadataCompat.METADATA_KEY_ALBUM) 42 | 43 | inline val MediaMetadataCompat.albumArtUri: Uri 44 | get() = this.getString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI).toUri() 45 | 46 | inline val MediaMetadataCompat.albumArt: Bitmap? 47 | get() = getBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART) 48 | 49 | 50 | fun getAlbumArtBitmap(context: Context, imageUri: Uri): Bitmap? { 51 | val cr = context.contentResolver 52 | return try { 53 | if (versionFrom(Build.VERSION_CODES.P)) { 54 | val src = ImageDecoder.createSource(cr, imageUri) 55 | ImageDecoder.decodeBitmap(src) 56 | } else MediaStore.Images.Media.getBitmap(cr, imageUri) 57 | } catch (e: FileNotFoundException) { 58 | BitmapFactory.decodeResource(context.resources, R.drawable.bg_no_art) 59 | } 60 | } 61 | 62 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/repositories/PlaylistRepository.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.repositories 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | import android.content.Context 17 | import android.database.Cursor 18 | import android.provider.MediaStore 19 | import com.github.odaridavid.zikk.models.Playlist 20 | 21 | internal class PlaylistRepository(val applicationContext: Context) { 22 | 23 | /** 24 | * Returns a list of playlists after extracting from a cursor 25 | */ 26 | fun loadAllPlaylists(): List { 27 | val playlists = mutableListOf() 28 | val cursor = getPlaylistCursor() ?: throw IllegalStateException("Playlists cursor is null") 29 | while (cursor.moveToNext()) { 30 | playlists.add(cursor.mapToPlaylistEntity()) 31 | } 32 | cursor.close() 33 | return playlists 34 | } 35 | 36 | private fun Cursor.mapToPlaylistEntity(): Playlist { 37 | val playlistId = getColumnIndexOrThrow(MediaStore.Audio.Playlists._ID) 38 | val playlistName = getColumnIndexOrThrow(MediaStore.Audio.Playlists.NAME) 39 | val playlistLastModifiedDate = 40 | getColumnIndexOrThrow(MediaStore.Audio.Playlists.DATE_MODIFIED) 41 | return Playlist( 42 | id = getLong(playlistId), 43 | name = getString(playlistName), 44 | modified = getString(playlistLastModifiedDate) 45 | ) 46 | } 47 | 48 | private fun getPlaylistCursor(): Cursor? { 49 | val cr = applicationContext.contentResolver 50 | val uri = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI 51 | val projection: Array = getPlaylistColumns() 52 | val sortOrder = "${MediaStore.Audio.Playlists.NAME} DESC" 53 | return cr.query(uri, projection, null, null, sortOrder) 54 | } 55 | 56 | private fun getPlaylistColumns(): Array { 57 | return arrayOf( 58 | MediaStore.Audio.Playlists._ID, 59 | MediaStore.Audio.Playlists.NAME, 60 | MediaStore.Audio.Playlists.DATE_MODIFIED 61 | ) 62 | } 63 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/repositories/ArtistRepository.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.repositories 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | import android.content.Context 17 | import android.database.Cursor 18 | import android.provider.MediaStore 19 | import com.github.odaridavid.zikk.models.Artist 20 | 21 | 22 | internal class ArtistRepository(val applicationContext: Context) { 23 | 24 | /** 25 | * Returns a list of artists after extracting from a cursor 26 | */ 27 | fun loadAllArtists(): List { 28 | val artists = mutableListOf() 29 | val cursor = getArtistCursor() ?: throw IllegalStateException("Artists cursor is null") 30 | while (cursor.moveToNext()) { 31 | artists.add(cursor.mapToArtistEntity()) 32 | } 33 | cursor.close() 34 | return artists 35 | } 36 | 37 | private fun Cursor.mapToArtistEntity(): Artist { 38 | val artistId = getColumnIndexOrThrow(MediaStore.Audio.Artists._ID) 39 | val artistName = getColumnIndexOrThrow(MediaStore.Audio.Artists.ARTIST) 40 | val noOfAlbums = getColumnIndexOrThrow(MediaStore.Audio.Artists.NUMBER_OF_ALBUMS) 41 | val noOfTracks = getColumnIndexOrThrow(MediaStore.Audio.Artists.NUMBER_OF_TRACKS) 42 | return Artist( 43 | id = getLong(artistId), 44 | name = getString(artistName), 45 | noOfAlbums = getInt(noOfAlbums), 46 | noOfTracks = getInt(noOfTracks) 47 | ) 48 | } 49 | 50 | private fun getArtistCursor(): Cursor? { 51 | val cr = applicationContext.contentResolver 52 | val uri = MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI 53 | val projection: Array = getArtistColumns() 54 | val sortOrder = "${MediaStore.Audio.Artists.ARTIST} DESC" 55 | return cr.query(uri, projection, null, null, sortOrder) 56 | } 57 | 58 | private fun getArtistColumns(): Array { 59 | return arrayOf( 60 | MediaStore.Audio.Artists._ID, 61 | MediaStore.Audio.Artists.ARTIST, 62 | MediaStore.Audio.Artists.NUMBER_OF_ALBUMS, 63 | MediaStore.Audio.Artists.NUMBER_OF_TRACKS 64 | ) 65 | } 66 | 67 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/now_playing_anim.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 10 | 14 | 15 | 16 | 17 | 18 | 19 | 26 | 33 | 41 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_track.xml: -------------------------------------------------------------------------------- 1 | 14 | 25 | 26 | 29 | 30 | 39 | 40 | 54 | 55 | 67 | 68 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright 2020 David Odari 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 6 | * in compliance with the License. You may obtain a copy of the License at 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * Unless required by applicable law or agreed to in writing, software distributed under the License 9 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 10 | * or implied. See the License for the specific language governing permissions and limitations under 11 | * the License. 12 | * 13 | **/ 14 | apply plugin: 'com.android.application' 15 | apply plugin: 'kotlin-android' 16 | apply plugin: 'kotlin-android-extensions' 17 | apply plugin: 'kotlin-kapt' 18 | 19 | android { 20 | compileSdkVersion 29 21 | buildToolsVersion "29.0.3" 22 | 23 | defaultConfig { 24 | applicationId "com.github.odaridavid.zikk" 25 | minSdkVersion 21 26 | targetSdkVersion 29 27 | versionCode 1 28 | versionName "1.0" 29 | 30 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 31 | } 32 | 33 | buildTypes { 34 | release { 35 | minifyEnabled false 36 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 37 | } 38 | } 39 | kotlinOptions { 40 | jvmTarget = JavaVersion.VERSION_1_8 41 | } 42 | viewBinding { 43 | enabled = true 44 | } 45 | 46 | } 47 | 48 | dependencies { 49 | 50 | def lifecycle_version = "2.2.0" 51 | def coil_version = "0.9.5" 52 | def room_version = "2.2.5" 53 | def dagger2_version = "2.27" 54 | def leak_canary_version = "2.2" 55 | def material_design_version = "1.2.0-alpha05" 56 | def timber_version = "4.7.1" 57 | 58 | implementation fileTree(dir: 'libs', include: ['*.jar']) 59 | 60 | //Kotlin 61 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 62 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.5' 63 | 64 | //Jetpack 65 | implementation 'androidx.appcompat:appcompat:1.1.0' 66 | implementation 'androidx.core:core-ktx:1.2.0' 67 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 68 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" 69 | implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" 70 | implementation "androidx.legacy:legacy-support-v4:1.0.0" 71 | implementation "androidx.activity:activity-ktx:1.1.0" 72 | 73 | //Material Design 74 | implementation "com.google.android.material:material:$material_design_version" 75 | 76 | //DI 77 | implementation "com.google.dagger:dagger:$dagger2_version" 78 | kapt "com.google.dagger:dagger-compiler:$dagger2_version" 79 | 80 | //Persistence 81 | implementation "androidx.room:room-runtime:$room_version" 82 | kapt "androidx.room:room-compiler:$room_version" 83 | implementation "androidx.room:room-ktx:$room_version" 84 | 85 | //Logging 86 | implementation "com.jakewharton.timber:timber:$timber_version" 87 | 88 | //Animations 89 | implementation 'jp.wasabeef:recyclerview-animators:3.0.0' 90 | 91 | //Image Loading 92 | implementation "io.coil-kt:coil:$coil_version" 93 | 94 | //Analytics 95 | implementation 'com.google.firebase:firebase-analytics:17.3.0' 96 | 97 | //Mem leaks 98 | debugImplementation "com.squareup.leakcanary:leakcanary-android:$leak_canary_version" 99 | 100 | //Unit Testing 101 | testImplementation 'junit:junit:4.13' 102 | testImplementation 'org.amshove.kluent:kluent-android:1.60' 103 | testImplementation "io.mockk:mockk:1.9.3" 104 | 105 | //Instrumented Testing 106 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 107 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 108 | } 109 | apply plugin: 'com.google.gms.google-services' 110 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/repositories/AlbumRepository.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.repositories 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | import android.content.Context 17 | import android.database.Cursor 18 | import android.net.Uri 19 | import android.provider.MediaStore 20 | import com.github.odaridavid.zikk.models.Album 21 | 22 | 23 | internal class AlbumRepository(private val applicationContext: Context) { 24 | 25 | /** 26 | * Returns a list of albums after extracting from a cursor 27 | */ 28 | fun loadAllAlbums(): List { 29 | val albums = mutableListOf() 30 | val cursor = getAlbumsCursor() ?: throw IllegalStateException("Albums cursor is null") 31 | while (cursor.moveToNext()) { 32 | albums.add(cursor.mapToAlbumEntity()) 33 | } 34 | cursor.close() 35 | return albums 36 | } 37 | 38 | /** 39 | * Returns a filtered list of albums based on the query after extracting from a cursor 40 | */ 41 | fun loadAlbumsByQuery(selection: String, selectionArgs: Array): List { 42 | val albums = mutableListOf() 43 | val cursor = getAlbumsCursor(selection, selectionArgs) 44 | ?: throw IllegalStateException("Albums cursor is null") 45 | if (cursor.count != 0) { 46 | cursor.moveToFirst() 47 | do { 48 | albums.add(cursor.mapToAlbumEntity()) 49 | } while (cursor.moveToNext()) 50 | } 51 | cursor.close() 52 | return albums 53 | } 54 | 55 | /** 56 | * Convert cursor rows to an album entity 57 | */ 58 | private fun Cursor.mapToAlbumEntity(): Album { 59 | val id = getColumnIndexOrThrow(MediaStore.Audio.Albums._ID) 60 | val albumTitle = getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM) 61 | val albumArtist = getColumnIndexOrThrow(MediaStore.Audio.Albums.ARTIST) 62 | val noOfSongs = getColumnIndexOrThrow(MediaStore.Audio.Albums.NUMBER_OF_SONGS) 63 | val albumArt = getAlbumArtPath(getLong(id)) 64 | return Album( 65 | id = getLong(id), 66 | title = getString(albumTitle), 67 | artist = getString(albumArtist), 68 | noOfSongs = getInt(noOfSongs), 69 | albumArt = albumArt 70 | ) 71 | } 72 | 73 | private fun getAlbumArtPath(id: Long): String { 74 | val artworkUri = Uri.parse("content://media/external/audio/albumart") 75 | return Uri.withAppendedPath(artworkUri, "$id").toString() 76 | } 77 | 78 | /** 79 | * Loads information on albums using a content resolver and returns a cursor object 80 | */ 81 | private fun getAlbumsCursor( 82 | selection: String? = null, 83 | selectionArgs: Array? = null 84 | ): Cursor? { 85 | val cr = applicationContext.contentResolver 86 | val uri = MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI 87 | val projection: Array = getAlbumColumns() 88 | val sortOrder = "${MediaStore.Audio.Albums.LAST_YEAR} DESC" 89 | return cr.query(uri, projection, selection, selectionArgs, sortOrder) 90 | } 91 | 92 | private fun getAlbumColumns(): Array { 93 | return arrayOf( 94 | MediaStore.Audio.Albums._ID, 95 | MediaStore.Audio.Albums.ARTIST, 96 | MediaStore.Audio.Albums.ALBUM, 97 | MediaStore.Audio.Albums.NUMBER_OF_SONGS 98 | ) 99 | } 100 | 101 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/notification/PlaybackNotificationBuilder.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.notification 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | import android.app.Notification 17 | import android.app.NotificationManager 18 | import android.content.Context 19 | import android.support.v4.media.session.MediaSessionCompat 20 | import android.support.v4.media.session.PlaybackStateCompat.* 21 | import androidx.core.app.NotificationCompat 22 | import androidx.core.content.ContextCompat 23 | import androidx.media.session.MediaButtonReceiver 24 | import com.github.odaridavid.zikk.R 25 | import com.github.odaridavid.zikk.notification.NotificationsChannelManager.Companion.PLAYBACK_CHANNEL_ID 26 | import com.github.odaridavid.zikk.utils.Constants.PLAYBACK_NOTIFICATION_ID 27 | import com.github.odaridavid.zikk.utils.album 28 | import com.github.odaridavid.zikk.utils.albumArt 29 | import com.github.odaridavid.zikk.utils.artist 30 | import com.github.odaridavid.zikk.utils.title 31 | import javax.inject.Inject 32 | 33 | /** 34 | * Responsible for playback notification. 35 | */ 36 | internal class PlaybackNotificationBuilder @Inject constructor( 37 | private val context: Context, 38 | private val notificationManager: NotificationManager, 39 | private val mediaSessionCompat: MediaSessionCompat 40 | ) { 41 | 42 | fun build(): Notification { 43 | 44 | val mediaMetadata = mediaSessionCompat.controller.metadata 45 | 46 | val notificationBuilder = NotificationCompat.Builder(context, PLAYBACK_CHANNEL_ID) 47 | .apply { 48 | // Add the metadata for the currently playing track 49 | setContentTitle(mediaMetadata.title) 50 | setContentText(mediaMetadata.artist) 51 | setSubText(mediaMetadata.album) 52 | setLargeIcon(mediaMetadata.albumArt) 53 | 54 | // Enable launching the player by clicking the notification 55 | setContentIntent(mediaSessionCompat.controller.sessionActivity) 56 | 57 | setDeleteIntent( 58 | MediaButtonReceiver.buildMediaButtonPendingIntent( 59 | context, 60 | ACTION_STOP 61 | ) 62 | ) 63 | 64 | setVisibility(NotificationCompat.VISIBILITY_PUBLIC) 65 | 66 | setSmallIcon(R.drawable.ic_music_note_black_24dp) 67 | color = ContextCompat.getColor(context, R.color.colorAccent) 68 | 69 | val playPauseRes = 70 | if (mediaSessionCompat.controller.playbackState.state == STATE_PLAYING) 71 | R.drawable.ic_pause_black_48dp 72 | else R.drawable.ic_play_black_48dp 73 | 74 | addAction( 75 | NotificationCompat.Action( 76 | playPauseRes, 77 | context.getString(R.string.playback_action_pause), 78 | MediaButtonReceiver.buildMediaButtonPendingIntent( 79 | context, 80 | ACTION_PLAY_PAUSE 81 | ) 82 | ) 83 | ) 84 | 85 | setStyle( 86 | androidx.media.app.NotificationCompat.MediaStyle() 87 | .setMediaSession(mediaSessionCompat.sessionToken) 88 | .setShowActionsInCompactView(0) 89 | ) 90 | } 91 | val notification = notificationBuilder.build() 92 | notificationManager.notify(PLAYBACK_NOTIFICATION_ID, notification) 93 | return notification 94 | } 95 | 96 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/playback/player/TrackPlayer.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.playback.player 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | import android.content.ContentUris 17 | import android.content.Context 18 | import android.media.AudioAttributes 19 | import android.media.MediaPlayer 20 | import android.os.PowerManager 21 | import android.provider.MediaStore 22 | import com.github.odaridavid.zikk.models.Track 23 | import com.github.odaridavid.zikk.repositories.TrackRepository 24 | import com.github.odaridavid.zikk.utils.convertMediaIdToTrackId 25 | import timber.log.Timber 26 | 27 | /** 28 | * Handles controlling of audio output,bound to a service to ensure playing persists even after 29 | * user leaves the app. 30 | */ 31 | internal class TrackPlayer( 32 | val context: Context, 33 | private val trackRepository: TrackRepository 34 | ) : MediaPlayer.OnPreparedListener, MediaPlayer.OnErrorListener { 35 | 36 | lateinit var mediaPlayer: MediaPlayer 37 | private var delayStart = false 38 | 39 | init { 40 | initPlayer() 41 | } 42 | 43 | private val isInitialized: Boolean 44 | get() = ::mediaPlayer.isInitialized 45 | 46 | val currentPosition: Int 47 | get() { 48 | return if (isInitialized) mediaPlayer.currentPosition else -1 49 | } 50 | 51 | val isPlaying: Boolean 52 | get() { 53 | return if (isInitialized) mediaPlayer.isPlaying else false 54 | } 55 | val duration: Int 56 | get() { 57 | return if (isInitialized) mediaPlayer.duration else -1 58 | } 59 | 60 | private fun initPlayer() { 61 | mediaPlayer = MediaPlayer().apply { 62 | setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK) 63 | setAudioAttributes( 64 | AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) 65 | .build() 66 | ) 67 | setOnPreparedListener(this@TrackPlayer) 68 | setOnErrorListener(this@TrackPlayer) 69 | } 70 | } 71 | 72 | fun prepare(delayStart: Boolean = false) { 73 | Timber.i("Media Player Preparing") 74 | this.delayStart = delayStart 75 | mediaPlayer.prepareAsync() 76 | } 77 | 78 | fun start() { 79 | Timber.i("Media Player Starting") 80 | mediaPlayer.start() 81 | } 82 | 83 | fun pause() { 84 | Timber.i("Media Player Paused") 85 | mediaPlayer.pause() 86 | } 87 | 88 | fun stop() { 89 | Timber.i("Media Player Stopped") 90 | mediaPlayer.stop() 91 | } 92 | 93 | fun reset() { 94 | Timber.i("Media Player Reset") 95 | mediaPlayer.reset() 96 | } 97 | 98 | fun release() { 99 | Timber.i("Media Player Released") 100 | mediaPlayer.release() 101 | } 102 | 103 | fun setDataSourceFromMediaId(context: Context, mediaId: String) { 104 | val trackId = convertMediaIdToTrackId(mediaId) 105 | if (trackId == 0L) return 106 | val contentUris = 107 | ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, trackId) 108 | mediaPlayer.setDataSource(context, contentUris) 109 | } 110 | 111 | override fun onPrepared(mediaPlayer: MediaPlayer?) { 112 | Timber.i("Media Player Prepared") 113 | if (!delayStart) 114 | start() 115 | } 116 | 117 | override fun onError(mediaPlayer: MediaPlayer?, what: Int, extra: Int): Boolean { 118 | Timber.i("Media Player Error : $what") 119 | reset() 120 | return true 121 | } 122 | 123 | fun getTrackInformationFromMediaId(mediaId: String): Track? { 124 | val trackId = convertMediaIdToTrackId(mediaId) 125 | return trackRepository.loadTrackForId(trackId.toString()) 126 | } 127 | 128 | fun getTrackInformationFromTrackId(trackId: Long): Track? { 129 | return trackRepository.loadTrackForId(trackId.toString()) 130 | } 131 | 132 | 133 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/ui/TracksAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.ui; 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | import android.graphics.drawable.Animatable2 17 | import android.graphics.drawable.AnimatedVectorDrawable 18 | import android.graphics.drawable.Drawable 19 | import android.os.Build 20 | import android.view.LayoutInflater 21 | import android.view.View 22 | import android.view.ViewGroup 23 | import android.widget.ImageView 24 | import android.widget.TextView 25 | import androidx.recyclerview.widget.RecyclerView 26 | import coil.api.load 27 | import com.github.odaridavid.zikk.R 28 | import com.github.odaridavid.zikk.models.PlayableTrack 29 | import com.github.odaridavid.zikk.utils.invisible 30 | import com.github.odaridavid.zikk.utils.show 31 | import com.github.odaridavid.zikk.utils.versionFrom 32 | 33 | internal class TracksAdapter(val onClick: (String?, Int) -> Unit) : 34 | RecyclerView.Adapter() { 35 | 36 | private lateinit var mediaItems: MutableList 37 | 38 | fun setList(mediaItem: MutableList) { 39 | mediaItems = mediaItem 40 | notifyDataSetChanged() 41 | } 42 | 43 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrackViewHolder { 44 | val context = parent.context 45 | val view = LayoutInflater.from(context).inflate(R.layout.item_track, parent, false) 46 | return TrackViewHolder(view) 47 | } 48 | 49 | override fun onBindViewHolder(holder: TrackViewHolder, position: Int) { 50 | holder.bind(mediaItems[position]) 51 | } 52 | 53 | override fun onBindViewHolder( 54 | holder: TrackViewHolder, 55 | position: Int, 56 | payloads: MutableList 57 | ) { 58 | if (payloads.isEmpty()) 59 | super.onBindViewHolder(holder, position, payloads) 60 | else 61 | holder.setIsPlaying(payloads[0] as Boolean) 62 | } 63 | 64 | 65 | inner class TrackViewHolder(private val view: View) : RecyclerView.ViewHolder(view) { 66 | 67 | private val nowPlayingImageView: ImageView = 68 | view.findViewById(R.id.track_now_playing_image_view) 69 | 70 | lateinit var animatedNowPlayingDrawable: AnimatedVectorDrawable 71 | 72 | fun bind(mediaItem: PlayableTrack) { 73 | with(view) { 74 | findViewById(R.id.track_art_image_view).apply { 75 | this.load(mediaItem.icon) 76 | contentDescription = "${mediaItem.title} Album Art" 77 | } 78 | findViewById(R.id.track_title_text_view).apply { 79 | text = mediaItem.title 80 | } 81 | findViewById(R.id.track_artist_text_view).apply { 82 | text = mediaItem.artist 83 | } 84 | 85 | nowPlayingImageView.apply { 86 | setBackgroundResource(R.drawable.now_playing_anim) 87 | animatedNowPlayingDrawable = background as AnimatedVectorDrawable 88 | if (versionFrom(Build.VERSION_CODES.M)) 89 | animatedNowPlayingDrawable.registerAnimationCallback(object : 90 | Animatable2.AnimationCallback() { 91 | override fun onAnimationEnd(drawable: Drawable?) { 92 | super.onAnimationEnd(drawable) 93 | animatedNowPlayingDrawable.start() 94 | } 95 | }) 96 | } 97 | 98 | setNowPlayingViewVisibility(mediaItem.isPlaying, nowPlayingImageView) 99 | 100 | setOnClickListener { 101 | onClick(mediaItem.mediaId, adapterPosition) 102 | } 103 | } 104 | } 105 | 106 | fun setIsPlaying(isPlaying: Boolean) { 107 | setNowPlayingViewVisibility(isPlaying, nowPlayingImageView) 108 | } 109 | 110 | private fun setNowPlayingViewVisibility(isPlaying: Boolean, showPlaying: ImageView) { 111 | if (isPlaying) { 112 | showPlaying.show() 113 | animatedNowPlayingDrawable.start() 114 | } else { 115 | showPlaying.invisible() 116 | animatedNowPlayingDrawable.stop() 117 | } 118 | } 119 | } 120 | 121 | fun updateIsPlaying(position: Int, payload: Any) { 122 | notifyItemChanged(position, payload) 123 | } 124 | 125 | override fun getItemCount(): Int { 126 | return if (::mediaItems.isInitialized) mediaItems.size else 0 127 | } 128 | 129 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/repositories/TrackRepository.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.repositories 2 | 3 | import android.content.Context 4 | import android.database.Cursor 5 | import android.net.Uri 6 | import android.provider.MediaStore 7 | import com.github.odaridavid.zikk.models.Track 8 | 9 | /** 10 | * 11 | * Copyright 2020 David Odari 12 | * 13 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 14 | * in compliance with the License. You may obtain a copy of the License at 15 | * http://www.apache.org/licenses/LICENSE-2.0 16 | * Unless required by applicable law or agreed to in writing, software distributed under the License 17 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 18 | * or implied. See the License for the specific language governing permissions and limitations under 19 | * the License. 20 | * 21 | **/ 22 | internal class TrackRepository( 23 | val applicationContext: Context, 24 | private val albumRepository: AlbumRepository 25 | ) { 26 | /** 27 | * Returns a list of tracks after extracting from a cursor 28 | */ 29 | fun loadAllTracks(): List { 30 | val tracks = mutableListOf() 31 | val cursor = getTrackCursor() ?: throw IllegalStateException("Tracks cursor is null") 32 | while (cursor.moveToNext()) { 33 | tracks.add(cursor.mapToTrackEntity()) 34 | } 35 | cursor.close() 36 | return tracks 37 | } 38 | 39 | private fun Cursor.mapToTrackEntity(): Track { 40 | val trackId = getColumnIndexOrThrow(MediaStore.Audio.Media._ID) 41 | val artistId = getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST_ID) 42 | val artistName = getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST) 43 | val albumName = getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM) 44 | val title = getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE) 45 | val displayName = getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME) 46 | val track = getColumnIndexOrThrow(MediaStore.Audio.Media.TRACK) 47 | val duration = getColumnIndexOrThrow(MEDIA_STORE_AUDIO_DURATION_COLUMN) 48 | val fileUri = 49 | Uri.withAppendedPath(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, getString(trackId)) 50 | val albumArt = getAlbumArt(getString(albumName)) 51 | return Track( 52 | id = getLong(trackId), 53 | artistId = getLong(artistId), 54 | artist = getString(artistName), 55 | album = getString(albumName), 56 | title = getString(title), 57 | displayName = getString(displayName), 58 | track = getString(track), 59 | duration = getString(duration), 60 | filePath = fileUri.toString(), 61 | albumArt = albumArt 62 | ) 63 | } 64 | 65 | private fun getAlbumArt(album: String): String { 66 | val albums = albumRepository.loadAlbumsByQuery("album=?", arrayOf(album)) 67 | return if (albums.isEmpty()) "" else albums[0].albumArt 68 | } 69 | 70 | private fun getTrackCursor(): Cursor? { 71 | val cr = applicationContext.contentResolver 72 | val uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI 73 | val projection: Array = getTrackColumns() 74 | val sortOrder = "${MediaStore.Audio.Media.DATE_MODIFIED} DESC" 75 | val selection = "is_music=?" //Selects only music,leaves out media such as notifications 76 | val selectionArgs = arrayOf("1") 77 | return cr.query(uri, projection, selection, selectionArgs, sortOrder) 78 | } 79 | 80 | private fun getTrackWithIdCursor(trackId: String): Cursor? { 81 | val cr = applicationContext.contentResolver 82 | val uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI 83 | val projection: Array = getTrackColumns() 84 | val sortOrder = "${MediaStore.Audio.Media.DATE_MODIFIED} DESC" 85 | val selection = 86 | "is_music=? AND _id=? " //Selects only music,leaves out media such as notifications 87 | val selectionArgs = arrayOf("1", trackId) 88 | return cr.query(uri, projection, selection, selectionArgs, sortOrder) 89 | } 90 | 91 | private fun getTrackColumns(): Array { 92 | return arrayOf( 93 | MediaStore.Audio.Media._ID, 94 | MediaStore.Audio.Media.ARTIST_ID, 95 | MediaStore.Audio.Media.ALBUM_ID, 96 | MediaStore.Audio.Media.TITLE, 97 | MediaStore.Audio.Media.ALBUM, 98 | MediaStore.Audio.Media.ARTIST, 99 | MediaStore.Audio.Media.DISPLAY_NAME, 100 | MediaStore.Audio.Media.TRACK, 101 | MEDIA_STORE_AUDIO_DURATION_COLUMN 102 | ) 103 | } 104 | 105 | fun loadTrackForId(trackId: String): Track? { 106 | val tracks = mutableListOf() 107 | val cursor = 108 | getTrackWithIdCursor(trackId) ?: throw IllegalStateException("Tracks cursor is null") 109 | while (cursor.moveToNext()) { 110 | tracks.add(cursor.mapToTrackEntity()) 111 | } 112 | cursor.close() 113 | return tracks.firstOrNull() 114 | } 115 | 116 | companion object { 117 | private const val MEDIA_STORE_AUDIO_DURATION_COLUMN = "duration" 118 | } 119 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /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/layout/activity_dashboard.xml: -------------------------------------------------------------------------------- 1 | 14 | 20 | 21 | 29 | 30 | 39 | 40 | 41 | 57 | 58 | 66 | 67 | 70 | 71 | 80 | 81 | 97 | 98 | 113 | 114 | 124 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/playback/session/ZikkMediaService.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.playback.session 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | import android.app.NotificationManager 17 | import android.app.PendingIntent 18 | import android.content.Intent 19 | import android.media.AudioManager 20 | import android.os.Bundle 21 | import android.support.v4.media.MediaBrowserCompat 22 | import android.support.v4.media.MediaMetadataCompat 23 | import android.support.v4.media.session.MediaSessionCompat 24 | import android.support.v4.media.session.PlaybackStateCompat 25 | import android.support.v4.media.session.PlaybackStateCompat.* 26 | import androidx.media.MediaBrowserServiceCompat 27 | import androidx.media.session.MediaButtonReceiver 28 | import com.github.odaridavid.zikk.data.LastPlayedTrackPreference 29 | import com.github.odaridavid.zikk.models.MediaId 30 | import com.github.odaridavid.zikk.notification.PlaybackNotificationBuilder 31 | import com.github.odaridavid.zikk.playback.BecomingNoisyReceiver 32 | import com.github.odaridavid.zikk.playback.player.TrackPlayer 33 | import com.github.odaridavid.zikk.utils.injector 34 | import kotlinx.coroutines.CoroutineScope 35 | import kotlinx.coroutines.Dispatchers 36 | import kotlinx.coroutines.launch 37 | import javax.inject.Inject 38 | 39 | 40 | /** 41 | * Media Browser Service which controls the media session and co-ordinates with the media 42 | * browser attached through the media player. 43 | * @see https://developer.android.com/guide/topics/media-apps/media-apps-overview 44 | */ 45 | internal class ZikkMediaService : MediaBrowserServiceCompat(), 46 | CoroutineScope by CoroutineScope(Dispatchers.IO) { 47 | private lateinit var mediaSessionCompat: MediaSessionCompat 48 | private lateinit var becomingNoisyReceiver: BecomingNoisyReceiver 49 | private val playbackStateCompatBuilder = PlaybackStateCompat.Builder() 50 | private var metadataCompatBuilder = MediaMetadataCompat.Builder() 51 | 52 | @Inject 53 | lateinit var mediaLoader: MediaLoader 54 | 55 | @Inject 56 | lateinit var audioManager: AudioManager 57 | 58 | @Inject 59 | lateinit var notificationManager: NotificationManager 60 | 61 | @Inject 62 | lateinit var trackPlayer: TrackPlayer 63 | 64 | @Inject 65 | lateinit var lastPlayedTrackPreference: LastPlayedTrackPreference 66 | 67 | override fun onCreate() { 68 | injector.inject(this) 69 | super.onCreate() 70 | mediaSessionCompat = createMediaSession() 71 | mediaSessionCompat.isActive = true 72 | 73 | val sessionActivityPendingIntent = 74 | packageManager?.getLaunchIntentForPackage(packageName)?.let { sessionIntent -> 75 | PendingIntent.getActivity(this, 0, sessionIntent, 0) 76 | } 77 | mediaSessionCompat.setSessionActivity(sessionActivityPendingIntent) 78 | 79 | this.sessionToken = mediaSessionCompat.sessionToken 80 | 81 | val playbackNotificationBuilder = PlaybackNotificationBuilder( 82 | this, 83 | notificationManager, 84 | mediaSessionCompat 85 | ) 86 | 87 | becomingNoisyReceiver = BecomingNoisyReceiver(mediaSessionCompat.controller) 88 | 89 | mediaSessionCompat.setCallback( 90 | MediaSessionCallback( 91 | this, 92 | playbackNotificationBuilder, 93 | mediaSessionCompat, 94 | audioManager, 95 | becomingNoisyReceiver, 96 | playbackStateCompatBuilder, 97 | metadataCompatBuilder, 98 | trackPlayer, 99 | lastPlayedTrackPreference 100 | ) 101 | ) 102 | } 103 | 104 | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { 105 | if (intent == null) { 106 | stopForeground(true) 107 | return START_STICKY 108 | } 109 | MediaButtonReceiver.handleIntent(mediaSessionCompat, intent) 110 | return START_STICKY 111 | } 112 | 113 | /** 114 | * Called to get information about the children nodes of a media item when subscribed to. 115 | * 116 | */ 117 | override fun onLoadChildren( 118 | parentId: String, 119 | result: Result> 120 | ) { 121 | // Browsing not allowed 122 | if (EMPTY_MEDIA_ROOT_ID == parentId) { 123 | result.sendResult(null) 124 | return 125 | } 126 | 127 | val mediaItems = mutableListOf() 128 | 129 | if (MEDIA_ROOT_ID == parentId) { 130 | //if this is the root menu build the MediaItem objects for the top level as browsable roots 131 | mediaLoader.buildMediaCategories(mediaItems) 132 | result.sendResult(mediaItems) 133 | } else { 134 | result.detach() 135 | launch { 136 | val mediaItemId = MediaId.values().find { id -> id.toString() == parentId } 137 | mediaLoader.getMediaItemChildren(mediaItems, mediaItemId) 138 | result.sendResult(mediaItems) 139 | } 140 | } 141 | } 142 | 143 | override fun onGetRoot( 144 | clientPackageName: String, 145 | clientUid: Int, 146 | rootHints: Bundle? 147 | ): BrowserRoot? { 148 | //Restrict Browsing Access to current app 149 | return if (clientPackageName == packageName) 150 | BrowserRoot(MEDIA_ROOT_ID, null) 151 | else BrowserRoot(EMPTY_MEDIA_ROOT_ID, null) 152 | } 153 | 154 | override fun onDestroy() { 155 | super.onDestroy() 156 | mediaSessionCompat.isActive = false 157 | trackPlayer.release() 158 | mediaSessionCompat.release() 159 | } 160 | 161 | private fun createMediaSession(): MediaSessionCompat { 162 | return MediaSessionCompat(this, "Zikk Media Service").apply { 163 | setPlaybackState(createPlaybackState()) 164 | setFlags( 165 | MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS 166 | or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS 167 | ) 168 | } 169 | } 170 | 171 | private fun createPlaybackState(): PlaybackStateCompat { 172 | return playbackStateCompatBuilder 173 | .setActions( 174 | ACTION_PLAY or ACTION_PLAY_PAUSE or ACTION_PAUSE or ACTION_STOP 175 | ) 176 | .build() 177 | 178 | } 179 | 180 | companion object { 181 | private const val MEDIA_ROOT_ID = "root" 182 | private const val EMPTY_MEDIA_ROOT_ID = "empty_root" 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/playback/session/MediaLoader.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.playback.session 2 | 3 | import android.content.Context 4 | import android.net.Uri 5 | import android.support.v4.media.MediaBrowserCompat 6 | import com.github.odaridavid.zikk.R 7 | import com.github.odaridavid.zikk.models.MediaCategoryInfo 8 | import com.github.odaridavid.zikk.models.MediaId 9 | import com.github.odaridavid.zikk.repositories.* 10 | import com.github.odaridavid.zikk.utils.convertMillisecondsToDuration 11 | import com.github.odaridavid.zikk.utils.createMediaItem 12 | import com.github.odaridavid.zikk.utils.getDrawableUri 13 | 14 | /** 15 | * 16 | * Copyright 2020 David Odari 17 | * 18 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 19 | * in compliance with the License. You may obtain a copy of the License at 20 | * http://www.apache.org/licenses/LICENSE-2.0 21 | * Unless required by applicable law or agreed to in writing, software distributed under the License 22 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 23 | * or implied. See the License for the specific language governing permissions and limitations under 24 | * the License. 25 | * 26 | **/ 27 | internal class MediaLoader( 28 | private val context: Context, 29 | private val albumRepository: AlbumRepository, 30 | private val artistRepository: ArtistRepository, 31 | private val genreRepository: GenreRepository, 32 | private val trackRepository: TrackRepository, 33 | private val playlistRepository: PlaylistRepository 34 | ) { 35 | 36 | fun getMediaItemChildren( 37 | mediaItems: MutableList, 38 | mediaId: MediaId? 39 | ) { 40 | if (mediaId != null) 41 | when (mediaId) { 42 | MediaId.ALBUM -> { 43 | val albums = albumRepository.loadAllAlbums() 44 | albums.forEach { album -> 45 | mediaItems.add( 46 | createMediaItem( 47 | album.title, 48 | context.resources.getQuantityString( 49 | R.plurals.number_of_songs, 50 | album.noOfSongs, 51 | album.noOfSongs 52 | ), 53 | "${MediaId.ALBUM}-${album.id}", 54 | null, 55 | "", 56 | MediaBrowserCompat.MediaItem.FLAG_BROWSABLE 57 | ) 58 | ) 59 | } 60 | } 61 | MediaId.ARTIST -> { 62 | val artists = artistRepository.loadAllArtists() 63 | artists.forEach { artist -> 64 | mediaItems.add( 65 | createMediaItem( 66 | artist.name, 67 | context.resources.getQuantityString( 68 | R.plurals.number_of_albums, 69 | artist.noOfAlbums, 70 | artist.noOfAlbums 71 | ), 72 | "${MediaId.ARTIST}-${artist.id}", 73 | null, 74 | "", 75 | MediaBrowserCompat.MediaItem.FLAG_BROWSABLE 76 | ) 77 | ) 78 | } 79 | } 80 | MediaId.GENRE -> { 81 | val genres = genreRepository.loadAllGenres() 82 | genres.forEach { genre -> 83 | mediaItems.add( 84 | createMediaItem( 85 | genre.name, 86 | "", 87 | "${MediaId.GENRE}-${genre.id}", 88 | null, 89 | "", 90 | MediaBrowserCompat.MediaItem.FLAG_BROWSABLE 91 | ) 92 | ) 93 | 94 | } 95 | } 96 | MediaId.PLAYLIST -> { 97 | val playlists = playlistRepository.loadAllPlaylists() 98 | playlists.forEach { playlist -> 99 | mediaItems.add( 100 | createMediaItem( 101 | playlist.name, 102 | playlist.modified, 103 | "${MediaId.PLAYLIST}-${playlist.id}", 104 | null, 105 | "", 106 | MediaBrowserCompat.MediaItem.FLAG_BROWSABLE 107 | ) 108 | ) 109 | } 110 | } 111 | MediaId.TRACK -> { 112 | val tracks = trackRepository.loadAllTracks() 113 | tracks.forEach { track -> 114 | mediaItems.add( 115 | createMediaItem( 116 | track.title, 117 | track.artist, 118 | "${MediaId.TRACK}-${track.id}", 119 | Uri.parse(track.albumArt), 120 | convertMillisecondsToDuration(track.duration.toLong()), 121 | MediaBrowserCompat.MediaItem.FLAG_PLAYABLE 122 | ) 123 | ) 124 | } 125 | } 126 | } 127 | } 128 | 129 | 130 | fun buildMediaCategories(mediaItems: MutableList) { 131 | with(context) { 132 | val mediaCategoriesInfo = 133 | mutableListOf( 134 | MediaCategoryInfo( 135 | MediaId.TRACK, 136 | getString(R.string.title_tracks), 137 | getString(R.string.subtitle_all_tracks), 138 | getDrawableUri(this, "ic_tracks_black_24dp"), 139 | "", 140 | MediaBrowserCompat.MediaItem.FLAG_PLAYABLE 141 | ), 142 | MediaCategoryInfo( 143 | MediaId.ALBUM, 144 | getString(R.string.title_albums), 145 | getString(R.string.subtitle_all_albums), 146 | getDrawableUri(this, "ic_album_black_24dp"), 147 | "", 148 | MediaBrowserCompat.MediaItem.FLAG_BROWSABLE 149 | ), 150 | MediaCategoryInfo( 151 | MediaId.ARTIST, 152 | getString(R.string.title_artists), 153 | getString(R.string.subtitle_all_artists), 154 | getDrawableUri(this, "ic_artist_black_24dp"), 155 | "", 156 | MediaBrowserCompat.MediaItem.FLAG_BROWSABLE 157 | ), 158 | MediaCategoryInfo( 159 | MediaId.PLAYLIST, 160 | getString(R.string.title_playlists), 161 | getString(R.string.subtitle_all_playlists), 162 | getDrawableUri(this, "ic_playlist_black_24dp"), 163 | "", 164 | MediaBrowserCompat.MediaItem.FLAG_BROWSABLE 165 | ), 166 | MediaCategoryInfo( 167 | MediaId.GENRE, 168 | getString(R.string.title_genres), 169 | getString(R.string.subtitle_all_genres), 170 | getDrawableUri(this, "ic_genres_black_24dp"), 171 | "", 172 | MediaBrowserCompat.MediaItem.FLAG_BROWSABLE 173 | ) 174 | ) 175 | 176 | for (mediaCategoryInfo in mediaCategoriesInfo) { 177 | mediaItems.add( 178 | createMediaItem( 179 | mediaCategoryInfo.title, 180 | mediaCategoryInfo.subtitle, 181 | mediaCategoryInfo.id.toString(), 182 | mediaCategoryInfo.iconUri, 183 | mediaCategoryInfo.description, 184 | mediaCategoryInfo.mediaFlags 185 | ) 186 | ) 187 | } 188 | } 189 | } 190 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/playback/session/MediaSessionCallback.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.playback.session 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | import android.app.Service 17 | import android.content.Intent 18 | import android.content.IntentFilter 19 | import android.media.AudioAttributes 20 | import android.media.AudioFocusRequest 21 | import android.media.AudioManager 22 | import android.os.Build 23 | import android.os.Bundle 24 | import android.support.v4.media.MediaMetadataCompat 25 | import android.support.v4.media.MediaMetadataCompat.* 26 | import android.support.v4.media.session.MediaSessionCompat 27 | import android.support.v4.media.session.PlaybackStateCompat 28 | import android.support.v4.media.session.PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN 29 | import androidx.core.net.toUri 30 | import com.github.odaridavid.zikk.data.LastPlayedTrackPreference 31 | import com.github.odaridavid.zikk.models.MediaId 32 | import com.github.odaridavid.zikk.models.Track 33 | import com.github.odaridavid.zikk.notification.PlaybackNotificationBuilder 34 | import com.github.odaridavid.zikk.playback.BecomingNoisyReceiver 35 | import com.github.odaridavid.zikk.playback.player.TrackPlayer 36 | import com.github.odaridavid.zikk.utils.Constants.PLAYBACK_NOTIFICATION_ID 37 | import com.github.odaridavid.zikk.utils.getAlbumArtBitmap 38 | import com.github.odaridavid.zikk.utils.versionFrom 39 | import timber.log.Timber 40 | 41 | /** 42 | * MediaSessionCallback receives updates from initiated media controller actions 43 | * 44 | * @see MediaSessionCallback 45 | */ 46 | internal class MediaSessionCallback( 47 | private val serviceContext: Service, 48 | private val playbackNotificationBuilder: PlaybackNotificationBuilder, 49 | private val mediaSessionCompat: MediaSessionCompat, 50 | private val audioManager: AudioManager, 51 | private val becomingNoisyReceiver: BecomingNoisyReceiver, 52 | private val playbackStateBuilder: PlaybackStateCompat.Builder, 53 | private val metadataCompatBuilder: MediaMetadataCompat.Builder, 54 | private val trackPlayer: TrackPlayer, 55 | lastPlayedTrackPreference: LastPlayedTrackPreference 56 | ) : MediaSessionCompat.Callback(), AudioManager.OnAudioFocusChangeListener { 57 | 58 | private var audioFocusRequest: AudioFocusRequest? = null 59 | 60 | init { 61 | setPlaybackState( 62 | playbackStateBuilder.setState( 63 | PlaybackStateCompat.STATE_PAUSED, 64 | PLAYBACK_POSITION_UNKNOWN, 65 | 0.0F 66 | ) 67 | ) 68 | val track = 69 | trackPlayer.getTrackInformationFromTrackId(lastPlayedTrackPreference.lastPlayedTrackId) 70 | track?.run { 71 | setSessionMetadata(this) 72 | mediaSessionCompat.controller 73 | .transportControls 74 | .prepareFromMediaId("${MediaId.TRACK}-${this.id}", null) 75 | } 76 | } 77 | 78 | override fun onPlayFromMediaId(mediaId: String, extras: Bundle?) { 79 | super.onPlayFromMediaId(mediaId, extras) 80 | val request = initAudioFocus() 81 | if (request == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { 82 | Timber.d("Playing from media id $mediaId") 83 | startPlayback(mediaId) 84 | } 85 | } 86 | 87 | override fun onPlay() { 88 | super.onPlay() 89 | val request = initAudioFocus() 90 | if (request == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { 91 | resumePlayback() 92 | } 93 | } 94 | 95 | override fun onPause() { 96 | super.onPause() 97 | with(serviceContext) { 98 | trackPlayer.pause() 99 | unregisterReceiver(becomingNoisyReceiver) 100 | val state = playbackStateBuilder.setState( 101 | PlaybackStateCompat.STATE_PAUSED, 102 | mediaSessionCompat.controller.playbackState.position, 103 | 0.0F 104 | ) 105 | setPlaybackState(state) 106 | updateMediaNotification() 107 | stopForeground(false) 108 | } 109 | } 110 | 111 | override fun onStop() { 112 | super.onStop() 113 | releaseAudioFocus() 114 | with(serviceContext) { 115 | trackPlayer.stop() 116 | val state = playbackStateBuilder.setState( 117 | PlaybackStateCompat.STATE_STOPPED, 118 | mediaSessionCompat.controller.playbackState.position, 119 | 0.0F 120 | ) 121 | setPlaybackState(state) 122 | stopForeground(true) 123 | stopSelf() 124 | } 125 | } 126 | 127 | override fun onPrepareFromMediaId(mediaId: String?, extras: Bundle?) { 128 | super.onPrepareFromMediaId(mediaId, extras) 129 | with(trackPlayer) { 130 | reset() 131 | setDataSourceFromMediaId(serviceContext, mediaId!!) 132 | prepare(delayStart = true) 133 | } 134 | trackPlayer.getTrackInformationFromMediaId(mediaId!!)?.let { track -> 135 | setSessionMetadata(track) 136 | } 137 | val state = playbackStateBuilder.setState( 138 | PlaybackStateCompat.STATE_PAUSED, 139 | PLAYBACK_POSITION_UNKNOWN, 140 | 0.0F 141 | ) 142 | setPlaybackState(state) 143 | } 144 | 145 | override fun onAudioFocusChange(focusChange: Int) { 146 | when (focusChange) { 147 | AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> { 148 | trackPlayer.pause() 149 | } 150 | AudioManager.AUDIOFOCUS_LOSS -> { 151 | releaseAudioFocus() 152 | trackPlayer.pause() 153 | } 154 | AudioManager.AUDIOFOCUS_GAIN -> { 155 | trackPlayer.start() 156 | } 157 | } 158 | } 159 | 160 | private fun startPlayback(mediaId: String) { 161 | Timber.i("Starting playback") 162 | with(serviceContext) { 163 | startService(Intent(this, ZikkMediaService::class.java)) 164 | with(trackPlayer) { 165 | reset() 166 | setDataSourceFromMediaId(serviceContext, mediaId) 167 | prepare() 168 | } 169 | trackPlayer.getTrackInformationFromMediaId(mediaId)?.let { track -> 170 | setSessionMetadata(track) 171 | } 172 | val state = playbackStateBuilder.setState( 173 | PlaybackStateCompat.STATE_PLAYING, 174 | mediaSessionCompat.controller.playbackState.position, 175 | 0.0F 176 | ) 177 | setPlaybackState(state) 178 | registerNoisyReceiver() 179 | createMediaNotification() 180 | } 181 | } 182 | 183 | private fun resumePlayback() { 184 | Timber.i("Resuming playback") 185 | with(serviceContext) { 186 | startService(Intent(this, ZikkMediaService::class.java)) 187 | trackPlayer.start() 188 | val state = playbackStateBuilder.setState( 189 | PlaybackStateCompat.STATE_PLAYING, 190 | mediaSessionCompat.controller.playbackState.position, 191 | 0.0F 192 | ) 193 | setPlaybackState(state) 194 | registerNoisyReceiver() 195 | createMediaNotification() 196 | } 197 | } 198 | 199 | private fun setPlaybackState(state: PlaybackStateCompat.Builder) { 200 | mediaSessionCompat.setPlaybackState(state.build()) 201 | } 202 | 203 | private fun Service.registerNoisyReceiver() { 204 | registerReceiver(becomingNoisyReceiver, intentFilter) 205 | } 206 | 207 | private fun Service.createMediaNotification() { 208 | val notification = playbackNotificationBuilder.build() 209 | startForeground(PLAYBACK_NOTIFICATION_ID, notification) 210 | } 211 | 212 | private fun updateMediaNotification() { 213 | playbackNotificationBuilder.build() 214 | } 215 | 216 | private fun initAudioFocus(): Int { 217 | return if (versionFrom(Build.VERSION_CODES.O)) { 218 | audioFocusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) 219 | .setOnAudioFocusChangeListener(this) 220 | .setAudioAttributes(AudioAttributes.Builder().run { 221 | setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) 222 | build() 223 | }) 224 | .build() 225 | audioManager.requestAudioFocus(audioFocusRequest!!) 226 | } else { 227 | audioManager.requestAudioFocus( 228 | this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN 229 | ) 230 | } 231 | } 232 | 233 | private fun setSessionMetadata(track: Track) { 234 | val trackMetadata = metadataCompatBuilder.apply { 235 | putString(METADATA_KEY_ALBUM, track.album) 236 | putString(METADATA_KEY_ARTIST, track.artist) 237 | putString(METADATA_KEY_TITLE, track.title) 238 | putString(METADATA_KEY_ALBUM_ART_URI, track.albumArt) 239 | putBitmap( 240 | METADATA_KEY_ALBUM_ART, 241 | getAlbumArtBitmap(serviceContext, track.albumArt.toUri()) 242 | ) 243 | putString(METADATA_KEY_MEDIA_ID, track.id.toString()) 244 | putLong(METADATA_KEY_DURATION, track.duration.toLong()) 245 | } 246 | mediaSessionCompat.setMetadata(trackMetadata.build()) 247 | } 248 | 249 | private fun releaseAudioFocus() { 250 | if (versionFrom(Build.VERSION_CODES.O)) { 251 | audioFocusRequest?.run { 252 | audioManager.abandonAudioFocusRequest(this) 253 | } 254 | } else { 255 | audioManager.abandonAudioFocus(this) 256 | } 257 | } 258 | 259 | companion object { 260 | private val intentFilter = IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY) 261 | } 262 | 263 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/odaridavid/zikk/ui/DashboardActivity.kt: -------------------------------------------------------------------------------- 1 | package com.github.odaridavid.zikk.ui 2 | 3 | /** 4 | * 5 | * Copyright 2020 David Odari 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | * 15 | **/ 16 | 17 | import android.Manifest 18 | import android.content.ComponentName 19 | import android.content.pm.PackageManager 20 | import android.media.AudioManager 21 | import android.os.Bundle 22 | import android.support.v4.media.MediaBrowserCompat 23 | import android.support.v4.media.MediaMetadataCompat 24 | import android.support.v4.media.session.MediaControllerCompat 25 | import android.support.v4.media.session.PlaybackStateCompat 26 | import androidx.activity.viewModels 27 | import androidx.core.app.ActivityCompat 28 | import androidx.lifecycle.Observer 29 | import coil.api.load 30 | import com.github.odaridavid.zikk.R 31 | import com.github.odaridavid.zikk.base.BaseActivity 32 | import com.github.odaridavid.zikk.data.LastPlayedTrackPreference 33 | import com.github.odaridavid.zikk.databinding.ActivityDashboardBinding 34 | import com.github.odaridavid.zikk.mappers.toTrack 35 | import com.github.odaridavid.zikk.models.MediaId 36 | import com.github.odaridavid.zikk.models.PlayableTrack 37 | import com.github.odaridavid.zikk.models.PlaybackStatus 38 | import com.github.odaridavid.zikk.playback.session.ZikkMediaService 39 | import com.github.odaridavid.zikk.utils.* 40 | import timber.log.Timber 41 | import javax.inject.Inject 42 | 43 | /** 44 | * Main screen on app launch 45 | * 46 | */ 47 | internal class DashboardActivity : BaseActivity() { 48 | 49 | @Inject 50 | lateinit var lastPlayedTrackPreference: LastPlayedTrackPreference 51 | 52 | private var mediaBrowser: MediaBrowserCompat? = null 53 | private lateinit var playbackList: MutableList 54 | lateinit var dashboardBinding: ActivityDashboardBinding 55 | private lateinit var tracksAdapter: TracksAdapter 56 | private val dashboardViewModel: DashboardViewModel by viewModels { 57 | DashboardViewModel.Factory( 58 | lastPlayedTrackPreference 59 | ) 60 | } 61 | private var mediaControllerCompatCallback = object : MediaControllerCompat.Callback() { 62 | 63 | override fun onPlaybackStateChanged(playbackState: PlaybackStateCompat) { 64 | super.onPlaybackStateChanged(playbackState) 65 | Timber.d("Playback changed") 66 | handlePlaybackState(playbackState.state) 67 | } 68 | 69 | override fun onMetadataChanged(metadata: MediaMetadataCompat) { 70 | super.onMetadataChanged(metadata) 71 | Timber.d("Metadata changed") 72 | bindMetadataToViews(metadata) 73 | } 74 | } 75 | 76 | private val mediaBrowserSubscriptionCallback = 77 | object : MediaBrowserCompat.SubscriptionCallback() { 78 | 79 | override fun onChildrenLoaded( 80 | parentId: String, 81 | children: MutableList 82 | ) { 83 | super.onChildrenLoaded(parentId, children) 84 | Timber.i("$parentId children loaded.") 85 | playbackList = children.map { it.toTrack() }.toMutableList() 86 | dashboardBinding.dashboardProgressBar.hide() 87 | tracksAdapter.setList(playbackList) 88 | } 89 | } 90 | 91 | private val mediaBrowserConnectionCallback = object : MediaBrowserCompat.ConnectionCallback() { 92 | 93 | override fun onConnected() { 94 | super.onConnected() 95 | Timber.i("Connection with media browser successful") 96 | dashboardViewModel.setIsConnected(true) 97 | mediaBrowser?.run { 98 | val mediaControllerCompat = 99 | MediaControllerCompat(this@DashboardActivity, sessionToken) 100 | 101 | MediaControllerCompat.setMediaController( 102 | this@DashboardActivity, 103 | mediaControllerCompat 104 | ) 105 | mediaControllerCompat.playbackState?.let { state -> 106 | handlePlaybackState(state.state) 107 | } 108 | mediaControllerCompat.metadata?.let { metadata -> 109 | bindMetadataToViews(metadata) 110 | } 111 | initPlayPause(mediaControllerCompat) 112 | mediaControllerCompat.registerCallback(mediaControllerCompatCallback) 113 | } 114 | } 115 | 116 | override fun onConnectionSuspended() { 117 | super.onConnectionSuspended() 118 | Timber.i("Disconnected from media browser ") 119 | dashboardViewModel.setIsConnected(false) 120 | } 121 | 122 | override fun onConnectionFailed() { 123 | super.onConnectionFailed() 124 | Timber.i("Connection with media browser failed") 125 | dashboardViewModel.setIsConnected(false) 126 | } 127 | } 128 | 129 | override fun onCreate(savedInstanceState: Bundle?) { 130 | injector.inject(this) 131 | super.onCreate(savedInstanceState) 132 | dashboardBinding = ActivityDashboardBinding.inflate(layoutInflater) 133 | val view = dashboardBinding.root 134 | setContentView(view) 135 | } 136 | 137 | override fun onStart() { 138 | super.onStart() 139 | checkPermissionsAndInit() 140 | initAdapter() 141 | } 142 | 143 | override fun onResume() { 144 | super.onResume() 145 | if (mediaBrowser?.isConnected == true) 146 | mediaControllerCompat?.playbackState?.let { state -> 147 | handlePlaybackState(state.state) 148 | } 149 | volumeControlStream = AudioManager.STREAM_MUSIC 150 | } 151 | 152 | private fun initPlayPause(mediaControllerCompat: MediaControllerCompat) { 153 | dashboardBinding.playPauseButton.setOnClickListener { 154 | mediaControllerCompat.playbackState?.let { state -> 155 | if (state.state == PlaybackStateCompat.STATE_PLAYING) { 156 | mediaTranspotControls?.pause() 157 | handlePlaybackState(PlaybackStateCompat.STATE_PAUSED) 158 | } else { 159 | mediaTranspotControls?.play() 160 | handlePlaybackState(PlaybackStateCompat.STATE_PLAYING) 161 | } 162 | } 163 | } 164 | } 165 | 166 | override fun onStop() { 167 | super.onStop() 168 | mediaBrowser?.disconnect() 169 | mediaControllerCompat?.unregisterCallback(mediaControllerCompatCallback) 170 | } 171 | 172 | override fun onRequestPermissionsResult( 173 | requestCode: Int, 174 | permissions: Array, 175 | grantResults: IntArray 176 | ) { 177 | if (requestCode == RQ_STORAGE_PERMISSIONS) { 178 | if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) { 179 | Timber.i("Storage Permissions Granted") 180 | initMediaBrowser() 181 | } else { 182 | Timber.i("Storage Permissions Not Granted") 183 | onPermissionNotGranted() 184 | } 185 | } 186 | } 187 | 188 | private fun initAdapter() { 189 | tracksAdapter = TracksAdapter { id, currentlyPlayingIndex -> 190 | val playbackStatus = PlaybackStatus(prevPlayingIndex, currentlyPlayingIndex) 191 | dashboardViewModel.setNowPlayingStatus(playbackStatus) 192 | prevPlayingIndex = currentlyPlayingIndex 193 | dashboardViewModel.setCurrentlyPlayingTrackId(convertMediaIdToTrackId(id!!)) 194 | mediaTranspotControls?.playFromMediaId(id, null) 195 | } 196 | dashboardBinding.tracksRecyclerView.adapter = tracksAdapter 197 | observeNowPlayingIcon() 198 | } 199 | 200 | private fun bindMetadataToViews(metadata: MediaMetadataCompat) { 201 | dashboardBinding.trackTitleTextView.text = metadata.title 202 | dashboardBinding.trackArtistTextView.text = metadata.artist 203 | dashboardBinding.albumArtImageView.load(metadata.albumArtUri) 204 | dashboardBinding.nowPlayingCard.show() 205 | } 206 | 207 | private fun handlePlaybackState(state: Int) { 208 | when (state) { 209 | PlaybackStateCompat.STATE_PLAYING -> { 210 | dashboardBinding.playPauseButton.setImageDrawable(getDrawable(R.drawable.ic_pause_black_48dp)) 211 | tracksAdapter.updateIsPlaying(prevPlayingIndex, true) 212 | } 213 | PlaybackStateCompat.STATE_PAUSED -> { 214 | dashboardBinding.playPauseButton.setImageDrawable(getDrawable(R.drawable.ic_play_black_48dp)) 215 | tracksAdapter.updateIsPlaying(prevPlayingIndex, false) 216 | } 217 | PlaybackStateCompat.STATE_STOPPED -> { 218 | dashboardBinding.nowPlayingCard.hide() 219 | tracksAdapter.updateIsPlaying(prevPlayingIndex, false) 220 | } 221 | PlaybackStateCompat.STATE_ERROR -> { 222 | //TODO 223 | } 224 | } 225 | } 226 | 227 | private fun observeNowPlayingIcon() { 228 | dashboardViewModel.playbackStatus.observe(this, Observer { playbackStatus -> 229 | if (playbackStatus.prevTrackIndex != -1) { 230 | tracksAdapter.updateIsPlaying(playbackStatus.prevTrackIndex, false) 231 | tracksAdapter.updateIsPlaying(playbackStatus.currentTrackIndex, true) 232 | } else { 233 | tracksAdapter.updateIsPlaying(playbackStatus.currentTrackIndex, true) 234 | } 235 | }) 236 | } 237 | 238 | private fun initMediaBrowser() { 239 | val cn = ComponentName(this, ZikkMediaService::class.java) 240 | mediaBrowser = MediaBrowserCompat(this, cn, mediaBrowserConnectionCallback, null) 241 | observeMediaBrowserConnection() 242 | mediaBrowser!!.connect() 243 | } 244 | 245 | private fun observeMediaBrowserConnection() { 246 | dashboardViewModel.isMediaBrowserConnected.observe(this, Observer { isConnected -> 247 | if (!isConnected) mediaBrowser?.run { unsubscribe(root) } 248 | else mediaBrowser?.subscribe(MediaId.TRACK.toString(), mediaBrowserSubscriptionCallback) 249 | }) 250 | } 251 | 252 | 253 | private fun checkPermissionsAndInit() { 254 | if (!PermissionUtils.checkAllPermissionsGranted( 255 | this, 256 | PermissionUtils.STORAGE_PERMISSIONS 257 | ) 258 | ) { 259 | if (ActivityCompat.shouldShowRequestPermissionRationale( 260 | this, 261 | Manifest.permission.READ_EXTERNAL_STORAGE 262 | ) 263 | ) { 264 | showDialog( 265 | title = R.string.title_need_storage_permission, 266 | message = R.string.info_storage_permission_reason, 267 | positiveBlock = ::requestStoragePermissions, 268 | negativeBlock = ::onPermissionNotGranted 269 | ) 270 | } else { 271 | requestStoragePermissions() 272 | } 273 | } else initMediaBrowser() 274 | } 275 | 276 | private fun requestStoragePermissions() { 277 | ActivityCompat.requestPermissions( 278 | this, PermissionUtils.STORAGE_PERMISSIONS, RQ_STORAGE_PERMISSIONS 279 | ) 280 | } 281 | 282 | private fun onPermissionNotGranted() { 283 | dashboardBinding.dashboardProgressBar.hide() 284 | showToast(getString(R.string.info_storage_permissions_not_granted)) 285 | finish() 286 | } 287 | 288 | companion object { 289 | private const val RQ_STORAGE_PERMISSIONS = 1000 290 | private var prevPlayingIndex = -1 291 | } 292 | } 293 | --------------------------------------------------------------------------------