├── .github ├── scripts │ └── gradlew_recursive.sh └── workflows │ ├── android.yml │ └── copy-branch.yml ├── .gitignore ├── .google └── packaging.yaml ├── .idea ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml └── vcs.xml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── TODO.md ├── app ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-web.png │ ├── java │ └── com │ │ └── example │ │ └── android │ │ └── uamp │ │ ├── MainActivity.kt │ │ ├── MediaItemAdapter.kt │ │ ├── MediaItemData.kt │ │ ├── cast │ │ └── UampCastOptionsProvider.kt │ │ ├── fragments │ │ ├── MediaItemFragment.kt │ │ └── NowPlayingFragment.kt │ │ ├── utils │ │ ├── Event.kt │ │ └── InjectorUtils.kt │ │ └── viewmodels │ │ ├── MainActivityViewModel.kt │ │ ├── MediaItemFragmentViewModel.kt │ │ └── NowPlayingFragmentViewModel.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ ├── ic_album_black_24dp.xml │ ├── ic_launcher_background.xml │ ├── ic_pause_black_24dp.xml │ ├── ic_play_arrow_black_24dp.xml │ ├── ic_signal_wifi_off_black_24dp.xml │ ├── media_item_background.xml │ ├── media_item_mask.xml │ └── media_overlay_background.xml │ ├── layout │ ├── activity_main.xml │ ├── cast_context_error.xml │ ├── fragment_mediaitem.xml │ ├── fragment_mediaitem_list.xml │ └── fragment_nowplaying.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml │ └── xml │ └── automotive_app_desc.xml ├── automotive ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── example │ │ └── android │ │ └── uamp │ │ └── automotive │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── android │ │ │ └── uamp │ │ │ └── automotive │ │ │ ├── AutomotiveMusicService.kt │ │ │ ├── PhoneSignInFragment.kt │ │ │ ├── PinCodeSignInFragment.kt │ │ │ ├── QrCodeSignInFragment.kt │ │ │ ├── SettingsActivity.kt │ │ │ ├── SettingsFragment.kt │ │ │ ├── SignInActivity.kt │ │ │ ├── SignInActivityViewModel.kt │ │ │ ├── SignInLandingPageFragment.kt │ │ │ └── UsernameAndPasswordSignInFragment.kt │ └── res │ │ ├── color │ │ ├── car_text_dark.xml │ │ └── car_text_light.xml │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── aural_logo.png │ │ ├── default_button_background.xml │ │ ├── google_logo.png │ │ ├── google_sign_in_button_background.xml │ │ ├── google_sign_in_button_logo.xml │ │ ├── ic_launcher_background.xml │ │ ├── pin_background.xml │ │ ├── sign_in_button_background.xml │ │ ├── sign_in_toolbar_back_icon.xml │ │ └── sign_in_toolbar_back_ripple_background.xml │ │ ├── layout-h900dp │ │ ├── phone_sign_in.xml │ │ ├── pin_sign_in.xml │ │ ├── qr_sign_in.xml │ │ ├── sign_in_landing_page.xml │ │ ├── sign_in_landing_page_with_username_and_password.xml │ │ └── username_and_password_sign_in.xml │ │ ├── layout │ │ ├── activity_login.xml │ │ ├── activity_settings.xml │ │ ├── activity_sign_in.xml │ │ ├── phone_sign_in.xml │ │ ├── pin_item.xml │ │ ├── pin_sign_in.xml │ │ ├── preference.xml │ │ ├── preference_category.xml │ │ ├── qr_sign_in.xml │ │ ├── sign_in_landing_page.xml │ │ ├── sign_in_landing_page_with_username_and_password.xml │ │ └── username_and_password_sign_in.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── values-h1060dp │ │ └── dimens.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ │ └── xml │ │ ├── automotive_app_desc.xml │ │ └── preferences.xml │ └── test │ └── java │ └── com │ └── example │ └── android │ └── uamp │ └── automotive │ └── ExampleUnitTest.java ├── build.gradle ├── common ├── build.gradle ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── default_art.png │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── android │ │ │ └── uamp │ │ │ ├── common │ │ │ └── MusicServiceConnection.kt │ │ │ └── media │ │ │ ├── CastMediaItemConverter.kt │ │ │ ├── MusicService.kt │ │ │ ├── PackageValidator.kt │ │ │ ├── PersistentStorage.kt │ │ │ ├── UampNotificationManager.kt │ │ │ ├── extensions │ │ │ ├── FileExt.kt │ │ │ ├── JavaLangExt.kt │ │ │ ├── MediaMetadataCompatExt.kt │ │ │ └── PlaybackStateCompatExt.kt │ │ │ └── library │ │ │ ├── AlbumArtContentProvider.kt │ │ │ ├── BrowseTree.kt │ │ │ ├── JsonSource.kt │ │ │ └── MusicSource.kt │ └── res │ │ ├── drawable-hdpi │ │ └── ic_notification.png │ │ ├── drawable-mdpi │ │ └── ic_notification.png │ │ ├── drawable-nodpi │ │ └── default_art.png │ │ ├── drawable-xhdpi │ │ └── ic_notification.png │ │ ├── drawable-xxhdpi │ │ └── ic_notification.png │ │ ├── drawable-xxxhdpi │ │ └── ic_notification.png │ │ ├── drawable │ │ ├── ic_album.xml │ │ └── ic_recommended.xml │ │ ├── ic_notification.png │ │ ├── menu │ │ └── main_activity_menu.xml │ │ ├── values │ │ └── strings.xml │ │ └── xml │ │ └── allowed_media_browser_callers.xml │ └── test │ └── java │ └── com │ └── example │ └── android │ └── uamp │ └── media │ └── library │ └── MusicSourceTest.kt ├── docs ├── FAQs.md ├── FullGuide.md └── images │ ├── 1-browse-albums-screenshot.png │ ├── 12-ui-class-diagram.png │ ├── 2-play-song-screenshot.png │ ├── 3-architecture-overview.png │ ├── 4-MusicService.png │ ├── 5-MediaController.png │ ├── 6-notification.png │ └── 9-mvvm.png ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.github/scripts/gradlew_recursive.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright (C) 2020 The Android Open Source Project 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -xe 18 | 19 | # Default Gradle settings are not optimal for Android builds, override them 20 | # here to make the most out of the GitHub Actions build servers 21 | GRADLE_OPTS="$GRADLE_OPTS -Xms4g -Xmx4g" 22 | GRADLE_OPTS="$GRADLE_OPTS -XX:+HeapDumpOnOutOfMemoryError" 23 | GRADLE_OPTS="$GRADLE_OPTS -Dorg.gradle.daemon=false" 24 | GRADLE_OPTS="$GRADLE_OPTS -Dorg.gradle.workers.max=2" 25 | GRADLE_OPTS="$GRADLE_OPTS -Dkotlin.incremental=false" 26 | GRADLE_OPTS="$GRADLE_OPTS -Dkotlin.compiler.execution.strategy=in-process" 27 | GRADLE_OPTS="$GRADLE_OPTS -Dfile.encoding=UTF-8" 28 | export GRADLE_OPTS 29 | 30 | # Crawl all gradlew files which indicate an Android project 31 | # You may edit this if your repo has a different project structure 32 | for GRADLEW in `find . -name "gradlew"` ; do 33 | SAMPLE=$(dirname "${GRADLEW}") 34 | # Tell Gradle that this is a CI environment and disable parallel compilation 35 | bash "$GRADLEW" -p "$SAMPLE" -Pci --no-parallel --stacktrace $@ 36 | done 37 | -------------------------------------------------------------------------------- /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2020 The Android Open Source Project 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: Android CI 16 | 17 | on: 18 | workflow_dispatch: 19 | push: 20 | branches: [ main ] 21 | pull_request: 22 | branches: [ main ] 23 | 24 | jobs: 25 | 26 | build: 27 | name: Build 28 | runs-on: ubuntu-18.04 29 | 30 | steps: 31 | - uses: actions/checkout@v1 32 | - name: set up JDK 1.8 33 | uses: actions/setup-java@v1 34 | with: 35 | java-version: 1.8 36 | - name: Build project 37 | run: .github/scripts/gradlew_recursive.sh assembleDebug 38 | - name: Zip artifacts 39 | run: zip -r assemble.zip . -i '**/build/*.apk' '**/build/*.aab' '**/build/*.aar' '**/build/*.so' 40 | - name: Upload artifacts 41 | uses: actions/upload-artifact@v1 42 | with: 43 | name: assemble 44 | path: assemble.zip 45 | -------------------------------------------------------------------------------- /.github/workflows/copy-branch.yml: -------------------------------------------------------------------------------- 1 | # Duplicates default main branch to the old master branch 2 | 3 | name: Duplicates main to old master branch 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the main branch 7 | on: 8 | workflow_dispatch: 9 | push: 10 | branches: [ main ] 11 | 12 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 13 | jobs: 14 | # This workflow contains a single job called "copy-branch" 15 | copy-branch: 16 | # The type of runner that the job will run on 17 | runs-on: ubuntu-latest 18 | 19 | # Steps represent a sequence of tasks that will be executed as part of the job 20 | steps: 21 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it, 22 | # but specifies master branch (old default). 23 | - uses: actions/checkout@v2 24 | with: 25 | fetch-depth: 0 26 | ref: master 27 | 28 | - run: | 29 | git config user.name github-actions 30 | git config user.email github-actions@github.com 31 | git merge origin/main 32 | git push 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /captures 7 | .externalNativeBuild 8 | 9 | # Generated files 10 | build/ 11 | 12 | # Extra (custom) settings 13 | extra-settings.gradle 14 | -------------------------------------------------------------------------------- /.google/packaging.yaml: -------------------------------------------------------------------------------- 1 | # GOOGLE SAMPLE PACKAGING DATA 2 | # 3 | # This file is used by Google as part of our samples packaging process. 4 | # End users may safely ignore this file. It has no relevance to other systems. 5 | --- 6 | status: PUBLISHED 7 | technologies: [Android, Android Auto, Android Automotive OS, Android Wear] 8 | categories: [Getting Started, Media, UI] 9 | languages: [Kotlin] 10 | solutions: [Mobile] 11 | 12 | github: googlesamples/android-UniversalMusicPlayer 13 | 14 | level: INTERMEDIATE 15 | 16 | icon: screenshots/icon-web.png 17 | 18 | apiRefs: 19 | - android:android.support.v4.media.session.MediaSessionCompat 20 | - android:android.support.v4.media.session.MediaControllerCompat 21 | - androidx.media.MediaBrowserServiceCompat 22 | - android:android.support.v4.media.MediaBrowserCompat 23 | - androidx.media.app.NotificationCompat.MediaStyle 24 | - android:com.google.android.exoplayer2.SimpleExoPlayer 25 | - android:com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector 26 | 27 | license: apache2-android 28 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 16 | 20 | 21 | 22 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | xmlns:android 31 | 32 | ^$ 33 | 34 | 35 | 36 |
37 |
38 | 39 | 40 | 41 | xmlns:.* 42 | 43 | ^$ 44 | 45 | 46 | BY_NAME 47 | 48 |
49 |
50 | 51 | 52 | 53 | .*:id 54 | 55 | http://schemas.android.com/apk/res/android 56 | 57 | 58 | 59 |
60 |
61 | 62 | 63 | 64 | .*:name 65 | 66 | http://schemas.android.com/apk/res/android 67 | 68 | 69 | 70 |
71 |
72 | 73 | 74 | 75 | name 76 | 77 | ^$ 78 | 79 | 80 | 81 |
82 |
83 | 84 | 85 | 86 | style 87 | 88 | ^$ 89 | 90 | 91 | 92 |
93 |
94 | 95 | 96 | 97 | .* 98 | 99 | ^$ 100 | 101 | 102 | BY_NAME 103 | 104 |
105 |
106 | 107 | 108 | 109 | .* 110 | 111 | http://schemas.android.com/apk/res/android 112 | 113 | 114 | ANDROID_ATTRIBUTE_ORDER 115 | 116 |
117 |
118 | 119 | 120 | 121 | .* 122 | 123 | .* 124 | 125 | 126 | BY_NAME 127 | 128 |
129 |
130 |
131 |
132 | 133 | 135 |
136 |
-------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to become a contributor and submit your own code 2 | 3 | ## Contributor License Agreements 4 | 5 | We'd love to accept your sample apps and patches! Before we can take them, we 6 | have to jump a couple of legal hurdles. 7 | 8 | Please fill out either the individual or corporate Contributor License Agreement (CLA). 9 | 10 | * If you are an individual writing original source code and you're sure you 11 | own the intellectual property, then you'll need to sign an [individual CLA] 12 | (https://developers.google.com/open-source/cla/individual). 13 | * If you work for a company that wants to allow you to contribute your work, 14 | then you'll need to sign a [corporate CLA] 15 | (https://developers.google.com/open-source/cla/corporate). 16 | 17 | Follow either of the two links above to access the appropriate CLA and 18 | instructions for how to sign and return it. Once we receive it, we'll be able to 19 | accept your pull requests. 20 | 21 | ## Contributing A Patch 22 | 23 | 1. Submit an issue describing your proposed change to the repo in question. 24 | 1. The repo owner will respond to your issue promptly. 25 | 1. If your proposed change is accepted, and you haven't already done so, sign a 26 | Contributor License Agreement (see details above). 27 | 1. Fork the desired repo, develop and test your code changes. 28 | 1. Ensure that your code adheres to the existing style in the sample to which 29 | you are contributing. Refer to the 30 | [Android Code Style Guide] 31 | (https://source.android.com/source/code-style.html) for the 32 | recommended coding standards for this organization. 33 | 1. Ensure that your code has an appropriate set of unit tests which all pass. 34 | 1. Submit a pull request. 35 | 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Universal Android Music Player Sample 2 | ===================================== 3 | The goal of this sample is to show how to implement an audio media app that works 4 | across multiple form factors and provides a consistent user experience 5 | on Android phones, tablets, Android Auto, Android Wear, Android TV, Google Cast devices, 6 | and with the Google Assistant. 7 | 8 | To get started with UAMP please read the [full guide](docs/FullGuide.md). 9 | 10 | ![Screenshot showing UAMP's UI for browsing albums and songs](docs/images/1-browse-albums-screenshot.png "Browse albums screenshot") 11 | ![Screenshot showing UAMP's UI for playing a song](docs/images/2-play-song-screenshot.png "Play song screenshot") 12 | 13 | Pre-requisites 14 | -------------- 15 | 16 | - Android Studio 3.x 17 | 18 | Getting Started 19 | --------------- 20 | 21 | This sample uses the Gradle build system. To build this project, use the 22 | "gradlew build" command or use "Import Project" in Android Studio. 23 | 24 | Support 25 | ------- 26 | 27 | - Check out the [FAQs page](docs/FAQs.md) 28 | - Stack Overflow: http://stackoverflow.com/questions/tagged/android 29 | 30 | If you've found an error in this sample, please 31 | [file an issue](https://github.com/android/UAMP/issues) 32 | 33 | Patches are encouraged and may be submitted by forking this project and 34 | submitting a pull request through GitHub. Please see [CONTRIBUTING.md](CONTRIBUTING.md) for more 35 | details. 36 | 37 | Audio 38 | ----- 39 | 40 | Music provided by the [Free Music Archive](http://freemusicarchive.org/). 41 | 42 | - [Wake Up](http://freemusicarchive.org/music/The_Kyoto_Connection/Wake_Up_1957/) by 43 | [The Kyoto Connection](http://freemusicarchive.org/music/The_Kyoto_Connection/). 44 | 45 | Recordings provided by the [Ambisonic Sound Library](https://library.soundfield.com/). 46 | 47 | - [Pre Game Marching Band](https://library.soundfield.com/track/163) by Watson Wu 48 | - [Chickens on a Farm](https://library.soundfield.com/track/129) by Watson Wu 49 | - [Rural Market Busker](https://library.soundfield.com/track/55) by Stephan Schutze 50 | - [Steamtrain Interior](https://library.soundfield.com/track/65) by Stephan Schutze 51 | - [Rural Road Car Pass](https://library.soundfield.com/track/57) by Stephan Schutze 52 | - [10 Feet from Shore](https://library.soundfield.com/track/114) by Watson Wu 53 | 54 | License 55 | ------- 56 | 57 | Copyright 2017 Google Inc. 58 | 59 | Licensed to the Apache Software Foundation (ASF) under one or more contributor 60 | license agreements. See the NOTICE file distributed with this work for 61 | additional information regarding copyright ownership. The ASF licenses this 62 | file to you under the Apache License, Version 2.0 (the "License"); you may not 63 | use this file except in compliance with the License. You may obtain a copy of 64 | the License at 65 | 66 | http://www.apache.org/licenses/LICENSE-2.0 67 | 68 | Unless required by applicable law or agreed to in writing, software 69 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 70 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 71 | License for the specific language governing permissions and limitations under 72 | the License. 73 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | TODOs 2 | ===== 3 | 4 | This file captures the high level goals of the project. This provides guidance for anyone who wants 5 | to contribute. If you see something in the list that you'd like to work on, 6 | the best approach would be to [create an 7 | issue](https://github.com/googlesamples/android-UniversalMusicPlayer/issues) first, 8 | and then provide a pull request once completed to have your work merged into the project. 9 | 10 | Service Side Tasks 11 | ------------------ 12 | 13 | - Implement rating (ideally "favorite" vs "thumbs up/down"). 14 | - Improve integration with the Google Assistant. 15 | 16 | UI Tasks 17 | -------- 18 | 19 | - Implement a "now playing" UI with current position and skip forward/back 30s ([BottomSheet](https://material.io/guidelines/components/bottom-sheets.html#bottom-sheets-persistent-bottom-sheets)). 20 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | apply plugin: 'com.android.application' 18 | apply plugin: 'kotlin-android' 19 | apply plugin: 'kotlin-kapt' 20 | apply plugin: 'kotlin-android-extensions' 21 | 22 | android { 23 | compileSdkVersion rootProject.compileSdkVersion 24 | 25 | defaultConfig { 26 | applicationId "com.example.android.uamp.next" 27 | versionCode 1 28 | versionName "1.0" 29 | 30 | minSdkVersion rootProject.minSdkVersion 31 | targetSdkVersion rootProject.targetSdkVersion 32 | multiDexEnabled true 33 | 34 | compileOptions { 35 | sourceCompatibility JavaVersion.VERSION_1_8 36 | targetCompatibility JavaVersion.VERSION_1_8 37 | } 38 | kotlinOptions { 39 | jvmTarget = "1.8" 40 | } 41 | 42 | vectorDrawables { 43 | useSupportLibrary true 44 | } 45 | } 46 | 47 | buildFeatures { 48 | viewBinding true 49 | dataBinding true 50 | } 51 | 52 | buildTypes { 53 | release { 54 | minifyEnabled false 55 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 56 | } 57 | } 58 | } 59 | 60 | dependencies { 61 | implementation "com.android.support:multidex:$multidex_version" 62 | 63 | implementation project(':common') 64 | 65 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 66 | 67 | implementation "androidx.appcompat:appcompat:$androidx_app_compat_version" 68 | implementation "androidx.fragment:fragment-ktx:$fragment_version" 69 | implementation "androidx.recyclerview:recyclerview:$recycler_view_version" 70 | 71 | implementation "androidx.constraintlayout:constraintlayout:$constraint_layout_version" 72 | implementation "androidx.lifecycle:lifecycle-extensions:$arch_lifecycle_version" 73 | 74 | // Glide dependencies 75 | implementation "com.github.bumptech.glide:glide:$glide_version" 76 | kapt "com.github.bumptech.glide:compiler:$glide_version" 77 | } 78 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 21 | 22 | 30 | 31 | 32 | 33 | 34 | 35 | 38 | 39 | 40 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 84 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/uamp/31769898337d0228af7457a7a475d1b755263fdd/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/uamp/MediaItemAdapter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.uamp 18 | 19 | import android.view.LayoutInflater 20 | import android.view.ViewGroup 21 | import android.widget.ImageView 22 | import android.widget.TextView 23 | import androidx.recyclerview.widget.ListAdapter 24 | import androidx.recyclerview.widget.RecyclerView 25 | import com.bumptech.glide.Glide 26 | import com.example.android.uamp.MediaItemData.Companion.PLAYBACK_RES_CHANGED 27 | import com.example.android.uamp.databinding.FragmentMediaitemBinding 28 | import com.example.android.uamp.fragments.MediaItemFragment 29 | 30 | /** 31 | * [RecyclerView.Adapter] of [MediaItemData]s used by the [MediaItemFragment]. 32 | */ 33 | class MediaItemAdapter( 34 | private val itemClickedListener: (MediaItemData) -> Unit 35 | ) : ListAdapter(MediaItemData.diffCallback) { 36 | 37 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaViewHolder { 38 | val inflater = LayoutInflater.from(parent.context) 39 | val binding = FragmentMediaitemBinding.inflate(inflater, parent, false) 40 | return MediaViewHolder(binding, itemClickedListener) 41 | } 42 | 43 | override fun onBindViewHolder( 44 | holder: MediaViewHolder, 45 | position: Int, 46 | payloads: MutableList 47 | ) { 48 | 49 | val mediaItem = getItem(position) 50 | var fullRefresh = payloads.isEmpty() 51 | 52 | if (payloads.isNotEmpty()) { 53 | payloads.forEach { payload -> 54 | when (payload) { 55 | PLAYBACK_RES_CHANGED -> { 56 | holder.playbackState.setImageResource(mediaItem.playbackRes) 57 | } 58 | // If the payload wasn't understood, refresh the full item (to be safe). 59 | else -> fullRefresh = true 60 | } 61 | } 62 | } 63 | 64 | // Normally we only fully refresh the list item if it's being initially bound, but 65 | // we might also do it if there was a payload that wasn't understood, just to ensure 66 | // there isn't a stale item. 67 | if (fullRefresh) { 68 | holder.item = mediaItem 69 | holder.titleView.text = mediaItem.title 70 | holder.subtitleView.text = mediaItem.subtitle 71 | holder.playbackState.setImageResource(mediaItem.playbackRes) 72 | 73 | Glide.with(holder.albumArt) 74 | .load(mediaItem.albumArtUri) 75 | .placeholder(R.drawable.default_art) 76 | .into(holder.albumArt) 77 | } 78 | } 79 | 80 | override fun onBindViewHolder(holder: MediaViewHolder, position: Int) { 81 | onBindViewHolder(holder, position, mutableListOf()) 82 | } 83 | } 84 | 85 | class MediaViewHolder( 86 | binding: FragmentMediaitemBinding, 87 | itemClickedListener: (MediaItemData) -> Unit 88 | ) : RecyclerView.ViewHolder(binding.root) { 89 | 90 | val titleView: TextView = binding.title 91 | val subtitleView: TextView = binding.subtitle 92 | val albumArt: ImageView = binding.albumArt 93 | val playbackState: ImageView = binding.itemState 94 | 95 | var item: MediaItemData? = null 96 | 97 | init { 98 | binding.root.setOnClickListener { 99 | item?.let { itemClickedListener(it) } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/uamp/MediaItemData.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.uamp 18 | 19 | import android.net.Uri 20 | import android.support.v4.media.MediaBrowserCompat 21 | import android.support.v4.media.MediaBrowserCompat.MediaItem 22 | import androidx.recyclerview.widget.DiffUtil 23 | import com.example.android.uamp.viewmodels.MediaItemFragmentViewModel 24 | 25 | /** 26 | * Data class to encapsulate properties of a [MediaItem]. 27 | * 28 | * If an item is [browsable] it means that it has a list of child media items that 29 | * can be retrieved by passing the mediaId to [MediaBrowserCompat.subscribe]. 30 | * 31 | * Objects of this class are built from [MediaItem]s in 32 | * [MediaItemFragmentViewModel.subscriptionCallback]. 33 | */ 34 | data class MediaItemData( 35 | val mediaId: String, 36 | val title: String, 37 | val subtitle: String, 38 | val albumArtUri: Uri, 39 | val browsable: Boolean, 40 | var playbackRes: Int 41 | ) { 42 | 43 | companion object { 44 | /** 45 | * Indicates [playbackRes] has changed. 46 | */ 47 | const val PLAYBACK_RES_CHANGED = 1 48 | 49 | /** 50 | * [DiffUtil.ItemCallback] for a [MediaItemData]. 51 | * 52 | * Since all [MediaItemData]s have a unique ID, it's easiest to check if two 53 | * items are the same by simply comparing that ID. 54 | * 55 | * To check if the contents are the same, we use the same ID, but it may be the 56 | * case that it's only the play state itself which has changed (from playing to 57 | * paused, or perhaps a different item is the active item now). In this case 58 | * we check both the ID and the playback resource. 59 | * 60 | * To calculate the payload, we use the simplest method possible: 61 | * - Since the title, subtitle, and albumArtUri are constant (with respect to mediaId), 62 | * there's no reason to check if they've changed. If the mediaId is the same, none of 63 | * those properties have changed. 64 | * - If the playback resource (playbackRes) has changed to reflect the change in playback 65 | * state, that's all that needs to be updated. We return [PLAYBACK_RES_CHANGED] as 66 | * the payload in this case. 67 | * - If something else changed, then refresh the full item for simplicity. 68 | */ 69 | val diffCallback = object : DiffUtil.ItemCallback() { 70 | override fun areItemsTheSame( 71 | oldItem: MediaItemData, 72 | newItem: MediaItemData 73 | ): Boolean = 74 | oldItem.mediaId == newItem.mediaId 75 | 76 | override fun areContentsTheSame(oldItem: MediaItemData, newItem: MediaItemData) = 77 | oldItem.mediaId == newItem.mediaId && oldItem.playbackRes == newItem.playbackRes 78 | 79 | override fun getChangePayload(oldItem: MediaItemData, newItem: MediaItemData) = 80 | if (oldItem.playbackRes != newItem.playbackRes) { 81 | PLAYBACK_RES_CHANGED 82 | } else null 83 | } 84 | } 85 | } 86 | 87 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/uamp/cast/UampCastOptionsProvider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.uamp.cast 18 | 19 | import android.content.Context 20 | import com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider.APP_ID_DEFAULT_RECEIVER_WITH_DRM 21 | import com.google.android.gms.cast.framework.CastOptions 22 | import com.google.android.gms.cast.framework.OptionsProvider 23 | import com.google.android.gms.cast.framework.SessionProvider 24 | import com.google.android.gms.cast.framework.media.CastMediaOptions 25 | 26 | 27 | class UampCastOptionsProvider : OptionsProvider { 28 | 29 | override fun getCastOptions(context: Context?): CastOptions? { 30 | return CastOptions.Builder() 31 | // Use the Default Media Receiver with DRM support. 32 | .setReceiverApplicationId(APP_ID_DEFAULT_RECEIVER_WITH_DRM) 33 | .setCastMediaOptions( 34 | CastMediaOptions.Builder() 35 | // We manage the media session and the notifications ourselves. 36 | .setMediaSessionEnabled(false) 37 | .setNotificationOptions(null) 38 | .build() 39 | ) 40 | .setStopReceiverApplicationWhenEndingSession(true).build() 41 | } 42 | 43 | override fun getAdditionalSessionProviders(context: Context?): List? { 44 | return null 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/uamp/fragments/MediaItemFragment.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.uamp.fragments 18 | 19 | import android.os.Bundle 20 | import android.view.LayoutInflater 21 | import android.view.View 22 | import android.view.ViewGroup 23 | import androidx.fragment.app.Fragment 24 | import androidx.fragment.app.activityViewModels 25 | import androidx.fragment.app.viewModels 26 | import androidx.lifecycle.Observer 27 | import com.example.android.uamp.MediaItemAdapter 28 | import com.example.android.uamp.databinding.FragmentMediaitemListBinding 29 | import com.example.android.uamp.utils.InjectorUtils 30 | import com.example.android.uamp.viewmodels.MainActivityViewModel 31 | import com.example.android.uamp.viewmodels.MediaItemFragmentViewModel 32 | 33 | /** 34 | * A fragment representing a list of MediaItems. 35 | */ 36 | class MediaItemFragment : Fragment() { 37 | private val mainActivityViewModel by activityViewModels { 38 | InjectorUtils.provideMainActivityViewModel(requireContext()) 39 | } 40 | private val mediaItemFragmentViewModel by viewModels { 41 | InjectorUtils.provideMediaItemFragmentViewModel(requireContext(), mediaId) 42 | } 43 | 44 | private lateinit var mediaId: String 45 | private lateinit var binding: FragmentMediaitemListBinding 46 | 47 | private val listAdapter = MediaItemAdapter { clickedItem -> 48 | mainActivityViewModel.mediaItemClicked(clickedItem) 49 | } 50 | 51 | companion object { 52 | fun newInstance(mediaId: String): MediaItemFragment { 53 | 54 | return MediaItemFragment().apply { 55 | arguments = Bundle().apply { 56 | putString(MEDIA_ID_ARG, mediaId) 57 | } 58 | } 59 | } 60 | } 61 | 62 | override fun onCreateView( 63 | inflater: LayoutInflater, container: ViewGroup?, 64 | savedInstanceState: Bundle? 65 | ): View? { 66 | binding = FragmentMediaitemListBinding.inflate(inflater, container, false) 67 | return binding.root 68 | } 69 | 70 | override fun onActivityCreated(savedInstanceState: Bundle?) { 71 | super.onActivityCreated(savedInstanceState) 72 | 73 | // Always true, but lets lint know that as well. 74 | mediaId = arguments?.getString(MEDIA_ID_ARG) ?: return 75 | 76 | mediaItemFragmentViewModel.mediaItems.observe(viewLifecycleOwner, 77 | Observer { list -> 78 | binding.loadingSpinner.visibility = 79 | if (list?.isNotEmpty() == true) View.GONE else View.VISIBLE 80 | listAdapter.submitList(list) 81 | }) 82 | mediaItemFragmentViewModel.networkError.observe(viewLifecycleOwner, 83 | Observer { error -> 84 | if (error) { 85 | binding.loadingSpinner.visibility = View.GONE 86 | binding.networkError.visibility = View.VISIBLE 87 | } else { 88 | binding.networkError.visibility = View.GONE 89 | } 90 | }) 91 | 92 | // Set the adapter 93 | binding.list.adapter = listAdapter 94 | } 95 | } 96 | 97 | private const val MEDIA_ID_ARG = "com.example.android.uamp.fragments.MediaItemFragment.MEDIA_ID" 98 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/uamp/fragments/NowPlayingFragment.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.uamp.fragments 18 | 19 | import android.net.Uri 20 | import android.os.Bundle 21 | import android.view.LayoutInflater 22 | import android.view.View 23 | import android.view.ViewGroup 24 | import androidx.fragment.app.Fragment 25 | import androidx.fragment.app.activityViewModels 26 | import androidx.fragment.app.viewModels 27 | import androidx.lifecycle.Observer 28 | import com.bumptech.glide.Glide 29 | import com.example.android.uamp.R 30 | import com.example.android.uamp.databinding.FragmentNowplayingBinding 31 | import com.example.android.uamp.utils.InjectorUtils 32 | import com.example.android.uamp.viewmodels.MainActivityViewModel 33 | import com.example.android.uamp.viewmodels.NowPlayingFragmentViewModel 34 | import com.example.android.uamp.viewmodels.NowPlayingFragmentViewModel.NowPlayingMetadata 35 | 36 | /** 37 | * A fragment representing the current media item being played. 38 | */ 39 | class NowPlayingFragment : Fragment() { 40 | private val mainActivityViewModel by activityViewModels { 41 | InjectorUtils.provideMainActivityViewModel(requireContext()) 42 | } 43 | private val nowPlayingViewModel by viewModels { 44 | InjectorUtils.provideNowPlayingFragmentViewModel(requireContext()) 45 | } 46 | 47 | lateinit var binding: FragmentNowplayingBinding 48 | 49 | companion object { 50 | fun newInstance() = NowPlayingFragment() 51 | } 52 | 53 | override fun onCreateView( 54 | inflater: LayoutInflater, container: ViewGroup?, 55 | savedInstanceState: Bundle? 56 | ): View? { 57 | binding = FragmentNowplayingBinding.inflate(inflater, container, false) 58 | return binding.root 59 | } 60 | 61 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 62 | super.onViewCreated(view, savedInstanceState) 63 | 64 | // Always true, but lets lint know that as well. 65 | val context = activity ?: return 66 | 67 | // Attach observers to the LiveData coming from this ViewModel 68 | nowPlayingViewModel.mediaMetadata.observe(viewLifecycleOwner, 69 | Observer { mediaItem -> updateUI(view, mediaItem) }) 70 | nowPlayingViewModel.mediaButtonRes.observe(viewLifecycleOwner, 71 | Observer { res -> 72 | binding.mediaButton.setImageResource(res) 73 | }) 74 | nowPlayingViewModel.mediaPosition.observe(viewLifecycleOwner, 75 | Observer { pos -> 76 | binding.position.text = NowPlayingMetadata.timestampToMSS(context, pos) 77 | }) 78 | 79 | // Setup UI handlers for buttons 80 | binding.mediaButton.setOnClickListener { 81 | nowPlayingViewModel.mediaMetadata.value?.let { mainActivityViewModel.playMediaId(it.id) } 82 | } 83 | 84 | // Initialize playback duration and position to zero 85 | binding.duration.text = NowPlayingMetadata.timestampToMSS(context, 0L) 86 | binding.position.text = NowPlayingMetadata.timestampToMSS(context, 0L) 87 | } 88 | 89 | /** 90 | * Internal function used to update all UI elements except for the current item playback 91 | */ 92 | private fun updateUI(view: View, metadata: NowPlayingMetadata) = with(binding) { 93 | if (metadata.albumArtUri == Uri.EMPTY) { 94 | albumArt.setImageResource(R.drawable.ic_album_black_24dp) 95 | } else { 96 | Glide.with(view) 97 | .load(metadata.albumArtUri) 98 | .into(albumArt) 99 | } 100 | title.text = metadata.title 101 | subtitle.text = metadata.subtitle 102 | duration.text = metadata.duration 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/uamp/utils/Event.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.uamp.utils 18 | 19 | /** 20 | * Used as a wrapper for data that is exposed via a LiveData that represents an event. 21 | * 22 | * For more information, see: 23 | * https://medium.com/google-developers/livedata-with-events-ac2622673150 24 | */ 25 | class Event(private val content: T) { 26 | 27 | var hasBeenHandled = false 28 | private set // Allow external read but not write 29 | 30 | /** 31 | * Returns the content and prevents its use again. 32 | */ 33 | fun getContentIfNotHandled(): T? { 34 | return if (hasBeenHandled) { 35 | null 36 | } else { 37 | hasBeenHandled = true 38 | content 39 | } 40 | } 41 | 42 | /** 43 | * Returns the content, even if it's already been handled. 44 | */ 45 | fun peekContent(): T = content 46 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/uamp/utils/InjectorUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.uamp.utils 18 | 19 | import android.app.Application 20 | import android.content.ComponentName 21 | import android.content.Context 22 | import com.example.android.uamp.common.MusicServiceConnection 23 | import com.example.android.uamp.media.MusicService 24 | import com.example.android.uamp.viewmodels.MainActivityViewModel 25 | import com.example.android.uamp.viewmodels.MediaItemFragmentViewModel 26 | import com.example.android.uamp.viewmodels.NowPlayingFragmentViewModel 27 | 28 | /** 29 | * Static methods used to inject classes needed for various Activities and Fragments. 30 | */ 31 | object InjectorUtils { 32 | private fun provideMusicServiceConnection(context: Context): MusicServiceConnection { 33 | return MusicServiceConnection.getInstance( 34 | context, 35 | ComponentName(context, MusicService::class.java) 36 | ) 37 | } 38 | 39 | fun provideMainActivityViewModel(context: Context): MainActivityViewModel.Factory { 40 | val applicationContext = context.applicationContext 41 | val musicServiceConnection = provideMusicServiceConnection(applicationContext) 42 | return MainActivityViewModel.Factory(musicServiceConnection) 43 | } 44 | 45 | fun provideMediaItemFragmentViewModel(context: Context, mediaId: String) 46 | : MediaItemFragmentViewModel.Factory { 47 | val applicationContext = context.applicationContext 48 | val musicServiceConnection = provideMusicServiceConnection(applicationContext) 49 | return MediaItemFragmentViewModel.Factory(mediaId, musicServiceConnection) 50 | } 51 | 52 | fun provideNowPlayingFragmentViewModel(context: Context) 53 | : NowPlayingFragmentViewModel.Factory { 54 | val applicationContext = context.applicationContext 55 | val musicServiceConnection = provideMusicServiceConnection(applicationContext) 56 | return NowPlayingFragmentViewModel.Factory( 57 | applicationContext as Application, musicServiceConnection 58 | ) 59 | } 60 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 23 | 28 | 29 | 35 | 38 | 41 | 42 | 43 | 44 | 50 | 51 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_album_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_pause_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_play_arrow_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_signal_wifi_off_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/media_item_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 23 | 24 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/media_item_mask.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 23 | 24 | 27 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/media_overlay_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/layout/cast_context_error.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_mediaitem.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 24 | 25 | 34 | 35 | 46 | 47 | 56 | 57 | 70 | 71 | 83 | 84 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_mediaitem_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 22 | 23 | 33 | 34 | 39 | 40 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/uamp/31769898337d0228af7457a7a475d1b755263fdd/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/uamp/31769898337d0228af7457a7a475d1b755263fdd/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/uamp/31769898337d0228af7457a7a475d1b755263fdd/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/uamp/31769898337d0228af7457a7a475d1b755263fdd/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/uamp/31769898337d0228af7457a7a475d1b755263fdd/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/uamp/31769898337d0228af7457a7a475d1b755263fdd/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/uamp/31769898337d0228af7457a7a475d1b755263fdd/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/uamp/31769898337d0228af7457a7a475d1b755263fdd/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/uamp/31769898337d0228af7457a7a475d1b755263fdd/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/uamp/31769898337d0228af7457a7a475d1b755263fdd/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/uamp/31769898337d0228af7457a7a475d1b755263fdd/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/uamp/31769898337d0228af7457a7a475d1b755263fdd/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/uamp/31769898337d0228af7457a7a475d1b755263fdd/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/uamp/31769898337d0228af7457a7a475d1b755263fdd/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/uamp/31769898337d0228af7457a7a475d1b755263fdd/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | #840255 19 | #710144 20 | #14A5A1 21 | 22 | #00000000 23 | 24 | #F1F1F1 25 | 26 | #f8f8f8 27 | 28 | #eff0f0 29 | #fdfdfd 30 | 31 | #ffffff 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 16dp 19 | 20 | 72dp 21 | 4dp 22 | 12dp 23 | 4dp 24 | 25 | 72dp 26 | 27 | 4dp 28 | 2dp 29 | -2dp 30 | 31 | 72dp 32 | 42dp 33 | 8dp 34 | 42dp 35 | 52dp 36 | 37 | 32dp 38 | 8dp 39 | 32dp 40 | 16dp 41 | 42 | 32sp 43 | 18sp 44 | 45 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | UAMP 19 | 20 | Skip back 10s 21 | Skip forward 10s 22 | Play 23 | Pause 24 | 25 | Queue 26 | Album art 27 | --:-- 28 | %d:%02d 29 | 30 | Failed to get Cast context. Try updating Google Play Services and restart the app. 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 25 | 26 | 29 | 30 | 33 | 34 | 38 | 39 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /app/src/main/res/xml/automotive_app_desc.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 22 | 23 | -------------------------------------------------------------------------------- /automotive/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | apply plugin: 'com.android.application' 18 | apply plugin: 'kotlin-android' 19 | apply plugin: 'kotlin-kapt' 20 | apply plugin: 'kotlin-android-extensions' 21 | 22 | android { 23 | compileSdkVersion rootProject.compileSdkVersion 24 | 25 | defaultConfig { 26 | applicationId "com.example.android.uamp.next" 27 | 28 | minSdkVersion 21 29 | targetSdkVersion rootProject.targetSdkVersion 30 | 31 | versionCode 1 32 | versionName "1.0" 33 | 34 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 35 | } 36 | 37 | buildFeatures { 38 | viewBinding true 39 | } 40 | 41 | compileOptions { 42 | sourceCompatibility 1.8 43 | targetCompatibility 1.8 44 | } 45 | kotlinOptions { 46 | jvmTarget = "1.8" 47 | } 48 | 49 | buildTypes { 50 | release { 51 | minifyEnabled false 52 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 53 | } 54 | } 55 | 56 | } 57 | 58 | dependencies { 59 | implementation project(':common') 60 | 61 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 62 | 63 | implementation "androidx.core:core-ktx:$androidx_core_ktx_version" 64 | implementation "androidx.preference:preference:$androidx_preference_version" 65 | implementation "androidx.car:car:$androidx_car_version" 66 | implementation "androidx.constraintlayout:constraintlayout:$constraint_layout_version" 67 | implementation "androidx.appcompat:appcompat:$androidx_app_compat_version" 68 | implementation "androidx.lifecycle:lifecycle-extensions:$arch_lifecycle_version" 69 | 70 | implementation "com.google.android.gms:play-services-auth:$play_services_auth_version" 71 | 72 | testImplementation "junit:junit:$junit_version" 73 | 74 | androidTestImplementation "androidx.test:runner:$androidx_test_runner_version" 75 | androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version" 76 | } 77 | -------------------------------------------------------------------------------- /automotive/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 | -------------------------------------------------------------------------------- /automotive/src/androidTest/java/com/example/android/uamp/automotive/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.uamp.automotive; 18 | 19 | import android.content.Context; 20 | 21 | import androidx.test.InstrumentationRegistry; 22 | import androidx.test.runner.AndroidJUnit4; 23 | 24 | import org.junit.Test; 25 | import org.junit.runner.RunWith; 26 | 27 | import static org.junit.Assert.assertEquals; 28 | 29 | /** 30 | * Instrumented test, which will execute on an Android device. 31 | * 32 | * @see Testing documentation 33 | */ 34 | @RunWith(AndroidJUnit4.class) 35 | public class ExampleInstrumentedTest { 36 | @Test 37 | public void useAppContext() { 38 | // Context of the app under test. 39 | Context appContext = InstrumentationRegistry.getTargetContext(); 40 | 41 | assertEquals("com.example.automotive", appContext.getPackageName()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /automotive/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 20 | 21 | 27 | 30 | 31 | 32 | 33 | 36 | 39 | 42 | 43 | 52 | 53 | 56 | 57 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 70 | 71 | 72 | 73 | 74 | 75 | 79 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /automotive/src/main/java/com/example/android/uamp/automotive/PhoneSignInFragment.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.uamp.automotive 18 | 19 | import android.os.Bundle 20 | import android.text.method.LinkMovementMethod 21 | import android.view.View 22 | import androidx.core.content.ContextCompat 23 | import androidx.core.text.HtmlCompat 24 | import androidx.fragment.app.Fragment 25 | import com.example.android.uamp.automotive.databinding.PhoneSignInBinding 26 | 27 | /** 28 | * Fragment that is used to facilitate phone sign-in. The fragment allows users to choose between 29 | * either the PIN or QR code sign-in flow. 30 | */ 31 | class PhoneSignInFragment : Fragment(R.layout.phone_sign_in) { 32 | 33 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 34 | super.onViewCreated(view, savedInstanceState) 35 | val context = requireContext() 36 | 37 | val binding = PhoneSignInBinding.bind(view) 38 | 39 | binding.toolbar.setNavigationOnClickListener { 40 | requireActivity().supportFragmentManager.popBackStack() 41 | } 42 | 43 | // Set up PIN sign in button. 44 | binding.appIcon.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.aural_logo)) 45 | binding.primaryMessage.text = getString(R.string.phone_sign_in_primary_text) 46 | binding.pinSignInButton.text = getString(R.string.pin_sign_in_button_label) 47 | binding.pinSignInButton.setOnClickListener { 48 | requireActivity().supportFragmentManager.beginTransaction() 49 | .replace(R.id.sign_in_container, PinCodeSignInFragment()) 50 | .addToBackStack("landingPage") 51 | .commit() 52 | } 53 | 54 | // Set up QR code sign in button. 55 | binding.qrSignInButton.text = getString(R.string.qr_sign_in_button_label) 56 | binding.qrSignInButton.setOnClickListener { 57 | requireActivity().supportFragmentManager.beginTransaction() 58 | .replace(R.id.sign_in_container, QrCodeSignInFragment()) 59 | .addToBackStack("landingPage") 60 | .commit() 61 | } 62 | 63 | // Links in footer text should be clickable. 64 | binding.footer.text = HtmlCompat.fromHtml( 65 | context.getString(R.string.sign_in_footer), 66 | HtmlCompat.FROM_HTML_MODE_LEGACY 67 | ) 68 | binding.footer.movementMethod = LinkMovementMethod.getInstance() 69 | } 70 | } -------------------------------------------------------------------------------- /automotive/src/main/java/com/example/android/uamp/automotive/PinCodeSignInFragment.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.uamp.automotive 18 | 19 | import android.os.Bundle 20 | import android.text.method.LinkMovementMethod 21 | import android.view.LayoutInflater 22 | import android.view.View 23 | import android.widget.TextView 24 | import androidx.core.content.ContextCompat 25 | import androidx.core.text.HtmlCompat 26 | import androidx.fragment.app.Fragment 27 | import androidx.lifecycle.ViewModelProvider 28 | import com.example.android.uamp.automotive.databinding.PinSignInBinding 29 | 30 | /** 31 | * Fragment that is used to facilitate PIN code sign-in. This fragment displayed a configurable 32 | * PIN code that users enter in a secondary device to perform sign-in. 33 | * 34 | *

This screen serves as a demo for UI best practices for PIN code sign in. Sign in implementation 35 | * will be app specific and is not included. 36 | */ 37 | class PinCodeSignInFragment : Fragment(R.layout.pin_sign_in) { 38 | 39 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 40 | super.onViewCreated(view, savedInstanceState) 41 | val context = requireContext() 42 | 43 | val binding = PinSignInBinding.bind(view) 44 | 45 | binding.toolbar.setNavigationOnClickListener { 46 | requireActivity().supportFragmentManager.popBackStack() 47 | } 48 | 49 | binding.appIcon.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.aural_logo)) 50 | binding.primaryMessage.text = getString(R.string.pin_sign_in_primary_text) 51 | binding.secondaryMessage.text = getString(R.string.pin_sign_in_secondary_text) 52 | 53 | // Links in footer text should be clickable. 54 | binding.footer.text = HtmlCompat.fromHtml( 55 | context.getString(R.string.sign_in_footer), 56 | HtmlCompat.FROM_HTML_MODE_LEGACY 57 | ) 58 | binding.footer.movementMethod = LinkMovementMethod.getInstance() 59 | 60 | val pin = ViewModelProvider(requireActivity()) 61 | .get(SignInActivityViewModel::class.java) 62 | .generatePin() 63 | 64 | // Remove existing PIN characters. 65 | if (binding.pinCodeContainer.childCount > 0) { 66 | binding.pinCodeContainer.removeAllViews() 67 | } 68 | 69 | for (element in pin) { 70 | val pinItem = LayoutInflater.from(context).inflate( 71 | R.layout.pin_item, 72 | binding.pinCodeContainer, 73 | false 74 | ) as TextView 75 | pinItem.text = element.toString() 76 | binding.pinCodeContainer.addView(pinItem) 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /automotive/src/main/java/com/example/android/uamp/automotive/QrCodeSignInFragment.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.uamp.automotive 18 | 19 | import android.os.Bundle 20 | import android.text.method.LinkMovementMethod 21 | import android.view.View 22 | import androidx.core.content.ContextCompat.getDrawable 23 | import androidx.core.text.HtmlCompat 24 | import androidx.fragment.app.Fragment 25 | import com.bumptech.glide.Glide 26 | import com.example.android.uamp.automotive.databinding.QrSignInBinding 27 | 28 | /** 29 | * Fragment that is used to facilitate QR code sign-in. Users scan a QR code rendered by this 30 | * fragment with their phones, which performs the authentication required for sign-in 31 | * 32 | *

This screen serves as a demo for UI best practices for QR code sign in. Sign in implementation 33 | * will be app specific and is not included. 34 | */ 35 | class QrCodeSignInFragment : Fragment(R.layout.qr_sign_in) { 36 | 37 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 38 | super.onViewCreated(view, savedInstanceState) 39 | val binding = QrSignInBinding.bind(view) 40 | 41 | binding.toolbar.setNavigationOnClickListener { 42 | requireActivity().supportFragmentManager.popBackStack() 43 | } 44 | 45 | binding.appIcon.setImageDrawable(getDrawable(requireContext(), R.drawable.aural_logo)) 46 | binding.primaryMessage.text = getString(R.string.qr_sign_in_primary_text) 47 | binding.secondaryMessage.text = getString(R.string.qr_sign_in_secondary_text) 48 | 49 | // Links in footer text should be clickable. 50 | binding.footer.text = HtmlCompat.fromHtml( 51 | requireContext().getString(R.string.sign_in_footer), 52 | HtmlCompat.FROM_HTML_MODE_LEGACY 53 | ) 54 | binding.footer.movementMethod = LinkMovementMethod.getInstance() 55 | 56 | Glide.with(this).load(getString(R.string.qr_code_url)).into(binding.qrCode) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /automotive/src/main/java/com/example/android/uamp/automotive/SettingsActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.uamp.automotive 18 | 19 | import android.os.Bundle 20 | import androidx.appcompat.app.AppCompatActivity 21 | import com.example.android.uamp.automotive.databinding.ActivitySettingsBinding 22 | 23 | /** 24 | * This class exposes application settings 25 | * for integration with MediaCenter in Android Automotive. 26 | */ 27 | class SettingsActivity : AppCompatActivity() { 28 | 29 | override fun onCreate(savedInstanceState: Bundle?) { 30 | super.onCreate(savedInstanceState) 31 | val binding = ActivitySettingsBinding.inflate(layoutInflater) 32 | setContentView(binding.root) 33 | 34 | setSupportActionBar(binding.toolbar) 35 | supportActionBar?.setHomeButtonEnabled(true) 36 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 37 | 38 | supportFragmentManager 39 | .beginTransaction() 40 | .replace(R.id.settings_container, SettingsFragment()) 41 | .commit() 42 | } 43 | 44 | override fun onBackPressed() { 45 | super.onBackPressed() 46 | finish() 47 | } 48 | 49 | override fun onSupportNavigateUp(): Boolean { 50 | onBackPressed() 51 | return true 52 | } 53 | } -------------------------------------------------------------------------------- /automotive/src/main/java/com/example/android/uamp/automotive/SettingsFragment.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.uamp.automotive 18 | 19 | import android.app.Application 20 | import android.content.ComponentName 21 | import android.os.Bundle 22 | import androidx.lifecycle.AndroidViewModel 23 | import androidx.lifecycle.ViewModelProvider 24 | import androidx.preference.Preference 25 | import androidx.preference.PreferenceFragmentCompat 26 | import com.example.android.uamp.common.MusicServiceConnection 27 | 28 | /** 29 | * Preference fragment hosted by [SettingsActivity]. Handles events to various preference changes. 30 | */ 31 | class SettingsFragment : PreferenceFragmentCompat() { 32 | private lateinit var viewModel: SettingsFragmentViewModel 33 | 34 | override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { 35 | setPreferencesFromResource(R.xml.preferences, rootKey) 36 | 37 | viewModel = ViewModelProvider(this) 38 | .get(SettingsFragmentViewModel::class.java) 39 | } 40 | 41 | override fun onPreferenceTreeClick(preference: Preference?): Boolean { 42 | return when (preference?.key) { 43 | "logout" -> { 44 | viewModel.logout() 45 | requireActivity().finish() 46 | true 47 | } 48 | else -> { 49 | super.onPreferenceTreeClick(preference) 50 | } 51 | } 52 | } 53 | } 54 | 55 | /** 56 | * Basic ViewModel for [SettingsFragment]. 57 | */ 58 | class SettingsFragmentViewModel(application: Application) : AndroidViewModel(application) { 59 | private val applicationContext = application.applicationContext 60 | private val musicServiceConnection = MusicServiceConnection( 61 | applicationContext, 62 | ComponentName(applicationContext, AutomotiveMusicService::class.java) 63 | ) 64 | 65 | fun logout() { 66 | // Logout is fire and forget. 67 | musicServiceConnection.sendCommand(LOGOUT, null) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /automotive/src/main/java/com/example/android/uamp/automotive/SignInActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.uamp.automotive 18 | 19 | import android.os.Bundle 20 | import android.widget.Toast 21 | import androidx.appcompat.app.AppCompatActivity 22 | import androidx.lifecycle.Observer 23 | import androidx.lifecycle.ViewModelProvider 24 | 25 | class SignInActivity : AppCompatActivity() { 26 | 27 | private lateinit var viewModel: SignInActivityViewModel 28 | 29 | override fun onCreate(savedInstanceState: Bundle?) { 30 | super.onCreate(savedInstanceState) 31 | setContentView(R.layout.activity_sign_in) 32 | 33 | viewModel = ViewModelProvider(this) 34 | .get(SignInActivityViewModel::class.java) 35 | 36 | viewModel.loggedIn.observe(this, Observer { loggedIn -> 37 | if (loggedIn == true) { 38 | Toast.makeText(this, R.string.sign_in_success_message, Toast.LENGTH_SHORT).show() 39 | finish() 40 | } 41 | }) 42 | 43 | supportFragmentManager.beginTransaction() 44 | .add(R.id.sign_in_container, SignInLandingPageFragment()) 45 | .commit() 46 | } 47 | } -------------------------------------------------------------------------------- /automotive/src/main/java/com/example/android/uamp/automotive/SignInActivityViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.uamp.automotive 18 | 19 | import android.app.Activity 20 | import android.app.Application 21 | import android.content.ComponentName 22 | import android.os.Bundle 23 | import android.text.TextUtils 24 | import android.widget.Toast 25 | import androidx.lifecycle.AndroidViewModel 26 | import androidx.lifecycle.LiveData 27 | import androidx.lifecycle.MutableLiveData 28 | import com.example.android.uamp.common.MusicServiceConnection 29 | import java.util.Random 30 | 31 | /** 32 | * Basic ViewModel for [SignInActivity]. 33 | */ 34 | class SignInActivityViewModel(application: Application) : AndroidViewModel(application) { 35 | private val applicationContext = application.applicationContext 36 | private val musicServiceConnection = MusicServiceConnection( 37 | applicationContext, 38 | ComponentName(applicationContext, AutomotiveMusicService::class.java) 39 | ) 40 | 41 | private val _loggedIn = MutableLiveData() 42 | val loggedIn: LiveData = _loggedIn 43 | 44 | fun login(email: String, password: String) { 45 | if (TextUtils.isEmpty(email) or TextUtils.isEmpty(password)) { 46 | Toast.makeText( 47 | applicationContext, 48 | applicationContext.getString(R.string.missing_fields_error), 49 | Toast.LENGTH_SHORT 50 | ).show() 51 | } else { 52 | val loginParams = Bundle().apply { 53 | putString(LOGIN_EMAIL, email) 54 | putString(LOGIN_PASSWORD, password) 55 | } 56 | musicServiceConnection.sendCommand(LOGIN, loginParams) { resultCode, _ -> 57 | _loggedIn.postValue(resultCode == Activity.RESULT_OK) 58 | } 59 | } 60 | } 61 | 62 | fun generatePin(): CharSequence { 63 | return String.format("%08d", Random().nextInt(99999999)) 64 | } 65 | } -------------------------------------------------------------------------------- /automotive/src/main/java/com/example/android/uamp/automotive/UsernameAndPasswordSignInFragment.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.uamp.automotive 18 | 19 | import android.os.Build 20 | import android.os.Bundle 21 | import android.text.method.LinkMovementMethod 22 | import android.view.LayoutInflater 23 | import android.view.View 24 | import android.view.ViewGroup 25 | import android.widget.Button 26 | import android.widget.ImageView 27 | import android.widget.TextView 28 | import androidx.appcompat.widget.Toolbar 29 | import androidx.core.content.ContextCompat 30 | import androidx.core.text.HtmlCompat 31 | import androidx.fragment.app.Fragment 32 | import androidx.lifecycle.ViewModelProvider 33 | import com.google.android.material.textfield.TextInputEditText 34 | import com.google.android.material.textfield.TextInputLayout 35 | 36 | /** 37 | * Fragment that is used to facilitates username and password sign-in. 38 | */ 39 | class UsernameAndPasswordSignInFragment : Fragment() { 40 | 41 | private lateinit var toolbar: Toolbar 42 | private lateinit var appIcon: ImageView 43 | private lateinit var primaryTextView: TextView 44 | private lateinit var passwordContainer: TextInputLayout 45 | private lateinit var passwordInput: TextInputEditText 46 | private lateinit var submitButton: Button 47 | private lateinit var footerTextView: TextView 48 | 49 | override fun onCreateView( 50 | inflater: LayoutInflater, container: ViewGroup?, 51 | savedInstanceState: Bundle? 52 | ): View? { 53 | return inflater.inflate(R.layout.username_and_password_sign_in, container, false) 54 | } 55 | 56 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 57 | super.onViewCreated(view, savedInstanceState) 58 | val context = requireContext() 59 | 60 | toolbar = view.findViewById(R.id.toolbar) 61 | appIcon = view.findViewById(R.id.app_icon) 62 | primaryTextView = view.findViewById(R.id.primary_message) 63 | passwordContainer = view.findViewById(R.id.password_container) 64 | passwordInput = view.findViewById(R.id.password_input) 65 | submitButton = view.findViewById(R.id.submit_button) 66 | footerTextView = view.findViewById(R.id.footer) 67 | 68 | toolbar.setNavigationOnClickListener { 69 | requireActivity().supportFragmentManager.popBackStack() 70 | } 71 | 72 | appIcon.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.aural_logo)) 73 | primaryTextView.text = getString(R.string.username_and_password_sign_in_primary_text) 74 | passwordContainer.hint = getString(R.string.password_hint) 75 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 76 | passwordInput.setAutofillHints(View.AUTOFILL_HINT_PASSWORD) 77 | } 78 | 79 | // Links in footer text should be clickable. 80 | footerTextView.text = HtmlCompat.fromHtml( 81 | context.getString(R.string.sign_in_footer), 82 | HtmlCompat.FROM_HTML_MODE_LEGACY 83 | ) 84 | footerTextView.movementMethod = LinkMovementMethod.getInstance() 85 | 86 | // Get user identifier from previous screen. 87 | val userId = arguments?.getString(SignInLandingPageFragment.CAR_SIGN_IN_IDENTIFIER_KEY) 88 | 89 | submitButton.text = getString(R.string.sign_in_submit_button_label) 90 | submitButton.setOnClickListener { 91 | onSignIn(userId!!, passwordInput.text.toString()) 92 | } 93 | } 94 | 95 | private fun onSignIn(userIdentifier: CharSequence, password: CharSequence) { 96 | ViewModelProvider(requireActivity()) 97 | .get(SignInActivityViewModel::class.java) 98 | .login(userIdentifier.toString(), password.toString()) 99 | } 100 | } -------------------------------------------------------------------------------- /automotive/src/main/res/color/car_text_dark.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /automotive/src/main/res/color/car_text_light.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /automotive/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /automotive/src/main/res/drawable/aural_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/uamp/31769898337d0228af7457a7a475d1b755263fdd/automotive/src/main/res/drawable/aural_logo.png -------------------------------------------------------------------------------- /automotive/src/main/res/drawable/default_button_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /automotive/src/main/res/drawable/google_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/uamp/31769898337d0228af7457a7a475d1b755263fdd/automotive/src/main/res/drawable/google_logo.png -------------------------------------------------------------------------------- /automotive/src/main/res/drawable/google_sign_in_button_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /automotive/src/main/res/drawable/google_sign_in_button_logo.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 22 | -------------------------------------------------------------------------------- /automotive/src/main/res/drawable/pin_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 23 | 24 | -------------------------------------------------------------------------------- /automotive/src/main/res/drawable/sign_in_button_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /automotive/src/main/res/drawable/sign_in_toolbar_back_icon.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 22 | 23 | 24 | 27 | 28 | -------------------------------------------------------------------------------- /automotive/src/main/res/drawable/sign_in_toolbar_back_ripple_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 20 | -------------------------------------------------------------------------------- /automotive/src/main/res/layout-h900dp/pin_sign_in.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 21 | 22 | 26 | 27 | 39 | 40 | 52 | 53 | 65 | 66 | 76 | 77 | 89 | 90 | 91 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /automotive/src/main/res/layout-h900dp/qr_sign_in.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 21 | 22 | 26 | 27 | 39 | 40 | 52 | 53 | 65 | 66 | 78 | 79 | 91 | 92 | 93 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /automotive/src/main/res/layout/activity_login.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 23 | 24 | 33 | 34 | 46 | 47 | 63 | 64 |