Full Code of android/uamp for AI

main 9498d991ab84 cached
129 files
414.6 KB
98.7k tokens
4 symbols
1 requests
Download .txt
Showing preview only (454K chars total). Download the full file or copy to clipboard to get everything.
Repository: android/uamp
Branch: main
Commit: 9498d991ab84
Files: 129
Total size: 414.6 KB

Directory structure:
gitextract_ek5wbsm9/

├── .github/
│   ├── scripts/
│   │   └── gradlew_recursive.sh
│   └── workflows/
│       ├── android.yml
│       └── copy-branch.yml
├── .gitignore
├── .google/
│   └── packaging.yaml
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── TODO.md
├── app/
│   ├── build.gradle
│   ├── proguard-rules.pro
│   └── src/
│       └── main/
│           ├── AndroidManifest.xml
│           ├── 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/
│               │   ├── 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
│               ├── drawable-v24/
│               │   └── ic_launcher_foreground.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
│               ├── 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/
│       │       │   ├── default_button_background.xml
│       │       │   ├── 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
│       │       ├── drawable-v24/
│       │       │   └── ic_launcher_foreground.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
│       │       ├── 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
│       │       ├── mipmap-anydpi-v26/
│       │       │   ├── ic_launcher.xml
│       │       │   └── ic_launcher_round.xml
│       │       ├── values/
│       │       │   ├── colors.xml
│       │       │   ├── dimens.xml
│       │       │   ├── strings.xml
│       │       │   └── styles.xml
│       │       ├── values-h1060dp/
│       │       │   └── dimens.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
│       │   ├── 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/
│       │       │   ├── ic_album.xml
│       │       │   └── ic_recommended.xml
│       │       ├── 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
├── gradle/
│   └── wrapper/
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
└── settings.gradle

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/scripts/gradlew_recursive.sh
================================================
#!/bin/bash

# Copyright (C) 2020 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

set -xe

# Default Gradle settings are not optimal for Android builds, override them
# here to make the most out of the GitHub Actions build servers
GRADLE_OPTS="$GRADLE_OPTS -Xms4g -Xmx4g"
GRADLE_OPTS="$GRADLE_OPTS -XX:+HeapDumpOnOutOfMemoryError"
GRADLE_OPTS="$GRADLE_OPTS -Dorg.gradle.daemon=false"
GRADLE_OPTS="$GRADLE_OPTS -Dorg.gradle.workers.max=2"
GRADLE_OPTS="$GRADLE_OPTS -Dkotlin.incremental=false"
GRADLE_OPTS="$GRADLE_OPTS -Dkotlin.compiler.execution.strategy=in-process"
GRADLE_OPTS="$GRADLE_OPTS -Dfile.encoding=UTF-8"
export GRADLE_OPTS

# Crawl all gradlew files which indicate an Android project
# You may edit this if your repo has a different project structure
for GRADLEW in `find . -name "gradlew"` ; do
    SAMPLE=$(dirname "${GRADLEW}")
    # Tell Gradle that this is a CI environment and disable parallel compilation
    bash "$GRADLEW" -p "$SAMPLE" -Pci --no-parallel --stacktrace $@
done


================================================
FILE: .github/workflows/android.yml
================================================
# Copyright (C) 2020 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

name: Android CI

on:
  workflow_dispatch:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:

  build:
    name: Build
    runs-on: ubuntu-18.04

    steps:
      - uses: actions/checkout@v1
      - name: set up JDK 1.8
        uses: actions/setup-java@v1
        with:
          java-version: 1.8
      - name: Build project
        run: .github/scripts/gradlew_recursive.sh assembleDebug
      - name: Zip artifacts
        run: zip -r assemble.zip . -i '**/build/*.apk' '**/build/*.aab' '**/build/*.aar' '**/build/*.so'
      - name: Upload artifacts
        uses: actions/upload-artifact@v1
        with:
          name: assemble
          path: assemble.zip


================================================
FILE: .github/workflows/copy-branch.yml
================================================
# Duplicates default main branch to the old master branch

name: Duplicates main to old master branch

# Controls when the action will run. Triggers the workflow on push or pull request
# events but only for the main branch
on:
  workflow_dispatch:
  push:
    branches: [ main ]

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
  # This workflow contains a single job called "copy-branch"
  copy-branch:
    # The type of runner that the job will run on
    runs-on: ubuntu-latest

    # Steps represent a sequence of tasks that will be executed as part of the job
    steps:
    # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it,
    # but specifies master branch (old default).
    - uses: actions/checkout@v2
      with:
        fetch-depth: 0
        ref: master
        
    - run: |
        git config user.name github-actions
        git config user.email github-actions@github.com
        git merge origin/main
        git push


================================================
FILE: .gitignore
================================================
*.iml
.gradle
/local.properties
/.idea
.DS_Store
/captures
.externalNativeBuild

# Generated files
build/

# Extra (custom) settings
extra-settings.gradle


================================================
FILE: .google/packaging.yaml
================================================
# GOOGLE SAMPLE PACKAGING DATA
#
# This file is used by Google as part of our samples packaging process.
# End users may safely ignore this file. It has no relevance to other systems.
---
status:       PUBLISHED
technologies: [Android, Android Auto, Android Automotive OS, Android Wear]
categories:   [Getting Started, Media, UI]
languages:    [Kotlin]
solutions:    [Mobile]

github:       googlesamples/android-UniversalMusicPlayer

level:        INTERMEDIATE

icon: screenshots/icon-web.png

apiRefs:
  - android:android.support.v4.media.session.MediaSessionCompat
  - android:android.support.v4.media.session.MediaControllerCompat
  - androidx.media.MediaBrowserServiceCompat
  - android:android.support.v4.media.MediaBrowserCompat
  - androidx.media.app.NotificationCompat.MediaStyle
  - android:com.google.android.exoplayer2.SimpleExoPlayer
  - android:com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector

license: apache2-android


================================================
FILE: CONTRIBUTING.md
================================================
# How to become a contributor and submit your own code

## Contributor License Agreements

We'd love to accept your sample apps and patches! Before we can take them, we
have to jump a couple of legal hurdles.

Please fill out either the individual or corporate Contributor License Agreement (CLA).

  * If you are an individual writing original source code and you're sure you
    own the intellectual property, then you'll need to sign an [individual CLA]
    (https://developers.google.com/open-source/cla/individual).
  * If you work for a company that wants to allow you to contribute your work,
    then you'll need to sign a [corporate CLA]
    (https://developers.google.com/open-source/cla/corporate).

Follow either of the two links above to access the appropriate CLA and
instructions for how to sign and return it. Once we receive it, we'll be able to
accept your pull requests.

## Contributing A Patch

1. Submit an issue describing your proposed change to the repo in question.
1. The repo owner will respond to your issue promptly.
1. If your proposed change is accepted, and you haven't already done so, sign a
   Contributor License Agreement (see details above).
1. Fork the desired repo, develop and test your code changes.
1. Ensure that your code adheres to the existing style in the sample to which
   you are contributing. Refer to the
   [Android Code Style Guide]
   (https://source.android.com/source/code-style.html) for the
   recommended coding standards for this organization.
1. Ensure that your code has an appropriate set of unit tests which all pass.
1. Submit a pull request.



================================================
FILE: LICENSE
================================================
                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright 2014 The Android Open Source Project

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.


================================================
FILE: README.md
================================================
> **Warning**
> This sample has been deprecated and is no longer being maintained.
> 
> To find other samples that may be of interest, see [https://developer.android.com/samples](https://developer.android.com/samples).

Universal Android Music Player Sample
=====================================
The goal of this sample is to show how to implement an audio media app that works
across multiple form factors and provides a consistent user experience
on Android phones, tablets, Android Auto, Android Wear, Android TV, Google Cast devices,
and with the Google Assistant. 

To get started with UAMP please read the [full guide](docs/FullGuide.md).

![Screenshot showing UAMP's UI for browsing albums and songs](docs/images/1-browse-albums-screenshot.png "Browse albums screenshot")
![Screenshot showing UAMP's UI for playing a song](docs/images/2-play-song-screenshot.png "Play song screenshot")

Pre-requisites
--------------

- Android Studio 3.x

Getting Started
---------------

This sample uses the Gradle build system. To build this project, use the
"gradlew build" command or use "Import Project" in Android Studio.

Support
-------

- Check out the [FAQs page](docs/FAQs.md)
- Stack Overflow: http://stackoverflow.com/questions/tagged/android

If you've found an error in this sample, please
[file an issue](https://github.com/android/UAMP/issues)

Patches are encouraged and may be submitted by forking this project and
submitting a pull request through GitHub. Please see [CONTRIBUTING.md](CONTRIBUTING.md) for more
details.

Audio
-----

Music provided by the [Free Music Archive](http://freemusicarchive.org/).

- [Wake Up](http://freemusicarchive.org/music/The_Kyoto_Connection/Wake_Up_1957/) by
[The Kyoto Connection](http://freemusicarchive.org/music/The_Kyoto_Connection/).

Recordings provided by the [Ambisonic Sound Library](https://library.soundfield.com/).

- [Pre Game Marching Band](https://library.soundfield.com/track/163) by Watson Wu
- [Chickens on a Farm](https://library.soundfield.com/track/129) by Watson Wu
- [Rural Market Busker](https://library.soundfield.com/track/55) by Stephan Schutze
- [Steamtrain Interior](https://library.soundfield.com/track/65) by Stephan Schutze
- [Rural Road Car Pass](https://library.soundfield.com/track/57) by Stephan Schutze
- [10 Feet from Shore](https://library.soundfield.com/track/114) by Watson Wu

License
-------

Copyright 2025 Google Inc.

Licensed to the Apache Software Foundation (ASF) under one or more contributor
license agreements.  See the NOTICE file distributed with this work for
additional information regarding copyright ownership.  The ASF licenses this
file to you under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance with the License.  You may obtain a copy of
the License at

  http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
License for the specific language governing permissions and limitations under
the License.


================================================
FILE: TODO.md
================================================
TODOs
=====

This file captures the high level goals of the project. This provides guidance for anyone who wants
to contribute. If you see something in the list that you'd like to work on,
the best approach would be to [create an
issue](https://github.com/googlesamples/android-UniversalMusicPlayer/issues) first,
and then provide a pull request once completed to have your work merged into the project.

Service Side Tasks
------------------

- Implement rating (ideally "favorite" vs "thumbs up/down").
- Improve integration with the Google Assistant.

UI Tasks
--------

- 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)).


================================================
FILE: app/build.gradle
================================================
/*
 * Copyright 2017 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion rootProject.compileSdkVersion

    defaultConfig {
        applicationId "com.example.android.uamp.next"
        versionCode 1
        versionName "1.0"

        minSdkVersion rootProject.minSdkVersion
        targetSdkVersion rootProject.targetSdkVersion
        multiDexEnabled true

        compileOptions {
            sourceCompatibility JavaVersion.VERSION_1_8
            targetCompatibility JavaVersion.VERSION_1_8
        }
        kotlinOptions {
            jvmTarget = "1.8"
        }

        vectorDrawables {
            useSupportLibrary true
        }
    }

    buildFeatures {
        viewBinding true
        dataBinding true
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation "com.android.support:multidex:$multidex_version"

    implementation project(':common')

    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"

    implementation "androidx.appcompat:appcompat:$androidx_app_compat_version"
    implementation "androidx.fragment:fragment-ktx:$fragment_version"
    implementation "androidx.recyclerview:recyclerview:$recycler_view_version"

    implementation "androidx.constraintlayout:constraintlayout:$constraint_layout_version"
    implementation "androidx.lifecycle:lifecycle-extensions:$arch_lifecycle_version"

    // Glide dependencies
    implementation "com.github.bumptech.glide:glide:$glide_version"
    kapt "com.github.bumptech.glide:compiler:$glide_version"
}


================================================
FILE: app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
#   http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
#   public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile


================================================
FILE: app/src/main/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright 2017 Google Inc. All rights reserved.
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:dist="http://schemas.android.com/apk/distribution"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.example.android.uamp">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:targetSandboxVersion="2"
        android:theme="@style/AppTheme">

        <!-- Enable instant app support -->
        <dist:module dist:instant="true" />

        <!-- Declare that UAMP supports Android Auto. -->
        <meta-data
            android:name="com.google.android.gms.car.application"
            android:resource="@xml/automotive_app_desc" />

        <!-- Declare that UAMP supports Cast. -->
        <meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
            android:value="com.example.android.uamp.cast.UampCastOptionsProvider"/>

        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

            <!-- App links for http -->
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />

                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />

                <data
                    android:host="example.android.com"
                    android:pathPattern="/uamp"
                    android:scheme="http" />
            </intent-filter>

            <!-- App links for https -->
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />

                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />

                <data
                    android:host="example.android.com"
                    android:pathPattern="/uamp"
                    android:scheme="https" />
            </intent-filter>

            <intent-filter>
                <action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
            </intent-filter>
        </activity>

        <!--
        Declare the common MediaBrowserService for use in the mobile app, including
        with the Android Auto app.
        -->
        <service
            android:name=".media.MusicService"
            android:enabled="true"
            android:exported="true"
            tools:ignore="ExportedService">

            <intent-filter>
                <action android:name="android.media.browse.MediaBrowserService" />
            </intent-filter>
        </service>

    </application>

</manifest>


================================================
FILE: app/src/main/java/com/example/android/uamp/MainActivity.kt
================================================
/*
 * Copyright 2017 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.android.uamp

import android.media.AudioManager
import android.os.Bundle
import android.util.Log
import android.view.Menu
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import com.example.android.uamp.fragments.MediaItemFragment
import com.example.android.uamp.media.MusicService
import com.example.android.uamp.utils.Event
import com.example.android.uamp.utils.InjectorUtils
import com.example.android.uamp.viewmodels.MainActivityViewModel
import com.google.android.gms.cast.framework.CastButtonFactory
import com.google.android.gms.cast.framework.CastContext

class MainActivity : AppCompatActivity() {

    private val viewModel by viewModels<MainActivityViewModel> {
        InjectorUtils.provideMainActivityViewModel(this)
    }
    private var castContext: CastContext? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Initialize the Cast context. This is required so that the media route button can be
        // created in the AppBar
        castContext = CastContext.getSharedInstance(this)

        setContentView(R.layout.activity_main)

        // Since UAMP is a music player, the volume controls should adjust the music volume while
        // in the app.
        volumeControlStream = AudioManager.STREAM_MUSIC

        /**
         * Observe [MainActivityViewModel.navigateToFragment] for [Event]s that request a
         * fragment swap.
         */
        viewModel.navigateToFragment.observe(this, Observer {
            it?.getContentIfNotHandled()?.let { fragmentRequest ->
                val transaction = supportFragmentManager.beginTransaction()
                transaction.replace(
                    R.id.fragmentContainer, fragmentRequest.fragment, fragmentRequest.tag
                )
                if (fragmentRequest.backStack) transaction.addToBackStack(null)
                transaction.commit()
            }
        })

        /**
         * Observe changes to the [MainActivityViewModel.rootMediaId]. When the app starts,
         * and the UI connects to [MusicService], this will be updated and the app will show
         * the initial list of media items.
         */
        viewModel.rootMediaId.observe(this,
            Observer<String> { rootMediaId ->
                rootMediaId?.let { navigateToMediaItem(it) }
            })

        /**
         * Observe [MainActivityViewModel.navigateToMediaItem] for [Event]s indicating
         * the user has requested to browse to a different [MediaItemData].
         */
        viewModel.navigateToMediaItem.observe(this, Observer {
            it?.getContentIfNotHandled()?.let { mediaId ->
                navigateToMediaItem(mediaId)
            }
        })
    }

    @Override
    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        super.onCreateOptionsMenu(menu)
        menuInflater.inflate(R.menu.main_activity_menu, menu)

        /**
         * Set up a MediaRouteButton to allow the user to control the current media playback route
         */
        CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item)
        return true
    }

    private fun navigateToMediaItem(mediaId: String) {
        var fragment: MediaItemFragment? = getBrowseFragment(mediaId)
        if (fragment == null) {
            fragment = MediaItemFragment.newInstance(mediaId)
            // If this is not the top level media (root), we add it to the fragment
            // back stack, so that actionbar toggle and Back will work appropriately:
            viewModel.showFragment(fragment, !isRootId(mediaId), mediaId)
        }
    }

    private fun isRootId(mediaId: String) = mediaId == viewModel.rootMediaId.value

    private fun getBrowseFragment(mediaId: String): MediaItemFragment? {
        return supportFragmentManager.findFragmentByTag(mediaId) as? MediaItemFragment
    }
}


================================================
FILE: app/src/main/java/com/example/android/uamp/MediaItemAdapter.kt
================================================
/*
 * Copyright 2017 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.android.uamp

import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.example.android.uamp.MediaItemData.Companion.PLAYBACK_RES_CHANGED
import com.example.android.uamp.databinding.FragmentMediaitemBinding
import com.example.android.uamp.fragments.MediaItemFragment

/**
 * [RecyclerView.Adapter] of [MediaItemData]s used by the [MediaItemFragment].
 */
class MediaItemAdapter(
    private val itemClickedListener: (MediaItemData) -> Unit
) : ListAdapter<MediaItemData, MediaViewHolder>(MediaItemData.diffCallback) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaViewHolder {
        val inflater = LayoutInflater.from(parent.context)
        val binding = FragmentMediaitemBinding.inflate(inflater, parent, false)
        return MediaViewHolder(binding, itemClickedListener)
    }

    override fun onBindViewHolder(
        holder: MediaViewHolder,
        position: Int,
        payloads: MutableList<Any>
    ) {

        val mediaItem = getItem(position)
        var fullRefresh = payloads.isEmpty()

        if (payloads.isNotEmpty()) {
            payloads.forEach { payload ->
                when (payload) {
                    PLAYBACK_RES_CHANGED -> {
                        holder.playbackState.setImageResource(mediaItem.playbackRes)
                    }
                    // If the payload wasn't understood, refresh the full item (to be safe).
                    else -> fullRefresh = true
                }
            }
        }

        // Normally we only fully refresh the list item if it's being initially bound, but
        // we might also do it if there was a payload that wasn't understood, just to ensure
        // there isn't a stale item.
        if (fullRefresh) {
            holder.item = mediaItem
            holder.titleView.text = mediaItem.title
            holder.subtitleView.text = mediaItem.subtitle
            holder.playbackState.setImageResource(mediaItem.playbackRes)

            Glide.with(holder.albumArt)
                .load(mediaItem.albumArtUri)
                .placeholder(R.drawable.default_art)
                .into(holder.albumArt)
        }
    }

    override fun onBindViewHolder(holder: MediaViewHolder, position: Int) {
        onBindViewHolder(holder, position, mutableListOf())
    }
}

class MediaViewHolder(
    binding: FragmentMediaitemBinding,
    itemClickedListener: (MediaItemData) -> Unit
) : RecyclerView.ViewHolder(binding.root) {

    val titleView: TextView = binding.title
    val subtitleView: TextView = binding.subtitle
    val albumArt: ImageView = binding.albumArt
    val playbackState: ImageView = binding.itemState

    var item: MediaItemData? = null

    init {
        binding.root.setOnClickListener {
            item?.let { itemClickedListener(it) }
        }
    }
}


================================================
FILE: app/src/main/java/com/example/android/uamp/MediaItemData.kt
================================================
/*
 * Copyright 2018 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.android.uamp

import android.net.Uri
import android.support.v4.media.MediaBrowserCompat
import android.support.v4.media.MediaBrowserCompat.MediaItem
import androidx.recyclerview.widget.DiffUtil
import com.example.android.uamp.viewmodels.MediaItemFragmentViewModel

/**
 * Data class to encapsulate properties of a [MediaItem].
 *
 * If an item is [browsable] it means that it has a list of child media items that
 * can be retrieved by passing the mediaId to [MediaBrowserCompat.subscribe].
 *
 * Objects of this class are built from [MediaItem]s in
 * [MediaItemFragmentViewModel.subscriptionCallback].
 */
data class MediaItemData(
    val mediaId: String,
    val title: String,
    val subtitle: String,
    val albumArtUri: Uri,
    val browsable: Boolean,
    var playbackRes: Int
) {

    companion object {
        /**
         * Indicates [playbackRes] has changed.
         */
        const val PLAYBACK_RES_CHANGED = 1

        /**
         * [DiffUtil.ItemCallback] for a [MediaItemData].
         *
         * Since all [MediaItemData]s have a unique ID, it's easiest to check if two
         * items are the same by simply comparing that ID.
         *
         * To check if the contents are the same, we use the same ID, but it may be the
         * case that it's only the play state itself which has changed (from playing to
         * paused, or perhaps a different item is the active item now). In this case
         * we check both the ID and the playback resource.
         *
         * To calculate the payload, we use the simplest method possible:
         * - Since the title, subtitle, and albumArtUri are constant (with respect to mediaId),
         *   there's no reason to check if they've changed. If the mediaId is the same, none of
         *   those properties have changed.
         * - If the playback resource (playbackRes) has changed to reflect the change in playback
         *   state, that's all that needs to be updated. We return [PLAYBACK_RES_CHANGED] as
         *   the payload in this case.
         * - If something else changed, then refresh the full item for simplicity.
         */
        val diffCallback = object : DiffUtil.ItemCallback<MediaItemData>() {
            override fun areItemsTheSame(
                oldItem: MediaItemData,
                newItem: MediaItemData
            ): Boolean =
                oldItem.mediaId == newItem.mediaId

            override fun areContentsTheSame(oldItem: MediaItemData, newItem: MediaItemData) =
                oldItem.mediaId == newItem.mediaId && oldItem.playbackRes == newItem.playbackRes

            override fun getChangePayload(oldItem: MediaItemData, newItem: MediaItemData) =
                if (oldItem.playbackRes != newItem.playbackRes) {
                    PLAYBACK_RES_CHANGED
                } else null
        }
    }
}



================================================
FILE: app/src/main/java/com/example/android/uamp/cast/UampCastOptionsProvider.kt
================================================
/*
 * Copyright 2020 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.android.uamp.cast

import android.content.Context
import com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider.APP_ID_DEFAULT_RECEIVER_WITH_DRM
import com.google.android.gms.cast.framework.CastOptions
import com.google.android.gms.cast.framework.OptionsProvider
import com.google.android.gms.cast.framework.SessionProvider
import com.google.android.gms.cast.framework.media.CastMediaOptions


class UampCastOptionsProvider : OptionsProvider {

    override fun getCastOptions(context: Context?): CastOptions? {
        return CastOptions.Builder()
            // Use the Default Media Receiver with DRM support.
            .setReceiverApplicationId(APP_ID_DEFAULT_RECEIVER_WITH_DRM)
            .setCastMediaOptions(
                CastMediaOptions.Builder()
                    // We manage the media session and the notifications ourselves.
                    .setMediaSessionEnabled(false)
                    .setNotificationOptions(null)
                    .build()
            )
            .setStopReceiverApplicationWhenEndingSession(true).build()
    }

    override fun getAdditionalSessionProviders(context: Context?): List<SessionProvider?>? {
        return null
    }
}

================================================
FILE: app/src/main/java/com/example/android/uamp/fragments/MediaItemFragment.kt
================================================
/*
 * Copyright 2017 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.android.uamp.fragments

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import com.example.android.uamp.MediaItemAdapter
import com.example.android.uamp.databinding.FragmentMediaitemListBinding
import com.example.android.uamp.utils.InjectorUtils
import com.example.android.uamp.viewmodels.MainActivityViewModel
import com.example.android.uamp.viewmodels.MediaItemFragmentViewModel

/**
 * A fragment representing a list of MediaItems.
 */
class MediaItemFragment : Fragment() {
    private val mainActivityViewModel by activityViewModels<MainActivityViewModel> {
        InjectorUtils.provideMainActivityViewModel(requireContext())
    }
    private val mediaItemFragmentViewModel by viewModels<MediaItemFragmentViewModel> {
        InjectorUtils.provideMediaItemFragmentViewModel(requireContext(), mediaId)
    }

    private lateinit var mediaId: String
    private lateinit var binding: FragmentMediaitemListBinding

    private val listAdapter = MediaItemAdapter { clickedItem ->
        mainActivityViewModel.mediaItemClicked(clickedItem)
    }

    companion object {
        fun newInstance(mediaId: String): MediaItemFragment {

            return MediaItemFragment().apply {
                arguments = Bundle().apply {
                    putString(MEDIA_ID_ARG, mediaId)
                }
            }
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = FragmentMediaitemListBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)

        // Always true, but lets lint know that as well.
        mediaId = arguments?.getString(MEDIA_ID_ARG) ?: return

        mediaItemFragmentViewModel.mediaItems.observe(viewLifecycleOwner,
            Observer { list ->
                binding.loadingSpinner.visibility =
                    if (list?.isNotEmpty() == true) View.GONE else View.VISIBLE
                listAdapter.submitList(list)
            })
        mediaItemFragmentViewModel.networkError.observe(viewLifecycleOwner,
            Observer { error ->
                if (error) {
                    binding.loadingSpinner.visibility = View.GONE
                    binding.networkError.visibility = View.VISIBLE
                } else {
                    binding.networkError.visibility = View.GONE
                }
            })

        // Set the adapter
        binding.list.adapter = listAdapter
    }
}

private const val MEDIA_ID_ARG = "com.example.android.uamp.fragments.MediaItemFragment.MEDIA_ID"


================================================
FILE: app/src/main/java/com/example/android/uamp/fragments/NowPlayingFragment.kt
================================================
/*
 * Copyright 2019 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.android.uamp.fragments

import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import com.bumptech.glide.Glide
import com.example.android.uamp.R
import com.example.android.uamp.databinding.FragmentNowplayingBinding
import com.example.android.uamp.utils.InjectorUtils
import com.example.android.uamp.viewmodels.MainActivityViewModel
import com.example.android.uamp.viewmodels.NowPlayingFragmentViewModel
import com.example.android.uamp.viewmodels.NowPlayingFragmentViewModel.NowPlayingMetadata

/**
 * A fragment representing the current media item being played.
 */
class NowPlayingFragment : Fragment() {
    private val mainActivityViewModel by activityViewModels<MainActivityViewModel> {
        InjectorUtils.provideMainActivityViewModel(requireContext())
    }
    private val nowPlayingViewModel by viewModels<NowPlayingFragmentViewModel> {
        InjectorUtils.provideNowPlayingFragmentViewModel(requireContext())
    }

    lateinit var binding: FragmentNowplayingBinding

    companion object {
        fun newInstance() = NowPlayingFragment()
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = FragmentNowplayingBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // Always true, but lets lint know that as well.
        val context = activity ?: return

        // Attach observers to the LiveData coming from this ViewModel
        nowPlayingViewModel.mediaMetadata.observe(viewLifecycleOwner,
            Observer { mediaItem -> updateUI(view, mediaItem) })
        nowPlayingViewModel.mediaButtonRes.observe(viewLifecycleOwner,
            Observer { res ->
                binding.mediaButton.setImageResource(res)
            })
        nowPlayingViewModel.mediaPosition.observe(viewLifecycleOwner,
            Observer { pos ->
                binding.position.text = NowPlayingMetadata.timestampToMSS(context, pos)
            })

        // Setup UI handlers for buttons
        binding.mediaButton.setOnClickListener {
            nowPlayingViewModel.mediaMetadata.value?.let { mainActivityViewModel.playMediaId(it.id) }
        }

        // Initialize playback duration and position to zero
        binding.duration.text = NowPlayingMetadata.timestampToMSS(context, 0L)
        binding.position.text = NowPlayingMetadata.timestampToMSS(context, 0L)
    }

    /**
     * Internal function used to update all UI elements except for the current item playback
     */
    private fun updateUI(view: View, metadata: NowPlayingMetadata) = with(binding) {
        if (metadata.albumArtUri == Uri.EMPTY) {
            albumArt.setImageResource(R.drawable.ic_album_black_24dp)
        } else {
            Glide.with(view)
                .load(metadata.albumArtUri)
                .into(albumArt)
        }
        title.text = metadata.title
        subtitle.text = metadata.subtitle
        duration.text = metadata.duration
    }
}


================================================
FILE: app/src/main/java/com/example/android/uamp/utils/Event.kt
================================================
/*
 * Copyright 2018 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.android.uamp.utils

/**
 * Used as a wrapper for data that is exposed via a LiveData that represents an event.
 *
 * For more information, see:
 * https://medium.com/google-developers/livedata-with-events-ac2622673150
 */
class Event<out T>(private val content: T) {

    var hasBeenHandled = false
        private set // Allow external read but not write

    /**
     * Returns the content and prevents its use again.
     */
    fun getContentIfNotHandled(): T? {
        return if (hasBeenHandled) {
            null
        } else {
            hasBeenHandled = true
            content
        }
    }

    /**
     * Returns the content, even if it's already been handled.
     */
    fun peekContent(): T = content
}

================================================
FILE: app/src/main/java/com/example/android/uamp/utils/InjectorUtils.kt
================================================
/*
 * Copyright 2018 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.android.uamp.utils

import android.app.Application
import android.content.ComponentName
import android.content.Context
import com.example.android.uamp.common.MusicServiceConnection
import com.example.android.uamp.media.MusicService
import com.example.android.uamp.viewmodels.MainActivityViewModel
import com.example.android.uamp.viewmodels.MediaItemFragmentViewModel
import com.example.android.uamp.viewmodels.NowPlayingFragmentViewModel

/**
 * Static methods used to inject classes needed for various Activities and Fragments.
 */
object InjectorUtils {
    private fun provideMusicServiceConnection(context: Context): MusicServiceConnection {
        return MusicServiceConnection.getInstance(
            context,
            ComponentName(context, MusicService::class.java)
        )
    }

    fun provideMainActivityViewModel(context: Context): MainActivityViewModel.Factory {
        val applicationContext = context.applicationContext
        val musicServiceConnection = provideMusicServiceConnection(applicationContext)
        return MainActivityViewModel.Factory(musicServiceConnection)
    }

    fun provideMediaItemFragmentViewModel(context: Context, mediaId: String)
            : MediaItemFragmentViewModel.Factory {
        val applicationContext = context.applicationContext
        val musicServiceConnection = provideMusicServiceConnection(applicationContext)
        return MediaItemFragmentViewModel.Factory(mediaId, musicServiceConnection)
    }

    fun provideNowPlayingFragmentViewModel(context: Context)
            : NowPlayingFragmentViewModel.Factory {
        val applicationContext = context.applicationContext
        val musicServiceConnection = provideMusicServiceConnection(applicationContext)
        return NowPlayingFragmentViewModel.Factory(
            applicationContext as Application, musicServiceConnection
        )
    }
}

================================================
FILE: app/src/main/java/com/example/android/uamp/viewmodels/MainActivityViewModel.kt
================================================
/*
 * Copyright 2018 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.android.uamp.viewmodels

import android.support.v4.media.MediaBrowserCompat
import android.util.Log
import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.example.android.uamp.MainActivity
import com.example.android.uamp.MediaItemData
import com.example.android.uamp.common.MusicServiceConnection
import com.example.android.uamp.fragments.NowPlayingFragment
import com.example.android.uamp.media.extensions.id
import com.example.android.uamp.media.extensions.isPlayEnabled
import com.example.android.uamp.media.extensions.isPlaying
import com.example.android.uamp.media.extensions.isPrepared
import com.example.android.uamp.utils.Event

/**
 * Small [ViewModel] that watches a [MusicServiceConnection] to become connected
 * and provides the root/initial media ID of the underlying [MediaBrowserCompat].
 */
class MainActivityViewModel(
    private val musicServiceConnection: MusicServiceConnection
) : ViewModel() {

    val rootMediaId: LiveData<String> =
        Transformations.map(musicServiceConnection.isConnected) { isConnected ->
            if (isConnected) {
                musicServiceConnection.rootMediaId
            } else {
                null
            }
        }

    /**
     * [navigateToMediaItem] acts as an "event", rather than state. [Observer]s
     * are notified of the change as usual with [LiveData], but only one [Observer]
     * will actually read the data. For more information, check the [Event] class.
     */
    val navigateToMediaItem: LiveData<Event<String>> get() = _navigateToMediaItem
    private val _navigateToMediaItem = MutableLiveData<Event<String>>()

    /**
     * This [LiveData] object is used to notify the MainActivity that the main
     * content fragment needs to be swapped. Information about the new fragment
     * is conveniently wrapped by the [Event] class.
     */
    val navigateToFragment: LiveData<Event<FragmentNavigationRequest>> get() = _navigateToFragment
    private val _navigateToFragment = MutableLiveData<Event<FragmentNavigationRequest>>()

    /**
     * This method takes a [MediaItemData] and routes it depending on whether it's
     * browsable (i.e.: it's the parent media item of a set of other media items,
     * such as an album), or not.
     *
     * If the item is browsable, handle it by sending an event to the Activity to
     * browse to it, otherwise play it.
     */
    fun mediaItemClicked(clickedItem: MediaItemData) {
        if (clickedItem.browsable) {
            browseToItem(clickedItem)
        } else {
            playMedia(clickedItem, pauseAllowed = false)
            showFragment(NowPlayingFragment.newInstance())
        }
    }


    /**
     * Convenience method used to swap the fragment shown in the main activity
     *
     * @param fragment the fragment to show
     * @param backStack if true, add this transaction to the back stack
     * @param tag the name to use for this fragment in the stack
     */
    fun showFragment(fragment: Fragment, backStack: Boolean = true, tag: String? = null) {
        _navigateToFragment.value = Event(FragmentNavigationRequest(fragment, backStack, tag))
    }


    /**
     * This posts a browse [Event] that will be handled by the
     * observer in [MainActivity].
     */
    private fun browseToItem(mediaItem: MediaItemData) {
        _navigateToMediaItem.value = Event(mediaItem.mediaId)
    }

    /**
     * This method takes a [MediaItemData] and does one of the following:
     * - If the item is *not* the active item, then play it directly.
     * - If the item *is* the active item, check whether "pause" is a permitted command. If it is,
     *   then pause playback, otherwise send "play" to resume playback.
     */
    fun playMedia(mediaItem: MediaItemData, pauseAllowed: Boolean = true) {
        val nowPlaying = musicServiceConnection.nowPlaying.value
        val transportControls = musicServiceConnection.transportControls

        val isPrepared = musicServiceConnection.playbackState.value?.isPrepared ?: false
        if (isPrepared && mediaItem.mediaId == nowPlaying?.id) {
            musicServiceConnection.playbackState.value?.let { playbackState ->
                when {
                    playbackState.isPlaying ->
                        if (pauseAllowed) transportControls.pause() else Unit
                    playbackState.isPlayEnabled -> transportControls.play()
                    else -> {
                        Log.w(
                            TAG, "Playable item clicked but neither play nor pause are enabled!" +
                                    " (mediaId=${mediaItem.mediaId})"
                        )
                    }
                }
            }
        } else {
            transportControls.playFromMediaId(mediaItem.mediaId, null)
        }
    }

    fun playMediaId(mediaId: String) {
        val nowPlaying = musicServiceConnection.nowPlaying.value
        val transportControls = musicServiceConnection.transportControls

        val isPrepared = musicServiceConnection.playbackState.value?.isPrepared ?: false
        if (isPrepared && mediaId == nowPlaying?.id) {
            musicServiceConnection.playbackState.value?.let { playbackState ->
                when {
                    playbackState.isPlaying -> transportControls.pause()
                    playbackState.isPlayEnabled -> transportControls.play()
                    else -> {
                        Log.w(
                            TAG, "Playable item clicked but neither play nor pause are enabled!" +
                                    " (mediaId=$mediaId)"
                        )
                    }
                }
            }
        } else {
            transportControls.playFromMediaId(mediaId, null)
        }
    }

    class Factory(
        private val musicServiceConnection: MusicServiceConnection
    ) : ViewModelProvider.NewInstanceFactory() {

        @Suppress("unchecked_cast")
        override fun <T : ViewModel?> create(modelClass: Class<T>): T {
            return MainActivityViewModel(musicServiceConnection) as T
        }
    }
}

/**
 * Helper class used to pass fragment navigation requests between MainActivity
 * and its corresponding ViewModel.
 */
data class FragmentNavigationRequest(
    val fragment: Fragment,
    val backStack: Boolean = false,
    val tag: String? = null
)

private const val TAG = "MainActivitytVM"


================================================
FILE: app/src/main/java/com/example/android/uamp/viewmodels/MediaItemFragmentViewModel.kt
================================================
/*
 * Copyright 2018 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.android.uamp.viewmodels

import android.support.v4.media.MediaBrowserCompat
import android.support.v4.media.MediaBrowserCompat.MediaItem
import android.support.v4.media.MediaBrowserCompat.SubscriptionCallback
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.PlaybackStateCompat
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.example.android.uamp.MediaItemData
import com.example.android.uamp.R
import com.example.android.uamp.common.EMPTY_PLAYBACK_STATE
import com.example.android.uamp.common.MusicServiceConnection
import com.example.android.uamp.common.NOTHING_PLAYING
import com.example.android.uamp.fragments.MediaItemFragment
import com.example.android.uamp.media.extensions.id
import com.example.android.uamp.media.extensions.isPlaying

/**
 * [ViewModel] for [MediaItemFragment].
 */
class MediaItemFragmentViewModel(
    private val mediaId: String,
    musicServiceConnection: MusicServiceConnection
) : ViewModel() {

    /**
     * Use a backing property so consumers of mediaItems only get a [LiveData] instance so
     * they don't inadvertently modify it.
     */
    private val _mediaItems = MutableLiveData<List<MediaItemData>>()
    val mediaItems: LiveData<List<MediaItemData>> = _mediaItems

    /**
     * Pass the status of the [MusicServiceConnection.networkFailure] through.
     */
    val networkError = Transformations.map(musicServiceConnection.networkFailure) { it }

    private val subscriptionCallback = object : SubscriptionCallback() {
        override fun onChildrenLoaded(parentId: String, children: List<MediaItem>) {
            val itemsList = children.map { child ->
                val subtitle = child.description.subtitle ?: ""
                MediaItemData(
                    child.mediaId!!,
                    child.description.title.toString(),
                    subtitle.toString(),
                    child.description.iconUri!!,
                    child.isBrowsable,
                    getResourceForMediaId(child.mediaId!!)
                )
            }
            _mediaItems.postValue(itemsList)
        }
    }

    /**
     * When the session's [PlaybackStateCompat] changes, the [mediaItems] need to be updated
     * so the correct [MediaItemData.playbackRes] is displayed on the active item.
     * (i.e.: play/pause button or blank)
     */
    private val playbackStateObserver = Observer<PlaybackStateCompat> {
        val playbackState = it ?: EMPTY_PLAYBACK_STATE
        val metadata = musicServiceConnection.nowPlaying.value ?: NOTHING_PLAYING
        if (metadata.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID) != null) {
            _mediaItems.postValue(updateState(playbackState, metadata))
        }
    }

    /**
     * When the session's [MediaMetadataCompat] changes, the [mediaItems] need to be updated
     * as it means the currently active item has changed. As a result, the new, and potentially
     * old item (if there was one), both need to have their [MediaItemData.playbackRes]
     * changed. (i.e.: play/pause button or blank)
     */
    private val mediaMetadataObserver = Observer<MediaMetadataCompat> {
        val playbackState = musicServiceConnection.playbackState.value ?: EMPTY_PLAYBACK_STATE
        val metadata = it ?: NOTHING_PLAYING
        if (metadata.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID) != null) {
            _mediaItems.postValue(updateState(playbackState, metadata))
        }
    }

    /**
     * Because there's a complex dance between this [ViewModel] and the [MusicServiceConnection]
     * (which is wrapping a [MediaBrowserCompat] object), the usual guidance of using
     * [Transformations] doesn't quite work.
     *
     * Specifically there's three things that are watched that will cause the single piece of
     * [LiveData] exposed from this class to be updated.
     *
     * [subscriptionCallback] (defined above) is called if/when the children of this
     * ViewModel's [mediaId] changes.
     *
     * [MusicServiceConnection.playbackState] changes state based on the playback state of
     * the player, which can change the [MediaItemData.playbackRes]s in the list.
     *
     * [MusicServiceConnection.nowPlaying] changes based on the item that's being played,
     * which can also change the [MediaItemData.playbackRes]s in the list.
     */
    private val musicServiceConnection = musicServiceConnection.also {
        it.subscribe(mediaId, subscriptionCallback)

        it.playbackState.observeForever(playbackStateObserver)
        it.nowPlaying.observeForever(mediaMetadataObserver)
    }

    /**
     * Since we use [LiveData.observeForever] above (in [musicServiceConnection]), we want
     * to call [LiveData.removeObserver] here to prevent leaking resources when the [ViewModel]
     * is not longer in use.
     *
     * For more details, see the kdoc on [musicServiceConnection] above.
     */
    override fun onCleared() {
        super.onCleared()

        // Remove the permanent observers from the MusicServiceConnection.
        musicServiceConnection.playbackState.removeObserver(playbackStateObserver)
        musicServiceConnection.nowPlaying.removeObserver(mediaMetadataObserver)

        // And then, finally, unsubscribe the media ID that was being watched.
        musicServiceConnection.unsubscribe(mediaId, subscriptionCallback)
    }

    private fun getResourceForMediaId(mediaId: String): Int {
        val isActive = mediaId == musicServiceConnection.nowPlaying.value?.id
        val isPlaying = musicServiceConnection.playbackState.value?.isPlaying ?: false
        return when {
            !isActive -> NO_RES
            isPlaying -> R.drawable.ic_pause_black_24dp
            else -> R.drawable.ic_play_arrow_black_24dp
        }
    }

    private fun updateState(
        playbackState: PlaybackStateCompat,
        mediaMetadata: MediaMetadataCompat
    ): List<MediaItemData> {

        val newResId = when (playbackState.isPlaying) {
            true -> R.drawable.ic_pause_black_24dp
            else -> R.drawable.ic_play_arrow_black_24dp
        }

        return mediaItems.value?.map {
            val useResId = if (it.mediaId == mediaMetadata.id) newResId else NO_RES
            it.copy(playbackRes = useResId)
        } ?: emptyList()
    }

    class Factory(
        private val mediaId: String,
        private val musicServiceConnection: MusicServiceConnection
    ) : ViewModelProvider.NewInstanceFactory() {

        @Suppress("unchecked_cast")
        override fun <T : ViewModel?> create(modelClass: Class<T>): T {
            return MediaItemFragmentViewModel(mediaId, musicServiceConnection) as T
        }
    }
}

private const val TAG = "MediaItemFragmentVM"
private const val NO_RES = 0


================================================
FILE: app/src/main/java/com/example/android/uamp/viewmodels/NowPlayingFragmentViewModel.kt
================================================
/*
 * Copyright 2019 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.android.uamp.viewmodels

import android.app.Application
import android.content.Context
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.support.v4.media.MediaBrowserCompat
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.PlaybackStateCompat
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.example.android.uamp.R
import com.example.android.uamp.common.EMPTY_PLAYBACK_STATE
import com.example.android.uamp.common.MusicServiceConnection
import com.example.android.uamp.common.NOTHING_PLAYING
import com.example.android.uamp.fragments.NowPlayingFragment
import com.example.android.uamp.media.extensions.albumArtUri
import com.example.android.uamp.media.extensions.currentPlayBackPosition
import com.example.android.uamp.media.extensions.displaySubtitle
import com.example.android.uamp.media.extensions.duration
import com.example.android.uamp.media.extensions.id
import com.example.android.uamp.media.extensions.isPlaying
import com.example.android.uamp.media.extensions.title

/**
 * [ViewModel] for [NowPlayingFragment] which displays the album art in full size.
 * It extends AndroidViewModel and uses the [Application]'s context to be able to reference string
 * resources.
 */
class NowPlayingFragmentViewModel(
    private val app: Application,
    musicServiceConnection: MusicServiceConnection
) : AndroidViewModel(app) {

    /**
     * Utility class used to represent the metadata necessary to display the
     * media item currently being played.
     */
    data class NowPlayingMetadata(
        val id: String,
        val albumArtUri: Uri,
        val title: String?,
        val subtitle: String?,
        val duration: String
    ) {

        companion object {
            /**
             * Utility method to convert milliseconds to a display of minutes and seconds
             */
            fun timestampToMSS(context: Context, position: Long): String {
                val totalSeconds = Math.floor(position / 1E3).toInt()
                val minutes = totalSeconds / 60
                val remainingSeconds = totalSeconds - (minutes * 60)
                return if (position < 0) context.getString(R.string.duration_unknown)
                else context.getString(R.string.duration_format).format(minutes, remainingSeconds)
            }
        }
    }

    private var playbackState: PlaybackStateCompat = EMPTY_PLAYBACK_STATE
    val mediaMetadata = MutableLiveData<NowPlayingMetadata>()
    val mediaPosition = MutableLiveData<Long>().apply {
        postValue(0L)
    }
    val mediaButtonRes = MutableLiveData<Int>().apply {
        postValue(R.drawable.ic_album_black_24dp)
    }

    private var updatePosition = true
    private val handler = Handler(Looper.getMainLooper())

    /**
     * When the session's [PlaybackStateCompat] changes, the [mediaItems] need to be updated
     * so the correct [MediaItemData.playbackRes] is displayed on the active item.
     * (i.e.: play/pause button or blank)
     */
    private val playbackStateObserver = Observer<PlaybackStateCompat> {
        playbackState = it ?: EMPTY_PLAYBACK_STATE
        val metadata = musicServiceConnection.nowPlaying.value ?: NOTHING_PLAYING
        updateState(playbackState, metadata)
    }

    /**
     * When the session's [MediaMetadataCompat] changes, the [mediaItems] need to be updated
     * as it means the currently active item has changed. As a result, the new, and potentially
     * old item (if there was one), both need to have their [MediaItemData.playbackRes]
     * changed. (i.e.: play/pause button or blank)
     */
    private val mediaMetadataObserver = Observer<MediaMetadataCompat> {
        updateState(playbackState, it)
    }

    /**
     * Because there's a complex dance between this [ViewModel] and the [MusicServiceConnection]
     * (which is wrapping a [MediaBrowserCompat] object), the usual guidance of using
     * [Transformations] doesn't quite work.
     *
     * Specifically there's three things that are watched that will cause the single piece of
     * [LiveData] exposed from this class to be updated.
     *
     * [MusicServiceConnection.playbackState] changes state based on the playback state of
     * the player, which can change the [MediaItemData.playbackRes]s in the list.
     *
     * [MusicServiceConnection.nowPlaying] changes based on the item that's being played,
     * which can also change the [MediaItemData.playbackRes]s in the list.
     */
    private val musicServiceConnection = musicServiceConnection.also {
        it.playbackState.observeForever(playbackStateObserver)
        it.nowPlaying.observeForever(mediaMetadataObserver)
        checkPlaybackPosition()
    }

    /**
     * Internal function that recursively calls itself every [POSITION_UPDATE_INTERVAL_MILLIS] ms
     * to check the current playback position and updates the corresponding LiveData object when it
     * has changed.
     */
    private fun checkPlaybackPosition(): Boolean = handler.postDelayed({
        val currPosition = playbackState.currentPlayBackPosition
        if (mediaPosition.value != currPosition)
            mediaPosition.postValue(currPosition)
        if (updatePosition)
            checkPlaybackPosition()
    }, POSITION_UPDATE_INTERVAL_MILLIS)

    /**
     * Since we use [LiveData.observeForever] above (in [musicServiceConnection]), we want
     * to call [LiveData.removeObserver] here to prevent leaking resources when the [ViewModel]
     * is not longer in use.
     *
     * For more details, see the kdoc on [musicServiceConnection] above.
     */
    override fun onCleared() {
        super.onCleared()

        // Remove the permanent observers from the MusicServiceConnection.
        musicServiceConnection.playbackState.removeObserver(playbackStateObserver)
        musicServiceConnection.nowPlaying.removeObserver(mediaMetadataObserver)

        // Stop updating the position
        updatePosition = false
    }

    private fun updateState(
        playbackState: PlaybackStateCompat,
        mediaMetadata: MediaMetadataCompat
    ) {

        // Only update media item once we have duration available
        if (mediaMetadata.duration != 0L && mediaMetadata.id != null) {
            val nowPlayingMetadata = NowPlayingMetadata(
                mediaMetadata.id!!,
                mediaMetadata.albumArtUri,
                mediaMetadata.title?.trim(),
                mediaMetadata.displaySubtitle?.trim(),
                NowPlayingMetadata.timestampToMSS(app, mediaMetadata.duration)
            )
            this.mediaMetadata.postValue(nowPlayingMetadata)
        }

        // Update the media button resource ID
        mediaButtonRes.postValue(
            when (playbackState.isPlaying) {
                true -> R.drawable.ic_pause_black_24dp
                else -> R.drawable.ic_play_arrow_black_24dp
            }
        )
    }

    class Factory(
        private val app: Application,
        private val musicServiceConnection: MusicServiceConnection
    ) : ViewModelProvider.NewInstanceFactory() {

        @Suppress("unchecked_cast")
        override fun <T : ViewModel?> create(modelClass: Class<T>): T {
            return NowPlayingFragmentViewModel(app, musicServiceConnection) as T
        }
    }
}

private const val TAG = "NowPlayingFragmentVM"
private const val POSITION_UPDATE_INTERVAL_MILLIS = 100L


================================================
FILE: app/src/main/res/drawable/ic_album_black_24dp.xml
================================================
<!--
  ~ Copyright 2017 Google Inc. All rights reserved.
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="36dp"
    android:height="36dp"
    android:viewportWidth="24.0"
    android:viewportHeight="24.0">
    <path
        android:fillColor="#FF000000"
        android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,16.5c-2.49,0 -4.5,-2.01 -4.5,-4.5S9.51,7.5 12,7.5s4.5,2.01 4.5,4.5 -2.01,4.5 -4.5,4.5zM12,11c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1z" />
</vector>


================================================
FILE: app/src/main/res/drawable/ic_launcher_background.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright 2018 Google Inc. All rights reserved.
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="108dp"
    android:height="108dp"
    android:viewportWidth="108"
    android:viewportHeight="108">
    <path
        android:fillColor="#26A69A"
        android:pathData="M0,0h108v108h-108z" />
    <path
        android:fillColor="#00000000"
        android:pathData="M9,0L9,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,0L19,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M29,0L29,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M39,0L39,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M49,0L49,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M59,0L59,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M69,0L69,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M79,0L79,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M89,0L89,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M99,0L99,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,9L108,9"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,19L108,19"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,29L108,29"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,39L108,39"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,49L108,49"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,59L108,59"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,69L108,69"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,79L108,79"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,89L108,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,99L108,99"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,29L89,29"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,39L89,39"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,49L89,49"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,59L89,59"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,69L89,69"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,79L89,79"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M29,19L29,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M39,19L39,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M49,19L49,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M59,19L59,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M69,19L69,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M79,19L79,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
</vector>


================================================
FILE: app/src/main/res/drawable/ic_pause_black_24dp.xml
================================================
<!--
  ~ Copyright 2017 Google Inc. All rights reserved.
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="24.0"
    android:viewportHeight="24.0">
    <path
        android:fillColor="#FF000000"
        android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z" />
</vector>


================================================
FILE: app/src/main/res/drawable/ic_play_arrow_black_24dp.xml
================================================
<!--
  ~ Copyright 2017 Google Inc. All rights reserved.
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="24.0"
    android:viewportHeight="24.0">
    <path
        android:fillColor="#FF000000"
        android:pathData="M8,5v14l11,-7z" />
</vector>


================================================
FILE: app/src/main/res/drawable/ic_signal_wifi_off_black_24dp.xml
================================================
<!--
  ~ Copyright 2018 Google Inc. All rights reserved.
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="24.0"
    android:viewportHeight="24.0">
    <path
        android:fillColor="#FF000000"
        android:pathData="M23.64,7c-0.45,-0.34 -4.93,-4 -11.64,-4 -1.5,0 -2.89,0.19 -4.15,0.48L18.18,13.8 23.64,7zM17.04,15.22L3.27,1.44 2,2.72l2.05,2.06C1.91,5.76 0.59,6.82 0.36,7l11.63,14.49 0.01,0.01 0.01,-0.01 3.9,-4.86 3.32,3.32 1.27,-1.27 -3.46,-3.46z" />
</vector>


================================================
FILE: app/src/main/res/drawable/media_item_background.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright 2018 Google Inc. All rights reserved.
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:bottom="@dimen/media_item_negative_half_radius"
        android:left="@dimen/media_item_negative_half_radius"
        android:right="@dimen/media_item_negative_half_radius"
        android:top="@dimen/media_item_negative_half_radius">
        <shape android:shape="rectangle">
            <stroke
                android:width="@dimen/media_item_half_radius"
                android:color="@color/mediaListBackground" />
            <corners android:radius="@dimen/media_item_radius" />
            <solid android:color="#ffffff" />
        </shape>
    </item>
</layer-list>

================================================
FILE: app/src/main/res/drawable/media_item_mask.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright 2018 Google Inc. All rights reserved.
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:bottom="@dimen/media_item_negative_half_radius"
        android:left="@dimen/media_item_negative_half_radius"
        android:right="@dimen/media_item_negative_half_radius"
        android:top="@dimen/media_item_negative_half_radius">
        <shape android:shape="rectangle">
            <stroke
                android:width="@dimen/media_item_half_radius"
                android:color="@color/mediaListBackground" />
            <corners
                android:bottomLeftRadius="@dimen/media_item_radius"
                android:bottomRightRadius="0dp"
                android:topLeftRadius="@dimen/media_item_radius"
                android:topRightRadius="0dp" />
        </shape>
    </item>
</layer-list>

================================================
FILE: app/src/main/res/drawable/media_overlay_background.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright 2019 Google Inc. All rights reserved.
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <gradient
        android:angle="90"
        android:centerColor="#AAFFFFFF"
        android:endColor="#00FFFFFF"
        android:startColor="#CCFFFFFF" />
</shape>


================================================
FILE: app/src/main/res/drawable-v24/ic_launcher_foreground.xml
================================================
<!--
  ~ Copyright 2017 Google Inc. All rights reserved.
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:aapt="http://schemas.android.com/aapt"
    android:width="108dp"
    android:height="108dp"
    android:viewportWidth="108"
    android:viewportHeight="108">
    <path
        android:fillType="evenOdd"
        android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
        android:strokeWidth="1"
        android:strokeColor="#00000000">
        <aapt:attr name="android:fillColor">
            <gradient
                android:endX="78.5885"
                android:endY="90.9159"
                android:startX="48.7653"
                android:startY="61.0927"
                android:type="linear">
                <item
                    android:color="#44000000"
                    android:offset="0.0" />
                <item
                    android:color="#00000000"
                    android:offset="1.0" />
            </gradient>
        </aapt:attr>
    </path>
    <path
        android:fillColor="#FFFFFF"
        android:fillType="nonZero"
        android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
        android:strokeWidth="1"
        android:strokeColor="#00000000" />
</vector>


================================================
FILE: app/src/main/res/layout/activity_main.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright 2017 Google Inc. All rights reserved.
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/fragmentContainer"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.android.uamp.MainActivity" />

================================================
FILE: app/src/main/res/layout/cast_context_error.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2018 The Android Open Source Project

     Licensed under the Apache License, Version 2.0 (the "License");
     you may not use this file except in compliance with the License.
     You may obtain a copy of the License at

          http://www.apache.org/licenses/LICENSE-2.0

     Unless required by applicable law or agreed to in writing, software
     distributed under the License is distributed on an "AS IS" BASIS,
     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     See the License for the specific language governing permissions and
     limitations under the License.
-->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/textView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:textSize="20sp"
    android:text="@string/cast_context_error"/>


================================================
FILE: app/src/main/res/layout/fragment_mediaitem.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright 2017 Google Inc. All rights reserved.
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="2dp"
    android:background="@drawable/media_item_background">

    <ImageView
        android:id="@+id/albumArt"
        android:layout_width="@dimen/media_item_art"
        android:layout_height="@dimen/media_item_art"
        android:scaleType="centerCrop"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:srcCompat="@drawable/ic_album_black_24dp"
        tools:ignore="ContentDescription" />

    <ImageView
        android:id="@+id/item_state"
        style="@style/MediaStateIcon"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:scaleType="center"
        app:layout_constraintBottom_toBottomOf="@+id/albumArt"
        app:layout_constraintLeft_toLeftOf="@+id/albumArt"
        app:layout_constraintRight_toRightOf="@+id/albumArt"
        app:layout_constraintTop_toTopOf="@+id/albumArt"
        tools:ignore="ContentDescription" />

    <ImageView
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:src="@drawable/media_item_mask"
        app:layout_constraintBottom_toBottomOf="@+id/albumArt"
        app:layout_constraintLeft_toLeftOf="@+id/albumArt"
        app:layout_constraintRight_toRightOf="@+id/albumArt"
        app:layout_constraintTop_toTopOf="@+id/albumArt"
        tools:ignore="ContentDescription" />

    <TextView
        android:id="@+id/title"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="@dimen/text_margin"
        android:layout_marginEnd="@dimen/text_margin"
        android:ellipsize="end"
        android:maxLines="1"
        android:textAppearance="@style/TextAppearance.Uamp.Title"
        app:layout_constraintBottom_toTopOf="@+id/center_guideline"
        app:layout_constraintLeft_toRightOf="@+id/albumArt"
        app:layout_constraintRight_toRightOf="parent"
        tools:text="Song Title" />

    <TextView
        android:id="@+id/subtitle"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginEnd="@dimen/text_margin"
        android:ellipsize="end"
        android:maxLines="1"
        android:textAppearance="@style/TextAppearance.Uamp.Subtitle"
        app:layout_constraintLeft_toLeftOf="@+id/title"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/center_guideline"
        tools:text="Artist" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/center_guideline"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_percent="0.5" />

</androidx.constraintlayout.widget.ConstraintLayout>


================================================
FILE: app/src/main/res/layout/fragment_mediaitem_list.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright 2017 Google Inc. All rights reserved.
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/list"
        android:name="com.example.android.uamp.MediaItemFragment"
        style="@style/MediaItemList"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        tools:context="com.example.android.uamp.fragments.MediaItemFragment"
        tools:listitem="@layout/fragment_mediaitem" />

    <ProgressBar
        android:id="@+id/loadingSpinner"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center" />

    <ImageView
        android:id="@+id/networkError"
        android:layout_width="64dp"
        android:layout_height="64dp"
        android:layout_gravity="center"
        android:tint="@color/colorAccent"
        android:visibility="gone"
        app:srcCompat="@drawable/ic_signal_wifi_off_black_24dp" />

</FrameLayout>



================================================
FILE: app/src/main/res/layout/fragment_nowplaying.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright 2019 Google Inc. All rights reserved.
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_margin="2dp"
    android:background="@drawable/media_item_background">

    <ImageView
        android:id="@+id/albumArt"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:contentDescription="@string/album_art_alt"
        android:scaleType="centerCrop"
        app:srcCompat="@android:color/transparent" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/divider"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_end="64dp" />

    <View
        android:layout_width="match_parent"
        android:layout_height="96dp"
        android:layout_marginTop="-24dp"
        android:background="@drawable/media_overlay_background"
        app:layout_constraintBottom_toBottomOf="@+id/albumArt"
        app:layout_constraintTop_toBottomOf="@id/divider" />

    <ImageButton
        android:id="@+id/media_button"
        android:layout_width="@dimen/exo_media_button_width"
        android:layout_height="@dimen/exo_media_button_height"
        android:background="?attr/selectableItemBackground"
        android:scaleType="centerInside"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toBottomOf="@id/divider"
        app:srcCompat="@drawable/ic_play_arrow_black_24dp"
        tools:ignore="ContentDescription" />

    <TextView
        android:id="@+id/title"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="@dimen/text_margin"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="@dimen/text_margin"
        android:ellipsize="end"
        android:maxLines="1"
        android:textAppearance="@style/TextAppearance.Uamp.Title"
        app:layout_constraintLeft_toRightOf="@id/media_button"
        app:layout_constraintRight_toLeftOf="@id/position"
        app:layout_constraintTop_toBottomOf="@id/divider"
        tools:text="Song Title" />

    <TextView
        android:id="@+id/subtitle"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="@dimen/text_margin"
        android:layout_marginEnd="@dimen/text_margin"
        android:ellipsize="end"
        android:maxLines="1"
        android:textAppearance="@style/TextAppearance.Uamp.Subtitle"
        app:layout_constraintLeft_toRightOf="@id/media_button"
        app:layout_constraintRight_toLeftOf="@id/position"
        app:layout_constraintTop_toBottomOf="@+id/title"
        tools:text="Artist" />

    <TextView
        android:id="@+id/position"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="@dimen/text_margin"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="@dimen/text_margin"
        android:ellipsize="end"
        android:maxLines="1"
        android:textAppearance="@style/TextAppearance.Uamp.Title"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/divider"
        tools:text="0:00" />

    <TextView
        android:id="@+id/duration"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="@dimen/text_margin"
        android:layout_marginEnd="@dimen/text_margin"
        android:ellipsize="end"
        android:maxLines="1"
        android:textAppearance="@style/TextAppearance.Uamp.Subtitle"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/position"
        tools:text="0:00" />

</androidx.constraintlayout.widget.ConstraintLayout>


================================================
FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright 2018 Google Inc. All rights reserved.
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->

<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
    <background android:drawable="@drawable/ic_launcher_background" />
    <foreground android:drawable="@mipmap/ic_launcher_foreground" />
</adaptive-icon>

================================================
FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright 2018 Google Inc. All rights reserved.
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->

<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
    <background android:drawable="@drawable/ic_launcher_background" />
    <foreground android:drawable="@mipmap/ic_launcher_foreground" />
</adaptive-icon>

================================================
FILE: app/src/main/res/values/colors.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright 2017 Google Inc. All rights reserved.
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->
<resources>
    <color name="colorPrimary">#840255</color>
    <color name="colorPrimaryDark">#710144</color>
    <color name="colorAccent">#14A5A1</color>

    <color name="transparent">#00000000</color>

    <color name="mediaListBackground">#F1F1F1</color>

    <color name="mediaControlColor">#f8f8f8</color>

    <color name="nowPlayingBackground">#eff0f0</color>
    <color name="nowPlayingWhiteBackground">#fdfdfd</color>

    <color name="menu_icon_tint">#ffffff</color>
</resources>


================================================
FILE: app/src/main/res/values/dimens.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright 2017 Google Inc. All rights reserved.
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->
<resources>
    <dimen name="text_margin">16dp</dimen>

    <dimen name="media_item_height">72dp</dimen>
    <dimen name="media_item_control_margin">4dp</dimen>
    <dimen name="media_item_icon_margin_start">12dp</dimen>
    <dimen name="media_item_text_margin_start">4dp</dimen>

    <dimen name="media_item_art">72dp</dimen>

    <dimen name="media_item_radius">4dp</dimen>
    <dimen name="media_item_half_radius">2dp</dimen>
    <dimen name="media_item_negative_half_radius">-2dp</dimen>

    <dimen name="now_playing_album_art_margin">72dp</dimen>
    <dimen name="now_playing_skip_top_margin">42dp</dimen>
    <dimen name="now_playing_skip_margin">8dp</dimen>
    <dimen name="now_playing_play_pause_top_margin">42dp</dimen>
    <dimen name="now_playing_play_pause_bottom_margin">52dp</dimen>

    <dimen name="now_playing_title_top_margin">32dp</dimen>
    <dimen name="now_playing_title_bottom_margin">8dp</dimen>
    <dimen name="now_playing_artist_bottom_margin">32dp</dimen>
    <dimen name="now_playing_text_padding">16dp</dimen>

    <dimen name="now_playing_title_size">32sp</dimen>
    <dimen name="now_playing_artist_size">18sp</dimen>
</resources>


================================================
FILE: app/src/main/res/values/strings.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright 2017 Google Inc. All rights reserved.
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->
<resources>
    <string name="app_name">UAMP</string>

    <string name="skip_back_10s">Skip back 10s</string>
    <string name="skip_forward_10s">Skip forward 10s</string>
    <string name="play">Play</string>
    <string name="pause">Pause</string>

    <string name="menu_music_queue">Queue</string>
    <string name="album_art_alt">Album art</string>
    <string name="duration_unknown">--:--</string>
    <string name="duration_format">%d:%02d</string>

    <string name="cast_context_error">Failed to get Cast context. Try updating Google Play Services and restart the app.</string>
</resources>


================================================
FILE: app/src/main/res/values/styles.xml
================================================
<!--
  ~ Copyright 2017 Google Inc. All rights reserved.
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->
<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

    <style name="TextAppearance.Uamp.Title" parent="@style/TextAppearance.AppCompat.Title">
        <item name="android:textColor">?android:textColorSecondary</item>
    </style>

    <style name="TextAppearance.Uamp.Subtitle" parent="@style/TextAppearance.AppCompat.Small">
        <item name="android:textColor">?android:textColorSecondary</item>
    </style>

    <style name="MediaStateIcon">
        <item name="tint">@color/mediaControlColor</item>
        <item name="android:layout_margin">@dimen/media_item_control_margin</item>
    </style>

    <style name="MediaItemList">
        <item name="android:background">@color/mediaListBackground</item>
        <item name="android:paddingBottom">2dp</item>
        <!-- In order to account for optical illusions,
        make the end padding a bit larger so it visually looks the same. -->
        <item name="android:paddingEnd">3dp</item>
        <item name="android:paddingStart">2dp</item>
        <item name="android:paddingTop">2dp</item>
    </style>

</resources>


================================================
FILE: app/src/main/res/xml/automotive_app_desc.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright 2018 Google Inc. All rights reserved.
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->
<automotiveApp>
    <!--
      ~ This lets Android Auto know that UAMP can act as a media app.
      ~ See: https://developer.android.com/training/auto/audio/ for more info.
      -->
    <uses name="media" />
</automotiveApp>

================================================
FILE: automotive/build.gradle
================================================
/*
 * Copyright 2019 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion rootProject.compileSdkVersion

    defaultConfig {
        applicationId "com.example.android.uamp.next"

        minSdkVersion 21
        targetSdkVersion rootProject.targetSdkVersion

        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildFeatures {
        viewBinding true
    }

    compileOptions {
        sourceCompatibility 1.8
        targetCompatibility 1.8
    }
    kotlinOptions {
        jvmTarget = "1.8"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

}

dependencies {
    implementation project(':common')

    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"

    implementation "androidx.core:core-ktx:$androidx_core_ktx_version"
    implementation "androidx.preference:preference:$androidx_preference_version"
    implementation "androidx.car:car:$androidx_car_version"
    implementation "androidx.constraintlayout:constraintlayout:$constraint_layout_version"
    implementation "androidx.appcompat:appcompat:$androidx_app_compat_version"
    implementation "androidx.lifecycle:lifecycle-extensions:$arch_lifecycle_version"

    implementation "com.google.android.gms:play-services-auth:$play_services_auth_version"

    testImplementation "junit:junit:$junit_version"

    androidTestImplementation "androidx.test:runner:$androidx_test_runner_version"
    androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version"
}


================================================
FILE: automotive/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
#   http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
#   public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile


================================================
FILE: automotive/src/androidTest/java/com/example/android/uamp/automotive/ExampleInstrumentedTest.java
================================================
/*
 * Copyright 2019 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.android.uamp.automotive;

import android.content.Context;

import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;

import org.junit.Test;
import org.junit.runner.RunWith;

import static org.junit.Assert.assertEquals;

/**
 * Instrumented test, which will execute on an Android device.
 *
 * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
 */
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
    @Test
    public void useAppContext() {
        // Context of the app under test.
        Context appContext = InstrumentationRegistry.getTargetContext();

        assertEquals("com.example.automotive", appContext.getPackageName());
    }
}


================================================
FILE: automotive/src/main/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?><!--
  ~ Copyright 2019 Google LLC
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~     https://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.example.android.uamp.automotive">

    <!--
    Since this module contains code exclusive to Android Automotive, require the feature here.

    If you were mixing projected and automotive code, then the feature should not be marked
    as required.
    -->
    <uses-feature
        android:name="android.hardware.type.automotive"
        android:required="true" />

    <uses-sdk tools:overrideLibrary="androidx.car" />

    <uses-feature
        android:name="android.hardware.wifi"
        android:required="false" />
    <uses-feature
        android:name="android.hardware.screen.portrait"
        android:required="false" />
    <uses-feature
        android:name="android.hardware.screen.landscape"
        android:required="false" />

    <application
        android:allowBackup="true"
        android:appCategory="audio"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme"
        tools:ignore="GoogleAppIndexingWarning">

        <meta-data
            android:name="com.android.automotive"
            android:resource="@xml/automotive_app_desc" />

        <activity
            android:name=".SignInActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.ACTION_SIGN_IN" />
            </intent-filter>
        </activity>

        <!-- Car compatible theme must use minSDK24 -->
        <activity
            android:name=".SettingsActivity"
            android:label="@string/settings_label"
            android:theme="@style/AppTheme.Drawer">
            <intent-filter>
                <action android:name="android.intent.action.APPLICATION_PREFERENCES" />
            </intent-filter>
        </activity>

        <!--
        Declare the MediaBrowserService which supports Android Automotive specific
        extensions.
        -->
        <service
            android:name=".AutomotiveMusicService"
            android:enabled="true"
            android:exported="true"
            tools:ignore="ExportedService">

            <intent-filter>
                <action android:name="android.media.browse.MediaBrowserService" />
            </intent-filter>
        </service>
    </application>
</manifest>


================================================
FILE: automotive/src/main/java/com/example/android/uamp/automotive/AutomotiveMusicService.kt
================================================
/*
 * Copyright 2019 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.android.uamp.automotive

import android.accounts.AccountManager
import android.app.Activity
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.ResultReceiver
import android.support.v4.media.session.PlaybackStateCompat
import android.util.Log
import androidx.core.content.edit
import com.example.android.uamp.media.MusicService
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
import kotlinx.coroutines.ExperimentalCoroutinesApi

/** UAMP specific command for logging into the service. */
const val LOGIN = "com.example.android.uamp.automotive.COMMAND.LOGIN"

/** UAMP specific command for logging out of the service. */
const val LOGOUT = "com.example.android.uamp.automotive.COMMAND.LOGOUT"

const val LOGIN_EMAIL = "com.example.android.uamp.automotive.ARGS.LOGIN_EMAIL"
const val LOGIN_PASSWORD = "com.example.android.uamp.automotive.ARGS.LOGIN_PASSWORD"

typealias CommandHandler = (parameters: Bundle, callback: ResultReceiver?) -> Boolean

/**
 * Android Automotive specific extensions for [MusicService].
 *
 * UAMP for Android Automotive OS requires the user to login in order to demonstrate
 * how authentication works on the system. If this doesn't apply to your application,
 * this class can be skipped in favor of its parent, [MusicService].
 *
 * If you'd like to support authentication, but not prevent using the system,
 * comment out the calls to [requireLogin].
 */
class AutomotiveMusicService : MusicService() {

    @ExperimentalCoroutinesApi
    override fun onCreate() {
        super.onCreate()

        // Register to handle login/logout commands.
        mediaSessionConnector.registerCustomCommandReceiver(AutomotiveCommandReceiver())

        // Require the user to be logged in for demonstration purposes.
        if (!isAuthenticated()) {
            requireLogin()
        }
    }

    private fun onLogin(email: String, password: String): Boolean {
        Log.i(TAG, "User logged in: $email")
        getSharedPreferences(AutomotiveMusicService::class.java.name, Context.MODE_PRIVATE).edit {
            putString(USER_TOKEN, "$email:${password.hashCode()}")
        }
        return true
    }

    private fun onLogout(): Boolean {
        Log.i(TAG, "User logged out")
        getSharedPreferences(AutomotiveMusicService::class.java.name, Context.MODE_PRIVATE).edit {
            remove(USER_TOKEN)
        }
        return false
    }

    /**
     * Verifies if the user has logged into the system.
     * In a real system, credentials should probably be handled by the
     * [AccountManager] APIs.
     */
    private fun isAuthenticated() =
        getSharedPreferences(AutomotiveMusicService::class.java.name, Context.MODE_PRIVATE)
            .contains(USER_TOKEN)

    /**
     * Sets [PlaybackStateCompat] values to indicate the user must login to continue.
     *
     * This routine sets the playback state and provides the resolution [PendingIntent]
     * that Android Automotive OS requires.
     */
    private fun requireLogin() {
        val loginIntent = Intent(this, SignInActivity::class.java)
        val loginActivityPendingIntent = PendingIntent.getActivity(this, 0, loginIntent, 0)
        val extras = Bundle().apply {
            putString(ERROR_RESOLUTION_ACTION_LABEL, getString(R.string.error_login_button))
            putParcelable(ERROR_RESOLUTION_ACTION_INTENT, loginActivityPendingIntent)
        }
        mediaSessionConnector.setCustomErrorMessage(
            getString(R.string.error_require_login),
            PlaybackStateCompat.ERROR_CODE_AUTHENTICATION_EXPIRED,
            extras
        )
    }

    /**
     * This is the entry point for custom commands received by ExoPlayer's
     * [MediaSessionConnector.customCommandReceivers].
     *
     * The extension will call each [CommandReceiver] in turn. If the [CommandReceiver] can
     * handle the command, it returns `true` to indicate the command's been handled and
     * processing should stop. If the [CommandReceiver] cannot/doesn't want to handle the
     * command, it should return `false`.
     *
     * We simplify this a bit by having our own [CommandHandler] that works with a single
     * command (either "log in" or "log out"). Each of these returns true at the end of its
     * processing.
     *
     * If the command received isn't either of our commands, we just return `false`.
     *
     * Suppress the warning because the original name, `cb` is not as clear as to its purpose.
     */
    private inner class AutomotiveCommandReceiver : MediaSessionConnector.CommandReceiver {
        @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE")
        override fun onCommand(
            player: Player,
            command: String,
            extras: Bundle?,
            callback: ResultReceiver?
        ): Boolean =
            when (command) {
                LOGIN -> loginCommand(extras ?: Bundle.EMPTY, callback)
                LOGOUT -> logoutCommand(extras ?: Bundle.EMPTY, callback)
                else -> false
            }
    }

    private val loginCommand: CommandHandler = { extras, callback ->
        val email = extras.getString(LOGIN_EMAIL) ?: ""
        val password = extras.getString(LOGIN_PASSWORD) ?: ""

        if (onLogin(email, password)) {
            // Updated state (including clearing the error) now that the user has logged in.
            mediaSessionConnector.setCustomErrorMessage(null)
            mediaSessionConnector.invalidateMediaSessionPlaybackState()

            callback?.send(Activity.RESULT_OK, Bundle.EMPTY)
        } else {
            // Login is required - note this.
            requireLogin()

            callback?.send(Activity.RESULT_CANCELED, Bundle.EMPTY)
        }
        true
    }

    private val logoutCommand: CommandHandler = { _, callback ->
        // Log the user out.
        onLogout()

        // Login is required - note this.
        requireLogin()
        callback?.send(Activity.RESULT_OK, Bundle.EMPTY)
        true
    }
}

private const val TAG = "AutomotiveMusicService"
private const val ERROR_RESOLUTION_ACTION_LABEL =
    "android.media.extras.ERROR_RESOLUTION_ACTION_LABEL"
private const val ERROR_RESOLUTION_ACTION_INTENT =
    "android.media.extras.ERROR_RESOLUTION_ACTION_INTENT"

private const val USER_TOKEN = "com.example.android.uamp.automotive.PREFS.USER_TOKEN"

================================================
FILE: automotive/src/main/java/com/example/android/uamp/automotive/PhoneSignInFragment.kt
================================================
/*
 * Copyright 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.android.uamp.automotive

import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.view.View
import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat
import androidx.fragment.app.Fragment
import com.example.android.uamp.automotive.databinding.PhoneSignInBinding

/**
 * Fragment that is used to facilitate phone sign-in. The fragment allows users to choose between
 * either the PIN or QR code sign-in flow.
 */
class PhoneSignInFragment : Fragment(R.layout.phone_sign_in) {

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val context = requireContext()

        val binding = PhoneSignInBinding.bind(view)

        binding.toolbar.setNavigationOnClickListener {
            requireActivity().supportFragmentManager.popBackStack()
        }

        // Set up PIN sign in button.
        binding.appIcon.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.aural_logo))
        binding.primaryMessage.text = getString(R.string.phone_sign_in_primary_text)
        binding.pinSignInButton.text = getString(R.string.pin_sign_in_button_label)
        binding.pinSignInButton.setOnClickListener {
            requireActivity().supportFragmentManager.beginTransaction()
                .replace(R.id.sign_in_container, PinCodeSignInFragment())
                .addToBackStack("landingPage")
                .commit()
        }

        // Set up QR code sign in button.
        binding.qrSignInButton.text = getString(R.string.qr_sign_in_button_label)
        binding.qrSignInButton.setOnClickListener {
            requireActivity().supportFragmentManager.beginTransaction()
                .replace(R.id.sign_in_container, QrCodeSignInFragment())
                .addToBackStack("landingPage")
                .commit()
        }

        // Links in footer text should be clickable.
        binding.footer.text = HtmlCompat.fromHtml(
            context.getString(R.string.sign_in_footer),
            HtmlCompat.FROM_HTML_MODE_LEGACY
        )
        binding.footer.movementMethod = LinkMovementMethod.getInstance()
    }
}

================================================
FILE: automotive/src/main/java/com/example/android/uamp/automotive/PinCodeSignInFragment.kt
================================================
/*
 * Copyright 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.android.uamp.automotive

import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.example.android.uamp.automotive.databinding.PinSignInBinding

/**
 * Fragment that is used to facilitate PIN code sign-in. This fragment displayed a configurable
 * PIN code that users enter in a secondary device to perform sign-in.
 *
 *<p>This screen serves as a demo for UI best practices for PIN code sign in. Sign in implementation
 * will be app specific and is not included.
 */
class PinCodeSignInFragment : Fragment(R.layout.pin_sign_in) {

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val context = requireContext()

        val binding = PinSignInBinding.bind(view)

        binding.toolbar.setNavigationOnClickListener {
            requireActivity().supportFragmentManager.popBackStack()
        }

        binding.appIcon.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.aural_logo))
        binding.primaryMessage.text = getString(R.string.pin_sign_in_primary_text)
        binding.secondaryMessage.text = getString(R.string.pin_sign_in_secondary_text)

        // Links in footer text should be clickable.
        binding.footer.text = HtmlCompat.fromHtml(
            context.getString(R.string.sign_in_footer),
            HtmlCompat.FROM_HTML_MODE_LEGACY
        )
        binding.footer.movementMethod = LinkMovementMethod.getInstance()

        val pin = ViewModelProvider(requireActivity())
            .get(SignInActivityViewModel::class.java)
            .generatePin()

        // Remove existing PIN characters.
        if (binding.pinCodeContainer.childCount > 0) {
            binding.pinCodeContainer.removeAllViews()
        }

        for (element in pin) {
            val pinItem = LayoutInflater.from(context).inflate(
                R.layout.pin_item,
                binding.pinCodeContainer,
                false
            ) as TextView
            pinItem.text = element.toString()
            binding.pinCodeContainer.addView(pinItem)
        }
    }
}

================================================
FILE: automotive/src/main/java/com/example/android/uamp/automotive/QrCodeSignInFragment.kt
================================================
/*
 * Copyright 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.android.uamp.automotive

import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.view.View
import androidx.core.content.ContextCompat.getDrawable
import androidx.core.text.HtmlCompat
import androidx.fragment.app.Fragment
import com.bumptech.glide.Glide
import com.example.android.uamp.automotive.databinding.QrSignInBinding

/**
 * Fragment that is used to facilitate QR code sign-in. Users scan a QR code rendered by this
 * fragment with their phones, which performs the authentication required for sign-in
 *
 * <p>This screen serves as a demo for UI best practices for QR code sign in. Sign in implementation
 * will be app specific and is not included.
 */
class QrCodeSignInFragment : Fragment(R.layout.qr_sign_in) {

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val binding = QrSignInBinding.bind(view)

        binding.toolbar.setNavigationOnClickListener {
            requireActivity().supportFragmentManager.popBackStack()
        }

        binding.appIcon.setImageDrawable(getDrawable(requireContext(), R.drawable.aural_logo))
        binding.primaryMessage.text = getString(R.string.qr_sign_in_primary_text)
        binding.secondaryMessage.text = getString(R.string.qr_sign_in_secondary_text)

        // Links in footer text should be clickable.
        binding.footer.text = HtmlCompat.fromHtml(
            requireContext().getString(R.string.sign_in_footer),
            HtmlCompat.FROM_HTML_MODE_LEGACY
        )
        binding.footer.movementMethod = LinkMovementMethod.getInstance()

        Glide.with(this).load(getString(R.string.qr_code_url)).into(binding.qrCode)
    }
}


================================================
FILE: automotive/src/main/java/com/example/android/uamp/automotive/SettingsActivity.kt
================================================
/*
 * Copyright 2019 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.android.uamp.automotive

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.example.android.uamp.automotive.databinding.ActivitySettingsBinding

/**
 * This class exposes application settings
 * for integration with MediaCenter in Android Automotive.
 */
class SettingsActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = ActivitySettingsBinding.inflate(layoutInflater)
        setContentView(binding.root)

        setSupportActionBar(binding.toolbar)
        supportActionBar?.setHomeButtonEnabled(true)
        supportActionBar?.setDisplayHomeAsUpEnabled(true)

        supportFragmentManager
            .beginTransaction()
            .replace(R.id.settings_container, SettingsFragment())
            .commit()
    }

    override fun onBackPressed() {
        super.onBackPressed()
        finish()
    }

    override fun onSupportNavigateUp(): Boolean {
        onBackPressed()
        return true
    }
}

================================================
FILE: automotive/src/main/java/com/example/android/uamp/automotive/SettingsFragment.kt
================================================
/*
 * Copyright 2019 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.android.uamp.automotive

import android.app.Application
import android.content.ComponentName
import android.os.Bundle
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.example.android.uamp.common.MusicServiceConnection

/**
 * Preference fragment hosted by [SettingsActivity]. Handles events to various preference changes.
 */
class SettingsFragment : PreferenceFragmentCompat() {
    private lateinit var viewModel: SettingsFragmentViewModel

    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
        setPreferencesFromResource(R.xml.preferences, rootKey)

        viewModel = ViewModelProvider(this)
            .get(SettingsFragmentViewModel::class.java)
    }

    override fun onPreferenceTreeClick(preference: Preference?): Boolean {
        return when (preference?.key) {
            "logout" -> {
                viewModel.logout()
                requireActivity().finish()
                true
            }
            else -> {
                super.onPreferenceTreeClick(preference)
            }
        }
    }
}

/**
 * Basic ViewModel for [SettingsFragment].
 */
class SettingsFragmentViewModel(application: Application) : AndroidViewModel(application) {
    private val applicationContext = application.applicationContext
    private val musicServiceConnection = MusicServiceConnection(
        applicationContext,
        ComponentName(applicationContext, AutomotiveMusicService::class.java)
    )

    fun logout() {
        // Logout is fire and forget.
        musicServiceConnection.sendCommand(LOGOUT, null)
    }
}


================================================
FILE: automotive/src/main/java/com/example/android/uamp/automotive/SignInActivity.kt
================================================
/*
 * Copyright 2019 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.android.uamp.automotive

import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider

class SignInActivity : AppCompatActivity() {

    private lateinit var viewModel: SignInActivityViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_sign_in)

        viewModel = ViewModelProvider(this)
            .get(SignInActivityViewModel::class.java)

        viewModel.loggedIn.observe(this, Observer { loggedIn ->
            if (loggedIn == true) {
                Toast.makeText(this, R.string.sign_in_success_message, Toast.LENGTH_SHORT).show()
                finish()
            }
        })

        supportFragmentManager.beginTransaction()
            .add(R.id.sign_in_container, SignInLandingPageFragment())
            .commit()
    }
}

================================================
FILE: automotive/src/main/java/com/example/android/uamp/automotive/SignInActivityViewModel.kt
================================================
/*
 * Copyright 2019 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.android.uamp.automotive

import android.app.Activity
import android.app.Application
import android.content.ComponentName
import android.os.Bundle
import android.text.TextUtils
import android.widget.Toast
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.example.android.uamp.common.MusicServiceConnection
import java.util.Random

/**
 * Basic ViewModel for [SignInActivity].
 */
class SignInActivityViewModel(application: Application) : AndroidViewModel(application) {
    private val applicationContext = application.applicationContext
    private val musicServiceConnection = MusicServiceConnection(
        applicationContext,
        ComponentName(applicationContext, AutomotiveMusicService::class.java)
    )

    private val _loggedIn = MutableLiveData<Boolean>()
    val loggedIn: LiveData<Boolean> = _loggedIn

    fun login(email: String, password: String) {
        if (TextUtils.isEmpty(email) or TextUtils.isEmpty(password)) {
            Toast.makeText(
                applicationContext,
                applicationContext.getString(R.string.missing_fields_error),
                Toast.LENGTH_SHORT
            ).show()
        } else {
            val loginParams = Bundle().apply {
                putString(LOGIN_EMAIL, email)
                putString(LOGIN_PASSWORD, password)
            }
            musicServiceConnection.sendCommand(LOGIN, loginParams) { resultCode, _ ->
                _loggedIn.postValue(resultCode == Activity.RESULT_OK)
            }
        }
    }

    fun generatePin(): CharSequence {
        return String.format("%08d", Random().nextInt(99999999))
    }
}

================================================
FILE: automotive/src/main/java/com/example/android/uamp/automotive/SignInLandingPageFragment.kt
================================================
/*
 * Copyright 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.android.uamp.automotive

import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.text.InputType
import android.text.TextUtils
import android.text.method.LinkMovementMethod
import android.view.LayoutInflater
import android.view.View
import android.view.View.AUTOFILL_HINT_USERNAME
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.widget.Toolbar
import androidx.core.text.HtmlCompat
import androidx.fragment.app.Fragment
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.gms.common.api.ApiException
import com.google.android.gms.tasks.Task
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout

const val RC_SIGN_IN = 9001
const val PLAY_SERVICES_RESOLUTION_REQUEST = 9000

// Control the supported sign in flows by toggling the constants below.
const val ENABLE_PIN_SIGN_IN = true
const val ENABLE_QR_CODE_SIGN_IN = true
const val ENABLE_GOOGLE_SIGN_IN = true
const val ENABLE_USERNAME_PASSWORD_SIGN_IN = true

/**
 * A fragment that renders the landing screen for a sign-in flow. This screen can be configured
 * to display third-party sign-in, PIN sign-in, QR-code sign-in and/or Google sign-in.
 */
class SignInLandingPageFragment : Fragment() {

    companion object {
        internal const val CAR_SIGN_IN_IDENTIFIER_KEY = "userID"
    }

    private lateinit var toolbar: Toolbar
    private lateinit var appIcon: ImageView
    private lateinit var phoneSignInButton: Button
    private lateinit var googleSignInButton: Button
    private lateinit var usernameAndPasswordSignInButton: Button
    private lateinit var primaryTextView: TextView
    private lateinit var identifierContainer: TextInputLayout
    private lateinit var identifierInput: TextInputEditText
    private lateinit var footerTextView: TextView

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val layout = if (ENABLE_USERNAME_PASSWORD_SIGN_IN)
            R.layout.sign_in_landing_page_with_username_and_password
        else R.layout.sign_in_landing_page

        return inflater.inflate(layout, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val context = requireContext()

        toolbar = view.findViewById(R.id.toolbar)
        appIcon = view.findViewById(R.id.app_icon)
        primaryTextView = view.findViewById(R.id.primary_message)
        footerTextView = view.findViewById(R.id.footer)
        phoneSignInButton = view.findViewById(R.id.phone_sign_in_button)
        googleSignInButton = view.findViewById(R.id.google_sign_in_button)

        if (ENABLE_USERNAME_PASSWORD_SIGN_IN) {
            usernameAndPasswordSignInButton = view.findViewById(R.id.sign_in_button)
            identifierContainer = view.findViewById(R.id.identifier_container)
            identifierInput = view.findViewById(R.id.identifier_input)

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                identifierInput.setAutofillHints(AUTOFILL_HINT_USERNAME)
            }
        }

        toolbar.setNavigationOnClickListener { requireActivity().finish() }

        appIcon.setImageDrawable(context.getDrawable(R.drawable.aural_logo))
        primaryTextView.text = getString(R.string.sign_in_primary_text)

        // Links in footer text should be clickable.
        footerTextView.text = HtmlCompat.fromHtml(
            context.getString(R.string.sign_in_footer),
            HtmlCompat.FROM_HTML_MODE_LEGACY
        )
        footerTextView.movementMethod = LinkMovementMethod.getInstance()

        configureUsernameAndPasswordSignIn()
        configurePhoneSignIn()
        configureGoogleSignIn()
    }

    private fun configureUsernameAndPasswordSignIn() {
        if (!ENABLE_USERNAME_PASSWORD_SIGN_IN) {
            return
        }

        identifierContainer.hint = getString(R.string.sign_in_user_id_hint)
        identifierInput.inputType = InputType.TYPE_CLASS_TEXT

        usernameAndPasswordSignInButton.text = getString(R.string.sign_in_next_button_label)
        usernameAndPasswordSignInButton.setOnClickListener {
            val identifier = identifierInput.text
            if (TextUtils.isEmpty(identifier)) {
                identifierInput.error = getString(R.string.sign_in_username_error)
            } else {
                val args = Bundle()
                args.putString(CAR_SIGN_IN_IDENTIFIER_KEY, identifierInput.text.toString())
                val fragment = UsernameAndPasswordSignInFragment()
                fragment.arguments = args

                requireActivity().supportFragmentManager.beginTransaction()
                    .replace(R.id.sign_in_container, fragment)
                    .addToBackStack("landingPage")
                    .commit()
            }
        }
    }

    private fun configurePhoneSignIn() {
        if (!ENABLE_QR_CODE_SIGN_IN && !ENABLE_PIN_SIGN_IN) {
            phoneSignInButton.visibility = View.GONE
            return
        }

        lateinit var phoneSignInFragment: Fragment

        if (ENABLE_QR_CODE_SIGN_IN && ENABLE_PIN_SIGN_IN) {
            // Reduce the number of choices displayed to the user in a single screen. If both PIN
            // and QR code sign in is enabled, separate the choice between the two options to a
            // new screen.
            phoneSignInFragment = PhoneSignInFragment()
        } else if (ENABLE_PIN_SIGN_IN) {
            phoneSignInFragment = PinCodeSignInFragment()
        } else if (ENABLE_QR_CODE_SIGN_IN) {
            phoneSignInFragment = QrCodeSignInFragment()
        }

        phoneSignInButton.text = getString(R.string.phone_sign_in_button_label)
        phoneSignInButton.setOnClickListener {
            requireActivity().supportFragmentManager.beginTransaction()
                .replace(R.id.sign_in_container, phoneSignInFragment)
                .addToBackStack("landingPage")
                .commit()
        }
    }

    /**
     * Configure the Google sign in option on the landing page.
     *
     * <p>https://developers.google.com/identity/sign-in/android/start provides additional
     * information on integrating Google sign in into your Android app.
     */
    private fun configureGoogleSignIn() {
        if (!ENABLE_GOOGLE_SIGN_IN or !checkPlayServices()) {
            googleSignInButton.visibility = View.GONE
            return
        }

        val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
            .requestIdToken(getString(R.string.server_client_id))
            .requestEmail()
            .build()

        googleSignInButton.text = getString(R.string.google_sign_in_button_label)

        googleSignInButton.setOnClickListener {
            val mGoogleSignInClient = GoogleSignIn.getClient(requireContext(), gso)
            val signInIntent = mGoogleSignInClient.signInIntent
            startActivityForResult(signInIntent, RC_SIGN_IN)
        }
    }

    private fun checkPlayServices(): Boolean {
        val apiAvailability = GoogleApiAvailability.getInstance();
        val resultCode = apiAvailability.isGooglePlayServicesAvailable(context);
        if (resultCode != ConnectionResult.SUCCESS) {
            if (apiAvailability.isUserResolvableError(resultCode)) {
                apiAvailability.getErrorDialog(
                    activity, resultCode, PLAY_SERVICES_RESOLUTION_REQUEST
                ).show();
            }
            return false;
        }
        return true;
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        if (requestCode == RC_SIGN_IN) {
            val task = GoogleSignIn.getSignedInAccountFromIntent(data)
            handleGoogleSignIn(task)
        }
    }

    private fun handleGoogleSignIn(completedTask: Task<GoogleSignInAccount>) {
        try {
            val account = completedTask.getResult(ApiException::class.java)
            @Suppress("unused_variable") val idToken = account?.idToken

            // Send ID Token to server and validate.

        } catch (e: ApiException) {
            // The ApiException status code indicates the detailed failure reason.
            // Please refer to the GoogleSignInStatusCodes class reference for more information.
            Toast.makeText(
                requireContext(), getString(R.string.sign_in_failed_message, e.statusCode),
                Toast.LENGTH_SHORT
            )
                .show()
        }
    }
}

================================================
FILE: automotive/src/main/java/com/example/android/uamp/automotive/UsernameAndPasswordSignInFragment.kt
================================================
/*
 * Copyright 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.android.uamp.automotive

import android.os.Build
import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout

/**
 * Fragment that is used to facilitates username and password sign-in.
 */
class UsernameAndPasswordSignInFragment : Fragment() {

    private lateinit var toolbar: Toolbar
    private lateinit var appIcon: ImageView
    private lateinit var primaryTextView: TextView
    private lateinit var passwordContainer: TextInputLayout
    private lateinit var passwordInput: TextInputEditText
    private lateinit var submitButton: Button
    private lateinit var footerTextView: TextView

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.username_and_password_sign_in, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val context = requireContext()

        toolbar = view.findViewById(R.id.toolbar)
        appIcon = view.findViewById(R.id.app_icon)
        primaryTextView = view.findViewById(R.id.primary_message)
        passwordContainer = view.findViewById(R.id.password_container)
        passwordInput = view.findViewById(R.id.password_input)
        submitButton = view.findViewById(R.id.submit_button)
        footerTextView = view.findViewById(R.id.footer)

        toolbar.setNavigationOnClickListener {
            requireActivity().supportFragmentManager.popBackStack()
        }

        appIcon.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.aural_logo))
        primaryTextView.text = getString(R.string.username_and_password_sign_in_primary_text)
        passwordContainer.hint = getString(R.string.password_hint)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            passwordInput.setAutofillHints(View.AUTOFILL_HINT_PASSWORD)
        }

        // Links in footer text should be clickable.
        footerTextView.text = HtmlCompat.fromHtml(
            context.getString(R.string.sign_in_footer),
            HtmlCompat.FROM_HTML_MODE_LEGACY
        )
        footerTextView.movementMethod = LinkMovementMethod.getInstance()

        // Get user identifier from previous screen.
        val userId = arguments?.getString(SignInLandingPageFragment.CAR_SIGN_IN_IDENTIFIER_KEY)

        submitButton.text = getString(R.string.sign_in_submit_button_label)
        submitButton.setOnClickListener {
            onSignIn(userId!!, passwordInput.text.toString())
        }
    }

    private fun onSignIn(userIdentifier: CharSequence, password: CharSequence) {
        ViewModelProvider(requireActivity())
            .get(SignInActivityViewModel::class.java)
            .login(userIdentifier.toString(), password.toString())
    }
}

================================================
FILE: automotive/src/main/res/color/car_text_dark.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
    Copyright (C) 2019 The Android Open Source Project

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
-->
<!-- Default text colors for car buttons when enabled/disabled. -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:alpha="0.40" android:color="@android:color/white" android:state_enabled="false" />
    <item android:color="@android:color/black" />
</selector>


================================================
FILE: automotive/src/main/res/color/car_text_light.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
    Copyright (C) 2019 The Android Open Source Project

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
-->
<!-- Default text colors for car buttons when enabled/disabled. -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:alpha="0.36" android:color="@android:color/white" android:state_enabled="false" />
    <item android:alpha="0.72" android:color="@android:color/white" />
</selector>


================================================
FILE: automotive/src/main/res/drawable/default_button_background.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright 2019 Google Inc. All rights reserved.
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->
<!-- Default background styles for car buttons when enabled/disabled. -->
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
    android:color="?attr/colorControlHighlight">
    <item>
        <shape android:shape="rectangle">
            <corners android:radius="@dimen/car_button_radius" />
            <solid android:color="@color/colorAccent" />
        </shape>
    </item>
</ripple>


================================================
FILE: automotive/src/main/res/drawable/google_sign_in_button_background.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright 2019 Google Inc. All rights reserved.
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <corners android:radius="@dimen/sign_in_button_corner_radius" />
    <solid android:color="@color/google_sign_in_button_background_color" />
</shape>


================================================
FILE: automotive/src/main/res/drawable/google_sign_in_button_logo.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright 2019 Google Inc. All rights reserved.
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:width="24dp"
        android:height="24dp"
        android:drawable="@drawable/google_logo" />
</layer-list>

================================================
FILE: automotive/src/main/res/drawable/ic_launcher_background.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright 2019 Google LLC
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="108dp"
    android:height="108dp"
    android:viewportWidth="108"
    android:viewportHeight="108">
    <path
        android:fillColor="#26A69A"
        android:pathData="M0,0h108v108h-108z" />
    <path
        android:fillColor="#00000000"
        android:pathData="M9,0L9,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,0L19,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M29,0L29,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M39,0L39,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M49,0L49,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M59,0L59,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M69,0L69,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M79,0L79,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M89,0L89,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M99,0L99,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,9L108,9"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,19L108,19"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,29L108,29"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,39L108,39"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,49L108,49"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,59L108,59"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,69L108,69"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,79L108,79"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,89L108,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,99L108,99"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,29L89,29"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,39L89,39"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,49L89,49"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,59L89,59"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,69L89,69"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,79L89,79"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M29,19L29,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M39,19L39,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M49,19L49,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M59,19L59,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M69,19L69,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M79,19L79,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
</vector>


================================================
FILE: automotive/src/main/res/drawable/pin_background.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright 2019 Google Inc. All rights reserved.
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <corners android:radius="@dimen/sign_in_pin_container_corner_radius" />
    <stroke
        android:width="1dp"
        android:color="@color/grey_300" />
</shape>


================================================
FILE: automotive/src/main/res/drawable/sign_in_button_background.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright 2019 Google Inc. All rights reserved.
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
    android:color="?attr/colorControlHighlight">
    <item>
        <shape android:shape="rectangle">
            <corners android:radius="@dimen/sign_in_button_corner_radius" />
            <solid android:color="?attr/colorAccent" />
        </shape>
    </item>
</ripple>


================================================
FILE: automotive/src/main/res/drawable/sign_in_toolbar_back_icon.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright 2019 Google Inc. All rights reserved.
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="@dimen/sign_in_toolbar_nav_button_size"
    android:height="@dimen/sign_in_toolbar_nav_button_size"
    android:viewportWidth="48"
    android:viewportHeight="48">

    <path android:pathData="M0 0h48v48H0z" />
    <path
        android:fillColor="#000000"
        android:pathData="M40 22H15.66l11.17-11.17L24 8 8 24l16 16 2.83-2.83L15.66 26H40v-4z" />
</vector>


================================================
FILE: automotive/src/main/res/drawable/sign_in_toolbar_back_ripple_background.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright 2019 Google Inc. All rights reserved.
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
    android:color="?attr/colorControlHighlight"
    android:radius="@dimen/sign_in_toolbar_nav_button_size" />


================================================
FILE: automotive/src/main/res/drawable-v24/ic_launcher_foreground.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright 2019 Google LLC
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="108dp"
    android:height="108dp"
    android:viewportWidth="24.0"
    android:viewportHeight="24.0">
    <path
        android:fillColor="#FF000000"
        android:pathData="M12,1c-4.97,0 -9,4.03 -9,9v7c0,1.66 1.34,3 3,3h3v-8H5v-2c0,-3.87 3.13,-7 7,-7s7,3.13 7,7v2h-4v8h3c1.66,0 3,-1.34 3,-3v-7c0,-4.97 -4.03,-9 -9,-9z" />
</vector>


================================================
FILE: automotive/src/main/res/layout/activity_login.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright 2019 Google Inc. All rights reserved.
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical">

    <ImageView
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:src="@drawable/ic_launcher_foreground"
        android:tint="@color/colorPrimary"
        app:layout_constraintBottom_toTopOf="@+id/email"
        app:layout_constraintLeft_toLeftOf="@id/email"
        app:layout_constraintRight_toRightOf="@id/email"
        app:layout_constraintTop_toTopOf="parent" />

    <EditText
        android:id="@+id/email"
        android:layout_width="400dp"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:hint="@string/email_hint"
        android:inputType="textEmailAddress"
        android:maxLines="1"
        android:singleLine="true"
        app:layout_constraintBottom_toTopOf="@+id/password"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent" />

    <EditText
        android:id="@+id/password"
        android:layout_width="400dp"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:focusableInTouchMode="true"
        android:hint="@string/password_hint"
        android:imeActionLabel="@+id/login"
        android:imeOptions="actionUnspecified"
        android:inputType="textPassword"
        android:maxLines="1"
        android:singleLine="true"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/sign_in_button"
        style="?android:textAppearanceSmall"
        android:layout_width="400dp"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginTop="16dp"
        android:background="@color/colorAccent"
        android:text="@string/login_button_label"
        android:textColor="@android:color/black"
        android:textStyle="bold"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/password" />

</androidx.constraintlayout.widget.ConstraintLayout>


================================================
FILE: automotive/src/main/res/layout/activity_settings.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright 2019 Google Inc. All rights reserved.
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/settings_activity"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary" />

    </com.google.android.material.appbar.AppBarLayout>

    <FrameLayout
        android:id="@+id/settings_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/darkBackground" />
</LinearLayout>


================================================
FILE: automotive/src/main/res/layout/activity_sign_in.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright 2019 Google Inc. All rights reserved.
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/sign_in_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"></FrameLayout>


================================================
FILE: automotive/src/main/res/layout/phone_sign_in.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright 2019 Google Inc. All rights reserved.
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <androidx.constraintlayout.widget.ConstraintLayout
            android:id="@+id/content_container"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:paddingTop="@dimen/sign_in_toolbar_height">

            <ImageView
                android:id="@+id/app_icon"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="@dimen/sign_in_app_icon_top_margin"
                android:adjustViewBounds="true"
                android:maxHeight="@dimen/sign_in_app_icon_max_height"
                android:scaleType="fitXY"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

            <TextView
                android:id="@+id/primary_message"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="@dimen/sign_in_primary_message_top_margin"
                android:adjustViewBounds="true"
                android:maxWidth="@dimen/sign_in_text_max_width"
                android:textAppearance="@style/PrimaryText"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/app_icon" />

            <androidx.constraintlayout.widget.Guideline
                android:id="@+id/start_guideline"
                android:layout_width="1dp"
                android:layout_height="wrap_content"
                android:orientation="vertical"
                app:layout_constraintGuide_percent="@dimen/sign_in_horizontal_start_guideline" />

            <androidx.constraintlayout.widget.Guideline
                android:id="@+id/end_guideline"
                android:layout_width="1dp"
                android:layout_height="wrap_content"
                android:orientation="vertical"
                app:layout_constraintGuide_percent="@dimen/sign_in_horizontal_end_guideline" />

            <Button
                android:id="@+id/pin_sign_in_button"
                style="@style/CarButton.SignIn"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginTop="@dimen/sign_in_button1_top_margin_no_input"
                app:layout_constraintEnd_toEndOf="@id/end_guideline"
                app:layout_constraintStart_toStartOf="@id/start_guideline"
                app:layout_constraintTop_toBottomOf="@id/primary_message"
                app:layout_constraintWidth_max="@dimen/sign_in_button_max_width" />

            <Button
                android:id="@+id/qr_sign_in_button"
                style="@style/CarButton.SignIn"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginTop="@dimen/sign_in_button2_top_margin_no_input"
                app:layout_constraintEnd_toEndOf="@id/end_guideline"
                app:layout_constraintStart_toStartOf="@id/start_guideline"
                app:layout_constraintTop_toBottomOf="@id/pin_sign_in_button"
                app:layout_constraintWidth_max="@dimen/sign_in_button_max_width" />

            <TextView
                android:id="@+id/footer"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="@dimen/sign_in_footer_top_margin"
                android:layout_marginBottom="@dimen/sign_in_footer_bottom_margin"
                android:adjustViewBounds="true"
                android:gravity="center"
                android:maxWidth="@dimen/sign_in_text_max_width"
                android:textAppearance="@style/SecondaryText"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toBottomOf="@id/qr_sign_in_button" />

        </androidx.constraintlayout.widget.ConstraintLayout>

    </ScrollView>

    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent">

    </androidx.appcompat.widget.Toolbar>

</FrameLayout>


================================================
FILE: automotive/src/main/res/layout/pin_item.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright 2019 Google Inc. All rights reserved.
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="@dimen/sign_in_pin_container_margin"
    android:layout_marginEnd="@dimen/sign_in_pin_container_margin"
    android:background="@drawable/pin_background"
    android:paddingStart="@dimen/sign_in_pin_container_padding"
    android:paddingEnd="@dimen/sign_in_pin_container_padding"
    android:textAppearance="@style/DisplayText"
    android:textColor="@color/colorAccent" />


================================================
FILE: automotive/src/main/res/layout/pin_sign_in.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright 2019 Google Inc. All rights reserved.
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in complia
Download .txt
gitextract_ek5wbsm9/

├── .github/
│   ├── scripts/
│   │   └── gradlew_recursive.sh
│   └── workflows/
│       ├── android.yml
│       └── copy-branch.yml
├── .gitignore
├── .google/
│   └── packaging.yaml
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── TODO.md
├── app/
│   ├── build.gradle
│   ├── proguard-rules.pro
│   └── src/
│       └── main/
│           ├── AndroidManifest.xml
│           ├── 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/
│               │   ├── 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
│               ├── drawable-v24/
│               │   └── ic_launcher_foreground.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
│               ├── 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/
│       │       │   ├── default_button_background.xml
│       │       │   ├── 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
│       │       ├── drawable-v24/
│       │       │   └── ic_launcher_foreground.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
│       │       ├── 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
│       │       ├── mipmap-anydpi-v26/
│       │       │   ├── ic_launcher.xml
│       │       │   └── ic_launcher_round.xml
│       │       ├── values/
│       │       │   ├── colors.xml
│       │       │   ├── dimens.xml
│       │       │   ├── strings.xml
│       │       │   └── styles.xml
│       │       ├── values-h1060dp/
│       │       │   └── dimens.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
│       │   ├── 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/
│       │       │   ├── ic_album.xml
│       │       │   └── ic_recommended.xml
│       │       ├── 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
├── gradle/
│   └── wrapper/
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
Download .txt
SYMBOL INDEX (4 symbols across 2 files)

FILE: automotive/src/androidTest/java/com/example/android/uamp/automotive/ExampleInstrumentedTest.java
  class ExampleInstrumentedTest (line 34) | @RunWith(AndroidJUnit4.class)
    method useAppContext (line 36) | @Test

FILE: automotive/src/test/java/com/example/android/uamp/automotive/ExampleUnitTest.java
  class ExampleUnitTest (line 28) | public class ExampleUnitTest {
    method addition_isCorrect (line 29) | @Test
Condensed preview — 129 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (451K chars).
[
  {
    "path": ".github/scripts/gradlew_recursive.sh",
    "chars": 1529,
    "preview": "#!/bin/bash\n\n# Copyright (C) 2020 The Android Open Source Project\n#\n# Licensed under the Apache License, Version 2.0 (th"
  },
  {
    "path": ".github/workflows/android.yml",
    "chars": 1291,
    "preview": "# Copyright (C) 2020 The Android Open Source Project\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");"
  },
  {
    "path": ".github/workflows/copy-branch.yml",
    "chars": 1014,
    "preview": "# Duplicates default main branch to the old master branch\n\nname: Duplicates main to old master branch\n\n# Controls when t"
  },
  {
    "path": ".gitignore",
    "chars": 155,
    "preview": "*.iml\n.gradle\n/local.properties\n/.idea\n.DS_Store\n/captures\n.externalNativeBuild\n\n# Generated files\nbuild/\n\n# Extra (cust"
  },
  {
    "path": ".google/packaging.yaml",
    "chars": 954,
    "preview": "# GOOGLE SAMPLE PACKAGING DATA\n#\n# This file is used by Google as part of our samples packaging process.\n# End users may"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 1612,
    "preview": "# How to become a contributor and submit your own code\n\n## Contributor License Agreements\n\nWe'd love to accept your samp"
  },
  {
    "path": "LICENSE",
    "chars": 11361,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "README.md",
    "chars": 3167,
    "preview": "> **Warning**\n> This sample has been deprecated and is no longer being maintained.\n> \n> To find other samples that may b"
  },
  {
    "path": "TODO.md",
    "chars": 771,
    "preview": "TODOs\n=====\n\nThis file captures the high level goals of the project. This provides guidance for anyone who wants\nto cont"
  },
  {
    "path": "app/build.gradle",
    "chars": 2389,
    "preview": "/*\n * Copyright 2017 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "app/proguard-rules.pro",
    "chars": 751,
    "preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "chars": 3649,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2017 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "app/src/main/java/com/example/android/uamp/MainActivity.kt",
    "chars": 4594,
    "preview": "/*\n * Copyright 2017 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "app/src/main/java/com/example/android/uamp/MediaItemAdapter.kt",
    "chars": 3649,
    "preview": "/*\n * Copyright 2017 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "app/src/main/java/com/example/android/uamp/MediaItemData.kt",
    "chars": 3483,
    "preview": "/*\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "app/src/main/java/com/example/android/uamp/cast/UampCastOptionsProvider.kt",
    "chars": 1836,
    "preview": "/*\n * Copyright 2020 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "app/src/main/java/com/example/android/uamp/fragments/MediaItemFragment.kt",
    "chars": 3559,
    "preview": "/*\n * Copyright 2017 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "app/src/main/java/com/example/android/uamp/fragments/NowPlayingFragment.kt",
    "chars": 4010,
    "preview": "/*\n * Copyright 2019 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "app/src/main/java/com/example/android/uamp/utils/Event.kt",
    "chars": 1361,
    "preview": "/*\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "app/src/main/java/com/example/android/uamp/utils/InjectorUtils.kt",
    "chars": 2508,
    "preview": "/*\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "app/src/main/java/com/example/android/uamp/viewmodels/MainActivityViewModel.kt",
    "chars": 7247,
    "preview": "/*\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "app/src/main/java/com/example/android/uamp/viewmodels/MediaItemFragmentViewModel.kt",
    "chars": 7597,
    "preview": "/*\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "app/src/main/java/com/example/android/uamp/viewmodels/NowPlayingFragmentViewModel.kt",
    "chars": 8211,
    "preview": "/*\n * Copyright 2019 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "app/src/main/res/drawable/ic_album_black_24dp.xml",
    "chars": 1134,
    "preview": "<!--\n  ~ Copyright 2017 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "app/src/main/res/drawable/ic_launcher_background.xml",
    "chars": 6240,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2018 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "app/src/main/res/drawable/ic_pause_black_24dp.xml",
    "chars": 948,
    "preview": "<!--\n  ~ Copyright 2017 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "app/src/main/res/drawable/ic_play_arrow_black_24dp.xml",
    "chars": 923,
    "preview": "<!--\n  ~ Copyright 2017 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "app/src/main/res/drawable/ic_signal_wifi_off_black_24dp.xml",
    "chars": 1140,
    "preview": "<!--\n  ~ Copyright 2018 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "app/src/main/res/drawable/media_item_background.xml",
    "chars": 1345,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2018 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "app/src/main/res/drawable/media_item_mask.xml",
    "chars": 1483,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2018 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "app/src/main/res/drawable/media_overlay_background.xml",
    "chars": 938,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "app/src/main/res/drawable-v24/ic_launcher_foreground.xml",
    "chars": 2514,
    "preview": "<!--\n  ~ Copyright 2017 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "app/src/main/res/layout/activity_main.xml",
    "chars": 976,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2017 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "app/src/main/res/layout/cast_context_error.xml",
    "chars": 956,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2018 The Android Open Source Project\n\n     Licensed under the "
  },
  {
    "path": "app/src/main/res/layout/fragment_mediaitem.xml",
    "chars": 3792,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2017 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "app/src/main/res/layout/fragment_mediaitem_list.xml",
    "chars": 1971,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2017 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "app/src/main/res/layout/fragment_nowplaying.xml",
    "chars": 4742,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "chars": 904,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2018 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "chars": 904,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2018 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "chars": 1164,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2017 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "app/src/main/res/values/dimens.xml",
    "chars": 1837,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2017 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "chars": 1274,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2017 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "app/src/main/res/values/styles.xml",
    "chars": 2031,
    "preview": "<!--\n  ~ Copyright 2017 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "app/src/main/res/xml/automotive_app_desc.xml",
    "chars": 898,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2018 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "automotive/build.gradle",
    "chars": 2392,
    "preview": "/*\n * Copyright 2019 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "automotive/proguard-rules.pro",
    "chars": 751,
    "preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
  },
  {
    "path": "automotive/src/androidTest/java/com/example/android/uamp/automotive/ExampleInstrumentedTest.java",
    "chars": 1335,
    "preview": "/*\n * Copyright 2019 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "automotive/src/main/AndroidManifest.xml",
    "chars": 3156,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2019 Google LLC\n  ~\n  ~ Licensed under the Apache License, Vers"
  },
  {
    "path": "automotive/src/main/java/com/example/android/uamp/automotive/AutomotiveMusicService.kt",
    "chars": 7103,
    "preview": "/*\n * Copyright 2019 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "automotive/src/main/java/com/example/android/uamp/automotive/PhoneSignInFragment.kt",
    "chars": 2809,
    "preview": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
  },
  {
    "path": "automotive/src/main/java/com/example/android/uamp/automotive/PinCodeSignInFragment.kt",
    "chars": 2997,
    "preview": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
  },
  {
    "path": "automotive/src/main/java/com/example/android/uamp/automotive/QrCodeSignInFragment.kt",
    "chars": 2357,
    "preview": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
  },
  {
    "path": "automotive/src/main/java/com/example/android/uamp/automotive/SettingsActivity.kt",
    "chars": 1665,
    "preview": "/*\n * Copyright 2019 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "automotive/src/main/java/com/example/android/uamp/automotive/SettingsFragment.kt",
    "chars": 2352,
    "preview": "/*\n * Copyright 2019 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "automotive/src/main/java/com/example/android/uamp/automotive/SignInActivity.kt",
    "chars": 1577,
    "preview": "/*\n * Copyright 2019 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "automotive/src/main/java/com/example/android/uamp/automotive/SignInActivityViewModel.kt",
    "chars": 2306,
    "preview": "/*\n * Copyright 2019 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "automotive/src/main/java/com/example/android/uamp/automotive/SignInLandingPageFragment.kt",
    "chars": 9710,
    "preview": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
  },
  {
    "path": "automotive/src/main/java/com/example/android/uamp/automotive/UsernameAndPasswordSignInFragment.kt",
    "chars": 3991,
    "preview": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
  },
  {
    "path": "automotive/src/main/res/color/car_text_dark.xml",
    "chars": 963,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n    Copyright (C) 2019 The Android Open Source Project\n\n    Licensed under t"
  },
  {
    "path": "automotive/src/main/res/color/car_text_light.xml",
    "chars": 984,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n    Copyright (C) 2019 The Android Open Source Project\n\n    Licensed under t"
  },
  {
    "path": "automotive/src/main/res/drawable/default_button_background.xml",
    "chars": 1077,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "automotive/src/main/res/drawable/google_sign_in_button_background.xml",
    "chars": 923,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "automotive/src/main/res/drawable/google_sign_in_button_logo.xml",
    "chars": 878,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "automotive/src/main/res/drawable/ic_launcher_background.xml",
    "chars": 6218,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google LLC\n  ~\n  ~ Licensed under the Apache License, Ver"
  },
  {
    "path": "automotive/src/main/res/drawable/pin_background.xml",
    "chars": 937,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "automotive/src/main/res/drawable/sign_in_button_background.xml",
    "chars": 1013,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "automotive/src/main/res/drawable/sign_in_toolbar_back_icon.xml",
    "chars": 1122,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "automotive/src/main/res/drawable/sign_in_toolbar_back_ripple_background.xml",
    "chars": 850,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "automotive/src/main/res/drawable-v24/ic_launcher_foreground.xml",
    "chars": 1071,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google LLC\n  ~\n  ~ Licensed under the Apache License, Ver"
  },
  {
    "path": "automotive/src/main/res/layout/activity_login.xml",
    "chars": 3214,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "automotive/src/main/res/layout/activity_settings.xml",
    "chars": 1565,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "automotive/src/main/res/layout/activity_sign_in.xml",
    "chars": 880,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "automotive/src/main/res/layout/phone_sign_in.xml",
    "chars": 5548,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "automotive/src/main/res/layout/pin_item.xml",
    "chars": 1228,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "automotive/src/main/res/layout/pin_sign_in.xml",
    "chars": 4810,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "automotive/src/main/res/layout/preference.xml",
    "chars": 1158,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google LLC\n  ~\n  ~ Licensed under the Apache License, Ver"
  },
  {
    "path": "automotive/src/main/res/layout/preference_category.xml",
    "chars": 1157,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google LLC\n  ~\n  ~ Licensed under the Apache License, Ver"
  },
  {
    "path": "automotive/src/main/res/layout/qr_sign_in.xml",
    "chars": 4816,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "automotive/src/main/res/layout/sign_in_landing_page.xml",
    "chars": 6398,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "automotive/src/main/res/layout/sign_in_landing_page_with_username_and_password.xml",
    "chars": 7929,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "automotive/src/main/res/layout/username_and_password_sign_in.xml",
    "chars": 6599,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "automotive/src/main/res/layout-h900dp/phone_sign_in.xml",
    "chars": 5182,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "automotive/src/main/res/layout-h900dp/pin_sign_in.xml",
    "chars": 4499,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "automotive/src/main/res/layout-h900dp/qr_sign_in.xml",
    "chars": 4497,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "automotive/src/main/res/layout-h900dp/sign_in_landing_page.xml",
    "chars": 5916,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "automotive/src/main/res/layout-h900dp/sign_in_landing_page_with_username_and_password.xml",
    "chars": 7438,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "automotive/src/main/res/layout-h900dp/username_and_password_sign_in.xml",
    "chars": 6041,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "automotive/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "chars": 904,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2018 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "automotive/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "chars": 882,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google LLC\n  ~\n  ~ Licensed under the Apache License, Ver"
  },
  {
    "path": "automotive/src/main/res/values/colors.xml",
    "chars": 1267,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google LLC\n  ~\n  ~ Licensed under the Apache License, Ver"
  },
  {
    "path": "automotive/src/main/res/values/dimens.xml",
    "chars": 2560,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "automotive/src/main/res/values/strings.xml",
    "chars": 3053,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google LLC\n  ~\n  ~ Licensed under the Apache License, Ver"
  },
  {
    "path": "automotive/src/main/res/values/styles.xml",
    "chars": 4539,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google LLC\n  ~\n  ~ Licensed under the Apache License, Ver"
  },
  {
    "path": "automotive/src/main/res/values-h1060dp/dimens.xml",
    "chars": 890,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "automotive/src/main/res/xml/automotive_app_desc.xml",
    "chars": 899,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2018 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "automotive/src/main/res/xml/preferences.xml",
    "chars": 1089,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "automotive/src/test/java/com/example/android/uamp/automotive/ExampleUnitTest.java",
    "chars": 1003,
    "preview": "/*\n * Copyright 2019 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "build.gradle",
    "chars": 2429,
    "preview": "/*\n * Copyright 2017 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "common/build.gradle",
    "chars": 2942,
    "preview": "/*\n * Copyright 2017 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "common/proguard-rules.pro",
    "chars": 751,
    "preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
  },
  {
    "path": "common/src/main/AndroidManifest.xml",
    "chars": 1820,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2017 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "common/src/main/java/com/example/android/uamp/common/MusicServiceConnection.kt",
    "chars": 7813,
    "preview": "/*\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "common/src/main/java/com/example/android/uamp/media/CastMediaItemConverter.kt",
    "chars": 3591,
    "preview": "package com.example.android.uamp.media\n\nimport android.net.Uri\nimport android.support.v4.media.MediaMetadataCompat\nimpor"
  },
  {
    "path": "common/src/main/java/com/example/android/uamp/media/MusicService.kt",
    "chars": 27462,
    "preview": "/*\n * Copyright 2017 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "common/src/main/java/com/example/android/uamp/media/PackageValidator.kt",
    "chars": 15057,
    "preview": "/*\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "common/src/main/java/com/example/android/uamp/media/PersistentStorage.kt",
    "chars": 4266,
    "preview": "/*\n * Copyright 2020 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "common/src/main/java/com/example/android/uamp/media/UampNotificationManager.kt",
    "chars": 5472,
    "preview": "/*\n * Copyright 2020 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "common/src/main/java/com/example/android/uamp/media/extensions/FileExt.kt",
    "chars": 1143,
    "preview": "/*\n * Copyright 2019 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "common/src/main/java/com/example/android/uamp/media/extensions/JavaLangExt.kt",
    "chars": 1730,
    "preview": "/*\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "common/src/main/java/com/example/android/uamp/media/extensions/MediaMetadataCompatExt.kt",
    "chars": 11503,
    "preview": "/*\n * Copyright 2017 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "common/src/main/java/com/example/android/uamp/media/extensions/PlaybackStateCompatExt.kt",
    "chars": 3061,
    "preview": "/*\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "common/src/main/java/com/example/android/uamp/media/library/AlbumArtContentProvider.kt",
    "chars": 3059,
    "preview": "/*\n * Copyright 2019 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "common/src/main/java/com/example/android/uamp/media/library/BrowseTree.kt",
    "chars": 6720,
    "preview": "/*\n * Copyright 2019 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "common/src/main/java/com/example/android/uamp/media/library/JsonSource.kt",
    "chars": 8790,
    "preview": "/*\n * Copyright 2017 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "common/src/main/java/com/example/android/uamp/media/library/MusicSource.kt",
    "chars": 7879,
    "preview": "/*\n * Copyright 2017 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "common/src/main/res/drawable/ic_album.xml",
    "chars": 1079,
    "preview": "<!--\n  ~ Copyright 2019 Google LLC\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may n"
  },
  {
    "path": "common/src/main/res/drawable/ic_recommended.xml",
    "chars": 1036,
    "preview": "<!--\n  ~ Copyright 2019 Google LLC\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may n"
  },
  {
    "path": "common/src/main/res/menu/main_activity_menu.xml",
    "chars": 1035,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2017 The Android Open Source Project\n\n     Licensed under the "
  },
  {
    "path": "common/src/main/res/values/strings.xml",
    "chars": 2018,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2018 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under t"
  },
  {
    "path": "common/src/main/res/xml/allowed_media_browser_callers.xml",
    "chars": 4418,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\nCopyright (C) 2018 The Android Open Source Project\n\nLicensed under the Apach"
  },
  {
    "path": "common/src/test/java/com/example/android/uamp/media/library/MusicSourceTest.kt",
    "chars": 5420,
    "preview": "/*\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "docs/FAQs.md",
    "chars": 703,
    "preview": "# Frequently Asked Questions\n\n## How can I change the music which UAMP plays?\nUAMP reads its [music catalog](https://sto"
  },
  {
    "path": "docs/FullGuide.md",
    "chars": 11286,
    "preview": "# Full Guide to UAMP\n\nThe Universal Android Music Player (UAMP) is an example music player app for Android written in [K"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "chars": 233,
    "preview": "#Thu Jun 18 22:31:29 CEST 2020\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER"
  },
  {
    "path": "gradle.properties",
    "chars": 1450,
    "preview": "#\n# Copyright 2017 Google Inc. All rights reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n"
  },
  {
    "path": "gradlew",
    "chars": 4971,
    "preview": "#!/usr/bin/env bash\n\n##############################################################################\n##\n##  Gradle start "
  },
  {
    "path": "gradlew.bat",
    "chars": 2404,
    "preview": "@if \"%DEBUG%\" == \"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@r"
  },
  {
    "path": "settings.gradle",
    "chars": 1740,
    "preview": "/*\n * Copyright 2017 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the android/uamp GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 129 files (414.6 KB), approximately 98.7k tokens, and a symbol index with 4 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!