[
  {
    "path": ".github/scripts/gradlew_recursive.sh",
    "content": "#!/bin/bash\n\n# Copyright (C) 2020 The Android Open Source Project\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nset -xe\n\n# Default Gradle settings are not optimal for Android builds, override them\n# here to make the most out of the GitHub Actions build servers\nGRADLE_OPTS=\"$GRADLE_OPTS -Xms4g -Xmx4g\"\nGRADLE_OPTS=\"$GRADLE_OPTS -XX:+HeapDumpOnOutOfMemoryError\"\nGRADLE_OPTS=\"$GRADLE_OPTS -Dorg.gradle.daemon=false\"\nGRADLE_OPTS=\"$GRADLE_OPTS -Dorg.gradle.workers.max=2\"\nGRADLE_OPTS=\"$GRADLE_OPTS -Dkotlin.incremental=false\"\nGRADLE_OPTS=\"$GRADLE_OPTS -Dkotlin.compiler.execution.strategy=in-process\"\nGRADLE_OPTS=\"$GRADLE_OPTS -Dfile.encoding=UTF-8\"\nexport GRADLE_OPTS\n\n# Crawl all gradlew files which indicate an Android project\n# You may edit this if your repo has a different project structure\nfor GRADLEW in `find . -name \"gradlew\"` ; do\n    SAMPLE=$(dirname \"${GRADLEW}\")\n    # Tell Gradle that this is a CI environment and disable parallel compilation\n    bash \"$GRADLEW\" -p \"$SAMPLE\" -Pci --no-parallel --stacktrace $@\ndone\n"
  },
  {
    "path": ".github/workflows/android.yml",
    "content": "# Copyright (C) 2020 The Android Open Source Project\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nname: Android CI\n\non:\n  workflow_dispatch:\n  push:\n    branches: [ main ]\n  pull_request:\n    branches: [ main ]\n\njobs:\n\n  build:\n    name: Build\n    runs-on: ubuntu-18.04\n\n    steps:\n      - uses: actions/checkout@v1\n      - name: set up JDK 1.8\n        uses: actions/setup-java@v1\n        with:\n          java-version: 1.8\n      - name: Build project\n        run: .github/scripts/gradlew_recursive.sh assembleDebug\n      - name: Zip artifacts\n        run: zip -r assemble.zip . -i '**/build/*.apk' '**/build/*.aab' '**/build/*.aar' '**/build/*.so'\n      - name: Upload artifacts\n        uses: actions/upload-artifact@v1\n        with:\n          name: assemble\n          path: assemble.zip\n"
  },
  {
    "path": ".github/workflows/copy-branch.yml",
    "content": "# Duplicates default main branch to the old master branch\n\nname: Duplicates main to old master branch\n\n# Controls when the action will run. Triggers the workflow on push or pull request\n# events but only for the main branch\non:\n  workflow_dispatch:\n  push:\n    branches: [ main ]\n\n# A workflow run is made up of one or more jobs that can run sequentially or in parallel\njobs:\n  # This workflow contains a single job called \"copy-branch\"\n  copy-branch:\n    # The type of runner that the job will run on\n    runs-on: ubuntu-latest\n\n    # Steps represent a sequence of tasks that will be executed as part of the job\n    steps:\n    # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it,\n    # but specifies master branch (old default).\n    - uses: actions/checkout@v2\n      with:\n        fetch-depth: 0\n        ref: master\n        \n    - run: |\n        git config user.name github-actions\n        git config user.email github-actions@github.com\n        git merge origin/main\n        git push\n"
  },
  {
    "path": ".gitignore",
    "content": "*.iml\n.gradle\n/local.properties\n/.idea\n.DS_Store\n/captures\n.externalNativeBuild\n\n# Generated files\nbuild/\n\n# Extra (custom) settings\nextra-settings.gradle\n"
  },
  {
    "path": ".google/packaging.yaml",
    "content": "# GOOGLE SAMPLE PACKAGING DATA\n#\n# This file is used by Google as part of our samples packaging process.\n# End users may safely ignore this file. It has no relevance to other systems.\n---\nstatus:       PUBLISHED\ntechnologies: [Android, Android Auto, Android Automotive OS, Android Wear]\ncategories:   [Getting Started, Media, UI]\nlanguages:    [Kotlin]\nsolutions:    [Mobile]\n\ngithub:       googlesamples/android-UniversalMusicPlayer\n\nlevel:        INTERMEDIATE\n\nicon: screenshots/icon-web.png\n\napiRefs:\n  - android:android.support.v4.media.session.MediaSessionCompat\n  - android:android.support.v4.media.session.MediaControllerCompat\n  - androidx.media.MediaBrowserServiceCompat\n  - android:android.support.v4.media.MediaBrowserCompat\n  - androidx.media.app.NotificationCompat.MediaStyle\n  - android:com.google.android.exoplayer2.SimpleExoPlayer\n  - android:com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector\n\nlicense: apache2-android\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# How to become a contributor and submit your own code\n\n## Contributor License Agreements\n\nWe'd love to accept your sample apps and patches! Before we can take them, we\nhave to jump a couple of legal hurdles.\n\nPlease fill out either the individual or corporate Contributor License Agreement (CLA).\n\n  * If you are an individual writing original source code and you're sure you\n    own the intellectual property, then you'll need to sign an [individual CLA]\n    (https://developers.google.com/open-source/cla/individual).\n  * If you work for a company that wants to allow you to contribute your work,\n    then you'll need to sign a [corporate CLA]\n    (https://developers.google.com/open-source/cla/corporate).\n\nFollow either of the two links above to access the appropriate CLA and\ninstructions for how to sign and return it. Once we receive it, we'll be able to\naccept your pull requests.\n\n## Contributing A Patch\n\n1. Submit an issue describing your proposed change to the repo in question.\n1. The repo owner will respond to your issue promptly.\n1. If your proposed change is accepted, and you haven't already done so, sign a\n   Contributor License Agreement (see details above).\n1. Fork the desired repo, develop and test your code changes.\n1. Ensure that your code adheres to the existing style in the sample to which\n   you are contributing. Refer to the\n   [Android Code Style Guide]\n   (https://source.android.com/source/code-style.html) for the\n   recommended coding standards for this organization.\n1. Ensure that your code has an appropriate set of unit tests which all pass.\n1. Submit a pull request.\n\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2014 The Android Open Source Project\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "> **Warning**\n> This sample has been deprecated and is no longer being maintained.\n> \n> To find other samples that may be of interest, see [https://developer.android.com/samples](https://developer.android.com/samples).\n\nUniversal Android Music Player Sample\n=====================================\nThe goal of this sample is to show how to implement an audio media app that works\nacross multiple form factors and provides a consistent user experience\non Android phones, tablets, Android Auto, Android Wear, Android TV, Google Cast devices,\nand with the Google Assistant. \n\nTo get started with UAMP please read the [full guide](docs/FullGuide.md).\n\n![Screenshot showing UAMP's UI for browsing albums and songs](docs/images/1-browse-albums-screenshot.png \"Browse albums screenshot\")\n![Screenshot showing UAMP's UI for playing a song](docs/images/2-play-song-screenshot.png \"Play song screenshot\")\n\nPre-requisites\n--------------\n\n- Android Studio 3.x\n\nGetting Started\n---------------\n\nThis sample uses the Gradle build system. To build this project, use the\n\"gradlew build\" command or use \"Import Project\" in Android Studio.\n\nSupport\n-------\n\n- Check out the [FAQs page](docs/FAQs.md)\n- Stack Overflow: http://stackoverflow.com/questions/tagged/android\n\nIf you've found an error in this sample, please\n[file an issue](https://github.com/android/UAMP/issues)\n\nPatches are encouraged and may be submitted by forking this project and\nsubmitting a pull request through GitHub. Please see [CONTRIBUTING.md](CONTRIBUTING.md) for more\ndetails.\n\nAudio\n-----\n\nMusic provided by the [Free Music Archive](http://freemusicarchive.org/).\n\n- [Wake Up](http://freemusicarchive.org/music/The_Kyoto_Connection/Wake_Up_1957/) by\n[The Kyoto Connection](http://freemusicarchive.org/music/The_Kyoto_Connection/).\n\nRecordings provided by the [Ambisonic Sound Library](https://library.soundfield.com/).\n\n- [Pre Game Marching Band](https://library.soundfield.com/track/163) by Watson Wu\n- [Chickens on a Farm](https://library.soundfield.com/track/129) by Watson Wu\n- [Rural Market Busker](https://library.soundfield.com/track/55) by Stephan Schutze\n- [Steamtrain Interior](https://library.soundfield.com/track/65) by Stephan Schutze\n- [Rural Road Car Pass](https://library.soundfield.com/track/57) by Stephan Schutze\n- [10 Feet from Shore](https://library.soundfield.com/track/114) by Watson Wu\n\nLicense\n-------\n\nCopyright 2025 Google Inc.\n\nLicensed to the Apache Software Foundation (ASF) under one or more contributor\nlicense agreements.  See the NOTICE file distributed with this work for\nadditional information regarding copyright ownership.  The ASF licenses this\nfile to you under the Apache License, Version 2.0 (the \"License\"); you may not\nuse this file except in compliance with the License.  You may obtain a copy of\nthe License at\n\n  http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\nWARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the\nLicense for the specific language governing permissions and limitations under\nthe License.\n"
  },
  {
    "path": "TODO.md",
    "content": "TODOs\n=====\n\nThis file captures the high level goals of the project. This provides guidance for anyone who wants\nto contribute. If you see something in the list that you'd like to work on,\nthe best approach would be to [create an\nissue](https://github.com/googlesamples/android-UniversalMusicPlayer/issues) first,\nand then provide a pull request once completed to have your work merged into the project.\n\nService Side Tasks\n------------------\n\n- Implement rating (ideally \"favorite\" vs \"thumbs up/down\").\n- Improve integration with the Google Assistant.\n\nUI Tasks\n--------\n\n- 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)).\n"
  },
  {
    "path": "app/build.gradle",
    "content": "/*\n * Copyright 2017 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\napply plugin: 'com.android.application'\napply plugin: 'kotlin-android'\napply plugin: 'kotlin-kapt'\napply plugin: 'kotlin-android-extensions'\n\nandroid {\n    compileSdkVersion rootProject.compileSdkVersion\n\n    defaultConfig {\n        applicationId \"com.example.android.uamp.next\"\n        versionCode 1\n        versionName \"1.0\"\n\n        minSdkVersion rootProject.minSdkVersion\n        targetSdkVersion rootProject.targetSdkVersion\n        multiDexEnabled true\n\n        compileOptions {\n            sourceCompatibility JavaVersion.VERSION_1_8\n            targetCompatibility JavaVersion.VERSION_1_8\n        }\n        kotlinOptions {\n            jvmTarget = \"1.8\"\n        }\n\n        vectorDrawables {\n            useSupportLibrary true\n        }\n    }\n\n    buildFeatures {\n        viewBinding true\n        dataBinding true\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n        }\n    }\n}\n\ndependencies {\n    implementation \"com.android.support:multidex:$multidex_version\"\n\n    implementation project(':common')\n\n    implementation \"org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version\"\n\n    implementation \"androidx.appcompat:appcompat:$androidx_app_compat_version\"\n    implementation \"androidx.fragment:fragment-ktx:$fragment_version\"\n    implementation \"androidx.recyclerview:recyclerview:$recycler_view_version\"\n\n    implementation \"androidx.constraintlayout:constraintlayout:$constraint_layout_version\"\n    implementation \"androidx.lifecycle:lifecycle-extensions:$arch_lifecycle_version\"\n\n    // Glide dependencies\n    implementation \"com.github.bumptech.glide:glide:$glide_version\"\n    kapt \"com.github.bumptech.glide:compiler:$glide_version\"\n}\n"
  },
  {
    "path": "app/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile\n"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2017 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:dist=\"http://schemas.android.com/apk/distribution\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    package=\"com.example.android.uamp\">\n\n    <application\n        android:allowBackup=\"true\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:roundIcon=\"@mipmap/ic_launcher_round\"\n        android:supportsRtl=\"true\"\n        android:targetSandboxVersion=\"2\"\n        android:theme=\"@style/AppTheme\">\n\n        <!-- Enable instant app support -->\n        <dist:module dist:instant=\"true\" />\n\n        <!-- Declare that UAMP supports Android Auto. -->\n        <meta-data\n            android:name=\"com.google.android.gms.car.application\"\n            android:resource=\"@xml/automotive_app_desc\" />\n\n        <!-- Declare that UAMP supports Cast. -->\n        <meta-data android:name=\"com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME\"\n            android:value=\"com.example.android.uamp.cast.UampCastOptionsProvider\"/>\n\n        <activity android:name=\".MainActivity\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n\n            <!-- App links for http -->\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:host=\"example.android.com\"\n                    android:pathPattern=\"/uamp\"\n                    android:scheme=\"http\" />\n            </intent-filter>\n\n            <!-- App links for https -->\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:host=\"example.android.com\"\n                    android:pathPattern=\"/uamp\"\n                    android:scheme=\"https\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.media.action.MEDIA_PLAY_FROM_SEARCH\" />\n            </intent-filter>\n        </activity>\n\n        <!--\n        Declare the common MediaBrowserService for use in the mobile app, including\n        with the Android Auto app.\n        -->\n        <service\n            android:name=\".media.MusicService\"\n            android:enabled=\"true\"\n            android:exported=\"true\"\n            tools:ignore=\"ExportedService\">\n\n            <intent-filter>\n                <action android:name=\"android.media.browse.MediaBrowserService\" />\n            </intent-filter>\n        </service>\n\n    </application>\n\n</manifest>\n"
  },
  {
    "path": "app/src/main/java/com/example/android/uamp/MainActivity.kt",
    "content": "/*\n * Copyright 2017 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.uamp\n\nimport android.media.AudioManager\nimport android.os.Bundle\nimport android.util.Log\nimport android.view.Menu\nimport androidx.activity.viewModels\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.lifecycle.Observer\nimport com.example.android.uamp.fragments.MediaItemFragment\nimport com.example.android.uamp.media.MusicService\nimport com.example.android.uamp.utils.Event\nimport com.example.android.uamp.utils.InjectorUtils\nimport com.example.android.uamp.viewmodels.MainActivityViewModel\nimport com.google.android.gms.cast.framework.CastButtonFactory\nimport com.google.android.gms.cast.framework.CastContext\n\nclass MainActivity : AppCompatActivity() {\n\n    private val viewModel by viewModels<MainActivityViewModel> {\n        InjectorUtils.provideMainActivityViewModel(this)\n    }\n    private var castContext: CastContext? = null\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        // Initialize the Cast context. This is required so that the media route button can be\n        // created in the AppBar\n        castContext = CastContext.getSharedInstance(this)\n\n        setContentView(R.layout.activity_main)\n\n        // Since UAMP is a music player, the volume controls should adjust the music volume while\n        // in the app.\n        volumeControlStream = AudioManager.STREAM_MUSIC\n\n        /**\n         * Observe [MainActivityViewModel.navigateToFragment] for [Event]s that request a\n         * fragment swap.\n         */\n        viewModel.navigateToFragment.observe(this, Observer {\n            it?.getContentIfNotHandled()?.let { fragmentRequest ->\n                val transaction = supportFragmentManager.beginTransaction()\n                transaction.replace(\n                    R.id.fragmentContainer, fragmentRequest.fragment, fragmentRequest.tag\n                )\n                if (fragmentRequest.backStack) transaction.addToBackStack(null)\n                transaction.commit()\n            }\n        })\n\n        /**\n         * Observe changes to the [MainActivityViewModel.rootMediaId]. When the app starts,\n         * and the UI connects to [MusicService], this will be updated and the app will show\n         * the initial list of media items.\n         */\n        viewModel.rootMediaId.observe(this,\n            Observer<String> { rootMediaId ->\n                rootMediaId?.let { navigateToMediaItem(it) }\n            })\n\n        /**\n         * Observe [MainActivityViewModel.navigateToMediaItem] for [Event]s indicating\n         * the user has requested to browse to a different [MediaItemData].\n         */\n        viewModel.navigateToMediaItem.observe(this, Observer {\n            it?.getContentIfNotHandled()?.let { mediaId ->\n                navigateToMediaItem(mediaId)\n            }\n        })\n    }\n\n    @Override\n    override fun onCreateOptionsMenu(menu: Menu?): Boolean {\n        super.onCreateOptionsMenu(menu)\n        menuInflater.inflate(R.menu.main_activity_menu, menu)\n\n        /**\n         * Set up a MediaRouteButton to allow the user to control the current media playback route\n         */\n        CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item)\n        return true\n    }\n\n    private fun navigateToMediaItem(mediaId: String) {\n        var fragment: MediaItemFragment? = getBrowseFragment(mediaId)\n        if (fragment == null) {\n            fragment = MediaItemFragment.newInstance(mediaId)\n            // If this is not the top level media (root), we add it to the fragment\n            // back stack, so that actionbar toggle and Back will work appropriately:\n            viewModel.showFragment(fragment, !isRootId(mediaId), mediaId)\n        }\n    }\n\n    private fun isRootId(mediaId: String) = mediaId == viewModel.rootMediaId.value\n\n    private fun getBrowseFragment(mediaId: String): MediaItemFragment? {\n        return supportFragmentManager.findFragmentByTag(mediaId) as? MediaItemFragment\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/example/android/uamp/MediaItemAdapter.kt",
    "content": "/*\n * Copyright 2017 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.uamp\n\nimport android.view.LayoutInflater\nimport android.view.ViewGroup\nimport android.widget.ImageView\nimport android.widget.TextView\nimport androidx.recyclerview.widget.ListAdapter\nimport androidx.recyclerview.widget.RecyclerView\nimport com.bumptech.glide.Glide\nimport com.example.android.uamp.MediaItemData.Companion.PLAYBACK_RES_CHANGED\nimport com.example.android.uamp.databinding.FragmentMediaitemBinding\nimport com.example.android.uamp.fragments.MediaItemFragment\n\n/**\n * [RecyclerView.Adapter] of [MediaItemData]s used by the [MediaItemFragment].\n */\nclass MediaItemAdapter(\n    private val itemClickedListener: (MediaItemData) -> Unit\n) : ListAdapter<MediaItemData, MediaViewHolder>(MediaItemData.diffCallback) {\n\n    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaViewHolder {\n        val inflater = LayoutInflater.from(parent.context)\n        val binding = FragmentMediaitemBinding.inflate(inflater, parent, false)\n        return MediaViewHolder(binding, itemClickedListener)\n    }\n\n    override fun onBindViewHolder(\n        holder: MediaViewHolder,\n        position: Int,\n        payloads: MutableList<Any>\n    ) {\n\n        val mediaItem = getItem(position)\n        var fullRefresh = payloads.isEmpty()\n\n        if (payloads.isNotEmpty()) {\n            payloads.forEach { payload ->\n                when (payload) {\n                    PLAYBACK_RES_CHANGED -> {\n                        holder.playbackState.setImageResource(mediaItem.playbackRes)\n                    }\n                    // If the payload wasn't understood, refresh the full item (to be safe).\n                    else -> fullRefresh = true\n                }\n            }\n        }\n\n        // Normally we only fully refresh the list item if it's being initially bound, but\n        // we might also do it if there was a payload that wasn't understood, just to ensure\n        // there isn't a stale item.\n        if (fullRefresh) {\n            holder.item = mediaItem\n            holder.titleView.text = mediaItem.title\n            holder.subtitleView.text = mediaItem.subtitle\n            holder.playbackState.setImageResource(mediaItem.playbackRes)\n\n            Glide.with(holder.albumArt)\n                .load(mediaItem.albumArtUri)\n                .placeholder(R.drawable.default_art)\n                .into(holder.albumArt)\n        }\n    }\n\n    override fun onBindViewHolder(holder: MediaViewHolder, position: Int) {\n        onBindViewHolder(holder, position, mutableListOf())\n    }\n}\n\nclass MediaViewHolder(\n    binding: FragmentMediaitemBinding,\n    itemClickedListener: (MediaItemData) -> Unit\n) : RecyclerView.ViewHolder(binding.root) {\n\n    val titleView: TextView = binding.title\n    val subtitleView: TextView = binding.subtitle\n    val albumArt: ImageView = binding.albumArt\n    val playbackState: ImageView = binding.itemState\n\n    var item: MediaItemData? = null\n\n    init {\n        binding.root.setOnClickListener {\n            item?.let { itemClickedListener(it) }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/example/android/uamp/MediaItemData.kt",
    "content": "/*\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.uamp\n\nimport android.net.Uri\nimport android.support.v4.media.MediaBrowserCompat\nimport android.support.v4.media.MediaBrowserCompat.MediaItem\nimport androidx.recyclerview.widget.DiffUtil\nimport com.example.android.uamp.viewmodels.MediaItemFragmentViewModel\n\n/**\n * Data class to encapsulate properties of a [MediaItem].\n *\n * If an item is [browsable] it means that it has a list of child media items that\n * can be retrieved by passing the mediaId to [MediaBrowserCompat.subscribe].\n *\n * Objects of this class are built from [MediaItem]s in\n * [MediaItemFragmentViewModel.subscriptionCallback].\n */\ndata class MediaItemData(\n    val mediaId: String,\n    val title: String,\n    val subtitle: String,\n    val albumArtUri: Uri,\n    val browsable: Boolean,\n    var playbackRes: Int\n) {\n\n    companion object {\n        /**\n         * Indicates [playbackRes] has changed.\n         */\n        const val PLAYBACK_RES_CHANGED = 1\n\n        /**\n         * [DiffUtil.ItemCallback] for a [MediaItemData].\n         *\n         * Since all [MediaItemData]s have a unique ID, it's easiest to check if two\n         * items are the same by simply comparing that ID.\n         *\n         * To check if the contents are the same, we use the same ID, but it may be the\n         * case that it's only the play state itself which has changed (from playing to\n         * paused, or perhaps a different item is the active item now). In this case\n         * we check both the ID and the playback resource.\n         *\n         * To calculate the payload, we use the simplest method possible:\n         * - Since the title, subtitle, and albumArtUri are constant (with respect to mediaId),\n         *   there's no reason to check if they've changed. If the mediaId is the same, none of\n         *   those properties have changed.\n         * - If the playback resource (playbackRes) has changed to reflect the change in playback\n         *   state, that's all that needs to be updated. We return [PLAYBACK_RES_CHANGED] as\n         *   the payload in this case.\n         * - If something else changed, then refresh the full item for simplicity.\n         */\n        val diffCallback = object : DiffUtil.ItemCallback<MediaItemData>() {\n            override fun areItemsTheSame(\n                oldItem: MediaItemData,\n                newItem: MediaItemData\n            ): Boolean =\n                oldItem.mediaId == newItem.mediaId\n\n            override fun areContentsTheSame(oldItem: MediaItemData, newItem: MediaItemData) =\n                oldItem.mediaId == newItem.mediaId && oldItem.playbackRes == newItem.playbackRes\n\n            override fun getChangePayload(oldItem: MediaItemData, newItem: MediaItemData) =\n                if (oldItem.playbackRes != newItem.playbackRes) {\n                    PLAYBACK_RES_CHANGED\n                } else null\n        }\n    }\n}\n\n"
  },
  {
    "path": "app/src/main/java/com/example/android/uamp/cast/UampCastOptionsProvider.kt",
    "content": "/*\n * Copyright 2020 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.uamp.cast\n\nimport android.content.Context\nimport com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider.APP_ID_DEFAULT_RECEIVER_WITH_DRM\nimport com.google.android.gms.cast.framework.CastOptions\nimport com.google.android.gms.cast.framework.OptionsProvider\nimport com.google.android.gms.cast.framework.SessionProvider\nimport com.google.android.gms.cast.framework.media.CastMediaOptions\n\n\nclass UampCastOptionsProvider : OptionsProvider {\n\n    override fun getCastOptions(context: Context?): CastOptions? {\n        return CastOptions.Builder()\n            // Use the Default Media Receiver with DRM support.\n            .setReceiverApplicationId(APP_ID_DEFAULT_RECEIVER_WITH_DRM)\n            .setCastMediaOptions(\n                CastMediaOptions.Builder()\n                    // We manage the media session and the notifications ourselves.\n                    .setMediaSessionEnabled(false)\n                    .setNotificationOptions(null)\n                    .build()\n            )\n            .setStopReceiverApplicationWhenEndingSession(true).build()\n    }\n\n    override fun getAdditionalSessionProviders(context: Context?): List<SessionProvider?>? {\n        return null\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/example/android/uamp/fragments/MediaItemFragment.kt",
    "content": "/*\n * Copyright 2017 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.uamp.fragments\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.fragment.app.Fragment\nimport androidx.fragment.app.activityViewModels\nimport androidx.fragment.app.viewModels\nimport androidx.lifecycle.Observer\nimport com.example.android.uamp.MediaItemAdapter\nimport com.example.android.uamp.databinding.FragmentMediaitemListBinding\nimport com.example.android.uamp.utils.InjectorUtils\nimport com.example.android.uamp.viewmodels.MainActivityViewModel\nimport com.example.android.uamp.viewmodels.MediaItemFragmentViewModel\n\n/**\n * A fragment representing a list of MediaItems.\n */\nclass MediaItemFragment : Fragment() {\n    private val mainActivityViewModel by activityViewModels<MainActivityViewModel> {\n        InjectorUtils.provideMainActivityViewModel(requireContext())\n    }\n    private val mediaItemFragmentViewModel by viewModels<MediaItemFragmentViewModel> {\n        InjectorUtils.provideMediaItemFragmentViewModel(requireContext(), mediaId)\n    }\n\n    private lateinit var mediaId: String\n    private lateinit var binding: FragmentMediaitemListBinding\n\n    private val listAdapter = MediaItemAdapter { clickedItem ->\n        mainActivityViewModel.mediaItemClicked(clickedItem)\n    }\n\n    companion object {\n        fun newInstance(mediaId: String): MediaItemFragment {\n\n            return MediaItemFragment().apply {\n                arguments = Bundle().apply {\n                    putString(MEDIA_ID_ARG, mediaId)\n                }\n            }\n        }\n    }\n\n    override fun onCreateView(\n        inflater: LayoutInflater, container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View? {\n        binding = FragmentMediaitemListBinding.inflate(inflater, container, false)\n        return binding.root\n    }\n\n    override fun onActivityCreated(savedInstanceState: Bundle?) {\n        super.onActivityCreated(savedInstanceState)\n\n        // Always true, but lets lint know that as well.\n        mediaId = arguments?.getString(MEDIA_ID_ARG) ?: return\n\n        mediaItemFragmentViewModel.mediaItems.observe(viewLifecycleOwner,\n            Observer { list ->\n                binding.loadingSpinner.visibility =\n                    if (list?.isNotEmpty() == true) View.GONE else View.VISIBLE\n                listAdapter.submitList(list)\n            })\n        mediaItemFragmentViewModel.networkError.observe(viewLifecycleOwner,\n            Observer { error ->\n                if (error) {\n                    binding.loadingSpinner.visibility = View.GONE\n                    binding.networkError.visibility = View.VISIBLE\n                } else {\n                    binding.networkError.visibility = View.GONE\n                }\n            })\n\n        // Set the adapter\n        binding.list.adapter = listAdapter\n    }\n}\n\nprivate const val MEDIA_ID_ARG = \"com.example.android.uamp.fragments.MediaItemFragment.MEDIA_ID\"\n"
  },
  {
    "path": "app/src/main/java/com/example/android/uamp/fragments/NowPlayingFragment.kt",
    "content": "/*\n * Copyright 2019 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.uamp.fragments\n\nimport android.net.Uri\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.fragment.app.Fragment\nimport androidx.fragment.app.activityViewModels\nimport androidx.fragment.app.viewModels\nimport androidx.lifecycle.Observer\nimport com.bumptech.glide.Glide\nimport com.example.android.uamp.R\nimport com.example.android.uamp.databinding.FragmentNowplayingBinding\nimport com.example.android.uamp.utils.InjectorUtils\nimport com.example.android.uamp.viewmodels.MainActivityViewModel\nimport com.example.android.uamp.viewmodels.NowPlayingFragmentViewModel\nimport com.example.android.uamp.viewmodels.NowPlayingFragmentViewModel.NowPlayingMetadata\n\n/**\n * A fragment representing the current media item being played.\n */\nclass NowPlayingFragment : Fragment() {\n    private val mainActivityViewModel by activityViewModels<MainActivityViewModel> {\n        InjectorUtils.provideMainActivityViewModel(requireContext())\n    }\n    private val nowPlayingViewModel by viewModels<NowPlayingFragmentViewModel> {\n        InjectorUtils.provideNowPlayingFragmentViewModel(requireContext())\n    }\n\n    lateinit var binding: FragmentNowplayingBinding\n\n    companion object {\n        fun newInstance() = NowPlayingFragment()\n    }\n\n    override fun onCreateView(\n        inflater: LayoutInflater, container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View? {\n        binding = FragmentNowplayingBinding.inflate(inflater, container, false)\n        return binding.root\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n\n        // Always true, but lets lint know that as well.\n        val context = activity ?: return\n\n        // Attach observers to the LiveData coming from this ViewModel\n        nowPlayingViewModel.mediaMetadata.observe(viewLifecycleOwner,\n            Observer { mediaItem -> updateUI(view, mediaItem) })\n        nowPlayingViewModel.mediaButtonRes.observe(viewLifecycleOwner,\n            Observer { res ->\n                binding.mediaButton.setImageResource(res)\n            })\n        nowPlayingViewModel.mediaPosition.observe(viewLifecycleOwner,\n            Observer { pos ->\n                binding.position.text = NowPlayingMetadata.timestampToMSS(context, pos)\n            })\n\n        // Setup UI handlers for buttons\n        binding.mediaButton.setOnClickListener {\n            nowPlayingViewModel.mediaMetadata.value?.let { mainActivityViewModel.playMediaId(it.id) }\n        }\n\n        // Initialize playback duration and position to zero\n        binding.duration.text = NowPlayingMetadata.timestampToMSS(context, 0L)\n        binding.position.text = NowPlayingMetadata.timestampToMSS(context, 0L)\n    }\n\n    /**\n     * Internal function used to update all UI elements except for the current item playback\n     */\n    private fun updateUI(view: View, metadata: NowPlayingMetadata) = with(binding) {\n        if (metadata.albumArtUri == Uri.EMPTY) {\n            albumArt.setImageResource(R.drawable.ic_album_black_24dp)\n        } else {\n            Glide.with(view)\n                .load(metadata.albumArtUri)\n                .into(albumArt)\n        }\n        title.text = metadata.title\n        subtitle.text = metadata.subtitle\n        duration.text = metadata.duration\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/example/android/uamp/utils/Event.kt",
    "content": "/*\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.uamp.utils\n\n/**\n * Used as a wrapper for data that is exposed via a LiveData that represents an event.\n *\n * For more information, see:\n * https://medium.com/google-developers/livedata-with-events-ac2622673150\n */\nclass Event<out T>(private val content: T) {\n\n    var hasBeenHandled = false\n        private set // Allow external read but not write\n\n    /**\n     * Returns the content and prevents its use again.\n     */\n    fun getContentIfNotHandled(): T? {\n        return if (hasBeenHandled) {\n            null\n        } else {\n            hasBeenHandled = true\n            content\n        }\n    }\n\n    /**\n     * Returns the content, even if it's already been handled.\n     */\n    fun peekContent(): T = content\n}"
  },
  {
    "path": "app/src/main/java/com/example/android/uamp/utils/InjectorUtils.kt",
    "content": "/*\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.uamp.utils\n\nimport android.app.Application\nimport android.content.ComponentName\nimport android.content.Context\nimport com.example.android.uamp.common.MusicServiceConnection\nimport com.example.android.uamp.media.MusicService\nimport com.example.android.uamp.viewmodels.MainActivityViewModel\nimport com.example.android.uamp.viewmodels.MediaItemFragmentViewModel\nimport com.example.android.uamp.viewmodels.NowPlayingFragmentViewModel\n\n/**\n * Static methods used to inject classes needed for various Activities and Fragments.\n */\nobject InjectorUtils {\n    private fun provideMusicServiceConnection(context: Context): MusicServiceConnection {\n        return MusicServiceConnection.getInstance(\n            context,\n            ComponentName(context, MusicService::class.java)\n        )\n    }\n\n    fun provideMainActivityViewModel(context: Context): MainActivityViewModel.Factory {\n        val applicationContext = context.applicationContext\n        val musicServiceConnection = provideMusicServiceConnection(applicationContext)\n        return MainActivityViewModel.Factory(musicServiceConnection)\n    }\n\n    fun provideMediaItemFragmentViewModel(context: Context, mediaId: String)\n            : MediaItemFragmentViewModel.Factory {\n        val applicationContext = context.applicationContext\n        val musicServiceConnection = provideMusicServiceConnection(applicationContext)\n        return MediaItemFragmentViewModel.Factory(mediaId, musicServiceConnection)\n    }\n\n    fun provideNowPlayingFragmentViewModel(context: Context)\n            : NowPlayingFragmentViewModel.Factory {\n        val applicationContext = context.applicationContext\n        val musicServiceConnection = provideMusicServiceConnection(applicationContext)\n        return NowPlayingFragmentViewModel.Factory(\n            applicationContext as Application, musicServiceConnection\n        )\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/example/android/uamp/viewmodels/MainActivityViewModel.kt",
    "content": "/*\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.uamp.viewmodels\n\nimport android.support.v4.media.MediaBrowserCompat\nimport android.util.Log\nimport androidx.fragment.app.Fragment\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.Observer\nimport androidx.lifecycle.Transformations\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.ViewModelProvider\nimport com.example.android.uamp.MainActivity\nimport com.example.android.uamp.MediaItemData\nimport com.example.android.uamp.common.MusicServiceConnection\nimport com.example.android.uamp.fragments.NowPlayingFragment\nimport com.example.android.uamp.media.extensions.id\nimport com.example.android.uamp.media.extensions.isPlayEnabled\nimport com.example.android.uamp.media.extensions.isPlaying\nimport com.example.android.uamp.media.extensions.isPrepared\nimport com.example.android.uamp.utils.Event\n\n/**\n * Small [ViewModel] that watches a [MusicServiceConnection] to become connected\n * and provides the root/initial media ID of the underlying [MediaBrowserCompat].\n */\nclass MainActivityViewModel(\n    private val musicServiceConnection: MusicServiceConnection\n) : ViewModel() {\n\n    val rootMediaId: LiveData<String> =\n        Transformations.map(musicServiceConnection.isConnected) { isConnected ->\n            if (isConnected) {\n                musicServiceConnection.rootMediaId\n            } else {\n                null\n            }\n        }\n\n    /**\n     * [navigateToMediaItem] acts as an \"event\", rather than state. [Observer]s\n     * are notified of the change as usual with [LiveData], but only one [Observer]\n     * will actually read the data. For more information, check the [Event] class.\n     */\n    val navigateToMediaItem: LiveData<Event<String>> get() = _navigateToMediaItem\n    private val _navigateToMediaItem = MutableLiveData<Event<String>>()\n\n    /**\n     * This [LiveData] object is used to notify the MainActivity that the main\n     * content fragment needs to be swapped. Information about the new fragment\n     * is conveniently wrapped by the [Event] class.\n     */\n    val navigateToFragment: LiveData<Event<FragmentNavigationRequest>> get() = _navigateToFragment\n    private val _navigateToFragment = MutableLiveData<Event<FragmentNavigationRequest>>()\n\n    /**\n     * This method takes a [MediaItemData] and routes it depending on whether it's\n     * browsable (i.e.: it's the parent media item of a set of other media items,\n     * such as an album), or not.\n     *\n     * If the item is browsable, handle it by sending an event to the Activity to\n     * browse to it, otherwise play it.\n     */\n    fun mediaItemClicked(clickedItem: MediaItemData) {\n        if (clickedItem.browsable) {\n            browseToItem(clickedItem)\n        } else {\n            playMedia(clickedItem, pauseAllowed = false)\n            showFragment(NowPlayingFragment.newInstance())\n        }\n    }\n\n\n    /**\n     * Convenience method used to swap the fragment shown in the main activity\n     *\n     * @param fragment the fragment to show\n     * @param backStack if true, add this transaction to the back stack\n     * @param tag the name to use for this fragment in the stack\n     */\n    fun showFragment(fragment: Fragment, backStack: Boolean = true, tag: String? = null) {\n        _navigateToFragment.value = Event(FragmentNavigationRequest(fragment, backStack, tag))\n    }\n\n\n    /**\n     * This posts a browse [Event] that will be handled by the\n     * observer in [MainActivity].\n     */\n    private fun browseToItem(mediaItem: MediaItemData) {\n        _navigateToMediaItem.value = Event(mediaItem.mediaId)\n    }\n\n    /**\n     * This method takes a [MediaItemData] and does one of the following:\n     * - If the item is *not* the active item, then play it directly.\n     * - If the item *is* the active item, check whether \"pause\" is a permitted command. If it is,\n     *   then pause playback, otherwise send \"play\" to resume playback.\n     */\n    fun playMedia(mediaItem: MediaItemData, pauseAllowed: Boolean = true) {\n        val nowPlaying = musicServiceConnection.nowPlaying.value\n        val transportControls = musicServiceConnection.transportControls\n\n        val isPrepared = musicServiceConnection.playbackState.value?.isPrepared ?: false\n        if (isPrepared && mediaItem.mediaId == nowPlaying?.id) {\n            musicServiceConnection.playbackState.value?.let { playbackState ->\n                when {\n                    playbackState.isPlaying ->\n                        if (pauseAllowed) transportControls.pause() else Unit\n                    playbackState.isPlayEnabled -> transportControls.play()\n                    else -> {\n                        Log.w(\n                            TAG, \"Playable item clicked but neither play nor pause are enabled!\" +\n                                    \" (mediaId=${mediaItem.mediaId})\"\n                        )\n                    }\n                }\n            }\n        } else {\n            transportControls.playFromMediaId(mediaItem.mediaId, null)\n        }\n    }\n\n    fun playMediaId(mediaId: String) {\n        val nowPlaying = musicServiceConnection.nowPlaying.value\n        val transportControls = musicServiceConnection.transportControls\n\n        val isPrepared = musicServiceConnection.playbackState.value?.isPrepared ?: false\n        if (isPrepared && mediaId == nowPlaying?.id) {\n            musicServiceConnection.playbackState.value?.let { playbackState ->\n                when {\n                    playbackState.isPlaying -> transportControls.pause()\n                    playbackState.isPlayEnabled -> transportControls.play()\n                    else -> {\n                        Log.w(\n                            TAG, \"Playable item clicked but neither play nor pause are enabled!\" +\n                                    \" (mediaId=$mediaId)\"\n                        )\n                    }\n                }\n            }\n        } else {\n            transportControls.playFromMediaId(mediaId, null)\n        }\n    }\n\n    class Factory(\n        private val musicServiceConnection: MusicServiceConnection\n    ) : ViewModelProvider.NewInstanceFactory() {\n\n        @Suppress(\"unchecked_cast\")\n        override fun <T : ViewModel?> create(modelClass: Class<T>): T {\n            return MainActivityViewModel(musicServiceConnection) as T\n        }\n    }\n}\n\n/**\n * Helper class used to pass fragment navigation requests between MainActivity\n * and its corresponding ViewModel.\n */\ndata class FragmentNavigationRequest(\n    val fragment: Fragment,\n    val backStack: Boolean = false,\n    val tag: String? = null\n)\n\nprivate const val TAG = \"MainActivitytVM\"\n"
  },
  {
    "path": "app/src/main/java/com/example/android/uamp/viewmodels/MediaItemFragmentViewModel.kt",
    "content": "/*\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.uamp.viewmodels\n\nimport android.support.v4.media.MediaBrowserCompat\nimport android.support.v4.media.MediaBrowserCompat.MediaItem\nimport android.support.v4.media.MediaBrowserCompat.SubscriptionCallback\nimport android.support.v4.media.MediaMetadataCompat\nimport android.support.v4.media.session.PlaybackStateCompat\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.Observer\nimport androidx.lifecycle.Transformations\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.ViewModelProvider\nimport com.example.android.uamp.MediaItemData\nimport com.example.android.uamp.R\nimport com.example.android.uamp.common.EMPTY_PLAYBACK_STATE\nimport com.example.android.uamp.common.MusicServiceConnection\nimport com.example.android.uamp.common.NOTHING_PLAYING\nimport com.example.android.uamp.fragments.MediaItemFragment\nimport com.example.android.uamp.media.extensions.id\nimport com.example.android.uamp.media.extensions.isPlaying\n\n/**\n * [ViewModel] for [MediaItemFragment].\n */\nclass MediaItemFragmentViewModel(\n    private val mediaId: String,\n    musicServiceConnection: MusicServiceConnection\n) : ViewModel() {\n\n    /**\n     * Use a backing property so consumers of mediaItems only get a [LiveData] instance so\n     * they don't inadvertently modify it.\n     */\n    private val _mediaItems = MutableLiveData<List<MediaItemData>>()\n    val mediaItems: LiveData<List<MediaItemData>> = _mediaItems\n\n    /**\n     * Pass the status of the [MusicServiceConnection.networkFailure] through.\n     */\n    val networkError = Transformations.map(musicServiceConnection.networkFailure) { it }\n\n    private val subscriptionCallback = object : SubscriptionCallback() {\n        override fun onChildrenLoaded(parentId: String, children: List<MediaItem>) {\n            val itemsList = children.map { child ->\n                val subtitle = child.description.subtitle ?: \"\"\n                MediaItemData(\n                    child.mediaId!!,\n                    child.description.title.toString(),\n                    subtitle.toString(),\n                    child.description.iconUri!!,\n                    child.isBrowsable,\n                    getResourceForMediaId(child.mediaId!!)\n                )\n            }\n            _mediaItems.postValue(itemsList)\n        }\n    }\n\n    /**\n     * When the session's [PlaybackStateCompat] changes, the [mediaItems] need to be updated\n     * so the correct [MediaItemData.playbackRes] is displayed on the active item.\n     * (i.e.: play/pause button or blank)\n     */\n    private val playbackStateObserver = Observer<PlaybackStateCompat> {\n        val playbackState = it ?: EMPTY_PLAYBACK_STATE\n        val metadata = musicServiceConnection.nowPlaying.value ?: NOTHING_PLAYING\n        if (metadata.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID) != null) {\n            _mediaItems.postValue(updateState(playbackState, metadata))\n        }\n    }\n\n    /**\n     * When the session's [MediaMetadataCompat] changes, the [mediaItems] need to be updated\n     * as it means the currently active item has changed. As a result, the new, and potentially\n     * old item (if there was one), both need to have their [MediaItemData.playbackRes]\n     * changed. (i.e.: play/pause button or blank)\n     */\n    private val mediaMetadataObserver = Observer<MediaMetadataCompat> {\n        val playbackState = musicServiceConnection.playbackState.value ?: EMPTY_PLAYBACK_STATE\n        val metadata = it ?: NOTHING_PLAYING\n        if (metadata.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID) != null) {\n            _mediaItems.postValue(updateState(playbackState, metadata))\n        }\n    }\n\n    /**\n     * Because there's a complex dance between this [ViewModel] and the [MusicServiceConnection]\n     * (which is wrapping a [MediaBrowserCompat] object), the usual guidance of using\n     * [Transformations] doesn't quite work.\n     *\n     * Specifically there's three things that are watched that will cause the single piece of\n     * [LiveData] exposed from this class to be updated.\n     *\n     * [subscriptionCallback] (defined above) is called if/when the children of this\n     * ViewModel's [mediaId] changes.\n     *\n     * [MusicServiceConnection.playbackState] changes state based on the playback state of\n     * the player, which can change the [MediaItemData.playbackRes]s in the list.\n     *\n     * [MusicServiceConnection.nowPlaying] changes based on the item that's being played,\n     * which can also change the [MediaItemData.playbackRes]s in the list.\n     */\n    private val musicServiceConnection = musicServiceConnection.also {\n        it.subscribe(mediaId, subscriptionCallback)\n\n        it.playbackState.observeForever(playbackStateObserver)\n        it.nowPlaying.observeForever(mediaMetadataObserver)\n    }\n\n    /**\n     * Since we use [LiveData.observeForever] above (in [musicServiceConnection]), we want\n     * to call [LiveData.removeObserver] here to prevent leaking resources when the [ViewModel]\n     * is not longer in use.\n     *\n     * For more details, see the kdoc on [musicServiceConnection] above.\n     */\n    override fun onCleared() {\n        super.onCleared()\n\n        // Remove the permanent observers from the MusicServiceConnection.\n        musicServiceConnection.playbackState.removeObserver(playbackStateObserver)\n        musicServiceConnection.nowPlaying.removeObserver(mediaMetadataObserver)\n\n        // And then, finally, unsubscribe the media ID that was being watched.\n        musicServiceConnection.unsubscribe(mediaId, subscriptionCallback)\n    }\n\n    private fun getResourceForMediaId(mediaId: String): Int {\n        val isActive = mediaId == musicServiceConnection.nowPlaying.value?.id\n        val isPlaying = musicServiceConnection.playbackState.value?.isPlaying ?: false\n        return when {\n            !isActive -> NO_RES\n            isPlaying -> R.drawable.ic_pause_black_24dp\n            else -> R.drawable.ic_play_arrow_black_24dp\n        }\n    }\n\n    private fun updateState(\n        playbackState: PlaybackStateCompat,\n        mediaMetadata: MediaMetadataCompat\n    ): List<MediaItemData> {\n\n        val newResId = when (playbackState.isPlaying) {\n            true -> R.drawable.ic_pause_black_24dp\n            else -> R.drawable.ic_play_arrow_black_24dp\n        }\n\n        return mediaItems.value?.map {\n            val useResId = if (it.mediaId == mediaMetadata.id) newResId else NO_RES\n            it.copy(playbackRes = useResId)\n        } ?: emptyList()\n    }\n\n    class Factory(\n        private val mediaId: String,\n        private val musicServiceConnection: MusicServiceConnection\n    ) : ViewModelProvider.NewInstanceFactory() {\n\n        @Suppress(\"unchecked_cast\")\n        override fun <T : ViewModel?> create(modelClass: Class<T>): T {\n            return MediaItemFragmentViewModel(mediaId, musicServiceConnection) as T\n        }\n    }\n}\n\nprivate const val TAG = \"MediaItemFragmentVM\"\nprivate const val NO_RES = 0\n"
  },
  {
    "path": "app/src/main/java/com/example/android/uamp/viewmodels/NowPlayingFragmentViewModel.kt",
    "content": "/*\n * Copyright 2019 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.uamp.viewmodels\n\nimport android.app.Application\nimport android.content.Context\nimport android.net.Uri\nimport android.os.Handler\nimport android.os.Looper\nimport android.support.v4.media.MediaBrowserCompat\nimport android.support.v4.media.MediaMetadataCompat\nimport android.support.v4.media.session.PlaybackStateCompat\nimport androidx.lifecycle.AndroidViewModel\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.Observer\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.ViewModelProvider\nimport com.example.android.uamp.R\nimport com.example.android.uamp.common.EMPTY_PLAYBACK_STATE\nimport com.example.android.uamp.common.MusicServiceConnection\nimport com.example.android.uamp.common.NOTHING_PLAYING\nimport com.example.android.uamp.fragments.NowPlayingFragment\nimport com.example.android.uamp.media.extensions.albumArtUri\nimport com.example.android.uamp.media.extensions.currentPlayBackPosition\nimport com.example.android.uamp.media.extensions.displaySubtitle\nimport com.example.android.uamp.media.extensions.duration\nimport com.example.android.uamp.media.extensions.id\nimport com.example.android.uamp.media.extensions.isPlaying\nimport com.example.android.uamp.media.extensions.title\n\n/**\n * [ViewModel] for [NowPlayingFragment] which displays the album art in full size.\n * It extends AndroidViewModel and uses the [Application]'s context to be able to reference string\n * resources.\n */\nclass NowPlayingFragmentViewModel(\n    private val app: Application,\n    musicServiceConnection: MusicServiceConnection\n) : AndroidViewModel(app) {\n\n    /**\n     * Utility class used to represent the metadata necessary to display the\n     * media item currently being played.\n     */\n    data class NowPlayingMetadata(\n        val id: String,\n        val albumArtUri: Uri,\n        val title: String?,\n        val subtitle: String?,\n        val duration: String\n    ) {\n\n        companion object {\n            /**\n             * Utility method to convert milliseconds to a display of minutes and seconds\n             */\n            fun timestampToMSS(context: Context, position: Long): String {\n                val totalSeconds = Math.floor(position / 1E3).toInt()\n                val minutes = totalSeconds / 60\n                val remainingSeconds = totalSeconds - (minutes * 60)\n                return if (position < 0) context.getString(R.string.duration_unknown)\n                else context.getString(R.string.duration_format).format(minutes, remainingSeconds)\n            }\n        }\n    }\n\n    private var playbackState: PlaybackStateCompat = EMPTY_PLAYBACK_STATE\n    val mediaMetadata = MutableLiveData<NowPlayingMetadata>()\n    val mediaPosition = MutableLiveData<Long>().apply {\n        postValue(0L)\n    }\n    val mediaButtonRes = MutableLiveData<Int>().apply {\n        postValue(R.drawable.ic_album_black_24dp)\n    }\n\n    private var updatePosition = true\n    private val handler = Handler(Looper.getMainLooper())\n\n    /**\n     * When the session's [PlaybackStateCompat] changes, the [mediaItems] need to be updated\n     * so the correct [MediaItemData.playbackRes] is displayed on the active item.\n     * (i.e.: play/pause button or blank)\n     */\n    private val playbackStateObserver = Observer<PlaybackStateCompat> {\n        playbackState = it ?: EMPTY_PLAYBACK_STATE\n        val metadata = musicServiceConnection.nowPlaying.value ?: NOTHING_PLAYING\n        updateState(playbackState, metadata)\n    }\n\n    /**\n     * When the session's [MediaMetadataCompat] changes, the [mediaItems] need to be updated\n     * as it means the currently active item has changed. As a result, the new, and potentially\n     * old item (if there was one), both need to have their [MediaItemData.playbackRes]\n     * changed. (i.e.: play/pause button or blank)\n     */\n    private val mediaMetadataObserver = Observer<MediaMetadataCompat> {\n        updateState(playbackState, it)\n    }\n\n    /**\n     * Because there's a complex dance between this [ViewModel] and the [MusicServiceConnection]\n     * (which is wrapping a [MediaBrowserCompat] object), the usual guidance of using\n     * [Transformations] doesn't quite work.\n     *\n     * Specifically there's three things that are watched that will cause the single piece of\n     * [LiveData] exposed from this class to be updated.\n     *\n     * [MusicServiceConnection.playbackState] changes state based on the playback state of\n     * the player, which can change the [MediaItemData.playbackRes]s in the list.\n     *\n     * [MusicServiceConnection.nowPlaying] changes based on the item that's being played,\n     * which can also change the [MediaItemData.playbackRes]s in the list.\n     */\n    private val musicServiceConnection = musicServiceConnection.also {\n        it.playbackState.observeForever(playbackStateObserver)\n        it.nowPlaying.observeForever(mediaMetadataObserver)\n        checkPlaybackPosition()\n    }\n\n    /**\n     * Internal function that recursively calls itself every [POSITION_UPDATE_INTERVAL_MILLIS] ms\n     * to check the current playback position and updates the corresponding LiveData object when it\n     * has changed.\n     */\n    private fun checkPlaybackPosition(): Boolean = handler.postDelayed({\n        val currPosition = playbackState.currentPlayBackPosition\n        if (mediaPosition.value != currPosition)\n            mediaPosition.postValue(currPosition)\n        if (updatePosition)\n            checkPlaybackPosition()\n    }, POSITION_UPDATE_INTERVAL_MILLIS)\n\n    /**\n     * Since we use [LiveData.observeForever] above (in [musicServiceConnection]), we want\n     * to call [LiveData.removeObserver] here to prevent leaking resources when the [ViewModel]\n     * is not longer in use.\n     *\n     * For more details, see the kdoc on [musicServiceConnection] above.\n     */\n    override fun onCleared() {\n        super.onCleared()\n\n        // Remove the permanent observers from the MusicServiceConnection.\n        musicServiceConnection.playbackState.removeObserver(playbackStateObserver)\n        musicServiceConnection.nowPlaying.removeObserver(mediaMetadataObserver)\n\n        // Stop updating the position\n        updatePosition = false\n    }\n\n    private fun updateState(\n        playbackState: PlaybackStateCompat,\n        mediaMetadata: MediaMetadataCompat\n    ) {\n\n        // Only update media item once we have duration available\n        if (mediaMetadata.duration != 0L && mediaMetadata.id != null) {\n            val nowPlayingMetadata = NowPlayingMetadata(\n                mediaMetadata.id!!,\n                mediaMetadata.albumArtUri,\n                mediaMetadata.title?.trim(),\n                mediaMetadata.displaySubtitle?.trim(),\n                NowPlayingMetadata.timestampToMSS(app, mediaMetadata.duration)\n            )\n            this.mediaMetadata.postValue(nowPlayingMetadata)\n        }\n\n        // Update the media button resource ID\n        mediaButtonRes.postValue(\n            when (playbackState.isPlaying) {\n                true -> R.drawable.ic_pause_black_24dp\n                else -> R.drawable.ic_play_arrow_black_24dp\n            }\n        )\n    }\n\n    class Factory(\n        private val app: Application,\n        private val musicServiceConnection: MusicServiceConnection\n    ) : ViewModelProvider.NewInstanceFactory() {\n\n        @Suppress(\"unchecked_cast\")\n        override fun <T : ViewModel?> create(modelClass: Class<T>): T {\n            return NowPlayingFragmentViewModel(app, musicServiceConnection) as T\n        }\n    }\n}\n\nprivate const val TAG = \"NowPlayingFragmentVM\"\nprivate const val POSITION_UPDATE_INTERVAL_MILLIS = 100L\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_album_black_24dp.xml",
    "content": "<!--\n  ~ Copyright 2017 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"36dp\"\n    android:height=\"36dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#FF000000\"\n        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\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2018 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"108\"\n    android:viewportHeight=\"108\">\n    <path\n        android:fillColor=\"#26A69A\"\n        android:pathData=\"M0,0h108v108h-108z\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M9,0L9,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,0L19,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M29,0L29,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M39,0L39,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M49,0L49,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M59,0L59,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M69,0L69,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M79,0L79,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M89,0L89,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M99,0L99,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,9L108,9\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,19L108,19\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,29L108,29\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,39L108,39\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,49L108,49\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,59L108,59\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,69L108,69\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,79L108,79\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,89L108,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,99L108,99\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,29L89,29\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,39L89,39\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,49L89,49\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,59L89,59\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,69L89,69\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,79L89,79\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M29,19L29,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M39,19L39,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M49,19L49,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M59,19L59,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M69,19L69,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M79,19L79,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_pause_black_24dp.xml",
    "content": "<!--\n  ~ Copyright 2017 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_play_arrow_black_24dp.xml",
    "content": "<!--\n  ~ Copyright 2017 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M8,5v14l11,-7z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_signal_wifi_off_black_24dp.xml",
    "content": "<!--\n  ~ Copyright 2018 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#FF000000\"\n        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\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/media_item_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2018 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item\n        android:bottom=\"@dimen/media_item_negative_half_radius\"\n        android:left=\"@dimen/media_item_negative_half_radius\"\n        android:right=\"@dimen/media_item_negative_half_radius\"\n        android:top=\"@dimen/media_item_negative_half_radius\">\n        <shape android:shape=\"rectangle\">\n            <stroke\n                android:width=\"@dimen/media_item_half_radius\"\n                android:color=\"@color/mediaListBackground\" />\n            <corners android:radius=\"@dimen/media_item_radius\" />\n            <solid android:color=\"#ffffff\" />\n        </shape>\n    </item>\n</layer-list>"
  },
  {
    "path": "app/src/main/res/drawable/media_item_mask.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2018 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item\n        android:bottom=\"@dimen/media_item_negative_half_radius\"\n        android:left=\"@dimen/media_item_negative_half_radius\"\n        android:right=\"@dimen/media_item_negative_half_radius\"\n        android:top=\"@dimen/media_item_negative_half_radius\">\n        <shape android:shape=\"rectangle\">\n            <stroke\n                android:width=\"@dimen/media_item_half_radius\"\n                android:color=\"@color/mediaListBackground\" />\n            <corners\n                android:bottomLeftRadius=\"@dimen/media_item_radius\"\n                android:bottomRightRadius=\"0dp\"\n                android:topLeftRadius=\"@dimen/media_item_radius\"\n                android:topRightRadius=\"0dp\" />\n        </shape>\n    </item>\n</layer-list>"
  },
  {
    "path": "app/src/main/res/drawable/media_overlay_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n    <gradient\n        android:angle=\"90\"\n        android:centerColor=\"#AAFFFFFF\"\n        android:endColor=\"#00FFFFFF\"\n        android:startColor=\"#CCFFFFFF\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable-v24/ic_launcher_foreground.xml",
    "content": "<!--\n  ~ Copyright 2017 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:aapt=\"http://schemas.android.com/aapt\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"108\"\n    android:viewportHeight=\"108\">\n    <path\n        android:fillType=\"evenOdd\"\n        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\"\n        android:strokeWidth=\"1\"\n        android:strokeColor=\"#00000000\">\n        <aapt:attr name=\"android:fillColor\">\n            <gradient\n                android:endX=\"78.5885\"\n                android:endY=\"90.9159\"\n                android:startX=\"48.7653\"\n                android:startY=\"61.0927\"\n                android:type=\"linear\">\n                <item\n                    android:color=\"#44000000\"\n                    android:offset=\"0.0\" />\n                <item\n                    android:color=\"#00000000\"\n                    android:offset=\"1.0\" />\n            </gradient>\n        </aapt:attr>\n    </path>\n    <path\n        android:fillColor=\"#FFFFFF\"\n        android:fillType=\"nonZero\"\n        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\"\n        android:strokeWidth=\"1\"\n        android:strokeColor=\"#00000000\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/layout/activity_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2017 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/fragmentContainer\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\"com.example.android.uamp.MainActivity\" />"
  },
  {
    "path": "app/src/main/res/layout/cast_context_error.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2018 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n     See the License for the specific language governing permissions and\n     limitations under the License.\n-->\n<TextView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/textView\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:gravity=\"center\"\n    android:textSize=\"20sp\"\n    android:text=\"@string/cast_context_error\"/>\n"
  },
  {
    "path": "app/src/main/res/layout/fragment_mediaitem.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2017 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:layout_margin=\"2dp\"\n    android:background=\"@drawable/media_item_background\">\n\n    <ImageView\n        android:id=\"@+id/albumArt\"\n        android:layout_width=\"@dimen/media_item_art\"\n        android:layout_height=\"@dimen/media_item_art\"\n        android:scaleType=\"centerCrop\"\n        app:layout_constraintLeft_toLeftOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        app:srcCompat=\"@drawable/ic_album_black_24dp\"\n        tools:ignore=\"ContentDescription\" />\n\n    <ImageView\n        android:id=\"@+id/item_state\"\n        style=\"@style/MediaStateIcon\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"0dp\"\n        android:scaleType=\"center\"\n        app:layout_constraintBottom_toBottomOf=\"@+id/albumArt\"\n        app:layout_constraintLeft_toLeftOf=\"@+id/albumArt\"\n        app:layout_constraintRight_toRightOf=\"@+id/albumArt\"\n        app:layout_constraintTop_toTopOf=\"@+id/albumArt\"\n        tools:ignore=\"ContentDescription\" />\n\n    <ImageView\n        android:layout_width=\"0dp\"\n        android:layout_height=\"0dp\"\n        android:src=\"@drawable/media_item_mask\"\n        app:layout_constraintBottom_toBottomOf=\"@+id/albumArt\"\n        app:layout_constraintLeft_toLeftOf=\"@+id/albumArt\"\n        app:layout_constraintRight_toRightOf=\"@+id/albumArt\"\n        app:layout_constraintTop_toTopOf=\"@+id/albumArt\"\n        tools:ignore=\"ContentDescription\" />\n\n    <TextView\n        android:id=\"@+id/title\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginStart=\"@dimen/text_margin\"\n        android:layout_marginEnd=\"@dimen/text_margin\"\n        android:ellipsize=\"end\"\n        android:maxLines=\"1\"\n        android:textAppearance=\"@style/TextAppearance.Uamp.Title\"\n        app:layout_constraintBottom_toTopOf=\"@+id/center_guideline\"\n        app:layout_constraintLeft_toRightOf=\"@+id/albumArt\"\n        app:layout_constraintRight_toRightOf=\"parent\"\n        tools:text=\"Song Title\" />\n\n    <TextView\n        android:id=\"@+id/subtitle\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginEnd=\"@dimen/text_margin\"\n        android:ellipsize=\"end\"\n        android:maxLines=\"1\"\n        android:textAppearance=\"@style/TextAppearance.Uamp.Subtitle\"\n        app:layout_constraintLeft_toLeftOf=\"@+id/title\"\n        app:layout_constraintRight_toRightOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@+id/center_guideline\"\n        tools:text=\"Artist\" />\n\n    <androidx.constraintlayout.widget.Guideline\n        android:id=\"@+id/center_guideline\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"horizontal\"\n        app:layout_constraintGuide_percent=\"0.5\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/fragment_mediaitem_list.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2017 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <androidx.recyclerview.widget.RecyclerView\n        android:id=\"@+id/list\"\n        android:name=\"com.example.android.uamp.MediaItemFragment\"\n        style=\"@style/MediaItemList\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:orientation=\"vertical\"\n        app:layoutManager=\"androidx.recyclerview.widget.LinearLayoutManager\"\n        tools:context=\"com.example.android.uamp.fragments.MediaItemFragment\"\n        tools:listitem=\"@layout/fragment_mediaitem\" />\n\n    <ProgressBar\n        android:id=\"@+id/loadingSpinner\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\" />\n\n    <ImageView\n        android:id=\"@+id/networkError\"\n        android:layout_width=\"64dp\"\n        android:layout_height=\"64dp\"\n        android:layout_gravity=\"center\"\n        android:tint=\"@color/colorAccent\"\n        android:visibility=\"gone\"\n        app:srcCompat=\"@drawable/ic_signal_wifi_off_black_24dp\" />\n\n</FrameLayout>\n\n"
  },
  {
    "path": "app/src/main/res/layout/fragment_nowplaying.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:layout_margin=\"2dp\"\n    android:background=\"@drawable/media_item_background\">\n\n    <ImageView\n        android:id=\"@+id/albumArt\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:contentDescription=\"@string/album_art_alt\"\n        android:scaleType=\"centerCrop\"\n        app:srcCompat=\"@android:color/transparent\" />\n\n    <androidx.constraintlayout.widget.Guideline\n        android:id=\"@+id/divider\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"horizontal\"\n        app:layout_constraintGuide_end=\"64dp\" />\n\n    <View\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"96dp\"\n        android:layout_marginTop=\"-24dp\"\n        android:background=\"@drawable/media_overlay_background\"\n        app:layout_constraintBottom_toBottomOf=\"@+id/albumArt\"\n        app:layout_constraintTop_toBottomOf=\"@id/divider\" />\n\n    <ImageButton\n        android:id=\"@+id/media_button\"\n        android:layout_width=\"@dimen/exo_media_button_width\"\n        android:layout_height=\"@dimen/exo_media_button_height\"\n        android:background=\"?attr/selectableItemBackground\"\n        android:scaleType=\"centerInside\"\n        app:layout_constraintLeft_toLeftOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/divider\"\n        app:srcCompat=\"@drawable/ic_play_arrow_black_24dp\"\n        tools:ignore=\"ContentDescription\" />\n\n    <TextView\n        android:id=\"@+id/title\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginStart=\"@dimen/text_margin\"\n        android:layout_marginTop=\"8dp\"\n        android:layout_marginEnd=\"@dimen/text_margin\"\n        android:ellipsize=\"end\"\n        android:maxLines=\"1\"\n        android:textAppearance=\"@style/TextAppearance.Uamp.Title\"\n        app:layout_constraintLeft_toRightOf=\"@id/media_button\"\n        app:layout_constraintRight_toLeftOf=\"@id/position\"\n        app:layout_constraintTop_toBottomOf=\"@id/divider\"\n        tools:text=\"Song Title\" />\n\n    <TextView\n        android:id=\"@+id/subtitle\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginStart=\"@dimen/text_margin\"\n        android:layout_marginEnd=\"@dimen/text_margin\"\n        android:ellipsize=\"end\"\n        android:maxLines=\"1\"\n        android:textAppearance=\"@style/TextAppearance.Uamp.Subtitle\"\n        app:layout_constraintLeft_toRightOf=\"@id/media_button\"\n        app:layout_constraintRight_toLeftOf=\"@id/position\"\n        app:layout_constraintTop_toBottomOf=\"@+id/title\"\n        tools:text=\"Artist\" />\n\n    <TextView\n        android:id=\"@+id/position\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginStart=\"@dimen/text_margin\"\n        android:layout_marginTop=\"8dp\"\n        android:layout_marginEnd=\"@dimen/text_margin\"\n        android:ellipsize=\"end\"\n        android:maxLines=\"1\"\n        android:textAppearance=\"@style/TextAppearance.Uamp.Title\"\n        app:layout_constraintRight_toRightOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/divider\"\n        tools:text=\"0:00\" />\n\n    <TextView\n        android:id=\"@+id/duration\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginStart=\"@dimen/text_margin\"\n        android:layout_marginEnd=\"@dimen/text_margin\"\n        android:ellipsize=\"end\"\n        android:maxLines=\"1\"\n        android:textAppearance=\"@style/TextAppearance.Uamp.Subtitle\"\n        app:layout_constraintRight_toRightOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/position\"\n        tools:text=\"0:00\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>\n"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2018 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@drawable/ic_launcher_background\" />\n    <foreground android:drawable=\"@mipmap/ic_launcher_foreground\" />\n</adaptive-icon>"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2018 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@drawable/ic_launcher_background\" />\n    <foreground android:drawable=\"@mipmap/ic_launcher_foreground\" />\n</adaptive-icon>"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2017 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<resources>\n    <color name=\"colorPrimary\">#840255</color>\n    <color name=\"colorPrimaryDark\">#710144</color>\n    <color name=\"colorAccent\">#14A5A1</color>\n\n    <color name=\"transparent\">#00000000</color>\n\n    <color name=\"mediaListBackground\">#F1F1F1</color>\n\n    <color name=\"mediaControlColor\">#f8f8f8</color>\n\n    <color name=\"nowPlayingBackground\">#eff0f0</color>\n    <color name=\"nowPlayingWhiteBackground\">#fdfdfd</color>\n\n    <color name=\"menu_icon_tint\">#ffffff</color>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/dimens.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2017 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<resources>\n    <dimen name=\"text_margin\">16dp</dimen>\n\n    <dimen name=\"media_item_height\">72dp</dimen>\n    <dimen name=\"media_item_control_margin\">4dp</dimen>\n    <dimen name=\"media_item_icon_margin_start\">12dp</dimen>\n    <dimen name=\"media_item_text_margin_start\">4dp</dimen>\n\n    <dimen name=\"media_item_art\">72dp</dimen>\n\n    <dimen name=\"media_item_radius\">4dp</dimen>\n    <dimen name=\"media_item_half_radius\">2dp</dimen>\n    <dimen name=\"media_item_negative_half_radius\">-2dp</dimen>\n\n    <dimen name=\"now_playing_album_art_margin\">72dp</dimen>\n    <dimen name=\"now_playing_skip_top_margin\">42dp</dimen>\n    <dimen name=\"now_playing_skip_margin\">8dp</dimen>\n    <dimen name=\"now_playing_play_pause_top_margin\">42dp</dimen>\n    <dimen name=\"now_playing_play_pause_bottom_margin\">52dp</dimen>\n\n    <dimen name=\"now_playing_title_top_margin\">32dp</dimen>\n    <dimen name=\"now_playing_title_bottom_margin\">8dp</dimen>\n    <dimen name=\"now_playing_artist_bottom_margin\">32dp</dimen>\n    <dimen name=\"now_playing_text_padding\">16dp</dimen>\n\n    <dimen name=\"now_playing_title_size\">32sp</dimen>\n    <dimen name=\"now_playing_artist_size\">18sp</dimen>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2017 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<resources>\n    <string name=\"app_name\">UAMP</string>\n\n    <string name=\"skip_back_10s\">Skip back 10s</string>\n    <string name=\"skip_forward_10s\">Skip forward 10s</string>\n    <string name=\"play\">Play</string>\n    <string name=\"pause\">Pause</string>\n\n    <string name=\"menu_music_queue\">Queue</string>\n    <string name=\"album_art_alt\">Album art</string>\n    <string name=\"duration_unknown\">--:--</string>\n    <string name=\"duration_format\">%d:%02d</string>\n\n    <string name=\"cast_context_error\">Failed to get Cast context. Try updating Google Play Services and restart the app.</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/styles.xml",
    "content": "<!--\n  ~ Copyright 2017 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<resources>\n\n    <!-- Base application theme. -->\n    <style name=\"AppTheme\" parent=\"Theme.AppCompat.Light.DarkActionBar\">\n        <!-- Customize your theme here. -->\n        <item name=\"colorPrimary\">@color/colorPrimary</item>\n        <item name=\"colorPrimaryDark\">@color/colorPrimaryDark</item>\n        <item name=\"colorAccent\">@color/colorAccent</item>\n    </style>\n\n    <style name=\"TextAppearance.Uamp.Title\" parent=\"@style/TextAppearance.AppCompat.Title\">\n        <item name=\"android:textColor\">?android:textColorSecondary</item>\n    </style>\n\n    <style name=\"TextAppearance.Uamp.Subtitle\" parent=\"@style/TextAppearance.AppCompat.Small\">\n        <item name=\"android:textColor\">?android:textColorSecondary</item>\n    </style>\n\n    <style name=\"MediaStateIcon\">\n        <item name=\"tint\">@color/mediaControlColor</item>\n        <item name=\"android:layout_margin\">@dimen/media_item_control_margin</item>\n    </style>\n\n    <style name=\"MediaItemList\">\n        <item name=\"android:background\">@color/mediaListBackground</item>\n        <item name=\"android:paddingBottom\">2dp</item>\n        <!-- In order to account for optical illusions,\n        make the end padding a bit larger so it visually looks the same. -->\n        <item name=\"android:paddingEnd\">3dp</item>\n        <item name=\"android:paddingStart\">2dp</item>\n        <item name=\"android:paddingTop\">2dp</item>\n    </style>\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/xml/automotive_app_desc.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2018 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<automotiveApp>\n    <!--\n      ~ This lets Android Auto know that UAMP can act as a media app.\n      ~ See: https://developer.android.com/training/auto/audio/ for more info.\n      -->\n    <uses name=\"media\" />\n</automotiveApp>"
  },
  {
    "path": "automotive/build.gradle",
    "content": "/*\n * Copyright 2019 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\napply plugin: 'com.android.application'\napply plugin: 'kotlin-android'\napply plugin: 'kotlin-kapt'\napply plugin: 'kotlin-android-extensions'\n\nandroid {\n    compileSdkVersion rootProject.compileSdkVersion\n\n    defaultConfig {\n        applicationId \"com.example.android.uamp.next\"\n\n        minSdkVersion 21\n        targetSdkVersion rootProject.targetSdkVersion\n\n        versionCode 1\n        versionName \"1.0\"\n\n        testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildFeatures {\n        viewBinding true\n    }\n\n    compileOptions {\n        sourceCompatibility 1.8\n        targetCompatibility 1.8\n    }\n    kotlinOptions {\n        jvmTarget = \"1.8\"\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'\n        }\n    }\n\n}\n\ndependencies {\n    implementation project(':common')\n\n    implementation \"org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version\"\n\n    implementation \"androidx.core:core-ktx:$androidx_core_ktx_version\"\n    implementation \"androidx.preference:preference:$androidx_preference_version\"\n    implementation \"androidx.car:car:$androidx_car_version\"\n    implementation \"androidx.constraintlayout:constraintlayout:$constraint_layout_version\"\n    implementation \"androidx.appcompat:appcompat:$androidx_app_compat_version\"\n    implementation \"androidx.lifecycle:lifecycle-extensions:$arch_lifecycle_version\"\n\n    implementation \"com.google.android.gms:play-services-auth:$play_services_auth_version\"\n\n    testImplementation \"junit:junit:$junit_version\"\n\n    androidTestImplementation \"androidx.test:runner:$androidx_test_runner_version\"\n    androidTestImplementation \"androidx.test.espresso:espresso-core:$espresso_version\"\n}\n"
  },
  {
    "path": "automotive/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile\n"
  },
  {
    "path": "automotive/src/androidTest/java/com/example/android/uamp/automotive/ExampleInstrumentedTest.java",
    "content": "/*\n * Copyright 2019 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.uamp.automotive;\n\nimport android.content.Context;\n\nimport androidx.test.InstrumentationRegistry;\nimport androidx.test.runner.AndroidJUnit4;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\nimport static org.junit.Assert.assertEquals;\n\n/**\n * Instrumented test, which will execute on an Android device.\n *\n * @see <a href=\"http://d.android.com/tools/testing\">Testing documentation</a>\n */\n@RunWith(AndroidJUnit4.class)\npublic class ExampleInstrumentedTest {\n    @Test\n    public void useAppContext() {\n        // Context of the app under test.\n        Context appContext = InstrumentationRegistry.getTargetContext();\n\n        assertEquals(\"com.example.automotive\", appContext.getPackageName());\n    }\n}\n"
  },
  {
    "path": "automotive/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2019 Google LLC\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     https://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    package=\"com.example.android.uamp.automotive\">\n\n    <!--\n    Since this module contains code exclusive to Android Automotive, require the feature here.\n\n    If you were mixing projected and automotive code, then the feature should not be marked\n    as required.\n    -->\n    <uses-feature\n        android:name=\"android.hardware.type.automotive\"\n        android:required=\"true\" />\n\n    <uses-sdk tools:overrideLibrary=\"androidx.car\" />\n\n    <uses-feature\n        android:name=\"android.hardware.wifi\"\n        android:required=\"false\" />\n    <uses-feature\n        android:name=\"android.hardware.screen.portrait\"\n        android:required=\"false\" />\n    <uses-feature\n        android:name=\"android.hardware.screen.landscape\"\n        android:required=\"false\" />\n\n    <application\n        android:allowBackup=\"true\"\n        android:appCategory=\"audio\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:roundIcon=\"@mipmap/ic_launcher_round\"\n        android:supportsRtl=\"true\"\n        android:theme=\"@style/AppTheme\"\n        tools:ignore=\"GoogleAppIndexingWarning\">\n\n        <meta-data\n            android:name=\"com.android.automotive\"\n            android:resource=\"@xml/automotive_app_desc\" />\n\n        <activity\n            android:name=\".SignInActivity\"\n            android:exported=\"true\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.ACTION_SIGN_IN\" />\n            </intent-filter>\n        </activity>\n\n        <!-- Car compatible theme must use minSDK24 -->\n        <activity\n            android:name=\".SettingsActivity\"\n            android:label=\"@string/settings_label\"\n            android:theme=\"@style/AppTheme.Drawer\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.APPLICATION_PREFERENCES\" />\n            </intent-filter>\n        </activity>\n\n        <!--\n        Declare the MediaBrowserService which supports Android Automotive specific\n        extensions.\n        -->\n        <service\n            android:name=\".AutomotiveMusicService\"\n            android:enabled=\"true\"\n            android:exported=\"true\"\n            tools:ignore=\"ExportedService\">\n\n            <intent-filter>\n                <action android:name=\"android.media.browse.MediaBrowserService\" />\n            </intent-filter>\n        </service>\n    </application>\n</manifest>\n"
  },
  {
    "path": "automotive/src/main/java/com/example/android/uamp/automotive/AutomotiveMusicService.kt",
    "content": "/*\n * Copyright 2019 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.uamp.automotive\n\nimport android.accounts.AccountManager\nimport android.app.Activity\nimport android.app.PendingIntent\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Bundle\nimport android.os.ResultReceiver\nimport android.support.v4.media.session.PlaybackStateCompat\nimport android.util.Log\nimport androidx.core.content.edit\nimport com.example.android.uamp.media.MusicService\nimport com.google.android.exoplayer2.Player\nimport com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\n\n/** UAMP specific command for logging into the service. */\nconst val LOGIN = \"com.example.android.uamp.automotive.COMMAND.LOGIN\"\n\n/** UAMP specific command for logging out of the service. */\nconst val LOGOUT = \"com.example.android.uamp.automotive.COMMAND.LOGOUT\"\n\nconst val LOGIN_EMAIL = \"com.example.android.uamp.automotive.ARGS.LOGIN_EMAIL\"\nconst val LOGIN_PASSWORD = \"com.example.android.uamp.automotive.ARGS.LOGIN_PASSWORD\"\n\ntypealias CommandHandler = (parameters: Bundle, callback: ResultReceiver?) -> Boolean\n\n/**\n * Android Automotive specific extensions for [MusicService].\n *\n * UAMP for Android Automotive OS requires the user to login in order to demonstrate\n * how authentication works on the system. If this doesn't apply to your application,\n * this class can be skipped in favor of its parent, [MusicService].\n *\n * If you'd like to support authentication, but not prevent using the system,\n * comment out the calls to [requireLogin].\n */\nclass AutomotiveMusicService : MusicService() {\n\n    @ExperimentalCoroutinesApi\n    override fun onCreate() {\n        super.onCreate()\n\n        // Register to handle login/logout commands.\n        mediaSessionConnector.registerCustomCommandReceiver(AutomotiveCommandReceiver())\n\n        // Require the user to be logged in for demonstration purposes.\n        if (!isAuthenticated()) {\n            requireLogin()\n        }\n    }\n\n    private fun onLogin(email: String, password: String): Boolean {\n        Log.i(TAG, \"User logged in: $email\")\n        getSharedPreferences(AutomotiveMusicService::class.java.name, Context.MODE_PRIVATE).edit {\n            putString(USER_TOKEN, \"$email:${password.hashCode()}\")\n        }\n        return true\n    }\n\n    private fun onLogout(): Boolean {\n        Log.i(TAG, \"User logged out\")\n        getSharedPreferences(AutomotiveMusicService::class.java.name, Context.MODE_PRIVATE).edit {\n            remove(USER_TOKEN)\n        }\n        return false\n    }\n\n    /**\n     * Verifies if the user has logged into the system.\n     * In a real system, credentials should probably be handled by the\n     * [AccountManager] APIs.\n     */\n    private fun isAuthenticated() =\n        getSharedPreferences(AutomotiveMusicService::class.java.name, Context.MODE_PRIVATE)\n            .contains(USER_TOKEN)\n\n    /**\n     * Sets [PlaybackStateCompat] values to indicate the user must login to continue.\n     *\n     * This routine sets the playback state and provides the resolution [PendingIntent]\n     * that Android Automotive OS requires.\n     */\n    private fun requireLogin() {\n        val loginIntent = Intent(this, SignInActivity::class.java)\n        val loginActivityPendingIntent = PendingIntent.getActivity(this, 0, loginIntent, 0)\n        val extras = Bundle().apply {\n            putString(ERROR_RESOLUTION_ACTION_LABEL, getString(R.string.error_login_button))\n            putParcelable(ERROR_RESOLUTION_ACTION_INTENT, loginActivityPendingIntent)\n        }\n        mediaSessionConnector.setCustomErrorMessage(\n            getString(R.string.error_require_login),\n            PlaybackStateCompat.ERROR_CODE_AUTHENTICATION_EXPIRED,\n            extras\n        )\n    }\n\n    /**\n     * This is the entry point for custom commands received by ExoPlayer's\n     * [MediaSessionConnector.customCommandReceivers].\n     *\n     * The extension will call each [CommandReceiver] in turn. If the [CommandReceiver] can\n     * handle the command, it returns `true` to indicate the command's been handled and\n     * processing should stop. If the [CommandReceiver] cannot/doesn't want to handle the\n     * command, it should return `false`.\n     *\n     * We simplify this a bit by having our own [CommandHandler] that works with a single\n     * command (either \"log in\" or \"log out\"). Each of these returns true at the end of its\n     * processing.\n     *\n     * If the command received isn't either of our commands, we just return `false`.\n     *\n     * Suppress the warning because the original name, `cb` is not as clear as to its purpose.\n     */\n    private inner class AutomotiveCommandReceiver : MediaSessionConnector.CommandReceiver {\n        @Suppress(\"PARAMETER_NAME_CHANGED_ON_OVERRIDE\")\n        override fun onCommand(\n            player: Player,\n            command: String,\n            extras: Bundle?,\n            callback: ResultReceiver?\n        ): Boolean =\n            when (command) {\n                LOGIN -> loginCommand(extras ?: Bundle.EMPTY, callback)\n                LOGOUT -> logoutCommand(extras ?: Bundle.EMPTY, callback)\n                else -> false\n            }\n    }\n\n    private val loginCommand: CommandHandler = { extras, callback ->\n        val email = extras.getString(LOGIN_EMAIL) ?: \"\"\n        val password = extras.getString(LOGIN_PASSWORD) ?: \"\"\n\n        if (onLogin(email, password)) {\n            // Updated state (including clearing the error) now that the user has logged in.\n            mediaSessionConnector.setCustomErrorMessage(null)\n            mediaSessionConnector.invalidateMediaSessionPlaybackState()\n\n            callback?.send(Activity.RESULT_OK, Bundle.EMPTY)\n        } else {\n            // Login is required - note this.\n            requireLogin()\n\n            callback?.send(Activity.RESULT_CANCELED, Bundle.EMPTY)\n        }\n        true\n    }\n\n    private val logoutCommand: CommandHandler = { _, callback ->\n        // Log the user out.\n        onLogout()\n\n        // Login is required - note this.\n        requireLogin()\n        callback?.send(Activity.RESULT_OK, Bundle.EMPTY)\n        true\n    }\n}\n\nprivate const val TAG = \"AutomotiveMusicService\"\nprivate const val ERROR_RESOLUTION_ACTION_LABEL =\n    \"android.media.extras.ERROR_RESOLUTION_ACTION_LABEL\"\nprivate const val ERROR_RESOLUTION_ACTION_INTENT =\n    \"android.media.extras.ERROR_RESOLUTION_ACTION_INTENT\"\n\nprivate const val USER_TOKEN = \"com.example.android.uamp.automotive.PREFS.USER_TOKEN\""
  },
  {
    "path": "automotive/src/main/java/com/example/android/uamp/automotive/PhoneSignInFragment.kt",
    "content": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.uamp.automotive\n\nimport android.os.Bundle\nimport android.text.method.LinkMovementMethod\nimport android.view.View\nimport androidx.core.content.ContextCompat\nimport androidx.core.text.HtmlCompat\nimport androidx.fragment.app.Fragment\nimport com.example.android.uamp.automotive.databinding.PhoneSignInBinding\n\n/**\n * Fragment that is used to facilitate phone sign-in. The fragment allows users to choose between\n * either the PIN or QR code sign-in flow.\n */\nclass PhoneSignInFragment : Fragment(R.layout.phone_sign_in) {\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        val context = requireContext()\n\n        val binding = PhoneSignInBinding.bind(view)\n\n        binding.toolbar.setNavigationOnClickListener {\n            requireActivity().supportFragmentManager.popBackStack()\n        }\n\n        // Set up PIN sign in button.\n        binding.appIcon.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.aural_logo))\n        binding.primaryMessage.text = getString(R.string.phone_sign_in_primary_text)\n        binding.pinSignInButton.text = getString(R.string.pin_sign_in_button_label)\n        binding.pinSignInButton.setOnClickListener {\n            requireActivity().supportFragmentManager.beginTransaction()\n                .replace(R.id.sign_in_container, PinCodeSignInFragment())\n                .addToBackStack(\"landingPage\")\n                .commit()\n        }\n\n        // Set up QR code sign in button.\n        binding.qrSignInButton.text = getString(R.string.qr_sign_in_button_label)\n        binding.qrSignInButton.setOnClickListener {\n            requireActivity().supportFragmentManager.beginTransaction()\n                .replace(R.id.sign_in_container, QrCodeSignInFragment())\n                .addToBackStack(\"landingPage\")\n                .commit()\n        }\n\n        // Links in footer text should be clickable.\n        binding.footer.text = HtmlCompat.fromHtml(\n            context.getString(R.string.sign_in_footer),\n            HtmlCompat.FROM_HTML_MODE_LEGACY\n        )\n        binding.footer.movementMethod = LinkMovementMethod.getInstance()\n    }\n}"
  },
  {
    "path": "automotive/src/main/java/com/example/android/uamp/automotive/PinCodeSignInFragment.kt",
    "content": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.uamp.automotive\n\nimport android.os.Bundle\nimport android.text.method.LinkMovementMethod\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.widget.TextView\nimport androidx.core.content.ContextCompat\nimport androidx.core.text.HtmlCompat\nimport androidx.fragment.app.Fragment\nimport androidx.lifecycle.ViewModelProvider\nimport com.example.android.uamp.automotive.databinding.PinSignInBinding\n\n/**\n * Fragment that is used to facilitate PIN code sign-in. This fragment displayed a configurable\n * PIN code that users enter in a secondary device to perform sign-in.\n *\n *<p>This screen serves as a demo for UI best practices for PIN code sign in. Sign in implementation\n * will be app specific and is not included.\n */\nclass PinCodeSignInFragment : Fragment(R.layout.pin_sign_in) {\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        val context = requireContext()\n\n        val binding = PinSignInBinding.bind(view)\n\n        binding.toolbar.setNavigationOnClickListener {\n            requireActivity().supportFragmentManager.popBackStack()\n        }\n\n        binding.appIcon.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.aural_logo))\n        binding.primaryMessage.text = getString(R.string.pin_sign_in_primary_text)\n        binding.secondaryMessage.text = getString(R.string.pin_sign_in_secondary_text)\n\n        // Links in footer text should be clickable.\n        binding.footer.text = HtmlCompat.fromHtml(\n            context.getString(R.string.sign_in_footer),\n            HtmlCompat.FROM_HTML_MODE_LEGACY\n        )\n        binding.footer.movementMethod = LinkMovementMethod.getInstance()\n\n        val pin = ViewModelProvider(requireActivity())\n            .get(SignInActivityViewModel::class.java)\n            .generatePin()\n\n        // Remove existing PIN characters.\n        if (binding.pinCodeContainer.childCount > 0) {\n            binding.pinCodeContainer.removeAllViews()\n        }\n\n        for (element in pin) {\n            val pinItem = LayoutInflater.from(context).inflate(\n                R.layout.pin_item,\n                binding.pinCodeContainer,\n                false\n            ) as TextView\n            pinItem.text = element.toString()\n            binding.pinCodeContainer.addView(pinItem)\n        }\n    }\n}"
  },
  {
    "path": "automotive/src/main/java/com/example/android/uamp/automotive/QrCodeSignInFragment.kt",
    "content": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.uamp.automotive\n\nimport android.os.Bundle\nimport android.text.method.LinkMovementMethod\nimport android.view.View\nimport androidx.core.content.ContextCompat.getDrawable\nimport androidx.core.text.HtmlCompat\nimport androidx.fragment.app.Fragment\nimport com.bumptech.glide.Glide\nimport com.example.android.uamp.automotive.databinding.QrSignInBinding\n\n/**\n * Fragment that is used to facilitate QR code sign-in. Users scan a QR code rendered by this\n * fragment with their phones, which performs the authentication required for sign-in\n *\n * <p>This screen serves as a demo for UI best practices for QR code sign in. Sign in implementation\n * will be app specific and is not included.\n */\nclass QrCodeSignInFragment : Fragment(R.layout.qr_sign_in) {\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        val binding = QrSignInBinding.bind(view)\n\n        binding.toolbar.setNavigationOnClickListener {\n            requireActivity().supportFragmentManager.popBackStack()\n        }\n\n        binding.appIcon.setImageDrawable(getDrawable(requireContext(), R.drawable.aural_logo))\n        binding.primaryMessage.text = getString(R.string.qr_sign_in_primary_text)\n        binding.secondaryMessage.text = getString(R.string.qr_sign_in_secondary_text)\n\n        // Links in footer text should be clickable.\n        binding.footer.text = HtmlCompat.fromHtml(\n            requireContext().getString(R.string.sign_in_footer),\n            HtmlCompat.FROM_HTML_MODE_LEGACY\n        )\n        binding.footer.movementMethod = LinkMovementMethod.getInstance()\n\n        Glide.with(this).load(getString(R.string.qr_code_url)).into(binding.qrCode)\n    }\n}\n"
  },
  {
    "path": "automotive/src/main/java/com/example/android/uamp/automotive/SettingsActivity.kt",
    "content": "/*\n * Copyright 2019 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.uamp.automotive\n\nimport android.os.Bundle\nimport androidx.appcompat.app.AppCompatActivity\nimport com.example.android.uamp.automotive.databinding.ActivitySettingsBinding\n\n/**\n * This class exposes application settings\n * for integration with MediaCenter in Android Automotive.\n */\nclass SettingsActivity : AppCompatActivity() {\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        val binding = ActivitySettingsBinding.inflate(layoutInflater)\n        setContentView(binding.root)\n\n        setSupportActionBar(binding.toolbar)\n        supportActionBar?.setHomeButtonEnabled(true)\n        supportActionBar?.setDisplayHomeAsUpEnabled(true)\n\n        supportFragmentManager\n            .beginTransaction()\n            .replace(R.id.settings_container, SettingsFragment())\n            .commit()\n    }\n\n    override fun onBackPressed() {\n        super.onBackPressed()\n        finish()\n    }\n\n    override fun onSupportNavigateUp(): Boolean {\n        onBackPressed()\n        return true\n    }\n}"
  },
  {
    "path": "automotive/src/main/java/com/example/android/uamp/automotive/SettingsFragment.kt",
    "content": "/*\n * Copyright 2019 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.uamp.automotive\n\nimport android.app.Application\nimport android.content.ComponentName\nimport android.os.Bundle\nimport androidx.lifecycle.AndroidViewModel\nimport androidx.lifecycle.ViewModelProvider\nimport androidx.preference.Preference\nimport androidx.preference.PreferenceFragmentCompat\nimport com.example.android.uamp.common.MusicServiceConnection\n\n/**\n * Preference fragment hosted by [SettingsActivity]. Handles events to various preference changes.\n */\nclass SettingsFragment : PreferenceFragmentCompat() {\n    private lateinit var viewModel: SettingsFragmentViewModel\n\n    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {\n        setPreferencesFromResource(R.xml.preferences, rootKey)\n\n        viewModel = ViewModelProvider(this)\n            .get(SettingsFragmentViewModel::class.java)\n    }\n\n    override fun onPreferenceTreeClick(preference: Preference?): Boolean {\n        return when (preference?.key) {\n            \"logout\" -> {\n                viewModel.logout()\n                requireActivity().finish()\n                true\n            }\n            else -> {\n                super.onPreferenceTreeClick(preference)\n            }\n        }\n    }\n}\n\n/**\n * Basic ViewModel for [SettingsFragment].\n */\nclass SettingsFragmentViewModel(application: Application) : AndroidViewModel(application) {\n    private val applicationContext = application.applicationContext\n    private val musicServiceConnection = MusicServiceConnection(\n        applicationContext,\n        ComponentName(applicationContext, AutomotiveMusicService::class.java)\n    )\n\n    fun logout() {\n        // Logout is fire and forget.\n        musicServiceConnection.sendCommand(LOGOUT, null)\n    }\n}\n"
  },
  {
    "path": "automotive/src/main/java/com/example/android/uamp/automotive/SignInActivity.kt",
    "content": "/*\n * Copyright 2019 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.uamp.automotive\n\nimport android.os.Bundle\nimport android.widget.Toast\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.lifecycle.Observer\nimport androidx.lifecycle.ViewModelProvider\n\nclass SignInActivity : AppCompatActivity() {\n\n    private lateinit var viewModel: SignInActivityViewModel\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContentView(R.layout.activity_sign_in)\n\n        viewModel = ViewModelProvider(this)\n            .get(SignInActivityViewModel::class.java)\n\n        viewModel.loggedIn.observe(this, Observer { loggedIn ->\n            if (loggedIn == true) {\n                Toast.makeText(this, R.string.sign_in_success_message, Toast.LENGTH_SHORT).show()\n                finish()\n            }\n        })\n\n        supportFragmentManager.beginTransaction()\n            .add(R.id.sign_in_container, SignInLandingPageFragment())\n            .commit()\n    }\n}"
  },
  {
    "path": "automotive/src/main/java/com/example/android/uamp/automotive/SignInActivityViewModel.kt",
    "content": "/*\n * Copyright 2019 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.uamp.automotive\n\nimport android.app.Activity\nimport android.app.Application\nimport android.content.ComponentName\nimport android.os.Bundle\nimport android.text.TextUtils\nimport android.widget.Toast\nimport androidx.lifecycle.AndroidViewModel\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\nimport com.example.android.uamp.common.MusicServiceConnection\nimport java.util.Random\n\n/**\n * Basic ViewModel for [SignInActivity].\n */\nclass SignInActivityViewModel(application: Application) : AndroidViewModel(application) {\n    private val applicationContext = application.applicationContext\n    private val musicServiceConnection = MusicServiceConnection(\n        applicationContext,\n        ComponentName(applicationContext, AutomotiveMusicService::class.java)\n    )\n\n    private val _loggedIn = MutableLiveData<Boolean>()\n    val loggedIn: LiveData<Boolean> = _loggedIn\n\n    fun login(email: String, password: String) {\n        if (TextUtils.isEmpty(email) or TextUtils.isEmpty(password)) {\n            Toast.makeText(\n                applicationContext,\n                applicationContext.getString(R.string.missing_fields_error),\n                Toast.LENGTH_SHORT\n            ).show()\n        } else {\n            val loginParams = Bundle().apply {\n                putString(LOGIN_EMAIL, email)\n                putString(LOGIN_PASSWORD, password)\n            }\n            musicServiceConnection.sendCommand(LOGIN, loginParams) { resultCode, _ ->\n                _loggedIn.postValue(resultCode == Activity.RESULT_OK)\n            }\n        }\n    }\n\n    fun generatePin(): CharSequence {\n        return String.format(\"%08d\", Random().nextInt(99999999))\n    }\n}"
  },
  {
    "path": "automotive/src/main/java/com/example/android/uamp/automotive/SignInLandingPageFragment.kt",
    "content": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.uamp.automotive\n\nimport android.content.Intent\nimport android.os.Build\nimport android.os.Bundle\nimport android.text.InputType\nimport android.text.TextUtils\nimport android.text.method.LinkMovementMethod\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.View.AUTOFILL_HINT_USERNAME\nimport android.view.ViewGroup\nimport android.widget.Button\nimport android.widget.ImageView\nimport android.widget.TextView\nimport android.widget.Toast\nimport androidx.appcompat.widget.Toolbar\nimport androidx.core.text.HtmlCompat\nimport androidx.fragment.app.Fragment\nimport com.google.android.gms.auth.api.signin.GoogleSignIn\nimport com.google.android.gms.auth.api.signin.GoogleSignInAccount\nimport com.google.android.gms.auth.api.signin.GoogleSignInOptions\nimport com.google.android.gms.common.ConnectionResult\nimport com.google.android.gms.common.GoogleApiAvailability\nimport com.google.android.gms.common.api.ApiException\nimport com.google.android.gms.tasks.Task\nimport com.google.android.material.textfield.TextInputEditText\nimport com.google.android.material.textfield.TextInputLayout\n\nconst val RC_SIGN_IN = 9001\nconst val PLAY_SERVICES_RESOLUTION_REQUEST = 9000\n\n// Control the supported sign in flows by toggling the constants below.\nconst val ENABLE_PIN_SIGN_IN = true\nconst val ENABLE_QR_CODE_SIGN_IN = true\nconst val ENABLE_GOOGLE_SIGN_IN = true\nconst val ENABLE_USERNAME_PASSWORD_SIGN_IN = true\n\n/**\n * A fragment that renders the landing screen for a sign-in flow. This screen can be configured\n * to display third-party sign-in, PIN sign-in, QR-code sign-in and/or Google sign-in.\n */\nclass SignInLandingPageFragment : Fragment() {\n\n    companion object {\n        internal const val CAR_SIGN_IN_IDENTIFIER_KEY = \"userID\"\n    }\n\n    private lateinit var toolbar: Toolbar\n    private lateinit var appIcon: ImageView\n    private lateinit var phoneSignInButton: Button\n    private lateinit var googleSignInButton: Button\n    private lateinit var usernameAndPasswordSignInButton: Button\n    private lateinit var primaryTextView: TextView\n    private lateinit var identifierContainer: TextInputLayout\n    private lateinit var identifierInput: TextInputEditText\n    private lateinit var footerTextView: TextView\n\n    override fun onCreateView(\n        inflater: LayoutInflater, container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View? {\n        val layout = if (ENABLE_USERNAME_PASSWORD_SIGN_IN)\n            R.layout.sign_in_landing_page_with_username_and_password\n        else R.layout.sign_in_landing_page\n\n        return inflater.inflate(layout, container, false)\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        val context = requireContext()\n\n        toolbar = view.findViewById(R.id.toolbar)\n        appIcon = view.findViewById(R.id.app_icon)\n        primaryTextView = view.findViewById(R.id.primary_message)\n        footerTextView = view.findViewById(R.id.footer)\n        phoneSignInButton = view.findViewById(R.id.phone_sign_in_button)\n        googleSignInButton = view.findViewById(R.id.google_sign_in_button)\n\n        if (ENABLE_USERNAME_PASSWORD_SIGN_IN) {\n            usernameAndPasswordSignInButton = view.findViewById(R.id.sign_in_button)\n            identifierContainer = view.findViewById(R.id.identifier_container)\n            identifierInput = view.findViewById(R.id.identifier_input)\n\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n                identifierInput.setAutofillHints(AUTOFILL_HINT_USERNAME)\n            }\n        }\n\n        toolbar.setNavigationOnClickListener { requireActivity().finish() }\n\n        appIcon.setImageDrawable(context.getDrawable(R.drawable.aural_logo))\n        primaryTextView.text = getString(R.string.sign_in_primary_text)\n\n        // Links in footer text should be clickable.\n        footerTextView.text = HtmlCompat.fromHtml(\n            context.getString(R.string.sign_in_footer),\n            HtmlCompat.FROM_HTML_MODE_LEGACY\n        )\n        footerTextView.movementMethod = LinkMovementMethod.getInstance()\n\n        configureUsernameAndPasswordSignIn()\n        configurePhoneSignIn()\n        configureGoogleSignIn()\n    }\n\n    private fun configureUsernameAndPasswordSignIn() {\n        if (!ENABLE_USERNAME_PASSWORD_SIGN_IN) {\n            return\n        }\n\n        identifierContainer.hint = getString(R.string.sign_in_user_id_hint)\n        identifierInput.inputType = InputType.TYPE_CLASS_TEXT\n\n        usernameAndPasswordSignInButton.text = getString(R.string.sign_in_next_button_label)\n        usernameAndPasswordSignInButton.setOnClickListener {\n            val identifier = identifierInput.text\n            if (TextUtils.isEmpty(identifier)) {\n                identifierInput.error = getString(R.string.sign_in_username_error)\n            } else {\n                val args = Bundle()\n                args.putString(CAR_SIGN_IN_IDENTIFIER_KEY, identifierInput.text.toString())\n                val fragment = UsernameAndPasswordSignInFragment()\n                fragment.arguments = args\n\n                requireActivity().supportFragmentManager.beginTransaction()\n                    .replace(R.id.sign_in_container, fragment)\n                    .addToBackStack(\"landingPage\")\n                    .commit()\n            }\n        }\n    }\n\n    private fun configurePhoneSignIn() {\n        if (!ENABLE_QR_CODE_SIGN_IN && !ENABLE_PIN_SIGN_IN) {\n            phoneSignInButton.visibility = View.GONE\n            return\n        }\n\n        lateinit var phoneSignInFragment: Fragment\n\n        if (ENABLE_QR_CODE_SIGN_IN && ENABLE_PIN_SIGN_IN) {\n            // Reduce the number of choices displayed to the user in a single screen. If both PIN\n            // and QR code sign in is enabled, separate the choice between the two options to a\n            // new screen.\n            phoneSignInFragment = PhoneSignInFragment()\n        } else if (ENABLE_PIN_SIGN_IN) {\n            phoneSignInFragment = PinCodeSignInFragment()\n        } else if (ENABLE_QR_CODE_SIGN_IN) {\n            phoneSignInFragment = QrCodeSignInFragment()\n        }\n\n        phoneSignInButton.text = getString(R.string.phone_sign_in_button_label)\n        phoneSignInButton.setOnClickListener {\n            requireActivity().supportFragmentManager.beginTransaction()\n                .replace(R.id.sign_in_container, phoneSignInFragment)\n                .addToBackStack(\"landingPage\")\n                .commit()\n        }\n    }\n\n    /**\n     * Configure the Google sign in option on the landing page.\n     *\n     * <p>https://developers.google.com/identity/sign-in/android/start provides additional\n     * information on integrating Google sign in into your Android app.\n     */\n    private fun configureGoogleSignIn() {\n        if (!ENABLE_GOOGLE_SIGN_IN or !checkPlayServices()) {\n            googleSignInButton.visibility = View.GONE\n            return\n        }\n\n        val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)\n            .requestIdToken(getString(R.string.server_client_id))\n            .requestEmail()\n            .build()\n\n        googleSignInButton.text = getString(R.string.google_sign_in_button_label)\n\n        googleSignInButton.setOnClickListener {\n            val mGoogleSignInClient = GoogleSignIn.getClient(requireContext(), gso)\n            val signInIntent = mGoogleSignInClient.signInIntent\n            startActivityForResult(signInIntent, RC_SIGN_IN)\n        }\n    }\n\n    private fun checkPlayServices(): Boolean {\n        val apiAvailability = GoogleApiAvailability.getInstance();\n        val resultCode = apiAvailability.isGooglePlayServicesAvailable(context);\n        if (resultCode != ConnectionResult.SUCCESS) {\n            if (apiAvailability.isUserResolvableError(resultCode)) {\n                apiAvailability.getErrorDialog(\n                    activity, resultCode, PLAY_SERVICES_RESOLUTION_REQUEST\n                ).show();\n            }\n            return false;\n        }\n        return true;\n    }\n\n    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {\n        super.onActivityResult(requestCode, resultCode, data)\n\n        if (requestCode == RC_SIGN_IN) {\n            val task = GoogleSignIn.getSignedInAccountFromIntent(data)\n            handleGoogleSignIn(task)\n        }\n    }\n\n    private fun handleGoogleSignIn(completedTask: Task<GoogleSignInAccount>) {\n        try {\n            val account = completedTask.getResult(ApiException::class.java)\n            @Suppress(\"unused_variable\") val idToken = account?.idToken\n\n            // Send ID Token to server and validate.\n\n        } catch (e: ApiException) {\n            // The ApiException status code indicates the detailed failure reason.\n            // Please refer to the GoogleSignInStatusCodes class reference for more information.\n            Toast.makeText(\n                requireContext(), getString(R.string.sign_in_failed_message, e.statusCode),\n                Toast.LENGTH_SHORT\n            )\n                .show()\n        }\n    }\n}"
  },
  {
    "path": "automotive/src/main/java/com/example/android/uamp/automotive/UsernameAndPasswordSignInFragment.kt",
    "content": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.uamp.automotive\n\nimport android.os.Build\nimport android.os.Bundle\nimport android.text.method.LinkMovementMethod\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.Button\nimport android.widget.ImageView\nimport android.widget.TextView\nimport androidx.appcompat.widget.Toolbar\nimport androidx.core.content.ContextCompat\nimport androidx.core.text.HtmlCompat\nimport androidx.fragment.app.Fragment\nimport androidx.lifecycle.ViewModelProvider\nimport com.google.android.material.textfield.TextInputEditText\nimport com.google.android.material.textfield.TextInputLayout\n\n/**\n * Fragment that is used to facilitates username and password sign-in.\n */\nclass UsernameAndPasswordSignInFragment : Fragment() {\n\n    private lateinit var toolbar: Toolbar\n    private lateinit var appIcon: ImageView\n    private lateinit var primaryTextView: TextView\n    private lateinit var passwordContainer: TextInputLayout\n    private lateinit var passwordInput: TextInputEditText\n    private lateinit var submitButton: Button\n    private lateinit var footerTextView: TextView\n\n    override fun onCreateView(\n        inflater: LayoutInflater, container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View? {\n        return inflater.inflate(R.layout.username_and_password_sign_in, container, false)\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        val context = requireContext()\n\n        toolbar = view.findViewById(R.id.toolbar)\n        appIcon = view.findViewById(R.id.app_icon)\n        primaryTextView = view.findViewById(R.id.primary_message)\n        passwordContainer = view.findViewById(R.id.password_container)\n        passwordInput = view.findViewById(R.id.password_input)\n        submitButton = view.findViewById(R.id.submit_button)\n        footerTextView = view.findViewById(R.id.footer)\n\n        toolbar.setNavigationOnClickListener {\n            requireActivity().supportFragmentManager.popBackStack()\n        }\n\n        appIcon.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.aural_logo))\n        primaryTextView.text = getString(R.string.username_and_password_sign_in_primary_text)\n        passwordContainer.hint = getString(R.string.password_hint)\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n            passwordInput.setAutofillHints(View.AUTOFILL_HINT_PASSWORD)\n        }\n\n        // Links in footer text should be clickable.\n        footerTextView.text = HtmlCompat.fromHtml(\n            context.getString(R.string.sign_in_footer),\n            HtmlCompat.FROM_HTML_MODE_LEGACY\n        )\n        footerTextView.movementMethod = LinkMovementMethod.getInstance()\n\n        // Get user identifier from previous screen.\n        val userId = arguments?.getString(SignInLandingPageFragment.CAR_SIGN_IN_IDENTIFIER_KEY)\n\n        submitButton.text = getString(R.string.sign_in_submit_button_label)\n        submitButton.setOnClickListener {\n            onSignIn(userId!!, passwordInput.text.toString())\n        }\n    }\n\n    private fun onSignIn(userIdentifier: CharSequence, password: CharSequence) {\n        ViewModelProvider(requireActivity())\n            .get(SignInActivityViewModel::class.java)\n            .login(userIdentifier.toString(), password.toString())\n    }\n}"
  },
  {
    "path": "automotive/src/main/res/color/car_text_dark.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n    Copyright (C) 2019 The Android Open Source Project\n\n    Licensed under the Apache License, Version 2.0 (the \"License\");\n    you may not use this file except in compliance with the License.\n    You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing, software\n    distributed under the License is distributed on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n    See the License for the specific language governing permissions and\n    limitations under the License.\n-->\n<!-- Default text colors for car buttons when enabled/disabled. -->\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:alpha=\"0.40\" android:color=\"@android:color/white\" android:state_enabled=\"false\" />\n    <item android:color=\"@android:color/black\" />\n</selector>\n"
  },
  {
    "path": "automotive/src/main/res/color/car_text_light.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n    Copyright (C) 2019 The Android Open Source Project\n\n    Licensed under the Apache License, Version 2.0 (the \"License\");\n    you may not use this file except in compliance with the License.\n    You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing, software\n    distributed under the License is distributed on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n    See the License for the specific language governing permissions and\n    limitations under the License.\n-->\n<!-- Default text colors for car buttons when enabled/disabled. -->\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:alpha=\"0.36\" android:color=\"@android:color/white\" android:state_enabled=\"false\" />\n    <item android:alpha=\"0.72\" android:color=\"@android:color/white\" />\n</selector>\n"
  },
  {
    "path": "automotive/src/main/res/drawable/default_button_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<!-- Default background styles for car buttons when enabled/disabled. -->\n<ripple xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:color=\"?attr/colorControlHighlight\">\n    <item>\n        <shape android:shape=\"rectangle\">\n            <corners android:radius=\"@dimen/car_button_radius\" />\n            <solid android:color=\"@color/colorAccent\" />\n        </shape>\n    </item>\n</ripple>\n"
  },
  {
    "path": "automotive/src/main/res/drawable/google_sign_in_button_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n    <corners android:radius=\"@dimen/sign_in_button_corner_radius\" />\n    <solid android:color=\"@color/google_sign_in_button_background_color\" />\n</shape>\n"
  },
  {
    "path": "automotive/src/main/res/drawable/google_sign_in_button_logo.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:drawable=\"@drawable/google_logo\" />\n</layer-list>"
  },
  {
    "path": "automotive/src/main/res/drawable/ic_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google LLC\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"108\"\n    android:viewportHeight=\"108\">\n    <path\n        android:fillColor=\"#26A69A\"\n        android:pathData=\"M0,0h108v108h-108z\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M9,0L9,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,0L19,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M29,0L29,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M39,0L39,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M49,0L49,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M59,0L59,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M69,0L69,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M79,0L79,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M89,0L89,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M99,0L99,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,9L108,9\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,19L108,19\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,29L108,29\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,39L108,39\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,49L108,49\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,59L108,59\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,69L108,69\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,79L108,79\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,89L108,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,99L108,99\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,29L89,29\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,39L89,39\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,49L89,49\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,59L89,59\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,69L89,69\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,79L89,79\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M29,19L29,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M39,19L39,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M49,19L49,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M59,19L59,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M69,19L69,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M79,19L79,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n</vector>\n"
  },
  {
    "path": "automotive/src/main/res/drawable/pin_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n    <corners android:radius=\"@dimen/sign_in_pin_container_corner_radius\" />\n    <stroke\n        android:width=\"1dp\"\n        android:color=\"@color/grey_300\" />\n</shape>\n"
  },
  {
    "path": "automotive/src/main/res/drawable/sign_in_button_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<ripple xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:color=\"?attr/colorControlHighlight\">\n    <item>\n        <shape android:shape=\"rectangle\">\n            <corners android:radius=\"@dimen/sign_in_button_corner_radius\" />\n            <solid android:color=\"?attr/colorAccent\" />\n        </shape>\n    </item>\n</ripple>\n"
  },
  {
    "path": "automotive/src/main/res/drawable/sign_in_toolbar_back_icon.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"@dimen/sign_in_toolbar_nav_button_size\"\n    android:height=\"@dimen/sign_in_toolbar_nav_button_size\"\n    android:viewportWidth=\"48\"\n    android:viewportHeight=\"48\">\n\n    <path android:pathData=\"M0 0h48v48H0z\" />\n    <path\n        android:fillColor=\"#000000\"\n        android:pathData=\"M40 22H15.66l11.17-11.17L24 8 8 24l16 16 2.83-2.83L15.66 26H40v-4z\" />\n</vector>\n"
  },
  {
    "path": "automotive/src/main/res/drawable/sign_in_toolbar_back_ripple_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<ripple xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:color=\"?attr/colorControlHighlight\"\n    android:radius=\"@dimen/sign_in_toolbar_nav_button_size\" />\n"
  },
  {
    "path": "automotive/src/main/res/drawable-v24/ic_launcher_foreground.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google LLC\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#FF000000\"\n        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\" />\n</vector>\n"
  },
  {
    "path": "automotive/src/main/res/layout/activity_login.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:gravity=\"center\"\n    android:orientation=\"vertical\">\n\n    <ImageView\n        android:layout_width=\"0dp\"\n        android:layout_height=\"0dp\"\n        android:src=\"@drawable/ic_launcher_foreground\"\n        android:tint=\"@color/colorPrimary\"\n        app:layout_constraintBottom_toTopOf=\"@+id/email\"\n        app:layout_constraintLeft_toLeftOf=\"@id/email\"\n        app:layout_constraintRight_toRightOf=\"@id/email\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n    <EditText\n        android:id=\"@+id/email\"\n        android:layout_width=\"400dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:hint=\"@string/email_hint\"\n        android:inputType=\"textEmailAddress\"\n        android:maxLines=\"1\"\n        android:singleLine=\"true\"\n        app:layout_constraintBottom_toTopOf=\"@+id/password\"\n        app:layout_constraintLeft_toLeftOf=\"parent\"\n        app:layout_constraintRight_toRightOf=\"parent\" />\n\n    <EditText\n        android:id=\"@+id/password\"\n        android:layout_width=\"400dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:focusableInTouchMode=\"true\"\n        android:hint=\"@string/password_hint\"\n        android:imeActionLabel=\"@+id/login\"\n        android:imeOptions=\"actionUnspecified\"\n        android:inputType=\"textPassword\"\n        android:maxLines=\"1\"\n        android:singleLine=\"true\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintLeft_toLeftOf=\"parent\"\n        app:layout_constraintRight_toRightOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n    <Button\n        android:id=\"@+id/sign_in_button\"\n        style=\"?android:textAppearanceSmall\"\n        android:layout_width=\"400dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:layout_marginTop=\"16dp\"\n        android:background=\"@color/colorAccent\"\n        android:text=\"@string/login_button_label\"\n        android:textColor=\"@android:color/black\"\n        android:textStyle=\"bold\"\n        app:layout_constraintLeft_toLeftOf=\"parent\"\n        app:layout_constraintRight_toRightOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@+id/password\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>\n"
  },
  {
    "path": "automotive/src/main/res/layout/activity_settings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/settings_activity\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n\n        <androidx.appcompat.widget.Toolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/actionBarSize\"\n            android:background=\"?attr/colorPrimary\" />\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <FrameLayout\n        android:id=\"@+id/settings_container\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:background=\"@color/darkBackground\" />\n</LinearLayout>\n"
  },
  {
    "path": "automotive/src/main/res/layout/activity_sign_in.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/sign_in_container\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"></FrameLayout>\n"
  },
  {
    "path": "automotive/src/main/res/layout/phone_sign_in.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <ScrollView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <androidx.constraintlayout.widget.ConstraintLayout\n            android:id=\"@+id/content_container\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:paddingTop=\"@dimen/sign_in_toolbar_height\">\n\n            <ImageView\n                android:id=\"@+id/app_icon\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"@dimen/sign_in_app_icon_top_margin\"\n                android:adjustViewBounds=\"true\"\n                android:maxHeight=\"@dimen/sign_in_app_icon_max_height\"\n                android:scaleType=\"fitXY\"\n                app:layout_constraintLeft_toLeftOf=\"parent\"\n                app:layout_constraintRight_toRightOf=\"parent\"\n                app:layout_constraintTop_toTopOf=\"parent\" />\n\n            <TextView\n                android:id=\"@+id/primary_message\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"@dimen/sign_in_primary_message_top_margin\"\n                android:adjustViewBounds=\"true\"\n                android:maxWidth=\"@dimen/sign_in_text_max_width\"\n                android:textAppearance=\"@style/PrimaryText\"\n                app:layout_constraintLeft_toLeftOf=\"parent\"\n                app:layout_constraintRight_toRightOf=\"parent\"\n                app:layout_constraintTop_toBottomOf=\"@+id/app_icon\" />\n\n            <androidx.constraintlayout.widget.Guideline\n                android:id=\"@+id/start_guideline\"\n                android:layout_width=\"1dp\"\n                android:layout_height=\"wrap_content\"\n                android:orientation=\"vertical\"\n                app:layout_constraintGuide_percent=\"@dimen/sign_in_horizontal_start_guideline\" />\n\n            <androidx.constraintlayout.widget.Guideline\n                android:id=\"@+id/end_guideline\"\n                android:layout_width=\"1dp\"\n                android:layout_height=\"wrap_content\"\n                android:orientation=\"vertical\"\n                app:layout_constraintGuide_percent=\"@dimen/sign_in_horizontal_end_guideline\" />\n\n            <Button\n                android:id=\"@+id/pin_sign_in_button\"\n                style=\"@style/CarButton.SignIn\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"@dimen/sign_in_button1_top_margin_no_input\"\n                app:layout_constraintEnd_toEndOf=\"@id/end_guideline\"\n                app:layout_constraintStart_toStartOf=\"@id/start_guideline\"\n                app:layout_constraintTop_toBottomOf=\"@id/primary_message\"\n                app:layout_constraintWidth_max=\"@dimen/sign_in_button_max_width\" />\n\n            <Button\n                android:id=\"@+id/qr_sign_in_button\"\n                style=\"@style/CarButton.SignIn\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"@dimen/sign_in_button2_top_margin_no_input\"\n                app:layout_constraintEnd_toEndOf=\"@id/end_guideline\"\n                app:layout_constraintStart_toStartOf=\"@id/start_guideline\"\n                app:layout_constraintTop_toBottomOf=\"@id/pin_sign_in_button\"\n                app:layout_constraintWidth_max=\"@dimen/sign_in_button_max_width\" />\n\n            <TextView\n                android:id=\"@+id/footer\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"@dimen/sign_in_footer_top_margin\"\n                android:layout_marginBottom=\"@dimen/sign_in_footer_bottom_margin\"\n                android:adjustViewBounds=\"true\"\n                android:gravity=\"center\"\n                android:maxWidth=\"@dimen/sign_in_text_max_width\"\n                android:textAppearance=\"@style/SecondaryText\"\n                app:layout_constraintBottom_toBottomOf=\"parent\"\n                app:layout_constraintLeft_toLeftOf=\"parent\"\n                app:layout_constraintRight_toRightOf=\"parent\"\n                app:layout_constraintTop_toBottomOf=\"@id/qr_sign_in_button\" />\n\n        </androidx.constraintlayout.widget.ConstraintLayout>\n\n    </ScrollView>\n\n    <androidx.appcompat.widget.Toolbar\n        android:id=\"@+id/toolbar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintTop_toTopOf=\"parent\">\n\n    </androidx.appcompat.widget.Toolbar>\n\n</FrameLayout>\n"
  },
  {
    "path": "automotive/src/main/res/layout/pin_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<TextView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:layout_marginStart=\"@dimen/sign_in_pin_container_margin\"\n    android:layout_marginEnd=\"@dimen/sign_in_pin_container_margin\"\n    android:background=\"@drawable/pin_background\"\n    android:paddingStart=\"@dimen/sign_in_pin_container_padding\"\n    android:paddingEnd=\"@dimen/sign_in_pin_container_padding\"\n    android:textAppearance=\"@style/DisplayText\"\n    android:textColor=\"@color/colorAccent\" />\n"
  },
  {
    "path": "automotive/src/main/res/layout/pin_sign_in.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <ScrollView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <androidx.constraintlayout.widget.ConstraintLayout\n            android:id=\"@+id/content_container\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:paddingTop=\"@dimen/sign_in_toolbar_height\">\n\n            <ImageView\n                android:id=\"@+id/app_icon\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"@dimen/sign_in_app_icon_top_margin\"\n                android:adjustViewBounds=\"true\"\n                android:maxHeight=\"@dimen/sign_in_app_icon_max_height\"\n                android:scaleType=\"fitXY\"\n                app:layout_constraintLeft_toLeftOf=\"parent\"\n                app:layout_constraintRight_toRightOf=\"parent\"\n                app:layout_constraintTop_toTopOf=\"parent\" />\n\n            <TextView\n                android:id=\"@+id/primary_message\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"@dimen/sign_in_primary_message_top_margin\"\n                android:adjustViewBounds=\"true\"\n                android:maxWidth=\"@dimen/sign_in_text_max_width\"\n                android:textAppearance=\"@style/PrimaryText\"\n                app:layout_constraintLeft_toLeftOf=\"parent\"\n                app:layout_constraintRight_toRightOf=\"parent\"\n                app:layout_constraintTop_toBottomOf=\"@+id/app_icon\" />\n\n            <TextView\n                android:id=\"@+id/secondary_message\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"@dimen/sign_in_secondary_message_top_margin\"\n                android:adjustViewBounds=\"true\"\n                android:maxWidth=\"@dimen/sign_in_text_max_width\"\n                android:textAppearance=\"@style/SecondaryText\"\n                app:layout_constraintLeft_toLeftOf=\"parent\"\n                app:layout_constraintRight_toRightOf=\"parent\"\n                app:layout_constraintTop_toBottomOf=\"@+id/primary_message\" />\n\n            <LinearLayout\n                android:id=\"@+id/pin_code_container\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"@dimen/sign_in_input_top_margin\"\n                android:orientation=\"horizontal\"\n                app:layout_constraintLeft_toLeftOf=\"parent\"\n                app:layout_constraintRight_toRightOf=\"parent\"\n                app:layout_constraintTop_toBottomOf=\"@id/secondary_message\" />\n\n            <TextView\n                android:id=\"@+id/footer\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"@dimen/sign_in_footer_top_margin\"\n                android:layout_marginBottom=\"@dimen/sign_in_footer_bottom_margin\"\n                android:adjustViewBounds=\"true\"\n                android:gravity=\"center\"\n                android:maxWidth=\"@dimen/sign_in_text_max_width\"\n                android:textAppearance=\"@style/SecondaryText\"\n                app:layout_constraintBottom_toBottomOf=\"parent\"\n                app:layout_constraintLeft_toLeftOf=\"parent\"\n                app:layout_constraintRight_toRightOf=\"parent\"\n                app:layout_constraintTop_toBottomOf=\"@id/pin_code_container\" />\n        </androidx.constraintlayout.widget.ConstraintLayout>\n\n    </ScrollView>\n\n    <androidx.appcompat.widget.Toolbar\n        android:id=\"@+id/toolbar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:background=\"@android:color/transparent\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n</FrameLayout>\n"
  },
  {
    "path": "automotive/src/main/res/layout/preference.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google LLC\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     https://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n-->\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:orientation=\"vertical\">\n\n    <TextView\n        android:id=\"@android:id/title\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginLeft=\"120dp\"\n        android:layout_marginTop=\"15dp\"\n        android:layout_marginBottom=\"15dp\"\n        android:textSize=\"20sp\" />\n\n</LinearLayout>"
  },
  {
    "path": "automotive/src/main/res/layout/preference_category.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google LLC\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     https://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n-->\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:orientation=\"vertical\">\n\n    <TextView\n        android:id=\"@android:id/title\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginLeft=\"60dp\"\n        android:layout_marginTop=\"25dp\"\n        android:layout_marginBottom=\"25dp\"\n        android:textSize=\"26sp\" />\n\n</LinearLayout>"
  },
  {
    "path": "automotive/src/main/res/layout/qr_sign_in.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <ScrollView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <androidx.constraintlayout.widget.ConstraintLayout\n            android:id=\"@+id/content_container\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:paddingTop=\"@dimen/sign_in_toolbar_height\">\n\n            <ImageView\n                android:id=\"@+id/app_icon\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"@dimen/sign_in_app_icon_top_margin\"\n                android:adjustViewBounds=\"true\"\n                android:maxHeight=\"@dimen/sign_in_app_icon_max_height\"\n                android:scaleType=\"fitXY\"\n                app:layout_constraintLeft_toLeftOf=\"parent\"\n                app:layout_constraintRight_toRightOf=\"parent\"\n                app:layout_constraintTop_toTopOf=\"parent\" />\n\n            <TextView\n                android:id=\"@+id/primary_message\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"@dimen/sign_in_primary_message_top_margin\"\n                android:adjustViewBounds=\"true\"\n                android:maxWidth=\"@dimen/sign_in_text_max_width\"\n                android:textAppearance=\"@style/PrimaryText\"\n                app:layout_constraintLeft_toLeftOf=\"parent\"\n                app:layout_constraintRight_toRightOf=\"parent\"\n                app:layout_constraintTop_toBottomOf=\"@+id/app_icon\" />\n\n            <TextView\n                android:id=\"@+id/secondary_message\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"@dimen/sign_in_secondary_message_top_margin\"\n                android:adjustViewBounds=\"true\"\n                android:maxWidth=\"@dimen/sign_in_text_max_width\"\n                android:textAppearance=\"@style/SecondaryText\"\n                app:layout_constraintLeft_toLeftOf=\"parent\"\n                app:layout_constraintRight_toRightOf=\"parent\"\n                app:layout_constraintTop_toBottomOf=\"@+id/primary_message\" />\n\n            <ImageView\n                android:id=\"@+id/qr_code\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"@dimen/sign_in_input_top_margin\"\n                android:adjustViewBounds=\"true\"\n                android:maxHeight=\"196dp\"\n                android:scaleType=\"fitCenter\"\n                app:layout_constraintLeft_toLeftOf=\"parent\"\n                app:layout_constraintRight_toRightOf=\"parent\"\n                app:layout_constraintTop_toBottomOf=\"@id/secondary_message\" />\n\n            <TextView\n                android:id=\"@+id/footer\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"@dimen/sign_in_footer_top_margin\"\n                android:layout_marginBottom=\"@dimen/sign_in_footer_bottom_margin\"\n                android:adjustViewBounds=\"true\"\n                android:gravity=\"center\"\n                android:maxWidth=\"@dimen/sign_in_text_max_width\"\n                android:textAppearance=\"@style/SecondaryText\"\n                app:layout_constraintBottom_toBottomOf=\"parent\"\n                app:layout_constraintLeft_toLeftOf=\"parent\"\n                app:layout_constraintRight_toRightOf=\"parent\"\n                app:layout_constraintTop_toBottomOf=\"@id/qr_code\" />\n        </androidx.constraintlayout.widget.ConstraintLayout>\n\n    </ScrollView>\n\n    <androidx.appcompat.widget.Toolbar\n        android:id=\"@+id/toolbar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n</FrameLayout>\n"
  },
  {
    "path": "automotive/src/main/res/layout/sign_in_landing_page.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <ScrollView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <androidx.constraintlayout.widget.ConstraintLayout\n            android:id=\"@+id/content_container\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:paddingTop=\"@dimen/sign_in_toolbar_height\">\n\n            <ImageView\n                android:id=\"@+id/app_icon\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"@dimen/sign_in_app_icon_top_margin\"\n                android:adjustViewBounds=\"true\"\n                android:maxHeight=\"@dimen/sign_in_app_icon_max_height\"\n                android:scaleType=\"fitXY\"\n                app:layout_constraintLeft_toLeftOf=\"parent\"\n                app:layout_constraintRight_toRightOf=\"parent\"\n                app:layout_constraintTop_toTopOf=\"parent\" />\n\n            <TextView\n                android:id=\"@+id/primary_message\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"@dimen/sign_in_primary_message_top_margin\"\n                android:adjustViewBounds=\"true\"\n                android:maxWidth=\"@dimen/sign_in_text_max_width\"\n                android:textAppearance=\"@style/PrimaryText\"\n                app:layout_constraintLeft_toLeftOf=\"parent\"\n                app:layout_constraintRight_toRightOf=\"parent\"\n                app:layout_constraintTop_toBottomOf=\"@+id/app_icon\" />\n\n            <androidx.constraintlayout.widget.Guideline\n                android:id=\"@+id/start_guideline\"\n                android:layout_width=\"1dp\"\n                android:layout_height=\"wrap_content\"\n                android:orientation=\"vertical\"\n                app:layout_constraintGuide_percent=\"@dimen/sign_in_horizontal_start_guideline\" />\n\n            <androidx.constraintlayout.widget.Guideline\n                android:id=\"@+id/end_guideline\"\n                android:layout_width=\"1dp\"\n                android:layout_height=\"wrap_content\"\n                android:orientation=\"vertical\"\n                app:layout_constraintGuide_percent=\"@dimen/sign_in_horizontal_end_guideline\" />\n\n            <Button\n                android:id=\"@+id/phone_sign_in_button\"\n                style=\"@style/CarButton.SignIn\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"@dimen/sign_in_button1_top_margin_no_input\"\n                app:layout_constraintEnd_toEndOf=\"@id/end_guideline\"\n                app:layout_constraintStart_toStartOf=\"@id/start_guideline\"\n                app:layout_constraintTop_toBottomOf=\"@id/primary_message\"\n                app:layout_constraintWidth_max=\"@dimen/sign_in_button_max_width\" />\n\n            <Button\n                android:id=\"@+id/google_sign_in_button\"\n                style=\"@style/CarButton.GoogleSignIn\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"@dimen/sign_in_button2_top_margin_no_input\"\n                app:layout_constraintEnd_toEndOf=\"@id/end_guideline\"\n                app:layout_constraintStart_toStartOf=\"@id/start_guideline\"\n                app:layout_constraintTop_toBottomOf=\"@id/phone_sign_in_button\"\n                app:layout_constraintWidth_max=\"@dimen/sign_in_button_max_width\"\n                app:layout_goneMarginTop=\"@dimen/sign_in_button1_top_margin_no_input\" />\n\n            <TextView\n                android:id=\"@+id/help_text\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"@dimen/sign_in_footer_top_margin\"\n                android:layout_marginBottom=\"@dimen/sign_in_footer_bottom_margin\"\n                android:adjustViewBounds=\"true\"\n                android:gravity=\"center\"\n                android:maxWidth=\"@dimen/sign_in_text_max_width\"\n                android:text=\"@string/username_help_text\"\n                android:textAppearance=\"@style/SecondaryText\"\n                app:layout_constraintBottom_toTopOf=\"@id/footer\"\n                app:layout_constraintLeft_toLeftOf=\"parent\"\n                app:layout_constraintRight_toRightOf=\"parent\"\n                app:layout_constraintTop_toBottomOf=\"@id/google_sign_in_button\" />\n\n            <TextView\n                android:id=\"@+id/footer\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginBottom=\"@dimen/sign_in_footer_bottom_margin\"\n                android:adjustViewBounds=\"true\"\n                android:gravity=\"center\"\n                android:maxWidth=\"@dimen/sign_in_text_max_width\"\n                android:textAppearance=\"@style/SecondaryText\"\n                app:layout_constraintBottom_toBottomOf=\"parent\"\n                app:layout_constraintLeft_toLeftOf=\"parent\"\n                app:layout_constraintRight_toRightOf=\"parent\"\n                app:layout_constraintTop_toBottomOf=\"@id/help_text\" />\n\n        </androidx.constraintlayout.widget.ConstraintLayout>\n\n    </ScrollView>\n\n    <androidx.appcompat.widget.Toolbar\n        android:id=\"@+id/toolbar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n</FrameLayout>\n"
  },
  {
    "path": "automotive/src/main/res/layout/sign_in_landing_page_with_username_and_password.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <ScrollView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <androidx.constraintlayout.widget.ConstraintLayout\n            android:id=\"@+id/content_container\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:paddingTop=\"@dimen/sign_in_toolbar_height\">\n\n            <ImageView\n                android:id=\"@+id/app_icon\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"@dimen/sign_in_app_icon_top_margin\"\n                android:adjustViewBounds=\"true\"\n                android:maxHeight=\"@dimen/sign_in_app_icon_max_height\"\n                android:scaleType=\"fitXY\"\n                app:layout_constraintLeft_toLeftOf=\"parent\"\n                app:layout_constraintRight_toRightOf=\"parent\"\n                app:layout_constraintTop_toTopOf=\"parent\" />\n\n            <TextView\n                android:id=\"@+id/primary_message\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"@dimen/sign_in_primary_message_top_margin\"\n                android:adjustViewBounds=\"true\"\n                android:maxWidth=\"@dimen/sign_in_text_max_width\"\n                android:textAppearance=\"@style/PrimaryText\"\n                app:layout_constraintLeft_toLeftOf=\"parent\"\n                app:layout_constraintRight_toRightOf=\"parent\"\n                app:layout_constraintTop_toBottomOf=\"@+id/app_icon\" />\n\n            <androidx.constraintlayout.widget.Guideline\n                android:id=\"@+id/start_guideline\"\n                android:layout_width=\"1dp\"\n                android:layout_height=\"wrap_content\"\n                android:orientation=\"vertical\"\n                app:layout_constraintGuide_percent=\"@dimen/sign_in_horizontal_start_guideline\" />\n\n            <androidx.constraintlayout.widget.Guideline\n                android:id=\"@+id/end_guideline\"\n                android:layout_width=\"1dp\"\n                android:layout_height=\"wrap_content\"\n                android:orientation=\"vertical\"\n                app:layout_constraintGuide_percent=\"@dimen/sign_in_horizontal_end_guideline\" />\n\n            <com.google.android.material.textfield.TextInputLayout\n                android:id=\"@+id/identifier_container\"\n                style=\"@style/InputBox\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"@dimen/sign_in_input_top_margin\"\n                app:errorTextAppearance=\"@style/SecondaryText\"\n                app:hintTextAppearance=\"@style/SecondaryText\"\n                app:layout_constraintEnd_toEndOf=\"@id/end_guideline\"\n                app:layout_constraintStart_toStartOf=\"@id/start_guideline\"\n                app:layout_constraintTop_toBottomOf=\"@id/primary_message\"\n                app:layout_constraintWidth_max=\"@dimen/sign_in_text_max_width\">\n\n                <com.google.android.material.textfield.TextInputEditText\n                    android:id=\"@+id/identifier_input\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:maxLines=\"1\"\n                    android:textAppearance=\"@style/SecondaryText\"\n                    android:textCursorDrawable=\"@null\" />\n            </com.google.android.material.textfield.TextInputLayout>\n\n            <Button\n                android:id=\"@+id/phone_sign_in_button\"\n                style=\"@style/CarButton.SignIn\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"@dimen/sign_in_button1_top_margin\"\n                app:layout_constraintEnd_toEndOf=\"@id/end_guideline\"\n                app:layout_constraintStart_toStartOf=\"@id/start_guideline\"\n                app:layout_constraintTop_toBottomOf=\"@id/identifier_container\"\n                app:layout_constraintWidth_max=\"@dimen/sign_in_button_max_width\" />\n\n            <Button\n                android:id=\"@+id/google_sign_in_button\"\n                style=\"@style/CarButton.GoogleSignIn\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"@dimen/sign_in_button2_top_margin\"\n                android:drawableStart=\"@drawable/google_sign_in_button_logo\"\n                app:layout_constraintEnd_toEndOf=\"@id/end_guideline\"\n                app:layout_constraintStart_toStartOf=\"@id/start_guideline\"\n                app:layout_constraintTop_toBottomOf=\"@id/phone_sign_in_button\"\n                app:layout_constraintWidth_max=\"@dimen/sign_in_button_max_width\"\n                app:layout_goneMarginTop=\"@dimen/sign_in_button1_top_margin\" />\n\n            <TextView\n                android:id=\"@+id/help_text\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"@dimen/sign_in_footer_top_margin\"\n                android:layout_marginBottom=\"@dimen/sign_in_footer_bottom_margin\"\n                android:adjustViewBounds=\"true\"\n                android:gravity=\"center\"\n                android:maxWidth=\"@dimen/sign_in_text_max_width\"\n                android:text=\"@string/username_help_text\"\n                android:textAppearance=\"@style/SecondaryText\"\n                app:layout_constraintBottom_toTopOf=\"@id/footer\"\n                app:layout_constraintLeft_toLeftOf=\"parent\"\n                app:layout_constraintRight_toRightOf=\"parent\"\n                app:layout_constraintTop_toBottomOf=\"@id/google_sign_in_button\" />\n\n            <TextView\n                android:id=\"@+id/footer\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginBottom=\"@dimen/sign_in_footer_bottom_margin\"\n                android:adjustViewBounds=\"true\"\n                android:gravity=\"center\"\n                android:maxWidth=\"@dimen/sign_in_text_max_width\"\n                android:textAppearance=\"@style/SecondaryText\"\n                app:layout_constraintBottom_toBottomOf=\"parent\"\n                app:layout_constraintLeft_toLeftOf=\"parent\"\n                app:layout_constraintRight_toRightOf=\"parent\"\n                app:layout_constraintTop_toBottomOf=\"@id/help_text\" />\n\n        </androidx.constraintlayout.widget.ConstraintLayout>\n\n    </ScrollView>\n\n    <androidx.appcompat.widget.Toolbar\n        android:id=\"@+id/toolbar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintTop_toTopOf=\"parent\">\n\n        <Button\n            android:id=\"@+id/sign_in_button\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"end\" />\n\n    </androidx.appcompat.widget.Toolbar>\n\n</FrameLayout>\n"
  },
  {
    "path": "automotive/src/main/res/layout/username_and_password_sign_in.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <ScrollView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <androidx.constraintlayout.widget.ConstraintLayout\n            android:id=\"@+id/content_container\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:paddingTop=\"@dimen/sign_in_toolbar_height\">\n\n            <ImageView\n                android:id=\"@+id/app_icon\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"@dimen/sign_in_app_icon_top_margin\"\n                android:adjustViewBounds=\"true\"\n                android:maxHeight=\"@dimen/sign_in_app_icon_max_height\"\n                android:scaleType=\"fitXY\"\n                app:layout_constraintLeft_toLeftOf=\"parent\"\n                app:layout_constraintRight_toRightOf=\"parent\"\n                app:layout_constraintTop_toTopOf=\"parent\" />\n\n            <TextView\n                android:id=\"@+id/primary_message\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"@dimen/sign_in_primary_message_top_margin\"\n                android:adjustViewBounds=\"true\"\n                android:maxWidth=\"@dimen/sign_in_text_max_width\"\n                android:textAppearance=\"@style/PrimaryText\"\n                app:layout_constraintLeft_toLeftOf=\"parent\"\n                app:layout_constraintRight_toRightOf=\"parent\"\n                app:layout_constraintTop_toBottomOf=\"@+id/app_icon\" />\n\n            <androidx.constraintlayout.widget.Guideline\n                android:id=\"@+id/start_guideline\"\n                android:layout_width=\"1dp\"\n                android:layout_height=\"wrap_content\"\n                android:orientation=\"vertical\"\n                app:layout_constraintGuide_percent=\"@dimen/sign_in_horizontal_start_guideline\" />\n\n            <androidx.constraintlayout.widget.Guideline\n                android:id=\"@+id/end_guideline\"\n                android:layout_width=\"1dp\"\n                android:layout_height=\"wrap_content\"\n                android:orientation=\"vertical\"\n                app:layout_constraintGuide_percent=\"@dimen/sign_in_horizontal_end_guideline\" />\n\n            <com.google.android.material.textfield.TextInputLayout\n                android:id=\"@+id/password_container\"\n                style=\"@style/InputBox\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"@dimen/sign_in_input_top_margin\"\n                app:errorTextAppearance=\"@style/SecondaryText\"\n                app:hintTextAppearance=\"@style/SecondaryText\"\n                app:layout_constraintEnd_toEndOf=\"@id/end_guideline\"\n                app:layout_constraintStart_toStartOf=\"@id/start_guideline\"\n                app:layout_constraintTop_toBottomOf=\"@id/primary_message\"\n                app:layout_constraintWidth_max=\"@dimen/sign_in_text_max_width\"\n                app:passwordToggleEnabled=\"true\">\n\n                <com.google.android.material.textfield.TextInputEditText\n                    android:id=\"@+id/password_input\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:inputType=\"textPassword\"\n                    android:maxLines=\"1\"\n                    android:textAppearance=\"@style/SecondaryText\" />\n            </com.google.android.material.textfield.TextInputLayout>\n\n            <TextView\n                android:id=\"@+id/help_text\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"@dimen/sign_in_footer_top_margin\"\n                android:layout_marginBottom=\"@dimen/sign_in_footer_bottom_margin\"\n                android:adjustViewBounds=\"true\"\n                android:gravity=\"center\"\n                android:maxWidth=\"@dimen/sign_in_text_max_width\"\n                android:text=\"@string/password_help_text\"\n                android:textAppearance=\"@style/SecondaryText\"\n                app:layout_constraintBottom_toTopOf=\"@id/footer\"\n                app:layout_constraintLeft_toLeftOf=\"parent\"\n                app:layout_constraintRight_toRightOf=\"parent\"\n                app:layout_constraintTop_toBottomOf=\"@id/password_container\" />\n\n            <TextView\n                android:id=\"@+id/footer\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginBottom=\"@dimen/sign_in_footer_bottom_margin\"\n                android:adjustViewBounds=\"true\"\n                android:gravity=\"center\"\n                android:maxWidth=\"@dimen/sign_in_text_max_width\"\n                android:textAppearance=\"@style/SecondaryText\"\n                app:layout_constraintBottom_toBottomOf=\"parent\"\n                app:layout_constraintLeft_toLeftOf=\"parent\"\n                app:layout_constraintRight_toRightOf=\"parent\"\n                app:layout_constraintTop_toBottomOf=\"@id/help_text\" />\n\n        </androidx.constraintlayout.widget.ConstraintLayout>\n\n    </ScrollView>\n\n    <androidx.appcompat.widget.Toolbar\n        android:id=\"@+id/toolbar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintTop_toTopOf=\"parent\">\n\n        <Button\n            android:id=\"@+id/submit_button\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"end\" />\n\n    </androidx.appcompat.widget.Toolbar>\n\n</FrameLayout>\n"
  },
  {
    "path": "automotive/src/main/res/layout-h900dp/phone_sign_in.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <androidx.constraintlayout.widget.ConstraintLayout\n        android:id=\"@+id/content_container\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <ImageView\n            android:id=\"@+id/app_icon\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:adjustViewBounds=\"true\"\n            android:maxHeight=\"@dimen/sign_in_app_icon_max_height\"\n            android:scaleType=\"fitXY\"\n            app:layout_constraintBottom_toTopOf=\"@id/primary_message\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"parent\"\n            app:layout_constraintVertical_chainStyle=\"packed\" />\n\n        <TextView\n            android:id=\"@+id/primary_message\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"@dimen/sign_in_primary_message_top_margin\"\n            android:adjustViewBounds=\"true\"\n            android:maxWidth=\"@dimen/sign_in_text_max_width\"\n            android:textAppearance=\"@style/PrimaryText\"\n            app:layout_constraintBottom_toTopOf=\"@+id/pin_sign_in_button\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@+id/app_icon\" />\n\n        <androidx.constraintlayout.widget.Guideline\n            android:id=\"@+id/start_guideline\"\n            android:layout_width=\"1dp\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"vertical\"\n            app:layout_constraintGuide_percent=\"@dimen/sign_in_horizontal_start_guideline\" />\n\n        <androidx.constraintlayout.widget.Guideline\n            android:id=\"@+id/end_guideline\"\n            android:layout_width=\"1dp\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"vertical\"\n            app:layout_constraintGuide_percent=\"@dimen/sign_in_horizontal_end_guideline\" />\n\n        <Button\n            android:id=\"@+id/pin_sign_in_button\"\n            style=\"@style/CarButton.SignIn\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"@dimen/sign_in_button1_top_margin_no_input\"\n            app:layout_constraintBottom_toTopOf=\"@id/qr_sign_in_button\"\n            app:layout_constraintEnd_toEndOf=\"@id/end_guideline\"\n            app:layout_constraintStart_toStartOf=\"@id/start_guideline\"\n            app:layout_constraintTop_toBottomOf=\"@id/primary_message\"\n            app:layout_constraintWidth_max=\"@dimen/sign_in_button_max_width\" />\n\n        <Button\n            android:id=\"@+id/qr_sign_in_button\"\n            style=\"@style/CarButton.SignIn\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"@dimen/sign_in_button2_top_margin_no_input\"\n            app:layout_constraintBottom_toTopOf=\"@id/footer\"\n            app:layout_constraintEnd_toEndOf=\"@id/end_guideline\"\n            app:layout_constraintStart_toStartOf=\"@id/start_guideline\"\n            app:layout_constraintTop_toBottomOf=\"@id/pin_sign_in_button\"\n            app:layout_constraintWidth_max=\"@dimen/sign_in_button_max_width\" />\n\n        <TextView\n            android:id=\"@+id/footer\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginBottom=\"@dimen/sign_in_footer_bottom_margin\"\n            android:adjustViewBounds=\"true\"\n            android:gravity=\"center\"\n            android:maxWidth=\"@dimen/sign_in_text_max_width\"\n            android:textAppearance=\"@style/SecondaryText\"\n            app:layout_constraintBottom_toBottomOf=\"parent\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\" />\n\n    </androidx.constraintlayout.widget.ConstraintLayout>\n\n    <androidx.appcompat.widget.Toolbar\n        android:id=\"@+id/toolbar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintTop_toTopOf=\"parent\"></androidx.appcompat.widget.Toolbar>\n\n</FrameLayout>\n"
  },
  {
    "path": "automotive/src/main/res/layout-h900dp/pin_sign_in.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <androidx.constraintlayout.widget.ConstraintLayout\n        android:id=\"@+id/content_container\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <ImageView\n            android:id=\"@+id/app_icon\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:adjustViewBounds=\"true\"\n            android:maxHeight=\"@dimen/sign_in_app_icon_max_height\"\n            android:scaleType=\"fitXY\"\n            app:layout_constraintBottom_toTopOf=\"@id/primary_message\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"parent\"\n            app:layout_constraintVertical_chainStyle=\"packed\" />\n\n        <TextView\n            android:id=\"@+id/primary_message\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"@dimen/sign_in_primary_message_top_margin\"\n            android:adjustViewBounds=\"true\"\n            android:maxWidth=\"@dimen/sign_in_text_max_width\"\n            android:textAppearance=\"@style/PrimaryText\"\n            app:layout_constraintBottom_toTopOf=\"@+id/secondary_message\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@+id/app_icon\" />\n\n        <TextView\n            android:id=\"@+id/secondary_message\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"@dimen/sign_in_secondary_message_top_margin\"\n            android:adjustViewBounds=\"true\"\n            android:maxWidth=\"@dimen/sign_in_text_max_width\"\n            android:textAppearance=\"@style/SecondaryText\"\n            app:layout_constraintBottom_toTopOf=\"@+id/pin_code_container\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@+id/primary_message\" />\n\n        <LinearLayout\n            android:id=\"@+id/pin_code_container\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"@dimen/sign_in_input_top_margin\"\n            android:orientation=\"horizontal\"\n            app:layout_constraintBottom_toTopOf=\"@+id/footer\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@id/secondary_message\" />\n\n        <TextView\n            android:id=\"@+id/footer\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginBottom=\"@dimen/sign_in_footer_bottom_margin\"\n            android:adjustViewBounds=\"true\"\n            android:gravity=\"center\"\n            android:maxWidth=\"@dimen/sign_in_text_max_width\"\n            android:textAppearance=\"@style/SecondaryText\"\n            app:layout_constraintBottom_toBottomOf=\"parent\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\" />\n    </androidx.constraintlayout.widget.ConstraintLayout>\n\n    <androidx.appcompat.widget.Toolbar\n        android:id=\"@+id/toolbar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:background=\"@android:color/transparent\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n</FrameLayout>\n"
  },
  {
    "path": "automotive/src/main/res/layout-h900dp/qr_sign_in.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <androidx.constraintlayout.widget.ConstraintLayout\n        android:id=\"@+id/content_container\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <ImageView\n            android:id=\"@+id/app_icon\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:adjustViewBounds=\"true\"\n            android:maxHeight=\"@dimen/sign_in_app_icon_max_height\"\n            android:scaleType=\"fitXY\"\n            app:layout_constraintBottom_toTopOf=\"@id/primary_message\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"parent\"\n            app:layout_constraintVertical_chainStyle=\"packed\" />\n\n        <TextView\n            android:id=\"@+id/primary_message\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"@dimen/sign_in_primary_message_top_margin\"\n            android:adjustViewBounds=\"true\"\n            android:maxWidth=\"@dimen/sign_in_text_max_width\"\n            android:textAppearance=\"@style/PrimaryText\"\n            app:layout_constraintBottom_toTopOf=\"@+id/secondary_message\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@+id/app_icon\" />\n\n        <TextView\n            android:id=\"@+id/secondary_message\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"@dimen/sign_in_secondary_message_top_margin\"\n            android:adjustViewBounds=\"true\"\n            android:maxWidth=\"@dimen/sign_in_text_max_width\"\n            android:textAppearance=\"@style/SecondaryText\"\n            app:layout_constraintBottom_toTopOf=\"@+id/qr_code\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@+id/primary_message\" />\n\n        <ImageView\n            android:id=\"@+id/qr_code\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"@dimen/sign_in_input_top_margin\"\n            android:adjustViewBounds=\"true\"\n            android:maxHeight=\"196dp\"\n            android:scaleType=\"fitCenter\"\n            app:layout_constraintBottom_toTopOf=\"@+id/footer\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@id/secondary_message\" />\n\n        <TextView\n            android:id=\"@+id/footer\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginBottom=\"@dimen/sign_in_footer_bottom_margin\"\n            android:adjustViewBounds=\"true\"\n            android:gravity=\"center\"\n            android:maxWidth=\"@dimen/sign_in_text_max_width\"\n            android:textAppearance=\"@style/SecondaryText\"\n            app:layout_constraintBottom_toBottomOf=\"parent\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\" />\n    </androidx.constraintlayout.widget.ConstraintLayout>\n\n    <androidx.appcompat.widget.Toolbar\n        android:id=\"@+id/toolbar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n</FrameLayout>\n"
  },
  {
    "path": "automotive/src/main/res/layout-h900dp/sign_in_landing_page.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <androidx.constraintlayout.widget.ConstraintLayout\n        android:id=\"@+id/content_container\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <ImageView\n            android:id=\"@+id/app_icon\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:adjustViewBounds=\"true\"\n            android:maxHeight=\"@dimen/sign_in_app_icon_max_height\"\n            android:scaleType=\"fitXY\"\n            app:layout_constraintBottom_toTopOf=\"@id/primary_message\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"parent\"\n            app:layout_constraintVertical_chainStyle=\"packed\" />\n\n        <TextView\n            android:id=\"@+id/primary_message\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"@dimen/sign_in_primary_message_top_margin\"\n            android:adjustViewBounds=\"true\"\n            android:maxWidth=\"@dimen/sign_in_text_max_width\"\n            android:textAppearance=\"@style/PrimaryText\"\n            app:layout_constraintBottom_toTopOf=\"@+id/phone_sign_in_button\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@+id/app_icon\" />\n\n        <androidx.constraintlayout.widget.Guideline\n            android:id=\"@+id/start_guideline\"\n            android:layout_width=\"1dp\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"vertical\"\n            app:layout_constraintGuide_percent=\"@dimen/sign_in_horizontal_start_guideline\" />\n\n        <androidx.constraintlayout.widget.Guideline\n            android:id=\"@+id/end_guideline\"\n            android:layout_width=\"1dp\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"vertical\"\n            app:layout_constraintGuide_percent=\"@dimen/sign_in_horizontal_end_guideline\" />\n\n        <Button\n            android:id=\"@+id/phone_sign_in_button\"\n            style=\"@style/CarButton.SignIn\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"@dimen/sign_in_button1_top_margin_no_input\"\n            app:layout_constraintBottom_toTopOf=\"@id/google_sign_in_button\"\n            app:layout_constraintEnd_toEndOf=\"@id/end_guideline\"\n            app:layout_constraintStart_toStartOf=\"@id/start_guideline\"\n            app:layout_constraintTop_toBottomOf=\"@id/primary_message\"\n            app:layout_constraintWidth_max=\"@dimen/sign_in_button_max_width\" />\n\n        <Button\n            android:id=\"@+id/google_sign_in_button\"\n            style=\"@style/CarButton.GoogleSignIn\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"@dimen/sign_in_button2_top_margin_no_input\"\n            app:layout_constraintBottom_toTopOf=\"@id/footer\"\n            app:layout_constraintEnd_toEndOf=\"@id/end_guideline\"\n            app:layout_constraintStart_toStartOf=\"@id/start_guideline\"\n            app:layout_constraintTop_toBottomOf=\"@id/phone_sign_in_button\"\n            app:layout_constraintWidth_max=\"@dimen/sign_in_button_max_width\"\n            app:layout_goneMarginTop=\"@dimen/sign_in_button1_top_margin_no_input\" />\n\n        <TextView\n            android:id=\"@+id/help_text\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginBottom=\"@dimen/sign_in_footer_bottom_margin\"\n            android:adjustViewBounds=\"true\"\n            android:gravity=\"center\"\n            android:maxWidth=\"@dimen/sign_in_text_max_width\"\n            android:text=\"@string/username_help_text\"\n            android:textAppearance=\"@style/SecondaryText\"\n            app:layout_constraintBottom_toTopOf=\"@id/footer\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\" />\n\n        <TextView\n            android:id=\"@+id/footer\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginBottom=\"@dimen/sign_in_footer_bottom_margin\"\n            android:adjustViewBounds=\"true\"\n            android:gravity=\"center\"\n            android:maxWidth=\"@dimen/sign_in_text_max_width\"\n            android:textAppearance=\"@style/SecondaryText\"\n            app:layout_constraintBottom_toBottomOf=\"parent\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\" />\n\n    </androidx.constraintlayout.widget.ConstraintLayout>\n\n    <androidx.appcompat.widget.Toolbar\n        android:id=\"@+id/toolbar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n</FrameLayout>\n"
  },
  {
    "path": "automotive/src/main/res/layout-h900dp/sign_in_landing_page_with_username_and_password.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <androidx.constraintlayout.widget.ConstraintLayout\n        android:id=\"@+id/content_container\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <ImageView\n            android:id=\"@+id/app_icon\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:adjustViewBounds=\"true\"\n            android:maxHeight=\"@dimen/sign_in_app_icon_max_height\"\n            android:scaleType=\"fitXY\"\n            app:layout_constraintBottom_toTopOf=\"@id/primary_message\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"parent\"\n            app:layout_constraintVertical_chainStyle=\"packed\" />\n\n        <TextView\n            android:id=\"@+id/primary_message\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"@dimen/sign_in_primary_message_top_margin\"\n            android:adjustViewBounds=\"true\"\n            android:maxWidth=\"@dimen/sign_in_text_max_width\"\n            android:textAppearance=\"@style/PrimaryText\"\n            app:layout_constraintBottom_toTopOf=\"@+id/identifier_container\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@+id/app_icon\" />\n\n        <androidx.constraintlayout.widget.Guideline\n            android:id=\"@+id/start_guideline\"\n            android:layout_width=\"1dp\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"vertical\"\n            app:layout_constraintGuide_percent=\"@dimen/sign_in_horizontal_start_guideline\" />\n\n        <androidx.constraintlayout.widget.Guideline\n            android:id=\"@+id/end_guideline\"\n            android:layout_width=\"1dp\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"vertical\"\n            app:layout_constraintGuide_percent=\"@dimen/sign_in_horizontal_end_guideline\" />\n\n        <com.google.android.material.textfield.TextInputLayout\n            android:id=\"@+id/identifier_container\"\n            style=\"@style/InputBox\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"@dimen/sign_in_input_top_margin\"\n            app:errorTextAppearance=\"@style/SecondaryText\"\n            app:hintTextAppearance=\"@style/SecondaryText\"\n            app:layout_constraintBottom_toTopOf=\"@id/phone_sign_in_button\"\n            app:layout_constraintEnd_toEndOf=\"@id/end_guideline\"\n            app:layout_constraintStart_toStartOf=\"@id/start_guideline\"\n            app:layout_constraintTop_toBottomOf=\"@id/primary_message\"\n            app:layout_constraintWidth_max=\"@dimen/sign_in_text_max_width\">\n\n            <com.google.android.material.textfield.TextInputEditText\n                android:id=\"@+id/identifier_input\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:maxLines=\"1\"\n                android:textAppearance=\"@style/SecondaryText\"\n                android:textCursorDrawable=\"@null\" />\n        </com.google.android.material.textfield.TextInputLayout>\n\n        <Button\n            android:id=\"@+id/phone_sign_in_button\"\n            style=\"@style/CarButton.SignIn\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"@dimen/sign_in_button1_top_margin\"\n            app:layout_constraintBottom_toTopOf=\"@id/google_sign_in_button\"\n            app:layout_constraintEnd_toEndOf=\"@id/end_guideline\"\n            app:layout_constraintStart_toStartOf=\"@id/start_guideline\"\n            app:layout_constraintTop_toBottomOf=\"@id/identifier_container\"\n            app:layout_constraintWidth_max=\"@dimen/sign_in_button_max_width\" />\n\n        <Button\n            android:id=\"@+id/google_sign_in_button\"\n            style=\"@style/CarButton.GoogleSignIn\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"@dimen/sign_in_button2_top_margin\"\n            android:drawableStart=\"@drawable/google_sign_in_button_logo\"\n            app:layout_constraintBottom_toTopOf=\"@id/footer\"\n            app:layout_constraintEnd_toEndOf=\"@id/end_guideline\"\n            app:layout_constraintStart_toStartOf=\"@id/start_guideline\"\n            app:layout_constraintTop_toBottomOf=\"@id/phone_sign_in_button\"\n            app:layout_constraintWidth_max=\"@dimen/sign_in_button_max_width\"\n            app:layout_goneMarginTop=\"@dimen/sign_in_button1_top_margin\" />\n\n        <TextView\n            android:id=\"@+id/help_text\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginBottom=\"@dimen/sign_in_footer_bottom_margin\"\n            android:adjustViewBounds=\"true\"\n            android:gravity=\"center\"\n            android:maxWidth=\"@dimen/sign_in_text_max_width\"\n            android:text=\"@string/username_help_text\"\n            android:textAppearance=\"@style/SecondaryText\"\n            app:layout_constraintBottom_toTopOf=\"@id/footer\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\" />\n\n        <TextView\n            android:id=\"@+id/footer\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginBottom=\"@dimen/sign_in_footer_bottom_margin\"\n            android:adjustViewBounds=\"true\"\n            android:gravity=\"center\"\n            android:maxWidth=\"@dimen/sign_in_text_max_width\"\n            android:textAppearance=\"@style/SecondaryText\"\n            app:layout_constraintBottom_toBottomOf=\"parent\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\" />\n\n    </androidx.constraintlayout.widget.ConstraintLayout>\n\n    <androidx.appcompat.widget.Toolbar\n        android:id=\"@+id/toolbar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintTop_toTopOf=\"parent\">\n\n        <Button\n            android:id=\"@+id/sign_in_button\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"end\" />\n\n    </androidx.appcompat.widget.Toolbar>\n\n</FrameLayout>\n"
  },
  {
    "path": "automotive/src/main/res/layout-h900dp/username_and_password_sign_in.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <androidx.constraintlayout.widget.ConstraintLayout\n        android:id=\"@+id/content_container\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <ImageView\n            android:id=\"@+id/app_icon\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:adjustViewBounds=\"true\"\n            android:maxHeight=\"@dimen/sign_in_app_icon_max_height\"\n            android:scaleType=\"fitXY\"\n            app:layout_constraintBottom_toTopOf=\"@id/primary_message\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"parent\"\n            app:layout_constraintVertical_chainStyle=\"packed\" />\n\n        <TextView\n            android:id=\"@+id/primary_message\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"@dimen/sign_in_primary_message_top_margin\"\n            android:adjustViewBounds=\"true\"\n            android:maxWidth=\"@dimen/sign_in_text_max_width\"\n            android:textAppearance=\"@style/PrimaryText\"\n            app:layout_constraintBottom_toTopOf=\"@+id/password_container\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@+id/app_icon\" />\n\n        <androidx.constraintlayout.widget.Guideline\n            android:id=\"@+id/start_guideline\"\n            android:layout_width=\"1dp\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"vertical\"\n            app:layout_constraintGuide_percent=\"@dimen/sign_in_horizontal_start_guideline\" />\n\n        <androidx.constraintlayout.widget.Guideline\n            android:id=\"@+id/end_guideline\"\n            android:layout_width=\"1dp\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"vertical\"\n            app:layout_constraintGuide_percent=\"@dimen/sign_in_horizontal_end_guideline\" />\n\n        <com.google.android.material.textfield.TextInputLayout\n            android:id=\"@+id/password_container\"\n            style=\"@style/InputBox\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"@dimen/sign_in_input_top_margin\"\n            app:errorTextAppearance=\"@style/SecondaryText\"\n            app:hintTextAppearance=\"@style/SecondaryText\"\n            app:layout_constraintBottom_toBottomOf=\"parent\"\n            app:layout_constraintEnd_toEndOf=\"@id/end_guideline\"\n            app:layout_constraintStart_toStartOf=\"@id/start_guideline\"\n            app:layout_constraintTop_toBottomOf=\"@id/primary_message\"\n            app:layout_constraintWidth_max=\"@dimen/sign_in_text_max_width\"\n            app:passwordToggleEnabled=\"true\">\n\n            <com.google.android.material.textfield.TextInputEditText\n                android:id=\"@+id/password_input\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:inputType=\"textPassword\"\n                android:maxLines=\"1\"\n                android:textAppearance=\"@style/SecondaryText\" />\n        </com.google.android.material.textfield.TextInputLayout>\n\n        <TextView\n            android:id=\"@+id/help_text\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginBottom=\"@dimen/sign_in_footer_bottom_margin\"\n            android:adjustViewBounds=\"true\"\n            android:gravity=\"center\"\n            android:maxWidth=\"@dimen/sign_in_text_max_width\"\n            android:text=\"@string/password_help_text\"\n            android:textAppearance=\"@style/SecondaryText\"\n            app:layout_constraintBottom_toTopOf=\"@id/footer\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\" />\n\n        <TextView\n            android:id=\"@+id/footer\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginBottom=\"@dimen/sign_in_footer_bottom_margin\"\n            android:adjustViewBounds=\"true\"\n            android:gravity=\"center\"\n            android:maxWidth=\"@dimen/sign_in_text_max_width\"\n            android:textAppearance=\"@style/SecondaryText\"\n            app:layout_constraintBottom_toBottomOf=\"parent\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\" />\n\n    </androidx.constraintlayout.widget.ConstraintLayout>\n\n    <androidx.appcompat.widget.Toolbar\n        android:id=\"@+id/toolbar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintTop_toTopOf=\"parent\">\n\n        <Button\n            android:id=\"@+id/submit_button\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"end\" />\n\n    </androidx.appcompat.widget.Toolbar>\n\n</FrameLayout>\n"
  },
  {
    "path": "automotive/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2018 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@drawable/ic_launcher_background\" />\n    <foreground android:drawable=\"@mipmap/ic_launcher_foreground\" />\n</adaptive-icon>"
  },
  {
    "path": "automotive/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google LLC\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@drawable/ic_launcher_background\" />\n    <foreground android:drawable=\"@mipmap/ic_launcher_foreground\" />\n</adaptive-icon>"
  },
  {
    "path": "automotive/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google LLC\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     https://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<resources>\n    <color name=\"colorPrimary\">#840255</color>\n    <color name=\"colorPrimaryDark\">#710144</color>\n    <color name=\"colorAccent\">#14A5A1</color>\n    <color name=\"darkBackground\">#202020</color>\n\n    <color name=\"grey_100\">#fff1f3f4</color>\n    <color name=\"grey_300\">#ffdadce0</color>\n\n    <color name=\"google_sign_in_button_background_color\">#ffffffff</color>\n\n    <!-- Workaround for issue where unfocused state for textInputLayout outline box does not reflect\n     set color. Resolved in 1.1.0-alpha2 . -->\n    <color name=\"mtrl_textinput_default_box_stroke_color\">@color/grey_300</color>\n</resources>\n"
  },
  {
    "path": "automotive/src/main/res/values/dimens.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<resources>\n    <dimen name=\"activity_horizontal_margin\">16dp</dimen>\n    <dimen name=\"activity_vertical_margin\">16dp</dimen>\n\n    <dimen name=\"display_text_size\">64sp</dimen>\n    <dimen name=\"primary_text_size\">32sp</dimen>\n    <dimen name=\"secondary_text_size\">26sp</dimen>\n\n    <dimen name=\"button_min_width\">158dp</dimen>\n    <dimen name=\"button_height\">56dp</dimen>\n    <dimen name=\"button_padding\">56dp</dimen>\n\n    <dimen name=\"sign_in_toolbar_padding\">24dp</dimen>\n    <dimen name=\"sign_in_toolbar_nav_button_size\">48dp</dimen>\n    <dimen name=\"sign_in_toolbar_height\">96dp</dimen>\n\n    <dimen name=\"sign_in_input_box_radius\">4dp</dimen>\n    <dimen name=\"sign_in_input_box_stroke_width\">2dp</dimen>\n\n    <dimen name=\"sign_in_button_corner_radius\">28dp</dimen>\n\n    <dimen name=\"sign_in_app_icon_top_margin\">48dp</dimen>\n    <dimen name=\"sign_in_app_icon_max_height\">96dp</dimen>\n\n    <dimen name=\"sign_in_primary_message_top_margin\">32dp</dimen>\n    <dimen name=\"sign_in_secondary_message_top_margin\">24dp</dimen>\n\n    <dimen name=\"sign_in_text_max_width\">642dp</dimen>\n    <dimen name=\"sign_in_button_max_width\">420dp</dimen>\n    <dimen name=\"sign_in_input_top_margin\">48dp</dimen>\n\n    <dimen name=\"sign_in_button1_top_margin\">48dp</dimen>\n    <dimen name=\"sign_in_button2_top_margin\">32dp</dimen>\n    <dimen name=\"sign_in_button1_top_margin_no_input\">64dp</dimen>\n    <dimen name=\"sign_in_button2_top_margin_no_input\">48dp</dimen>\n\n    <dimen name=\"sign_in_pin_container_margin\">16dp</dimen>\n    <dimen name=\"sign_in_pin_container_padding\">8dp</dimen>\n    <dimen name=\"sign_in_pin_container_corner_radius\">4dp</dimen>\n\n    <dimen name=\"sign_in_footer_top_margin\">96dp</dimen>\n    <dimen name=\"sign_in_footer_bottom_margin\">32dp</dimen>\n    <dimen name=\"sign_in_horizontal_start_guideline\">0.1</dimen>\n    <dimen name=\"sign_in_horizontal_end_guideline\">0.9</dimen>\n</resources>\n"
  },
  {
    "path": "automotive/src/main/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google LLC\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     https://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<resources>\n    <string name=\"app_name\">UAMP</string>\n    <string name=\"settings_label\">UAMP Settings</string>\n\n    <!-- Sign in -->\n    <string name=\"error_login_button\">Login</string>\n    <string name=\"error_require_login\">Authentication required</string>\n    <string name=\"password_hint\">Enter password</string>\n    <string name=\"email_hint\">Email</string>\n    <string name=\"login_button_label\">Login</string>\n    <string name=\"missing_fields_error\">Please fill in missing fields</string>\n    <string name=\"username_help_text\">Forgot username? | Use phone app to sign in</string>\n    <string name=\"password_help_text\">Forgot password? | Reset with phone app</string>\n    <string name=\"sign_in_next_button_label\">Next</string>\n    <string name=\"sign_in_submit_button_label\">Next</string>\n    <string name=\"sign_in_user_id_hint\">Enter username</string>\n    <string name=\"sign_in_username_error\">Invalid input</string>\n    <string name=\"sign_in_primary_text\">Sign in with Aural</string>\n    <string name=\"username_and_password_sign_in_primary_text\">Sign in with Aural</string>\n    <string name=\"phone_sign_in_button_label\">Sign in with phone</string>\n    <string name=\"phone_sign_in_primary_text\">Choose phone sign-in method</string>\n    <string name=\"pin_sign_in_button_label\">PIN</string>\n    <string name=\"pin_sign_in_primary_text\">Sign in with PIN</string>\n    <string name=\"pin_sign_in_secondary_text\">Go to example.com/fast-pair on your phone</string>\n    <string name=\"qr_sign_in_button_label\">QR code</string>\n    <string name=\"qr_sign_in_primary_text\">Sign in with QR code</string>\n    <string name=\"qr_sign_in_secondary_text\">Use your phone\\'s camera to capture this code</string>\n    <string name=\"google_sign_in_button_label\">Sign in with Google</string>\n    <string name=\"sign_in_footer\">By signing in with Aural, you agree to the &lt;a href=&quot;http://www.google.com&quot;&gt;Terms &amp; Conditions&lt;/a&gt; and &lt;a href=&quot;http://www.google.com&quot;&gt;Privacy policy&lt;/a&gt;</string>\n    <string name=\"qr_code_url\">https://chart.apis.google.com/chart?cht=qr&amp;chs=392x392&amp;chl=https://www.youtube.com/watch?v=8I8mCFYYfdk</string>\n    <string name=\"sign_in_success_message\">Sign in successful</string>\n    <string name=\"sign_in_failed_message\">Sign in failed with error code: %1$d</string>\n\n    <string name=\"server_client_id\">YOUR_SERVER_CLIENT_ID</string>\n</resources>\n"
  },
  {
    "path": "automotive/src/main/res/values/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google LLC\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     https://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<resources>\n\n    <!-- Base application theme. -->\n    <style name=\"AppTheme\" parent=\"Theme.MaterialComponents.NoActionBar.Bridge\">\n        <!-- Customize your theme here. -->\n        <item name=\"toolbarNavigationButtonStyle\">@style/CarToolbarNavButton</item>\n        <item name=\"colorPrimary\">@color/colorPrimary</item>\n        <item name=\"colorPrimaryDark\">@color/colorPrimaryDark</item>\n        <item name=\"colorAccent\">@color/colorAccent</item>\n        <item name=\"android:buttonStyle\">@style/CarButton</item>\n        <item name=\"android:windowBackground\">@color/darkBackground</item>\n        <item name=\"toolbarStyle\">@style/CarToolbar</item>\n        <item name=\"android:textColorPrimary\">@color/car_text_light</item>\n    </style>\n\n    <style name=\"CarToolbar\" parent=\"Widget.MaterialComponents.Toolbar\">\n        <item name=\"android:background\">@android:color/transparent</item>\n        <item name=\"android:paddingStart\">@dimen/sign_in_toolbar_padding</item>\n        <item name=\"android:paddingEnd\">@dimen/sign_in_toolbar_padding</item>\n        <item name=\"android:minHeight\">@dimen/sign_in_toolbar_height</item>\n        <item name=\"navigationIcon\">@drawable/sign_in_toolbar_back_icon</item>\n    </style>\n\n    <style name=\"CarToolbarNavButton\" parent=\"Widget.AppCompat.Toolbar.Button.Navigation\">\n        <item name=\"tint\">@android:color/white</item>\n        <item name=\"android:background\">@drawable/sign_in_toolbar_back_ripple_background</item>\n        <item name=\"android:minWidth\">@dimen/sign_in_toolbar_nav_button_size</item>\n    </style>\n\n    <style name=\"AppTheme.Drawer\" parent=\"Theme.Car.Dark.NoActionBar.Drawer\">\n        <item name=\"carToolbarStyle\">@style/CarToolbar</item>\n    </style>\n\n    <style name=\"CarButton\" parent=\"Widget.AppCompat.Button\">\n        <item name=\"android:fontFamily\">sans-serif-medium</item>\n        <item name=\"android:ellipsize\">none</item>\n        <item name=\"android:minHeight\">@dimen/button_height</item>\n        <item name=\"android:minWidth\">@dimen/button_min_width</item>\n        <item name=\"android:paddingStart\">@dimen/button_padding</item>\n        <item name=\"android:paddingEnd\">@dimen/button_padding</item>\n        <item name=\"android:singleLine\">true</item>\n        <item name=\"android:textAllCaps\">false</item>\n        <item name=\"android:background\">@drawable/default_button_background</item>\n        <item name=\"android:textAppearance\">@style/SecondaryText.Dark</item>\n    </style>\n\n    <style name=\"CarButton.SignIn\">\n        <item name=\"android:background\">@drawable/sign_in_button_background</item>\n    </style>\n\n    <style name=\"CarButton.GoogleSignIn\">\n        <item name=\"android:background\">@drawable/google_sign_in_button_background</item>\n    </style>\n\n    <style name=\"DisplayText\" parent=\"TextAppearance.AppCompat\">\n        <item name=\"android:textSize\">@dimen/display_text_size</item>\n    </style>\n\n    <style name=\"PrimaryText\" parent=\"TextAppearance.AppCompat\">\n        <item name=\"android:textSize\">@dimen/primary_text_size</item>\n    </style>\n\n    <style name=\"SecondaryText\" parent=\"TextAppearance.AppCompat\">\n        <item name=\"android:textSize\">@dimen/secondary_text_size</item>\n    </style>\n\n    <style name=\"SecondaryText.Dark\">\n        <item name=\"android:textColor\">@color/car_text_dark</item>\n    </style>\n\n    <style name=\"InputBox\" parent=\"@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox\">\n        <item name=\"boxStrokeColor\">@color/grey_300</item>\n        <item name=\"boxStrokeWidth\">@dimen/sign_in_input_box_stroke_width</item>\n        <item name=\"boxCornerRadiusBottomEnd\">@dimen/sign_in_input_box_radius</item>\n        <item name=\"boxCornerRadiusBottomStart\">@dimen/sign_in_input_box_radius</item>\n        <item name=\"boxCornerRadiusTopEnd\">@dimen/sign_in_input_box_radius</item>\n        <item name=\"boxCornerRadiusTopStart\">@dimen/sign_in_input_box_radius</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "automotive/src/main/res/values-h1060dp/dimens.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<resources>\n    <dimen name=\"sign_in_button1_top_margin\">64dp</dimen>\n    <dimen name=\"sign_in_button1_top_margin_no_input\">116dp</dimen>\n    <dimen name=\"sign_in_button2_top_margin_no_input\">64dp</dimen>\n</resources>\n"
  },
  {
    "path": "automotive/src/main/res/xml/automotive_app_desc.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2018 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<automotiveApp>\n    <!--\n      ~ This lets Android Auto know that UAMP can act as a media app.\n      ~ See: https://developer.android.com/training/auto/audio/ for more info.\n      -->\n    <uses name=\"media\" />\n</automotiveApp>\n"
  },
  {
    "path": "automotive/src/main/res/xml/preferences.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2019 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n<androidx.preference.PreferenceScreen xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <PreferenceCategory\n        android:layout=\"@layout/preference_category\"\n        android:title=\"Account\">\n\n        <Preference\n            android:key=\"logout\"\n            android:layout=\"@layout/preference\"\n            android:title=\"Log Out\" />\n\n    </PreferenceCategory>\n\n</androidx.preference.PreferenceScreen>"
  },
  {
    "path": "automotive/src/test/java/com/example/android/uamp/automotive/ExampleUnitTest.java",
    "content": "/*\n * Copyright 2019 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.uamp.automotive;\n\nimport org.junit.Test;\n\nimport static org.junit.Assert.assertEquals;\n\n/**\n * Example local unit test, which will execute on the development machine (host).\n *\n * @see <a href=\"http://d.android.com/tools/testing\">Testing documentation</a>\n */\npublic class ExampleUnitTest {\n    @Test\n    public void addition_isCorrect() {\n        assertEquals(4, 2 + 2);\n    }\n}"
  },
  {
    "path": "build.gradle",
    "content": "/*\n * Copyright 2017 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// Top-level build file where you can add configuration options common to all sub-projects/modules.\n\nbuildscript {\n    ext {\n        // App SDK versions.\n        compileSdkVersion = 30\n        minSdkVersion = 19\n        targetSdkVersion = 30\n\n        // Dependency versions.\n        androidx_app_compat_version = '1.2.0'\n        androidx_car_version = '1.0.0-alpha7'\n        androidx_core_ktx_version = '1.3.1'\n        androidx_media_version = '1.0.1'\n        androidx_preference_version = '1.1.1'\n        androidx_test_runner_version = '1.3.0'\n        arch_lifecycle_version = '2.2.0'\n        constraint_layout_version = '2.0.1'\n        espresso_version = '3.3.0'\n        exoplayer_version = '2.16.0'\n        fragment_version = '1.2.5'\n        glide_version = '4.11.0'\n        gms_strict_version_matcher_version = '1.0.3'\n        gradle_version = '3.1.4'\n        gson_version = '2.8.5'\n        junit_version = '4.13'\n        kotlin_version = '1.3.72'\n        kotlin_coroutines_version = '1.1.0'\n        multidex_version = '1.0.3'\n        play_services_auth_version = '18.1.0'\n        recycler_view_version = '1.1.0'\n        robolectric_version = '4.2'\n        test_runner_version = '1.1.0'\n    }\n\n    repositories {\n        google()\n        mavenCentral()\n    }\n    dependencies {\n        classpath 'com.android.tools.build:gradle:4.0.1'\n        classpath \"org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version\"\n        classpath \"com.google.android.gms:strict-version-matcher-plugin:$gms_strict_version_matcher_version\"\n\n        // NOTE: Do not place your application dependencies here; they belong\n        // in the individual module build.gradle files\n    }\n}\n\nallprojects {\n    repositories {\n        google()\n        mavenCentral()\n    }\n}\n\ntask clean(type: Delete) {\n    delete rootProject.buildDir\n}\n"
  },
  {
    "path": "common/build.gradle",
    "content": "/*\n * Copyright 2017 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\napply plugin: 'com.android.library'\napply plugin: 'kotlin-android'\napply plugin: 'kotlin-kapt'\napply plugin: 'kotlin-android-extensions'\n\nandroid {\n    compileSdkVersion rootProject.compileSdkVersion\n\n    defaultConfig {\n        versionCode 1\n        versionName \"1.0\"\n\n        minSdkVersion rootProject.minSdkVersion\n        targetSdkVersion rootProject.targetSdkVersion\n\n        testOptions.unitTests.includeAndroidResources = true\n        testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n        }\n    }\n    compileOptions {\n        targetCompatibility = '1.8'\n    }\n\n}\n\ndependencies {\n    api \"org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version\"\n    api \"org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version\"\n    api \"org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version\"\n\n    api \"androidx.media:media:$androidx_media_version\"\n\n    api \"com.google.code.gson:gson:$gson_version\"\n\n    // ExoPlayer dependencies\n\n    // This allows UAMP to utilize a local version of ExoPlayer, which is particularly\n    // useful for extending the MediaSession extension, as well as for testing and\n    // customization. If the \":exoplayer-library-core\" project is included, we assume\n    // the others are included as well.\n    if (findProject(':exoplayer-library-core') != null) {\n        api project(':exoplayer-library-core')\n        api project(':exoplayer-library-ui')\n        api project(':exoplayer-extension-mediasession')\n        api project(':exoplayer-extension-cast')\n    } else {\n        api \"com.google.android.exoplayer:exoplayer-core:$exoplayer_version\"\n        api \"com.google.android.exoplayer:exoplayer-ui:$exoplayer_version\"\n        api \"com.google.android.exoplayer:extension-mediasession:$exoplayer_version\"\n        api \"com.google.android.exoplayer:extension-cast:$exoplayer_version\"\n    }\n\n    // Glide dependencies\n    api \"com.github.bumptech.glide:glide:$glide_version\"\n    kapt \"com.github.bumptech.glide:compiler:$glide_version\"\n\n    // Testing\n    testImplementation \"junit:junit:$junit_version\"\n    testImplementation \"org.robolectric:robolectric:$robolectric_version\"\n}\n"
  },
  {
    "path": "common/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile\n"
  },
  {
    "path": "common/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2017 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.example.android.uamp.media\">\n\n    <uses-permission android:name=\"android.permission.FOREGROUND_SERVICE\" />\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n\n    <application>\n\n        <!--\n             MediaSession, prior to API 21, uses a broadcast receiver to communicate with a\n             media session. It does not have to be this broadcast receiver, but it must\n             handle the action \"android.intent.action.MEDIA_BUTTON\".\n\n             Additionally, this is used to resume the service from an inactive state upon\n             receiving a media button event (such as \"play\").\n        -->\n        <receiver android:name=\"androidx.media.session.MediaButtonReceiver\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MEDIA_BUTTON\" />\n            </intent-filter>\n        </receiver>\n\n        <provider\n            android:name=\".library.AlbumArtContentProvider\"\n            android:authorities=\"com.example.android.uamp\"\n            android:exported=\"true\" />\n\n    </application>\n</manifest>\n"
  },
  {
    "path": "common/src/main/java/com/example/android/uamp/common/MusicServiceConnection.kt",
    "content": "/*\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.uamp.common\n\nimport android.content.ComponentName\nimport android.content.Context\nimport android.os.Bundle\nimport android.os.Handler\nimport android.os.ResultReceiver\nimport android.support.v4.media.MediaBrowserCompat\nimport android.support.v4.media.MediaMetadataCompat\nimport android.support.v4.media.session.MediaControllerCompat\nimport android.support.v4.media.session.MediaSessionCompat\nimport android.support.v4.media.session.PlaybackStateCompat\nimport androidx.lifecycle.MutableLiveData\nimport androidx.media.MediaBrowserServiceCompat\nimport com.example.android.uamp.common.MusicServiceConnection.MediaBrowserConnectionCallback\nimport com.example.android.uamp.media.NETWORK_FAILURE\nimport com.example.android.uamp.media.extensions.id\n\n/**\n * Class that manages a connection to a [MediaBrowserServiceCompat] instance, typically a\n * [MusicService] or one of its subclasses.\n *\n * Typically it's best to construct/inject dependencies either using DI or, as UAMP does,\n * using [InjectorUtils] in the app module. There are a few difficulties for that here:\n * - [MediaBrowserCompat] is a final class, so mocking it directly is difficult.\n * - A [MediaBrowserConnectionCallback] is a parameter into the construction of\n *   a [MediaBrowserCompat], and provides callbacks to this class.\n * - [MediaBrowserCompat.ConnectionCallback.onConnected] is the best place to construct\n *   a [MediaControllerCompat] that will be used to control the [MediaSessionCompat].\n *\n *  Because of these reasons, rather than constructing additional classes, this is treated as\n *  a black box (which is why there's very little logic here).\n *\n *  This is also why the parameters to construct a [MusicServiceConnection] are simple\n *  parameters, rather than private properties. They're only required to build the\n *  [MediaBrowserConnectionCallback] and [MediaBrowserCompat] objects.\n */\nclass MusicServiceConnection(context: Context, serviceComponent: ComponentName) {\n    val isConnected = MutableLiveData<Boolean>()\n        .apply { postValue(false) }\n    val networkFailure = MutableLiveData<Boolean>()\n        .apply { postValue(false) }\n\n    val rootMediaId: String get() = mediaBrowser.root\n\n    val playbackState = MutableLiveData<PlaybackStateCompat>()\n        .apply { postValue(EMPTY_PLAYBACK_STATE) }\n    val nowPlaying = MutableLiveData<MediaMetadataCompat>()\n        .apply { postValue(NOTHING_PLAYING) }\n\n    val transportControls: MediaControllerCompat.TransportControls\n        get() = mediaController.transportControls\n\n    private val mediaBrowserConnectionCallback = MediaBrowserConnectionCallback(context)\n    private val mediaBrowser = MediaBrowserCompat(\n        context,\n        serviceComponent,\n        mediaBrowserConnectionCallback, null\n    ).apply { connect() }\n    private lateinit var mediaController: MediaControllerCompat\n\n    fun subscribe(parentId: String, callback: MediaBrowserCompat.SubscriptionCallback) {\n        mediaBrowser.subscribe(parentId, callback)\n    }\n\n    fun unsubscribe(parentId: String, callback: MediaBrowserCompat.SubscriptionCallback) {\n        mediaBrowser.unsubscribe(parentId, callback)\n    }\n\n    fun sendCommand(command: String, parameters: Bundle?) =\n        sendCommand(command, parameters) { _, _ -> }\n\n    fun sendCommand(\n        command: String,\n        parameters: Bundle?,\n        resultCallback: ((Int, Bundle?) -> Unit)\n    ) = if (mediaBrowser.isConnected) {\n        mediaController.sendCommand(command, parameters, object : ResultReceiver(Handler()) {\n            override fun onReceiveResult(resultCode: Int, resultData: Bundle?) {\n                resultCallback(resultCode, resultData)\n            }\n        })\n        true\n    } else {\n        false\n    }\n\n    private inner class MediaBrowserConnectionCallback(private val context: Context) :\n        MediaBrowserCompat.ConnectionCallback() {\n        /**\n         * Invoked after [MediaBrowserCompat.connect] when the request has successfully\n         * completed.\n         */\n        override fun onConnected() {\n            // Get a MediaController for the MediaSession.\n            mediaController = MediaControllerCompat(context, mediaBrowser.sessionToken).apply {\n                registerCallback(MediaControllerCallback())\n            }\n\n            isConnected.postValue(true)\n        }\n\n        /**\n         * Invoked when the client is disconnected from the media browser.\n         */\n        override fun onConnectionSuspended() {\n            isConnected.postValue(false)\n        }\n\n        /**\n         * Invoked when the connection to the media browser failed.\n         */\n        override fun onConnectionFailed() {\n            isConnected.postValue(false)\n        }\n    }\n\n    private inner class MediaControllerCallback : MediaControllerCompat.Callback() {\n\n        override fun onPlaybackStateChanged(state: PlaybackStateCompat?) {\n            playbackState.postValue(state ?: EMPTY_PLAYBACK_STATE)\n        }\n\n        override fun onMetadataChanged(metadata: MediaMetadataCompat?) {\n            // When ExoPlayer stops we will receive a callback with \"empty\" metadata. This is a\n            // metadata object which has been instantiated with default values. The default value\n            // for media ID is null so we assume that if this value is null we are not playing\n            // anything.\n            nowPlaying.postValue(\n                if (metadata?.id == null) {\n                    NOTHING_PLAYING\n                } else {\n                    metadata\n                }\n            )\n        }\n\n        override fun onQueueChanged(queue: MutableList<MediaSessionCompat.QueueItem>?) {\n        }\n\n        override fun onSessionEvent(event: String?, extras: Bundle?) {\n            super.onSessionEvent(event, extras)\n            when (event) {\n                NETWORK_FAILURE -> networkFailure.postValue(true)\n            }\n        }\n\n        /**\n         * Normally if a [MediaBrowserServiceCompat] drops its connection the callback comes via\n         * [MediaControllerCompat.Callback] (here). But since other connection status events\n         * are sent to [MediaBrowserCompat.ConnectionCallback], we catch the disconnect here and\n         * send it on to the other callback.\n         */\n        override fun onSessionDestroyed() {\n            mediaBrowserConnectionCallback.onConnectionSuspended()\n        }\n    }\n\n    companion object {\n        // For Singleton instantiation.\n        @Volatile\n        private var instance: MusicServiceConnection? = null\n\n        fun getInstance(context: Context, serviceComponent: ComponentName) =\n            instance ?: synchronized(this) {\n                instance ?: MusicServiceConnection(context, serviceComponent)\n                    .also { instance = it }\n            }\n    }\n}\n\n@Suppress(\"PropertyName\")\nval EMPTY_PLAYBACK_STATE: PlaybackStateCompat = PlaybackStateCompat.Builder()\n    .setState(PlaybackStateCompat.STATE_NONE, 0, 0f)\n    .build()\n\n@Suppress(\"PropertyName\")\nval NOTHING_PLAYING: MediaMetadataCompat = MediaMetadataCompat.Builder()\n    .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, \"\")\n    .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, 0)\n    .build()\n"
  },
  {
    "path": "common/src/main/java/com/example/android/uamp/media/CastMediaItemConverter.kt",
    "content": "package com.example.android.uamp.media\n\nimport android.net.Uri\nimport android.support.v4.media.MediaMetadataCompat\nimport com.example.android.uamp.media.library.JsonSource\nimport com.google.android.exoplayer2.MediaItem\nimport com.google.android.exoplayer2.ext.cast.DefaultMediaItemConverter\nimport com.google.android.exoplayer2.ext.cast.MediaItemConverter\nimport com.google.android.exoplayer2.util.MimeTypes\nimport com.google.android.gms.cast.MediaInfo\nimport com.google.android.gms.cast.MediaMetadata\nimport com.google.android.gms.cast.MediaQueueItem\nimport com.google.android.gms.common.images.WebImage\n\n/**\n * A [MediaItemConverter] to convert from a [MediaItem] to a Cast [MediaQueueItem].\n *\n * It adds all audio specific metadata properties and creates a Cast metadata object of type\n * [MediaMetadata.MEDIA_TYPE_MUSIC_TRACK].\n *\n * To create an artwork for Cast we can't use the standard [MediaItem#mediaMetadata#artworkUri]\n * because UAMP uses a content provider to serve cached bitmaps. The URIs starting with `content://`\n * are useless on a Cast device, so we need to use the original HTTP URI that the [JsonSource]\n * stores in the metadata extra with key `JsonSource.ORIGINAL_ARTWORK_URI_KEY`.\n */\ninternal class CastMediaItemConverter : MediaItemConverter {\n\n    private val defaultMediaItemConverter = DefaultMediaItemConverter()\n\n    override fun toMediaQueueItem(mediaItem: MediaItem): MediaQueueItem {\n        val castMediaMetadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MUSIC_TRACK)\n        mediaItem.mediaMetadata.title?.let {\n            castMediaMetadata.putString(MediaMetadata.KEY_TITLE, it.toString() )\n        }\n        mediaItem.mediaMetadata.subtitle?.let {\n            castMediaMetadata.putString(MediaMetadata.KEY_SUBTITLE, it.toString())\n        }\n        mediaItem.mediaMetadata.artist?.let {\n            castMediaMetadata.putString(MediaMetadata.KEY_ARTIST, it.toString())\n        }\n        mediaItem.mediaMetadata.albumTitle?.let {\n            castMediaMetadata.putString(MediaMetadata.KEY_ALBUM_TITLE, it.toString())\n        }\n        mediaItem.mediaMetadata.albumArtist?.let {\n            castMediaMetadata.putString(MediaMetadata.KEY_ALBUM_ARTIST, it.toString())\n        }\n        mediaItem.mediaMetadata.composer?.let {\n            castMediaMetadata.putString(MediaMetadata.KEY_COMPOSER, it.toString())\n        }\n        mediaItem.mediaMetadata.trackNumber?.let{\n            castMediaMetadata.putInt(MediaMetadata.KEY_TRACK_NUMBER, it)\n        }\n        mediaItem.mediaMetadata.discNumber?.let {\n            castMediaMetadata.putInt(MediaMetadata.KEY_DISC_NUMBER, it)\n        }\n        val mediaInfo = MediaInfo.Builder(mediaItem.localConfiguration!!.uri.toString())\n            .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)\n            .setContentType(MimeTypes.AUDIO_MPEG)\n        mediaItem.localConfiguration?.let {\n            mediaInfo.setContentUrl(it.uri.toString())\n        }\n        mediaItem.mediaMetadata.extras?.let { bundle ->\n            // Use the original artwork URI for Cast.\n            bundle.getString(JsonSource.ORIGINAL_ARTWORK_URI_KEY)?.let {\n                castMediaMetadata.addImage(WebImage(Uri.parse(it)))\n            }\n            mediaInfo.setStreamDuration(\n                bundle.getLong(MediaMetadataCompat.METADATA_KEY_DURATION,0))\n        }\n        mediaInfo.setMetadata(castMediaMetadata)\n        return MediaQueueItem.Builder(mediaInfo.build()).build()\n    }\n\n    override fun toMediaItem(mediaQueueItem: MediaQueueItem): MediaItem {\n        return defaultMediaItemConverter.toMediaItem(mediaQueueItem)\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/example/android/uamp/media/MusicService.kt",
    "content": "/*\n * Copyright 2017 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.uamp.media\n\nimport android.app.Notification\nimport android.app.PendingIntent\nimport android.content.ComponentName\nimport android.content.Intent\nimport android.content.pm.PackageManager\nimport android.net.Uri\nimport android.os.Bundle\nimport android.os.ResultReceiver\nimport android.support.v4.media.MediaBrowserCompat\nimport android.support.v4.media.MediaBrowserCompat.MediaItem\nimport android.support.v4.media.MediaDescriptionCompat\nimport android.support.v4.media.MediaMetadataCompat\nimport android.support.v4.media.session.MediaSessionCompat\nimport android.support.v4.media.session.PlaybackStateCompat\nimport android.util.Log\nimport android.widget.Toast\nimport androidx.core.content.ContextCompat\nimport androidx.media.MediaBrowserServiceCompat\nimport androidx.media.MediaBrowserServiceCompat.BrowserRoot.EXTRA_RECENT\nimport com.example.android.uamp.media.extensions.album\nimport com.example.android.uamp.media.extensions.flag\nimport com.example.android.uamp.media.extensions.id\nimport com.example.android.uamp.media.extensions.toMediaItem\nimport com.example.android.uamp.media.extensions.trackNumber\nimport com.example.android.uamp.media.library.AbstractMusicSource\nimport com.example.android.uamp.media.library.BrowseTree\nimport com.example.android.uamp.media.library.JsonSource\nimport com.example.android.uamp.media.library.MEDIA_SEARCH_SUPPORTED\nimport com.example.android.uamp.media.library.MusicSource\nimport com.example.android.uamp.media.library.UAMP_BROWSABLE_ROOT\nimport com.example.android.uamp.media.library.UAMP_EMPTY_ROOT\nimport com.example.android.uamp.media.library.UAMP_RECENT_ROOT\nimport com.google.android.exoplayer2.C\nimport com.google.android.exoplayer2.ExoPlayer\nimport com.google.android.exoplayer2.PlaybackException\nimport com.google.android.exoplayer2.Player\nimport com.google.android.exoplayer2.Player.EVENT_MEDIA_ITEM_TRANSITION\nimport com.google.android.exoplayer2.Player.EVENT_PLAY_WHEN_READY_CHANGED\nimport com.google.android.exoplayer2.Player.EVENT_POSITION_DISCONTINUITY\nimport com.google.android.exoplayer2.Player.EVENT_TIMELINE_CHANGED\nimport com.google.android.exoplayer2.SimpleExoPlayer\nimport com.google.android.exoplayer2.audio.AudioAttributes\nimport com.google.android.exoplayer2.ext.cast.CastPlayer\nimport com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener\nimport com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector\nimport com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator\nimport com.google.android.exoplayer2.ui.PlayerNotificationManager\nimport com.google.android.exoplayer2.util.Util\nimport com.google.android.exoplayer2.util.Util.constrainValue\nimport com.google.android.gms.cast.framework.CastContext\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.SupervisorJob\nimport kotlinx.coroutines.launch\nimport kotlin.math.max\nimport kotlin.math.min\n\n/**\n * This class is the entry point for browsing and playback commands from the APP's UI\n * and other apps that wish to play music via UAMP (for example, Android Auto or\n * the Google Assistant).\n *\n * Browsing begins with the method [MusicService.onGetRoot], and continues in\n * the callback [MusicService.onLoadChildren].\n *\n * For more information on implementing a MediaBrowserService,\n * visit [https://developer.android.com/guide/topics/media-apps/audio-app/building-a-mediabrowserservice.html](https://developer.android.com/guide/topics/media-apps/audio-app/building-a-mediabrowserservice.html).\n *\n * This class also handles playback for Cast sessions.\n * When a Cast session is active, playback commands are passed to a\n * [CastPlayer](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/ext/cast/CastPlayer.html),\n * otherwise they are passed to an ExoPlayer for local playback.\n */\nopen class MusicService : MediaBrowserServiceCompat() {\n\n    private lateinit var notificationManager: UampNotificationManager\n    private lateinit var mediaSource: MusicSource\n    private lateinit var packageValidator: PackageValidator\n\n    // The current player will either be an ExoPlayer (for local playback) or a CastPlayer (for\n    // remote playback through a Cast device).\n    private lateinit var currentPlayer: Player\n\n    private val serviceJob = SupervisorJob()\n    private val serviceScope = CoroutineScope(Dispatchers.Main + serviceJob)\n\n    protected lateinit var mediaSession: MediaSessionCompat\n    protected lateinit var mediaSessionConnector: MediaSessionConnector\n    private var currentPlaylistItems: List<MediaMetadataCompat> = emptyList()\n    private var currentMediaItemIndex: Int = 0\n\n    private lateinit var storage: PersistentStorage\n\n    /**\n     * This must be `by lazy` because the source won't initially be ready.\n     * See [MusicService.onLoadChildren] to see where it's accessed (and first\n     * constructed).\n     */\n    private val browseTree: BrowseTree by lazy {\n        BrowseTree(applicationContext, mediaSource)\n    }\n\n    private var isForegroundService = false\n\n    private val remoteJsonSource: Uri =\n        Uri.parse(\"https://storage.googleapis.com/androiddevelopers/samples_assets/uamp/catalog.json\")\n\n    private val uAmpAudioAttributes = AudioAttributes.Builder()\n        .setContentType(C.CONTENT_TYPE_MUSIC)\n        .setUsage(C.USAGE_MEDIA)\n        .build()\n\n    private val playerListener = PlayerEventListener()\n\n    /**\n     * Configure ExoPlayer to handle audio focus for us.\n     * See [Player.AudioComponent.setAudioAttributes] for details.\n     */\n    private val exoPlayer: ExoPlayer by lazy {\n        SimpleExoPlayer.Builder(this).build().apply {\n            setAudioAttributes(uAmpAudioAttributes, true)\n            setHandleAudioBecomingNoisy(true)\n            addListener(playerListener)\n        }\n    }\n\n    /**\n     * If Cast is available, create a CastPlayer to handle communication with a Cast session.\n     */\n    private val castPlayer: CastPlayer? by lazy {\n        try {\n            val castContext = CastContext.getSharedInstance(this)\n            CastPlayer(castContext, CastMediaItemConverter()).apply {\n                setSessionAvailabilityListener(UampCastSessionAvailabilityListener())\n                addListener(playerListener)\n            }\n        } catch (e : Exception) {\n            // We wouldn't normally catch the generic `Exception` however\n            // calling `CastContext.getSharedInstance` can throw various exceptions, all of which\n            // indicate that Cast is unavailable.\n            // Related internal bug b/68009560.\n            Log.i(TAG, \"Cast is not available on this device. \" +\n                    \"Exception thrown when attempting to obtain CastContext. \" + e.message)\n            null\n        }\n    }\n\n    @ExperimentalCoroutinesApi\n    override fun onCreate() {\n        super.onCreate()\n\n        // Build a PendingIntent that can be used to launch the UI.\n        val sessionActivityPendingIntent =\n            packageManager?.getLaunchIntentForPackage(packageName)?.let { sessionIntent ->\n                PendingIntent.getActivity(this, 0, sessionIntent, 0)\n            }\n\n        // Create a new MediaSession.\n        mediaSession = MediaSessionCompat(this, \"MusicService\")\n            .apply {\n                setSessionActivity(sessionActivityPendingIntent)\n                isActive = true\n            }\n        /**\n         * In order for [MediaBrowserCompat.ConnectionCallback.onConnected] to be called,\n         * a [MediaSessionCompat.Token] needs to be set on the [MediaBrowserServiceCompat].\n         *\n         * It is possible to wait to set the session token, if required for a specific use-case.\n         * However, the token *must* be set by the time [MediaBrowserServiceCompat.onGetRoot]\n         * returns, or the connection will fail silently. (The system will not even call\n         * [MediaBrowserCompat.ConnectionCallback.onConnectionFailed].)\n         */\n        sessionToken = mediaSession.sessionToken\n\n        /**\n         * The notification manager will use our player and media session to decide when to post\n         * notifications. When notifications are posted or removed our listener will be called, this\n         * allows us to promote the service to foreground (required so that we're not killed if\n         * the main UI is not visible).\n         */\n        notificationManager = UampNotificationManager(\n            this,\n            mediaSession.sessionToken,\n            PlayerNotificationListener()\n        )\n\n        // The media library is built from a remote JSON file. We'll create the source here,\n        // and then use a suspend function to perform the download off the main thread.\n        mediaSource = JsonSource(source = remoteJsonSource)\n        serviceScope.launch {\n            mediaSource.load()\n        }\n\n        // ExoPlayer will manage the MediaSession for us.\n        mediaSessionConnector = MediaSessionConnector(mediaSession)\n        mediaSessionConnector.setPlaybackPreparer(UampPlaybackPreparer())\n        mediaSessionConnector.setQueueNavigator(UampQueueNavigator(mediaSession))\n\n        switchToPlayer(\n            previousPlayer = null,\n            newPlayer = if (castPlayer?.isCastSessionAvailable == true) castPlayer!! else exoPlayer\n        )\n        notificationManager.showNotificationForPlayer(currentPlayer)\n\n        packageValidator = PackageValidator(this, R.xml.allowed_media_browser_callers)\n\n        storage = PersistentStorage.getInstance(applicationContext)\n    }\n\n    /**\n     * This is the code that causes UAMP to stop playing when swiping the activity away from\n     * recents. The choice to do this is app specific. Some apps stop playback, while others allow\n     * playback to continue and allow users to stop it with the notification.\n     */\n    override fun onTaskRemoved(rootIntent: Intent) {\n        saveRecentSongToStorage()\n        super.onTaskRemoved(rootIntent)\n\n        /**\n         * By stopping playback, the player will transition to [Player.STATE_IDLE] triggering\n         * [Player.EventListener.onPlayerStateChanged] to be called. This will cause the\n         * notification to be hidden and trigger\n         * [PlayerNotificationManager.NotificationListener.onNotificationCancelled] to be called.\n         * The service will then remove itself as a foreground service, and will call\n         * [stopSelf].\n         */\n        currentPlayer.stop(/* reset= */true)\n    }\n\n    override fun onDestroy() {\n        mediaSession.run {\n            isActive = false\n            release()\n        }\n\n        // Cancel coroutines when the service is going away.\n        serviceJob.cancel()\n\n        // Free ExoPlayer resources.\n        exoPlayer.removeListener(playerListener)\n        exoPlayer.release()\n    }\n\n    /**\n     * Returns the \"root\" media ID that the client should request to get the list of\n     * [MediaItem]s to browse/play.\n     */\n    override fun onGetRoot(\n        clientPackageName: String,\n        clientUid: Int,\n        rootHints: Bundle?\n    ): BrowserRoot? {\n\n        /*\n         * By default, all known clients are permitted to search, but only tell unknown callers\n         * about search if permitted by the [BrowseTree].\n         */\n        val isKnownCaller = packageValidator.isKnownCaller(clientPackageName, clientUid)\n        val rootExtras = Bundle().apply {\n            putBoolean(\n                MEDIA_SEARCH_SUPPORTED,\n                isKnownCaller || browseTree.searchableByUnknownCaller\n            )\n            putBoolean(CONTENT_STYLE_SUPPORTED, true)\n            putInt(CONTENT_STYLE_BROWSABLE_HINT, CONTENT_STYLE_GRID)\n            putInt(CONTENT_STYLE_PLAYABLE_HINT, CONTENT_STYLE_LIST)\n        }\n\n        return if (isKnownCaller) {\n            /**\n             * By default return the browsable root. Treat the EXTRA_RECENT flag as a special case\n             * and return the recent root instead.\n             */\n            val isRecentRequest = rootHints?.getBoolean(EXTRA_RECENT) ?: false\n            val browserRootPath = if (isRecentRequest) UAMP_RECENT_ROOT else UAMP_BROWSABLE_ROOT\n            BrowserRoot(browserRootPath, rootExtras)\n        } else {\n            /**\n             * Unknown caller. There are two main ways to handle this:\n             * 1) Return a root without any content, which still allows the connecting client\n             * to issue commands.\n             * 2) Return `null`, which will cause the system to disconnect the app.\n             *\n             * UAMP takes the first approach for a variety of reasons, but both are valid\n             * options.\n             */\n            BrowserRoot(UAMP_EMPTY_ROOT, rootExtras)\n        }\n    }\n\n    /**\n     * Returns (via the [result] parameter) a list of [MediaItem]s that are child\n     * items of the provided [parentMediaId]. See [BrowseTree] for more details on\n     * how this is build/more details about the relationships.\n     */\n    override fun onLoadChildren(\n        parentMediaId: String,\n        result: Result<List<MediaItem>>\n    ) {\n\n        /**\n         * If the caller requests the recent root, return the most recently played song.\n         */\n        if (parentMediaId == UAMP_RECENT_ROOT) {\n            result.sendResult(storage.loadRecentSong()?.let { song -> listOf(song) })\n        } else {\n            // If the media source is ready, the results will be set synchronously here.\n            val resultsSent = mediaSource.whenReady { successfullyInitialized ->\n                if (successfullyInitialized) {\n                    val children = browseTree[parentMediaId]?.map { item ->\n                        MediaItem(item.description, item.flag)\n                    }\n                    result.sendResult(children)\n                } else {\n                    mediaSession.sendSessionEvent(NETWORK_FAILURE, null)\n                    result.sendResult(null)\n                }\n            }\n\n            // If the results are not ready, the service must \"detach\" the results before\n            // the method returns. After the source is ready, the lambda above will run,\n            // and the caller will be notified that the results are ready.\n            //\n            // See [MediaItemFragmentViewModel.subscriptionCallback] for how this is passed to the\n            // UI/displayed in the [RecyclerView].\n            if (!resultsSent) {\n                result.detach()\n            }\n        }\n    }\n\n    /**\n     * Returns a list of [MediaItem]s that match the given search query\n     */\n    override fun onSearch(\n        query: String,\n        extras: Bundle?,\n        result: Result<List<MediaItem>>\n    ) {\n\n        val resultsSent = mediaSource.whenReady { successfullyInitialized ->\n            if (successfullyInitialized) {\n                val resultsList = mediaSource.search(query, extras ?: Bundle.EMPTY)\n                    .map { mediaMetadata ->\n                        MediaItem(mediaMetadata.description, mediaMetadata.flag)\n                    }\n                result.sendResult(resultsList)\n            }\n        }\n\n        if (!resultsSent) {\n            result.detach()\n        }\n    }\n\n    /**\n     * Load the supplied list of songs and the song to play into the current player.\n     */\n    private fun preparePlaylist(\n        metadataList: List<MediaMetadataCompat>,\n        itemToPlay: MediaMetadataCompat?,\n        playWhenReady: Boolean,\n        playbackStartPositionMs: Long\n    ) {\n        // Since the playlist was probably based on some ordering (such as tracks\n        // on an album), find which window index to play first so that the song the\n        // user actually wants to hear plays first.\n        val initialWindowIndex = if (itemToPlay == null) 0 else metadataList.indexOf(itemToPlay)\n        currentPlaylistItems = metadataList\n\n        currentPlayer.playWhenReady = playWhenReady\n        currentPlayer.stop()\n        // Set playlist and prepare.\n        currentPlayer.setMediaItems(\n            metadataList.map { it.toMediaItem() }, initialWindowIndex, playbackStartPositionMs)\n        currentPlayer.prepare()\n    }\n\n    private fun switchToPlayer(previousPlayer: Player?, newPlayer: Player) {\n        if (previousPlayer == newPlayer) {\n            return\n        }\n        currentPlayer = newPlayer\n        if (previousPlayer != null) {\n            val playbackState = previousPlayer.playbackState\n            if (currentPlaylistItems.isEmpty()) {\n                // We are joining a playback session. Loading the session from the new player is\n                // not supported, so we stop playback.\n                currentPlayer.clearMediaItems()\n                currentPlayer.stop()\n            } else if (playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED) {\n                preparePlaylist(\n                    metadataList = currentPlaylistItems,\n                    itemToPlay = currentPlaylistItems[currentMediaItemIndex],\n                    playWhenReady = previousPlayer.playWhenReady,\n                    playbackStartPositionMs = previousPlayer.currentPosition\n                )\n            }\n        }\n        mediaSessionConnector.setPlayer(newPlayer)\n        previousPlayer?.stop(/* reset= */true)\n    }\n\n    private fun saveRecentSongToStorage() {\n\n        // Obtain the current song details *before* saving them on a separate thread, otherwise\n        // the current player may have been unloaded by the time the save routine runs.\n        if (currentPlaylistItems.isEmpty()) {\n            return\n        }\n        val description = currentPlaylistItems[currentMediaItemIndex].description\n        val position = currentPlayer.currentPosition\n\n        serviceScope.launch {\n            storage.saveRecentSong(\n                description,\n                position\n            )\n        }\n    }\n\n    private inner class UampCastSessionAvailabilityListener : SessionAvailabilityListener {\n\n        /**\n         * Called when a Cast session has started and the user wishes to control playback on a\n         * remote Cast receiver rather than play audio locally.\n         */\n        override fun onCastSessionAvailable() {\n            switchToPlayer(currentPlayer, castPlayer!!)\n        }\n\n        /**\n         * Called when a Cast session has ended and the user wishes to control playback locally.\n         */\n        override fun onCastSessionUnavailable() {\n            switchToPlayer(currentPlayer, exoPlayer)\n        }\n    }\n\n    private inner class UampQueueNavigator(\n        mediaSession: MediaSessionCompat\n    ) : TimelineQueueNavigator(mediaSession) {\n        override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat {\n            if (windowIndex < currentPlaylistItems.size) {\n                return currentPlaylistItems[windowIndex].description\n            }\n            return MediaDescriptionCompat.Builder().build()\n        }\n    }\n\n    private inner class UampPlaybackPreparer : MediaSessionConnector.PlaybackPreparer {\n\n        /**\n         * UAMP supports preparing (and playing) from search, as well as media ID, so those\n         * capabilities are declared here.\n         *\n         * TODO: Add support for ACTION_PREPARE and ACTION_PLAY, which mean \"prepare/play something\".\n         */\n        override fun getSupportedPrepareActions(): Long =\n            PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID or\n                    PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID or\n                    PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH or\n                    PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH\n\n        override fun onPrepare(playWhenReady: Boolean) {\n            val recentSong = storage.loadRecentSong() ?: return\n            onPrepareFromMediaId(\n                recentSong.mediaId!!,\n                playWhenReady,\n                recentSong.description.extras\n            )\n        }\n\n        override fun onPrepareFromMediaId(\n            mediaId: String,\n            playWhenReady: Boolean,\n            extras: Bundle?\n        ) {\n            mediaSource.whenReady {\n                val itemToPlay: MediaMetadataCompat? = mediaSource.find { item ->\n                    item.id == mediaId\n                }\n                if (itemToPlay == null) {\n                    Log.w(TAG, \"Content not found: MediaID=$mediaId\")\n                    // TODO: Notify caller of the error.\n                } else {\n\n                    val playbackStartPositionMs =\n                        extras?.getLong(MEDIA_DESCRIPTION_EXTRAS_START_PLAYBACK_POSITION_MS, C.TIME_UNSET)\n                            ?: C.TIME_UNSET\n\n                    preparePlaylist(\n                        buildPlaylist(itemToPlay),\n                        itemToPlay,\n                        playWhenReady,\n                        playbackStartPositionMs\n                    )\n                }\n            }\n        }\n\n        /**\n         * This method is used by the Google Assistant to respond to requests such as:\n         * - Play Geisha from Wake Up on UAMP\n         * - Play electronic music on UAMP\n         * - Play music on UAMP\n         *\n         * For details on how search is handled, see [AbstractMusicSource.search].\n         */\n        override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) {\n            mediaSource.whenReady {\n                val metadataList = mediaSource.search(query, extras ?: Bundle.EMPTY)\n                if (metadataList.isNotEmpty()) {\n                    preparePlaylist(\n                        metadataList,\n                        metadataList[0],\n                        playWhenReady,\n                        playbackStartPositionMs = C.TIME_UNSET\n                    )\n                }\n            }\n        }\n\n        override fun onPrepareFromUri(uri: Uri, playWhenReady: Boolean, extras: Bundle?) = Unit\n\n        override fun onCommand(\n            player: Player,\n            command: String,\n            extras: Bundle?,\n            cb: ResultReceiver?\n        ) = false\n\n        /**\n         * Builds a playlist based on a [MediaMetadataCompat].\n         *\n         * TODO: Support building a playlist by artist, genre, etc...\n         *\n         * @param item Item to base the playlist on.\n         * @return a [List] of [MediaMetadataCompat] objects representing a playlist.\n         */\n        private fun buildPlaylist(item: MediaMetadataCompat): List<MediaMetadataCompat> =\n            mediaSource.filter { it.album == item.album }.sortedBy { it.trackNumber }\n    }\n\n    /**\n     * Listen for notification events.\n     */\n    private inner class PlayerNotificationListener :\n        PlayerNotificationManager.NotificationListener {\n        override fun onNotificationPosted(\n            notificationId: Int,\n            notification: Notification,\n            ongoing: Boolean\n        ) {\n            if (ongoing && !isForegroundService) {\n                ContextCompat.startForegroundService(\n                    applicationContext,\n                    Intent(applicationContext, this@MusicService.javaClass)\n                )\n\n                startForeground(notificationId, notification)\n                isForegroundService = true\n            }\n        }\n\n        override fun onNotificationCancelled(notificationId: Int, dismissedByUser: Boolean) {\n            stopForeground(true)\n            isForegroundService = false\n            stopSelf()\n        }\n    }\n\n    /**\n     * Listen for events from ExoPlayer.\n     */\n    private inner class PlayerEventListener : Player.Listener {\n\n        override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {\n            when (playbackState) {\n                Player.STATE_BUFFERING,\n                Player.STATE_READY -> {\n                    notificationManager.showNotificationForPlayer(currentPlayer)\n                    if (playbackState == Player.STATE_READY) {\n\n                        // When playing/paused save the current media item in persistent\n                        // storage so that playback can be resumed between device reboots.\n                        // Search for \"media resumption\" for more information.\n                        saveRecentSongToStorage()\n\n                        if (!playWhenReady) {\n                            // If playback is paused we remove the foreground state which allows the\n                            // notification to be dismissed. An alternative would be to provide a\n                            // \"close\" button in the notification which stops playback and clears\n                            // the notification.\n                            stopForeground(false)\n                            isForegroundService = false\n                        }\n                    }\n                }\n                else -> {\n                    notificationManager.hideNotification()\n                }\n            }\n        }\n\n        override fun onEvents(player: Player, events: Player.Events) {\n            if (events.contains(EVENT_POSITION_DISCONTINUITY)\n                || events.contains(EVENT_MEDIA_ITEM_TRANSITION)\n                || events.contains(EVENT_PLAY_WHEN_READY_CHANGED)) {\n                currentMediaItemIndex = if (currentPlaylistItems.isNotEmpty()) {\n                    constrainValue(\n                        player.currentMediaItemIndex,\n                        /* min= */ 0,\n                        /* max= */ currentPlaylistItems.size - 1\n                    )\n                } else 0\n            }\n        }\n\n        override fun onPlayerError(error: PlaybackException) {\n            var message = R.string.generic_error;\n            Log.e(TAG, \"Player error: \" + error.errorCodeName + \" (\" + error.errorCode + \")\");\n            if (error.errorCode == PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS\n                || error.errorCode == PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND) {\n                message = R.string.error_media_not_found;\n            }\n            Toast.makeText(\n                applicationContext,\n                message,\n                Toast.LENGTH_LONG\n            ).show()\n        }\n    }\n}\n\n/*\n * (Media) Session events\n */\nconst val NETWORK_FAILURE = \"com.example.android.uamp.media.session.NETWORK_FAILURE\"\n\n/** Content styling constants */\nprivate const val CONTENT_STYLE_BROWSABLE_HINT = \"android.media.browse.CONTENT_STYLE_BROWSABLE_HINT\"\nprivate const val CONTENT_STYLE_PLAYABLE_HINT = \"android.media.browse.CONTENT_STYLE_PLAYABLE_HINT\"\nprivate const val CONTENT_STYLE_SUPPORTED = \"android.media.browse.CONTENT_STYLE_SUPPORTED\"\nprivate const val CONTENT_STYLE_LIST = 1\nprivate const val CONTENT_STYLE_GRID = 2\n\nprivate const val UAMP_USER_AGENT = \"uamp.next\"\n\nval MEDIA_DESCRIPTION_EXTRAS_START_PLAYBACK_POSITION_MS = \"playback_start_position_ms\"\n\nprivate const val TAG = \"MusicService\"\n"
  },
  {
    "path": "common/src/main/java/com/example/android/uamp/media/PackageValidator.kt",
    "content": "/*\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.uamp.media\n\nimport android.Manifest.permission.BIND_NOTIFICATION_LISTENER_SERVICE\nimport android.Manifest.permission.MEDIA_CONTENT_CONTROL\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.content.pm.PackageInfo\nimport android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED\nimport android.content.pm.PackageManager\nimport android.content.res.XmlResourceParser\nimport android.os.Process\nimport android.support.v4.media.session.MediaSessionCompat\nimport android.util.Base64\nimport android.util.Log\nimport androidx.annotation.XmlRes\nimport androidx.core.app.NotificationManagerCompat\nimport androidx.media.MediaBrowserServiceCompat\nimport org.xmlpull.v1.XmlPullParserException\nimport java.io.IOException\nimport java.security.MessageDigest\nimport java.security.NoSuchAlgorithmException\n\n/**\n * Validates that the calling package is authorized to browse a [MediaBrowserServiceCompat].\n *\n * The list of allowed signing certificates and their corresponding package names is defined in\n * res/xml/allowed_media_browser_callers.xml.\n *\n * If you want to add a new caller to allowed_media_browser_callers.xml and you don't know\n * its signature, this class will print to logcat (INFO level) a message with the proper\n * xml tags to add to allow the caller.\n *\n * For more information, see res/xml/allowed_media_browser_callers.xml.\n */\ninternal class PackageValidator(context: Context, @XmlRes xmlResId: Int) {\n    private val context: Context\n    private val packageManager: PackageManager\n\n    private val certificateAllowList: Map<String, KnownCallerInfo>\n    private val platformSignature: String\n\n    private val callerChecked = mutableMapOf<String, Pair<Int, Boolean>>()\n\n    init {\n        val parser = context.resources.getXml(xmlResId)\n        this.context = context.applicationContext\n        this.packageManager = this.context.packageManager\n\n        certificateAllowList = buildCertificateAllowList(parser)\n        platformSignature = getSystemSignature()\n    }\n\n    /**\n     * Checks whether the caller attempting to connect to a [MediaBrowserServiceCompat] is known.\n     * See [MusicService.onGetRoot] for where this is utilized.\n     *\n     * @param callingPackage The package name of the caller.\n     * @param callingUid The user id of the caller.\n     * @return `true` if the caller is known, `false` otherwise.\n     */\n    fun isKnownCaller(callingPackage: String, callingUid: Int): Boolean {\n        // If the caller has already been checked, return the previous result here.\n        val (checkedUid, checkResult) = callerChecked[callingPackage] ?: Pair(0, false)\n        if (checkedUid == callingUid) {\n            return checkResult\n        }\n\n        /**\n         * Because some of these checks can be slow, we save the results in [callerChecked] after\n         * this code is run.\n         *\n         * In particular, there's little reason to recompute the calling package's certificate\n         * signature (SHA-256) each call.\n         *\n         * This is safe to do as we know the UID matches the package's UID (from the check above),\n         * and app UIDs are set at install time. Additionally, a package name + UID is guaranteed to\n         * be constant until a reboot. (After a reboot then a previously assigned UID could be\n         * reassigned.)\n         */\n\n        // Build the caller info for the rest of the checks here.\n        val callerPackageInfo = buildCallerInfo(callingPackage)\n            ?: throw IllegalStateException(\"Caller wasn't found in the system?\")\n\n        // Verify that things aren't ... broken. (This test should always pass.)\n        if (callerPackageInfo.uid != callingUid) {\n            throw IllegalStateException(\"Caller's package UID doesn't match caller's actual UID?\")\n        }\n\n        val callerSignature = callerPackageInfo.signature\n        val isPackageInAllowList = certificateAllowList[callingPackage]?.signatures?.first {\n            it.signature == callerSignature\n        } != null\n\n        val isCallerKnown = when {\n            // If it's our own app making the call, allow it.\n            callingUid == Process.myUid() -> true\n            // If it's one of the apps on the allow list, allow it.\n            isPackageInAllowList -> true\n            // If the system is making the call, allow it.\n            callingUid == Process.SYSTEM_UID -> true\n            // If the app was signed by the same certificate as the platform itself, also allow it.\n            callerSignature == platformSignature -> true\n            /**\n             * [MEDIA_CONTENT_CONTROL] permission is only available to system applications, and\n             * while it isn't required to allow these apps to connect to a\n             * [MediaBrowserServiceCompat], allowing this ensures optimal compatability with apps\n             * such as Android TV and the Google Assistant.\n             */\n            callerPackageInfo.permissions.contains(MEDIA_CONTENT_CONTROL) -> true\n            /**\n             * If the calling app has a notification listener it is able to retrieve notifications\n             * and can connect to an active [MediaSessionCompat].\n             *\n             * It's not required to allow apps with a notification listener to\n             * connect to your [MediaBrowserServiceCompat], but it does allow easy compatibility\n             * with apps such as Wear OS.\n             */\n            NotificationManagerCompat.getEnabledListenerPackages(this.context)\n                .contains(callerPackageInfo.packageName) -> true\n\n            // If none of the previous checks succeeded, then the caller is unrecognized.\n            else -> false\n        }\n\n        if (!isCallerKnown) {\n            logUnknownCaller(callerPackageInfo)\n        }\n\n        // Save our work for next time.\n        callerChecked[callingPackage] = Pair(callingUid, isCallerKnown)\n        return isCallerKnown\n    }\n\n    /**\n     * Logs an info level message with details of how to add a caller to the allowed callers list\n     * when the app is debuggable.\n     */\n    private fun logUnknownCaller(callerPackageInfo: CallerPackageInfo) {\n        if (BuildConfig.DEBUG && callerPackageInfo.signature != null) {\n            val formattedLog =\n                context.getString(\n                    R.string.allowed_caller_log,\n                    callerPackageInfo.name,\n                    callerPackageInfo.packageName,\n                    callerPackageInfo.signature\n                )\n            Log.i(TAG, formattedLog)\n        }\n    }\n\n    /**\n     * Builds a [CallerPackageInfo] for a given package that can be used for all the\n     * various checks that are performed before allowing an app to connect to a\n     * [MediaBrowserServiceCompat].\n     */\n    private fun buildCallerInfo(callingPackage: String): CallerPackageInfo? {\n        val packageInfo = getPackageInfo(callingPackage) ?: return null\n\n        val appName = packageInfo.applicationInfo.loadLabel(packageManager).toString()\n        val uid = packageInfo.applicationInfo.uid\n        val signature = getSignature(packageInfo)\n\n        val requestedPermissions = packageInfo.requestedPermissions\n        val permissionFlags = packageInfo.requestedPermissionsFlags\n        val activePermissions = mutableSetOf<String>()\n        requestedPermissions?.forEachIndexed { index, permission ->\n            if (permissionFlags[index] and REQUESTED_PERMISSION_GRANTED != 0) {\n                activePermissions += permission\n            }\n        }\n\n        return CallerPackageInfo(appName, callingPackage, uid, signature, activePermissions.toSet())\n    }\n\n    /**\n     * Looks up the [PackageInfo] for a package name.\n     * This requests both the signatures (for checking if an app is on the allow list) and\n     * the app's permissions, which allow for more flexibility in the allow list.\n     *\n     * @return [PackageInfo] for the package name or null if it's not found.\n     */\n    @Suppress(\"deprecation\")\n    @SuppressLint(\"PackageManagerGetSignatures\")\n    private fun getPackageInfo(callingPackage: String): PackageInfo? =\n        packageManager.getPackageInfo(\n            callingPackage,\n            PackageManager.GET_SIGNATURES or PackageManager.GET_PERMISSIONS\n        )\n\n    /**\n     * Gets the signature of a given package's [PackageInfo].\n     *\n     * The \"signature\" is a SHA-256 hash of the public key of the signing certificate used by\n     * the app.\n     *\n     * If the app is not found, or if the app does not have exactly one signature, this method\n     * returns `null` as the signature.\n     */\n    @Suppress(\"deprecation\")\n    private fun getSignature(packageInfo: PackageInfo): String? =\n        if (packageInfo.signatures == null || packageInfo.signatures.size != 1) {\n            // Security best practices dictate that an app should be signed with exactly one (1)\n            // signature. Because of this, if there are multiple signatures, reject it.\n            null\n        } else {\n            val certificate = packageInfo.signatures[0].toByteArray()\n            getSignatureSha256(certificate)\n        }\n\n    private fun buildCertificateAllowList(parser: XmlResourceParser): Map<String, KnownCallerInfo> {\n\n        val certificateAllowList = LinkedHashMap<String, KnownCallerInfo>()\n        try {\n            var eventType = parser.next()\n            while (eventType != XmlResourceParser.END_DOCUMENT) {\n                if (eventType == XmlResourceParser.START_TAG) {\n                    val callerInfo = when (parser.name) {\n                        \"signing_certificate\" -> parseV1Tag(parser)\n                        \"signature\" -> parseV2Tag(parser)\n                        else -> null\n                    }\n\n                    callerInfo?.let { info ->\n                        val packageName = info.packageName\n                        val existingCallerInfo = certificateAllowList[packageName]\n                        if (existingCallerInfo != null) {\n                            existingCallerInfo.signatures += callerInfo.signatures\n                        } else {\n                            certificateAllowList[packageName] = callerInfo\n                        }\n                    }\n                }\n\n                eventType = parser.next()\n            }\n        } catch (xmlException: XmlPullParserException) {\n            Log.e(TAG, \"Could not read allowed callers from XML.\", xmlException)\n        } catch (ioException: IOException) {\n            Log.e(TAG, \"Could not read allowed callers from XML.\", ioException)\n        }\n\n        return certificateAllowList\n    }\n\n    /**\n     * Parses a v1 format tag. See allowed_media_browser_callers.xml for more details.\n     */\n    private fun parseV1Tag(parser: XmlResourceParser): KnownCallerInfo {\n        val name = parser.getAttributeValue(null, \"name\")\n        val packageName = parser.getAttributeValue(null, \"package\")\n        val isRelease = parser.getAttributeBooleanValue(null, \"release\", false)\n        val certificate = parser.nextText().replace(WHITESPACE_REGEX, \"\")\n        val signature = getSignatureSha256(certificate)\n\n        val callerSignature = KnownSignature(signature, isRelease)\n        return KnownCallerInfo(name, packageName, mutableSetOf(callerSignature))\n    }\n\n    /**\n     * Parses a v2 format tag. See allowed_media_browser_callers.xml for more details.\n     */\n    private fun parseV2Tag(parser: XmlResourceParser): KnownCallerInfo {\n        val name = parser.getAttributeValue(null, \"name\")\n        val packageName = parser.getAttributeValue(null, \"package\")\n\n        val callerSignatures = mutableSetOf<KnownSignature>()\n        var eventType = parser.next()\n        while (eventType != XmlResourceParser.END_TAG) {\n            val isRelease = parser.getAttributeBooleanValue(null, \"release\", false)\n            val signature = parser.nextText().replace(WHITESPACE_REGEX, \"\").toLowerCase()\n            callerSignatures += KnownSignature(signature, isRelease)\n\n            eventType = parser.next()\n        }\n\n        return KnownCallerInfo(name, packageName, callerSignatures)\n    }\n\n    /**\n     * Finds the Android platform signing key signature. This key is never null.\n     */\n    private fun getSystemSignature(): String =\n        getPackageInfo(ANDROID_PLATFORM)?.let { platformInfo ->\n            getSignature(platformInfo)\n        } ?: throw IllegalStateException(\"Platform signature not found\")\n\n    /**\n     * Creates a SHA-256 signature given a Base64 encoded certificate.\n     */\n    private fun getSignatureSha256(certificate: String): String {\n        return getSignatureSha256(Base64.decode(certificate, Base64.DEFAULT))\n    }\n\n    /**\n     * Creates a SHA-256 signature given a certificate byte array.\n     */\n    private fun getSignatureSha256(certificate: ByteArray): String {\n        val md: MessageDigest\n        try {\n            md = MessageDigest.getInstance(\"SHA256\")\n        } catch (noSuchAlgorithmException: NoSuchAlgorithmException) {\n            Log.e(TAG, \"No such algorithm: $noSuchAlgorithmException\")\n            throw RuntimeException(\"Could not find SHA256 hash algorithm\", noSuchAlgorithmException)\n        }\n        md.update(certificate)\n\n        // This code takes the byte array generated by `md.digest()` and joins each of the bytes\n        // to a string, applying the string format `%02x` on each digit before it's appended, with\n        // a colon (':') between each of the items.\n        // For example: input=[0,2,4,6,8,10,12], output=\"00:02:04:06:08:0a:0c\"\n        return md.digest().joinToString(\":\") { String.format(\"%02x\", it) }\n    }\n\n    private data class KnownCallerInfo(\n        internal val name: String,\n        internal val packageName: String,\n        internal val signatures: MutableSet<KnownSignature>\n    )\n\n    private data class KnownSignature(\n        internal val signature: String,\n        internal val release: Boolean\n    )\n\n    /**\n     * Convenience class to hold all of the information about an app that's being checked\n     * to see if it's a known caller.\n     */\n    private data class CallerPackageInfo(\n        internal val name: String,\n        internal val packageName: String,\n        internal val uid: Int,\n        internal val signature: String?,\n        internal val permissions: Set<String>\n    )\n}\n\nprivate const val TAG = \"PackageValidator\"\nprivate const val ANDROID_PLATFORM = \"android\"\nprivate val WHITESPACE_REGEX = \"\\\\s|\\\\n\".toRegex()\n"
  },
  {
    "path": "common/src/main/java/com/example/android/uamp/media/PersistentStorage.kt",
    "content": "/*\n * Copyright 2020 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.uamp.media\n\nimport android.content.Context\nimport android.content.SharedPreferences\nimport android.net.Uri\nimport android.os.Bundle\nimport android.support.v4.media.MediaBrowserCompat\nimport android.support.v4.media.MediaBrowserCompat.MediaItem.FLAG_PLAYABLE\nimport android.support.v4.media.MediaDescriptionCompat\nimport com.bumptech.glide.Glide\nimport com.example.android.uamp.media.extensions.asAlbumArtContentUri\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\n\ninternal class PersistentStorage private constructor(val context: Context) {\n\n    /**\n     * Store any data which must persist between restarts, such as the most recently played song.\n     */\n    private var preferences: SharedPreferences = context.getSharedPreferences(\n        PREFERENCES_NAME,\n        Context.MODE_PRIVATE\n    )\n\n    companion object {\n\n        @Volatile\n        private var instance: PersistentStorage? = null\n\n        fun getInstance(context: Context) =\n            instance ?: synchronized(this) {\n                instance ?: PersistentStorage(context).also { instance = it }\n            }\n    }\n\n    suspend fun saveRecentSong(description: MediaDescriptionCompat, position: Long) {\n\n        withContext(Dispatchers.IO) {\n\n            /**\n             * After booting, Android will attempt to build static media controls for the most\n             * recently played song. Artwork for these media controls should not be loaded\n             * from the network as it may be too slow or unavailable immediately after boot. Instead\n             * we convert the iconUri to point to the Glide on-disk cache.\n             */\n            val localIconUri = Glide.with(context).asFile().load(description.iconUri)\n                .submit(NOTIFICATION_LARGE_ICON_SIZE, NOTIFICATION_LARGE_ICON_SIZE).get()\n                .asAlbumArtContentUri()\n\n            preferences.edit()\n                .putString(RECENT_SONG_MEDIA_ID_KEY, description.mediaId)\n                .putString(RECENT_SONG_TITLE_KEY, description.title.toString())\n                .putString(RECENT_SONG_SUBTITLE_KEY, description.subtitle.toString())\n                .putString(RECENT_SONG_ICON_URI_KEY, localIconUri.toString())\n                .putLong(RECENT_SONG_POSITION_KEY, position)\n                .apply()\n        }\n    }\n\n    fun loadRecentSong(): MediaBrowserCompat.MediaItem? {\n        val mediaId = preferences.getString(RECENT_SONG_MEDIA_ID_KEY, null)\n        return if (mediaId == null) {\n            null\n        } else {\n            val extras = Bundle().also {\n                val position = preferences.getLong(RECENT_SONG_POSITION_KEY, 0L)\n                it.putLong(MEDIA_DESCRIPTION_EXTRAS_START_PLAYBACK_POSITION_MS, position)\n            }\n\n            MediaBrowserCompat.MediaItem(\n                MediaDescriptionCompat.Builder()\n                    .setMediaId(mediaId)\n                    .setTitle(preferences.getString(RECENT_SONG_TITLE_KEY, \"\"))\n                    .setSubtitle(preferences.getString(RECENT_SONG_SUBTITLE_KEY, \"\"))\n                    .setIconUri(Uri.parse(preferences.getString(RECENT_SONG_ICON_URI_KEY, \"\")))\n                    .setExtras(extras)\n                    .build(), FLAG_PLAYABLE\n            )\n        }\n    }\n}\n\nprivate const val PREFERENCES_NAME = \"uamp\"\nprivate const val RECENT_SONG_MEDIA_ID_KEY = \"recent_song_media_id\"\nprivate const val RECENT_SONG_TITLE_KEY = \"recent_song_title\"\nprivate const val RECENT_SONG_SUBTITLE_KEY = \"recent_song_subtitle\"\nprivate const val RECENT_SONG_ICON_URI_KEY = \"recent_song_icon_uri\"\nprivate const val RECENT_SONG_POSITION_KEY = \"recent_song_position\""
  },
  {
    "path": "common/src/main/java/com/example/android/uamp/media/UampNotificationManager.kt",
    "content": "/*\n * Copyright 2020 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.uamp.media\n\nimport android.app.NotificationManager\nimport android.app.PendingIntent\nimport android.content.Context\nimport android.graphics.Bitmap\nimport android.net.Uri\nimport android.support.v4.media.session.MediaControllerCompat\nimport android.support.v4.media.session.MediaSessionCompat\nimport com.bumptech.glide.Glide\nimport com.bumptech.glide.load.engine.DiskCacheStrategy\nimport com.bumptech.glide.request.RequestOptions\nimport com.google.android.exoplayer2.Player\nimport com.google.android.exoplayer2.ui.PlayerNotificationManager\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.SupervisorJob\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\n\nconst val NOW_PLAYING_CHANNEL_ID = \"com.example.android.uamp.media.NOW_PLAYING\"\nconst val NOW_PLAYING_NOTIFICATION_ID = 0xb339 // Arbitrary number used to identify our notification\n\n/**\n * A wrapper class for ExoPlayer's PlayerNotificationManager. It sets up the notification shown to\n * the user during audio playback and provides track metadata, such as track title and icon image.\n */\ninternal class UampNotificationManager(\n    private val context: Context,\n    sessionToken: MediaSessionCompat.Token,\n    notificationListener: PlayerNotificationManager.NotificationListener\n) {\n\n    private var player: Player? = null\n    private val serviceJob = SupervisorJob()\n    private val serviceScope = CoroutineScope(Dispatchers.Main + serviceJob)\n    private val notificationManager: PlayerNotificationManager\n    private val platformNotificationManager: NotificationManager =\n        context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager\n\n    init {\n        val mediaController = MediaControllerCompat(context, sessionToken)\n\n        val builder = PlayerNotificationManager.Builder(context, NOW_PLAYING_NOTIFICATION_ID, NOW_PLAYING_CHANNEL_ID)\n        with (builder) {\n            setMediaDescriptionAdapter(DescriptionAdapter(mediaController))\n            setNotificationListener(notificationListener)\n            setChannelNameResourceId(R.string.notification_channel)\n            setChannelDescriptionResourceId(R.string.notification_channel_description)\n        }\n        notificationManager = builder.build()\n        notificationManager.setMediaSessionToken(sessionToken)\n        notificationManager.setSmallIcon(R.drawable.ic_notification)\n        notificationManager.setUseRewindAction(false)\n        notificationManager.setUseFastForwardAction(false)\n    }\n\n    fun hideNotification() {\n        notificationManager.setPlayer(null)\n    }\n\n    fun showNotificationForPlayer(player: Player){\n        notificationManager.setPlayer(player)\n    }\n\n    private inner class DescriptionAdapter(private val controller: MediaControllerCompat) :\n        PlayerNotificationManager.MediaDescriptionAdapter {\n\n        var currentIconUri: Uri? = null\n        var currentBitmap: Bitmap? = null\n\n        override fun createCurrentContentIntent(player: Player): PendingIntent? =\n            controller.sessionActivity\n\n        override fun getCurrentContentText(player: Player) =\n            controller.metadata.description.subtitle.toString()\n\n        override fun getCurrentContentTitle(player: Player) =\n            controller.metadata.description.title.toString()\n\n        override fun getCurrentLargeIcon(\n            player: Player,\n            callback: PlayerNotificationManager.BitmapCallback\n        ): Bitmap? {\n            val iconUri = controller.metadata.description.iconUri\n            return if (currentIconUri != iconUri || currentBitmap == null) {\n\n                // Cache the bitmap for the current song so that successive calls to\n                // `getCurrentLargeIcon` don't cause the bitmap to be recreated.\n                currentIconUri = iconUri\n                serviceScope.launch {\n                    currentBitmap = iconUri?.let {\n                        resolveUriAsBitmap(it)\n                    }\n                    currentBitmap?.let { callback.onBitmap(it) }\n                }\n                null\n            } else {\n                currentBitmap\n            }\n        }\n\n        private suspend fun resolveUriAsBitmap(uri: Uri): Bitmap? {\n            return withContext(Dispatchers.IO) {\n                // Block on downloading artwork.\n                Glide.with(context).applyDefaultRequestOptions(glideOptions)\n                    .asBitmap()\n                    .load(uri)\n                    .submit(NOTIFICATION_LARGE_ICON_SIZE, NOTIFICATION_LARGE_ICON_SIZE)\n                    .get()\n            }\n        }\n    }\n}\n\nconst val NOTIFICATION_LARGE_ICON_SIZE = 144 // px\n\nprivate val glideOptions = RequestOptions()\n    .fallback(R.drawable.default_art)\n    .diskCacheStrategy(DiskCacheStrategy.DATA)\n\nprivate const val MODE_READ_ONLY = \"r\"\n"
  },
  {
    "path": "common/src/main/java/com/example/android/uamp/media/extensions/FileExt.kt",
    "content": "/*\n * Copyright 2019 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.uamp.media.extensions\n\nimport android.content.ContentResolver\nimport android.net.Uri\nimport java.io.File\n\n/**\n * This file contains extension methods for the java.io.File class.\n */\n\n/**\n * Returns a Content Uri for the AlbumArtContentProvider\n */\nfun File.asAlbumArtContentUri(): Uri {\n    return Uri.Builder()\n        .scheme(ContentResolver.SCHEME_CONTENT)\n        .authority(AUTHORITY)\n        .appendPath(this.path)\n        .build()\n}\n\nprivate const val AUTHORITY = \"com.example.android.uamp\"\n"
  },
  {
    "path": "common/src/main/java/com/example/android/uamp/media/extensions/JavaLangExt.kt",
    "content": "/*\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.uamp.media.extensions\n\nimport android.net.Uri\nimport java.net.URLEncoder\nimport java.nio.charset.Charset\nimport java.util.Locale\n\n/**\n * This file contains extension methods for the java.lang package.\n */\n\n/**\n * Helper method to check if a [String] contains another in a case insensitive way.\n */\nfun String?.containsCaseInsensitive(other: String?) =\n    if (this != null && other != null) {\n        toLowerCase(Locale.getDefault()).contains(other.toLowerCase(Locale.getDefault()))\n    } else {\n        this == other\n    }\n\n/**\n * Helper extension to URL encode a [String]. Returns an empty string when called on null.\n */\ninline val String?.urlEncoded: String\n    get() = if (Charset.isSupported(\"UTF-8\")) {\n        URLEncoder.encode(this ?: \"\", \"UTF-8\")\n    } else {\n        // If UTF-8 is not supported, use the default charset.\n        @Suppress(\"deprecation\")\n        URLEncoder.encode(this ?: \"\")\n    }\n\n/**\n * Helper extension to convert a potentially null [String] to a [Uri] falling back to [Uri.EMPTY]\n */\nfun String?.toUri(): Uri = this?.let { Uri.parse(it) } ?: Uri.EMPTY\n"
  },
  {
    "path": "common/src/main/java/com/example/android/uamp/media/extensions/MediaMetadataCompatExt.kt",
    "content": "/*\n * Copyright 2017 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.uamp.media.extensions\n\nimport android.graphics.Bitmap\nimport android.net.Uri\nimport android.os.Bundle\nimport android.support.v4.media.MediaBrowserCompat.MediaItem\nimport android.support.v4.media.MediaMetadataCompat\nimport com.example.android.uamp.media.library.JsonSource\nimport com.google.android.exoplayer2.MediaMetadata\nimport com.google.android.exoplayer2.util.MimeTypes\n\n/**\n * Useful extensions for [MediaMetadataCompat].\n */\ninline val MediaMetadataCompat.id: String?\n    get() = getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID)\n\ninline val MediaMetadataCompat.title: String?\n    get() = getString(MediaMetadataCompat.METADATA_KEY_TITLE)\n\ninline val MediaMetadataCompat.artist: String?\n    get() = getString(MediaMetadataCompat.METADATA_KEY_ARTIST)\n\ninline val MediaMetadataCompat.duration\n    get() = getLong(MediaMetadataCompat.METADATA_KEY_DURATION)\n\ninline val MediaMetadataCompat.album: String?\n    get() = getString(MediaMetadataCompat.METADATA_KEY_ALBUM)\n\ninline val MediaMetadataCompat.author: String?\n    get() = getString(MediaMetadataCompat.METADATA_KEY_AUTHOR)\n\ninline val MediaMetadataCompat.writer: String?\n    get() = getString(MediaMetadataCompat.METADATA_KEY_WRITER)\n\ninline val MediaMetadataCompat.composer: String?\n    get() = getString(MediaMetadataCompat.METADATA_KEY_COMPOSER)\n\ninline val MediaMetadataCompat.compilation: String?\n    get() = getString(MediaMetadataCompat.METADATA_KEY_COMPILATION)\n\ninline val MediaMetadataCompat.date: String?\n    get() = getString(MediaMetadataCompat.METADATA_KEY_DATE)\n\ninline val MediaMetadataCompat.year: String?\n    get() = getString(MediaMetadataCompat.METADATA_KEY_YEAR)\n\ninline val MediaMetadataCompat.genre: String?\n    get() = getString(MediaMetadataCompat.METADATA_KEY_GENRE)\n\ninline val MediaMetadataCompat.trackNumber\n    get() = getLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER)\n\ninline val MediaMetadataCompat.trackCount\n    get() = getLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS)\n\ninline val MediaMetadataCompat.discNumber\n    get() = getLong(MediaMetadataCompat.METADATA_KEY_DISC_NUMBER)\n\ninline val MediaMetadataCompat.albumArtist: String?\n    get() = getString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST)\n\ninline val MediaMetadataCompat.art: Bitmap\n    get() = getBitmap(MediaMetadataCompat.METADATA_KEY_ART)\n\ninline val MediaMetadataCompat.artUri: Uri\n    get() = this.getString(MediaMetadataCompat.METADATA_KEY_ART_URI).toUri()\n\ninline val MediaMetadataCompat.albumArt: Bitmap?\n    get() = getBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART)\n\ninline val MediaMetadataCompat.albumArtUri: Uri\n    get() = this.getString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI).toUri()\n\ninline val MediaMetadataCompat.userRating\n    get() = getLong(MediaMetadataCompat.METADATA_KEY_USER_RATING)\n\ninline val MediaMetadataCompat.rating\n    get() = getLong(MediaMetadataCompat.METADATA_KEY_RATING)\n\ninline val MediaMetadataCompat.displayTitle: String?\n    get() = getString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE)\n\ninline val MediaMetadataCompat.displaySubtitle: String?\n    get() = getString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE)\n\ninline val MediaMetadataCompat.displayDescription: String?\n    get() = getString(MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION)\n\ninline val MediaMetadataCompat.displayIcon: Bitmap\n    get() = getBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON)\n\ninline val MediaMetadataCompat.displayIconUri: Uri\n    get() = this.getString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI).toUri()\n\ninline val MediaMetadataCompat.mediaUri: Uri\n    get() = this.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI).toUri()\n\ninline val MediaMetadataCompat.downloadStatus\n    get() = getLong(MediaMetadataCompat.METADATA_KEY_DOWNLOAD_STATUS)\n\n/**\n * Custom property for storing whether a [MediaMetadataCompat] item represents an\n * item that is [MediaItem.FLAG_BROWSABLE] or [MediaItem.FLAG_PLAYABLE].\n */\ninline val MediaMetadataCompat.flag\n    get() = this.getLong(METADATA_KEY_UAMP_FLAGS).toInt()\n\n/**\n * Useful extensions for [MediaMetadataCompat.Builder].\n */\n\n// These do not have getters, so create a message for the error.\nconst val NO_GET = \"Property does not have a 'get'\"\n\ninline var MediaMetadataCompat.Builder.id: String\n    @Deprecated(NO_GET, level = DeprecationLevel.ERROR)\n    get() = throw IllegalAccessException(\"Cannot get from MediaMetadataCompat.Builder\")\n    set(value) {\n        putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, value)\n    }\n\ninline var MediaMetadataCompat.Builder.title: String?\n    @Deprecated(NO_GET, level = DeprecationLevel.ERROR)\n    get() = throw IllegalAccessException(\"Cannot get from MediaMetadataCompat.Builder\")\n    set(value) {\n        putString(MediaMetadataCompat.METADATA_KEY_TITLE, value)\n    }\n\ninline var MediaMetadataCompat.Builder.artist: String?\n    @Deprecated(NO_GET, level = DeprecationLevel.ERROR)\n    get() = throw IllegalAccessException(\"Cannot get from MediaMetadataCompat.Builder\")\n    set(value) {\n        putString(MediaMetadataCompat.METADATA_KEY_ARTIST, value)\n    }\n\ninline var MediaMetadataCompat.Builder.album: String?\n    @Deprecated(NO_GET, level = DeprecationLevel.ERROR)\n    get() = throw IllegalAccessException(\"Cannot get from MediaMetadataCompat.Builder\")\n    set(value) {\n        putString(MediaMetadataCompat.METADATA_KEY_ALBUM, value)\n    }\n\ninline var MediaMetadataCompat.Builder.duration: Long\n    @Deprecated(NO_GET, level = DeprecationLevel.ERROR)\n    get() = throw IllegalAccessException(\"Cannot get from MediaMetadataCompat.Builder\")\n    set(value) {\n        putLong(MediaMetadataCompat.METADATA_KEY_DURATION, value)\n    }\n\ninline var MediaMetadataCompat.Builder.genre: String?\n    @Deprecated(NO_GET, level = DeprecationLevel.ERROR)\n    get() = throw IllegalAccessException(\"Cannot get from MediaMetadataCompat.Builder\")\n    set(value) {\n        putString(MediaMetadataCompat.METADATA_KEY_GENRE, value)\n    }\n\ninline var MediaMetadataCompat.Builder.mediaUri: String?\n    @Deprecated(NO_GET, level = DeprecationLevel.ERROR)\n    get() = throw IllegalAccessException(\"Cannot get from MediaMetadataCompat.Builder\")\n    set(value) {\n        putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI, value)\n    }\n\ninline var MediaMetadataCompat.Builder.albumArtUri: String?\n    @Deprecated(NO_GET, level = DeprecationLevel.ERROR)\n    get() = throw IllegalAccessException(\"Cannot get from MediaMetadataCompat.Builder\")\n    set(value) {\n        putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, value)\n    }\n\ninline var MediaMetadataCompat.Builder.albumArt: Bitmap?\n    @Deprecated(NO_GET, level = DeprecationLevel.ERROR)\n    get() = throw IllegalAccessException(\"Cannot get from MediaMetadataCompat.Builder\")\n    set(value) {\n        putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, value)\n    }\n\ninline var MediaMetadataCompat.Builder.trackNumber: Long\n    @Deprecated(NO_GET, level = DeprecationLevel.ERROR)\n    get() = throw IllegalAccessException(\"Cannot get from MediaMetadataCompat.Builder\")\n    set(value) {\n        putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, value)\n    }\n\ninline var MediaMetadataCompat.Builder.trackCount: Long\n    @Deprecated(NO_GET, level = DeprecationLevel.ERROR)\n    get() = throw IllegalAccessException(\"Cannot get from MediaMetadataCompat.Builder\")\n    set(value) {\n        putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, value)\n    }\n\ninline var MediaMetadataCompat.Builder.displayTitle: String?\n    @Deprecated(NO_GET, level = DeprecationLevel.ERROR)\n    get() = throw IllegalAccessException(\"Cannot get from MediaMetadataCompat.Builder\")\n    set(value) {\n        putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, value)\n    }\n\ninline var MediaMetadataCompat.Builder.displaySubtitle: String?\n    @Deprecated(NO_GET, level = DeprecationLevel.ERROR)\n    get() = throw IllegalAccessException(\"Cannot get from MediaMetadataCompat.Builder\")\n    set(value) {\n        putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, value)\n    }\n\ninline var MediaMetadataCompat.Builder.displayDescription: String?\n    @Deprecated(NO_GET, level = DeprecationLevel.ERROR)\n    get() = throw IllegalAccessException(\"Cannot get from MediaMetadataCompat.Builder\")\n    set(value) {\n        putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION, value)\n    }\n\ninline var MediaMetadataCompat.Builder.displayIconUri: String?\n    @Deprecated(NO_GET, level = DeprecationLevel.ERROR)\n    get() = throw IllegalAccessException(\"Cannot get from MediaMetadataCompat.Builder\")\n    set(value) {\n        putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, value)\n    }\n\ninline var MediaMetadataCompat.Builder.downloadStatus: Long\n    @Deprecated(NO_GET, level = DeprecationLevel.ERROR)\n    get() = throw IllegalAccessException(\"Cannot get from MediaMetadataCompat.Builder\")\n    set(value) {\n        putLong(MediaMetadataCompat.METADATA_KEY_DOWNLOAD_STATUS, value)\n    }\n\n/**\n * Custom property for storing whether a [MediaMetadataCompat] item represents an\n * item that is [MediaItem.FLAG_BROWSABLE] or [MediaItem.FLAG_PLAYABLE].\n */\ninline var MediaMetadataCompat.Builder.flag: Int\n    @Deprecated(NO_GET, level = DeprecationLevel.ERROR)\n    get() = throw IllegalAccessException(\"Cannot get from MediaMetadataCompat.Builder\")\n    set(value) {\n        putLong(METADATA_KEY_UAMP_FLAGS, value.toLong())\n    }\n\nfun MediaMetadataCompat.toMediaItemMetadata(): com.google.android.exoplayer2.MediaMetadata {\n    return with(MediaMetadata.Builder()) {\n        setTitle(title)\n        setDisplayTitle(displayTitle)\n        setAlbumArtist(artist)\n        setAlbumTitle(album)\n        setComposer(composer)\n        setTrackNumber(trackNumber.toInt())\n        setTotalTrackCount(trackCount.toInt())\n        setDiscNumber(discNumber.toInt())\n        setWriter(writer)\n        setArtworkUri(albumArtUri)\n        val extras = Bundle()\n        getString(JsonSource.ORIGINAL_ARTWORK_URI_KEY)?.let {\n            // album art is a content:// URI. Keep the original for Cast.\n            extras.putString(\n                JsonSource.ORIGINAL_ARTWORK_URI_KEY,\n                getString(JsonSource.ORIGINAL_ARTWORK_URI_KEY)\n            )\n        }\n        extras.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration)\n        setExtras(extras)\n    }.build()\n}\n\nfun MediaMetadataCompat.toMediaItem(): com.google.android.exoplayer2.MediaItem {\n    return with(com.google.android.exoplayer2.MediaItem.Builder()) {\n        setMediaId(mediaUri.toString())\n        setUri(mediaUri)\n        setMimeType(MimeTypes.AUDIO_MPEG)\n        setMediaMetadata(toMediaItemMetadata())\n    }.build()\n}\n\n/**\n * Custom property that holds whether an item is [MediaItem.FLAG_BROWSABLE] or\n * [MediaItem.FLAG_PLAYABLE].\n */\nconst val METADATA_KEY_UAMP_FLAGS = \"com.example.android.uamp.media.METADATA_KEY_UAMP_FLAGS\"\n\n"
  },
  {
    "path": "common/src/main/java/com/example/android/uamp/media/extensions/PlaybackStateCompatExt.kt",
    "content": "/*\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.uamp.media.extensions\n\nimport android.os.SystemClock\nimport android.support.v4.media.session.PlaybackStateCompat\n\n/**\n * Useful extension methods for [PlaybackStateCompat].\n */\ninline val PlaybackStateCompat.isPrepared\n    get() = (state == PlaybackStateCompat.STATE_BUFFERING) ||\n            (state == PlaybackStateCompat.STATE_PLAYING) ||\n            (state == PlaybackStateCompat.STATE_PAUSED)\n\ninline val PlaybackStateCompat.isPlaying\n    get() = (state == PlaybackStateCompat.STATE_BUFFERING) ||\n            (state == PlaybackStateCompat.STATE_PLAYING)\n\ninline val PlaybackStateCompat.isPlayEnabled\n    get() = (actions and PlaybackStateCompat.ACTION_PLAY != 0L) ||\n            ((actions and PlaybackStateCompat.ACTION_PLAY_PAUSE != 0L) &&\n                    (state == PlaybackStateCompat.STATE_PAUSED))\n\ninline val PlaybackStateCompat.isPauseEnabled\n    get() = (actions and PlaybackStateCompat.ACTION_PAUSE != 0L) ||\n            ((actions and PlaybackStateCompat.ACTION_PLAY_PAUSE != 0L) &&\n                    (state == PlaybackStateCompat.STATE_BUFFERING ||\n                            state == PlaybackStateCompat.STATE_PLAYING))\n\ninline val PlaybackStateCompat.isSkipToNextEnabled\n    get() = actions and PlaybackStateCompat.ACTION_SKIP_TO_NEXT != 0L\n\ninline val PlaybackStateCompat.isSkipToPreviousEnabled\n    get() = actions and PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS != 0L\n\ninline val PlaybackStateCompat.stateName\n    get() = when (state) {\n        PlaybackStateCompat.STATE_NONE -> \"STATE_NONE\"\n        PlaybackStateCompat.STATE_STOPPED -> \"STATE_STOPPED\"\n        PlaybackStateCompat.STATE_PAUSED -> \"STATE_PAUSED\"\n        PlaybackStateCompat.STATE_PLAYING -> \"STATE_PLAYING\"\n        PlaybackStateCompat.STATE_FAST_FORWARDING -> \"STATE_FAST_FORWARDING\"\n        PlaybackStateCompat.STATE_REWINDING -> \"STATE_REWINDING\"\n        PlaybackStateCompat.STATE_BUFFERING -> \"STATE_BUFFERING\"\n        PlaybackStateCompat.STATE_ERROR -> \"STATE_ERROR\"\n        else -> \"UNKNOWN_STATE\"\n    }\n\n/**\n * Calculates the current playback position based on last update time along with playback\n * state and speed.\n */\ninline val PlaybackStateCompat.currentPlayBackPosition: Long\n    get() = if (state == PlaybackStateCompat.STATE_PLAYING) {\n        val timeDelta = SystemClock.elapsedRealtime() - lastPositionUpdateTime\n        (position + (timeDelta * playbackSpeed)).toLong()\n    } else {\n        position\n    }\n\n"
  },
  {
    "path": "common/src/main/java/com/example/android/uamp/media/library/AlbumArtContentProvider.kt",
    "content": "/*\n * Copyright 2019 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.uamp.media.library\n\nimport android.content.ContentProvider\nimport android.content.ContentResolver\nimport android.content.ContentValues\nimport android.database.Cursor\nimport android.net.Uri\nimport android.os.ParcelFileDescriptor\nimport com.bumptech.glide.Glide\nimport java.io.File\nimport java.io.FileNotFoundException\nimport java.util.concurrent.TimeUnit\n\n// The amount of time to wait for the album art file to download before timing out.\nconst val DOWNLOAD_TIMEOUT_SECONDS = 30L\n\ninternal class AlbumArtContentProvider : ContentProvider() {\n\n    companion object {\n        private val uriMap = mutableMapOf<Uri, Uri>()\n\n        fun mapUri(uri: Uri): Uri {\n            val path = uri.encodedPath?.substring(1)?.replace('/', ':') ?: return Uri.EMPTY\n            val contentUri = Uri.Builder()\n                .scheme(ContentResolver.SCHEME_CONTENT)\n                .authority(\"com.example.android.uamp\")\n                .path(path)\n                .build()\n            uriMap[contentUri] = uri\n            return contentUri\n        }\n    }\n\n    override fun onCreate() = true\n\n    override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {\n        val context = this.context ?: return null\n        val remoteUri = uriMap[uri] ?: throw FileNotFoundException(uri.path)\n\n        var file = File(context.cacheDir, uri.path)\n\n        if (!file.exists()) {\n            // Use Glide to download the album art.\n            val cacheFile = Glide.with(context)\n                .asFile()\n                .load(remoteUri)\n                .submit()\n                .get(DOWNLOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS)\n\n            // Rename the file Glide created to match our own scheme.\n            cacheFile.renameTo(file)\n\n            file = cacheFile\n        }\n        return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)\n    }\n\n    override fun insert(uri: Uri, values: ContentValues?): Uri? = null\n\n    override fun query(\n        uri: Uri,\n        projection: Array<String>?,\n        selection: String?,\n        selectionArgs: Array<String>?,\n        sortOrder: String?\n    ): Cursor? = null\n\n    override fun update(\n        uri: Uri,\n        values: ContentValues?,\n        selection: String?,\n        selectionArgs: Array<String>?\n    ) = 0\n\n    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?) = 0\n\n    override fun getType(uri: Uri): String? = null\n\n}\n"
  },
  {
    "path": "common/src/main/java/com/example/android/uamp/media/library/BrowseTree.kt",
    "content": "/*\n * Copyright 2019 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.uamp.media.library\n\nimport android.content.Context\nimport android.support.v4.media.MediaBrowserCompat\nimport android.support.v4.media.MediaBrowserCompat.MediaItem\nimport android.support.v4.media.MediaMetadataCompat\nimport com.example.android.uamp.media.MusicService\nimport com.example.android.uamp.media.R\nimport com.example.android.uamp.media.extensions.album\nimport com.example.android.uamp.media.extensions.albumArt\nimport com.example.android.uamp.media.extensions.albumArtUri\nimport com.example.android.uamp.media.extensions.artist\nimport com.example.android.uamp.media.extensions.flag\nimport com.example.android.uamp.media.extensions.id\nimport com.example.android.uamp.media.extensions.title\nimport com.example.android.uamp.media.extensions.trackNumber\nimport com.example.android.uamp.media.extensions.urlEncoded\n\n/**\n * Represents a tree of media that's used by [MusicService.onLoadChildren].\n *\n * [BrowseTree] maps a media id (see: [MediaMetadataCompat.METADATA_KEY_MEDIA_ID]) to one (or\n * more) [MediaMetadataCompat] objects, which are children of that media id.\n *\n * For example, given the following conceptual tree:\n * root\n *  +-- Albums\n *  |    +-- Album_A\n *  |    |    +-- Song_1\n *  |    |    +-- Song_2\n *  ...\n *  +-- Artists\n *  ...\n *\n *  Requesting `browseTree[\"root\"]` would return a list that included \"Albums\", \"Artists\", and\n *  any other direct children. Taking the media ID of \"Albums\" (\"Albums\" in this example),\n *  `browseTree[\"Albums\"]` would return a single item list \"Album_A\", and, finally,\n *  `browseTree[\"Album_A\"]` would return \"Song_1\" and \"Song_2\". Since those are leaf nodes,\n *  requesting `browseTree[\"Song_1\"]` would return null (there aren't any children of it).\n */\nclass BrowseTree(\n    val context: Context,\n    musicSource: MusicSource,\n    val recentMediaId: String? = null\n) {\n    private val mediaIdToChildren = mutableMapOf<String, MutableList<MediaMetadataCompat>>()\n\n    /**\n     * Whether to allow clients which are unknown (not on the allowed list) to use search on this\n     * [BrowseTree].\n     */\n    val searchableByUnknownCaller = true\n\n    /**\n     * In this example, there's a single root node (identified by the constant\n     * [UAMP_BROWSABLE_ROOT]). The root's children are each album included in the\n     * [MusicSource], and the children of each album are the songs on that album.\n     * (See [BrowseTree.buildAlbumRoot] for more details.)\n     *\n     * TODO: Expand to allow more browsing types.\n     */\n    init {\n        val rootList = mediaIdToChildren[UAMP_BROWSABLE_ROOT] ?: mutableListOf()\n\n        val recommendedMetadata = MediaMetadataCompat.Builder().apply {\n            id = UAMP_RECOMMENDED_ROOT\n            title = context.getString(R.string.recommended_title)\n            albumArtUri = RESOURCE_ROOT_URI +\n                    context.resources.getResourceEntryName(R.drawable.ic_recommended)\n            flag = MediaBrowserCompat.MediaItem.FLAG_BROWSABLE\n        }.build()\n\n        val albumsMetadata = MediaMetadataCompat.Builder().apply {\n            id = UAMP_ALBUMS_ROOT\n            title = context.getString(R.string.albums_title)\n            albumArtUri = RESOURCE_ROOT_URI +\n                    context.resources.getResourceEntryName(R.drawable.ic_album)\n            flag = MediaBrowserCompat.MediaItem.FLAG_BROWSABLE\n        }.build()\n\n        rootList += recommendedMetadata\n        rootList += albumsMetadata\n        mediaIdToChildren[UAMP_BROWSABLE_ROOT] = rootList\n\n        musicSource.forEach { mediaItem ->\n            val albumMediaId = mediaItem.album.urlEncoded\n            val albumChildren = mediaIdToChildren[albumMediaId] ?: buildAlbumRoot(mediaItem)\n            albumChildren += mediaItem\n\n            // Add the first track of each album to the 'Recommended' category\n            if (mediaItem.trackNumber == 1L) {\n                val recommendedChildren = mediaIdToChildren[UAMP_RECOMMENDED_ROOT]\n                    ?: mutableListOf()\n                recommendedChildren += mediaItem\n                mediaIdToChildren[UAMP_RECOMMENDED_ROOT] = recommendedChildren\n            }\n\n            // If this was recently played, add it to the recent root.\n            if (mediaItem.id == recentMediaId) {\n                mediaIdToChildren[UAMP_RECENT_ROOT] = mutableListOf(mediaItem)\n            }\n        }\n    }\n\n    /**\n     * Provide access to the list of children with the `get` operator.\n     * i.e.: `browseTree\\[UAMP_BROWSABLE_ROOT\\]`\n     */\n    operator fun get(mediaId: String) = mediaIdToChildren[mediaId]\n\n    /**\n     * Builds a node, under the root, that represents an album, given\n     * a [MediaMetadataCompat] object that's one of the songs on that album,\n     * marking the item as [MediaItem.FLAG_BROWSABLE], since it will have child\n     * node(s) AKA at least 1 song.\n     */\n    private fun buildAlbumRoot(mediaItem: MediaMetadataCompat): MutableList<MediaMetadataCompat> {\n        val albumMetadata = MediaMetadataCompat.Builder().apply {\n            id = mediaItem.album.urlEncoded\n            title = mediaItem.album\n            artist = mediaItem.artist\n            albumArt = mediaItem.albumArt\n            albumArtUri = mediaItem.albumArtUri.toString()\n            flag = MediaItem.FLAG_BROWSABLE\n        }.build()\n\n        // Adds this album to the 'Albums' category.\n        val rootList = mediaIdToChildren[UAMP_ALBUMS_ROOT] ?: mutableListOf()\n        rootList += albumMetadata\n        mediaIdToChildren[UAMP_ALBUMS_ROOT] = rootList\n\n        // Insert the album's root with an empty list for its children, and return the list.\n        return mutableListOf<MediaMetadataCompat>().also {\n            mediaIdToChildren[albumMetadata.id!!] = it\n        }\n    }\n}\n\nconst val UAMP_BROWSABLE_ROOT = \"/\"\nconst val UAMP_EMPTY_ROOT = \"@empty@\"\nconst val UAMP_RECOMMENDED_ROOT = \"__RECOMMENDED__\"\nconst val UAMP_ALBUMS_ROOT = \"__ALBUMS__\"\nconst val UAMP_RECENT_ROOT = \"__RECENT__\"\n\nconst val MEDIA_SEARCH_SUPPORTED = \"android.media.browse.SEARCH_SUPPORTED\"\n\nconst val RESOURCE_ROOT_URI = \"android.resource://com.example.android.uamp.next/drawable/\"\n"
  },
  {
    "path": "common/src/main/java/com/example/android/uamp/media/library/JsonSource.kt",
    "content": "/*\n * Copyright 2017 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.uamp.media.library\n\nimport android.net.Uri\nimport android.support.v4.media.MediaBrowserCompat.MediaItem\nimport android.support.v4.media.MediaDescriptionCompat.STATUS_NOT_DOWNLOADED\nimport android.support.v4.media.MediaMetadataCompat\nimport com.example.android.uamp.media.extensions.album\nimport com.example.android.uamp.media.extensions.albumArtUri\nimport com.example.android.uamp.media.extensions.artist\nimport com.example.android.uamp.media.extensions.displayDescription\nimport com.example.android.uamp.media.extensions.displayIconUri\nimport com.example.android.uamp.media.extensions.displaySubtitle\nimport com.example.android.uamp.media.extensions.displayTitle\nimport com.example.android.uamp.media.extensions.downloadStatus\nimport com.example.android.uamp.media.extensions.duration\nimport com.example.android.uamp.media.extensions.flag\nimport com.example.android.uamp.media.extensions.genre\nimport com.example.android.uamp.media.extensions.id\nimport com.example.android.uamp.media.extensions.mediaUri\nimport com.example.android.uamp.media.extensions.title\nimport com.example.android.uamp.media.extensions.trackCount\nimport com.example.android.uamp.media.extensions.trackNumber\nimport com.google.android.exoplayer2.C\nimport com.google.gson.Gson\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport java.io.BufferedReader\nimport java.io.IOException\nimport java.io.InputStreamReader\nimport java.net.URL\nimport java.util.concurrent.TimeUnit\n\n/**\n * Source of [MediaMetadataCompat] objects created from a basic JSON stream.\n *\n * The definition of the JSON is specified in the docs of [JsonMusic] in this file,\n * which is the object representation of it.\n */\ninternal class JsonSource(private val source: Uri) : AbstractMusicSource() {\n\n    companion object {\n        const val ORIGINAL_ARTWORK_URI_KEY = \"com.example.android.uamp.JSON_ARTWORK_URI\"\n    }\n\n    private var catalog: List<MediaMetadataCompat> = emptyList()\n\n    init {\n        state = STATE_INITIALIZING\n    }\n\n    override fun iterator(): Iterator<MediaMetadataCompat> = catalog.iterator()\n\n    override suspend fun load() {\n        updateCatalog(source)?.let { updatedCatalog ->\n            catalog = updatedCatalog\n            state = STATE_INITIALIZED\n        } ?: run {\n            catalog = emptyList()\n            state = STATE_ERROR\n        }\n    }\n\n    /**\n     * Function to connect to a remote URI and download/process the JSON file that corresponds to\n     * [MediaMetadataCompat] objects.\n     */\n    private suspend fun updateCatalog(catalogUri: Uri): List<MediaMetadataCompat>? {\n        return withContext(Dispatchers.IO) {\n            val musicCat = try {\n                downloadJson(catalogUri)\n            } catch (ioException: IOException) {\n                return@withContext null\n            }\n\n            // Get the base URI to fix up relative references later.\n            val baseUri = catalogUri.toString().removeSuffix(catalogUri.lastPathSegment ?: \"\")\n\n            val mediaMetadataCompats = musicCat.music.map { song ->\n                // The JSON may have paths that are relative to the source of the JSON\n                // itself. We need to fix them up here to turn them into absolute paths.\n                catalogUri.scheme?.let { scheme ->\n                    if (!song.source.startsWith(scheme)) {\n                        song.source = baseUri + song.source\n                    }\n                    if (!song.image.startsWith(scheme)) {\n                        song.image = baseUri + song.image\n                    }\n                }\n                val jsonImageUri = Uri.parse(song.image)\n                val imageUri = AlbumArtContentProvider.mapUri(jsonImageUri)\n\n                MediaMetadataCompat.Builder()\n                    .from(song)\n                    .apply {\n                        displayIconUri = imageUri.toString() // Used by ExoPlayer and Notification\n                        albumArtUri = imageUri.toString()\n                        // Keep the original artwork URI for being included in Cast metadata object.\n                        putString(ORIGINAL_ARTWORK_URI_KEY, jsonImageUri.toString())\n                    }\n                    .build()\n            }.toList()\n            // Add description keys to be used by the ExoPlayer MediaSession extension when\n            // announcing metadata changes.\n            mediaMetadataCompats.forEach { it.description.extras?.putAll(it.bundle) }\n            mediaMetadataCompats\n        }\n    }\n\n\n    /**\n     * Attempts to download a catalog from a given Uri.\n     *\n     * @param catalogUri URI to attempt to download the catalog form.\n     * @return The catalog downloaded, or an empty catalog if an error occurred.\n     */\n    @Throws(IOException::class)\n    private fun downloadJson(catalogUri: Uri): JsonCatalog {\n        val catalogConn = URL(catalogUri.toString())\n        val reader = BufferedReader(InputStreamReader(catalogConn.openStream()))\n        return Gson().fromJson(reader, JsonCatalog::class.java)\n    }\n}\n\n/**\n * Extension method for [MediaMetadataCompat.Builder] to set the fields from\n * our JSON constructed object (to make the code a bit easier to see).\n */\nfun MediaMetadataCompat.Builder.from(jsonMusic: JsonMusic): MediaMetadataCompat.Builder {\n    // The duration from the JSON is given in seconds, but the rest of the code works in\n    // milliseconds. Here's where we convert to the proper units.\n    val durationMs = TimeUnit.SECONDS.toMillis(jsonMusic.duration)\n\n    id = jsonMusic.id\n    title = jsonMusic.title\n    artist = jsonMusic.artist\n    album = jsonMusic.album\n    duration = durationMs\n    genre = jsonMusic.genre\n    mediaUri = jsonMusic.source\n    albumArtUri = jsonMusic.image\n    trackNumber = jsonMusic.trackNumber\n    trackCount = jsonMusic.totalTrackCount\n    flag = MediaItem.FLAG_PLAYABLE\n\n    // To make things easier for *displaying* these, set the display properties as well.\n    displayTitle = jsonMusic.title\n    displaySubtitle = jsonMusic.artist\n    displayDescription = jsonMusic.album\n    displayIconUri = jsonMusic.image\n\n    // Add downloadStatus to force the creation of an \"extras\" bundle in the resulting\n    // MediaMetadataCompat object. This is needed to send accurate metadata to the\n    // media session during updates.\n    downloadStatus = STATUS_NOT_DOWNLOADED\n\n    // Allow it to be used in the typical builder style.\n    return this\n}\n\n/**\n * Wrapper object for our JSON in order to be processed easily by GSON.\n */\nclass JsonCatalog {\n    var music: List<JsonMusic> = ArrayList()\n}\n\n/**\n * An individual piece of music included in our JSON catalog.\n * The format from the server is as specified:\n * ```\n *     { \"music\" : [\n *     { \"title\" : // Title of the piece of music\n *     \"album\" : // Album title of the piece of music\n *     \"artist\" : // Artist of the piece of music\n *     \"genre\" : // Primary genre of the music\n *     \"source\" : // Path to the music, which may be relative\n *     \"image\" : // Path to the art for the music, which may be relative\n *     \"trackNumber\" : // Track number\n *     \"totalTrackCount\" : // Track count\n *     \"duration\" : // Duration of the music in seconds\n *     \"site\" : // Source of the music, if applicable\n *     }\n *     ]}\n * ```\n *\n * `source` and `image` can be provided in either relative or\n * absolute paths. For example:\n * ``\n *     \"source\" : \"https://www.example.com/music/ode_to_joy.mp3\",\n *     \"image\" : \"ode_to_joy.jpg\"\n * ``\n *\n * The `source` specifies the full URI to download the piece of music from, but\n * `image` will be fetched relative to the path of the JSON file itself. This means\n * that if the JSON was at \"https://www.example.com/json/music.json\" then the image would be found\n * at \"https://www.example.com/json/ode_to_joy.jpg\".\n */\n@Suppress(\"unused\")\nclass JsonMusic {\n    var id: String = \"\"\n    var title: String = \"\"\n    var album: String = \"\"\n    var artist: String = \"\"\n    var genre: String = \"\"\n    var source: String = \"\"\n    var image: String = \"\"\n    var trackNumber: Long = 0\n    var totalTrackCount: Long = 0\n    var duration: Long = C.TIME_UNSET\n    var site: String = \"\"\n}\n"
  },
  {
    "path": "common/src/main/java/com/example/android/uamp/media/library/MusicSource.kt",
    "content": "/*\n * Copyright 2017 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.uamp.media.library\n\nimport android.os.Build\nimport android.os.Bundle\nimport android.provider.MediaStore\nimport android.support.v4.media.MediaMetadataCompat\nimport android.util.Log\nimport androidx.annotation.IntDef\nimport com.example.android.uamp.media.MusicService\nimport com.example.android.uamp.media.extensions.album\nimport com.example.android.uamp.media.extensions.albumArtist\nimport com.example.android.uamp.media.extensions.artist\nimport com.example.android.uamp.media.extensions.containsCaseInsensitive\nimport com.example.android.uamp.media.extensions.genre\nimport com.example.android.uamp.media.extensions.title\n\n/**\n * Interface used by [MusicService] for looking up [MediaMetadataCompat] objects.\n *\n * Because Kotlin provides methods such as [Iterable.find] and [Iterable.filter],\n * this is a convenient interface to have on sources.\n */\ninterface MusicSource : Iterable<MediaMetadataCompat> {\n\n    /**\n     * Begins loading the data for this music source.\n     */\n    suspend fun load()\n\n    /**\n     * Method which will perform a given action after this [MusicSource] is ready to be used.\n     *\n     * @param performAction A lambda expression to be called with a boolean parameter when\n     * the source is ready. `true` indicates the source was successfully prepared, `false`\n     * indicates an error occurred.\n     */\n    fun whenReady(performAction: (Boolean) -> Unit): Boolean\n\n    fun search(query: String, extras: Bundle): List<MediaMetadataCompat>\n}\n\n@IntDef(\n    STATE_CREATED,\n    STATE_INITIALIZING,\n    STATE_INITIALIZED,\n    STATE_ERROR\n)\n@Retention(AnnotationRetention.SOURCE)\nannotation class State\n\n/**\n * State indicating the source was created, but no initialization has performed.\n */\nconst val STATE_CREATED = 1\n\n/**\n * State indicating initialization of the source is in progress.\n */\nconst val STATE_INITIALIZING = 2\n\n/**\n * State indicating the source has been initialized and is ready to be used.\n */\nconst val STATE_INITIALIZED = 3\n\n/**\n * State indicating an error has occurred.\n */\nconst val STATE_ERROR = 4\n\n/**\n * Base class for music sources in UAMP.\n */\nabstract class AbstractMusicSource : MusicSource {\n    @State\n    var state: Int = STATE_CREATED\n        set(value) {\n            if (value == STATE_INITIALIZED || value == STATE_ERROR) {\n                synchronized(onReadyListeners) {\n                    field = value\n                    onReadyListeners.forEach { listener ->\n                        listener(state == STATE_INITIALIZED)\n                    }\n                }\n            } else {\n                field = value\n            }\n        }\n\n    private val onReadyListeners = mutableListOf<(Boolean) -> Unit>()\n\n    /**\n     * Performs an action when this MusicSource is ready.\n     *\n     * This method is *not* threadsafe. Ensure actions and state changes are only performed\n     * on a single thread.\n     */\n    override fun whenReady(performAction: (Boolean) -> Unit): Boolean =\n        when (state) {\n            STATE_CREATED, STATE_INITIALIZING -> {\n                onReadyListeners += performAction\n                false\n            }\n            else -> {\n                performAction(state != STATE_ERROR)\n                true\n            }\n        }\n\n    /**\n     * Handles searching a [MusicSource] from a focused voice search, often coming\n     * from the Google Assistant.\n     */\n    override fun search(query: String, extras: Bundle): List<MediaMetadataCompat> {\n        // First attempt to search with the \"focus\" that's provided in the extras.\n        val focusSearchResult = when (extras[MediaStore.EXTRA_MEDIA_FOCUS]) {\n            MediaStore.Audio.Genres.ENTRY_CONTENT_TYPE -> {\n                // For a Genre focused search, only genre is set.\n                val genre = extras[EXTRA_MEDIA_GENRE]\n                Log.d(TAG, \"Focused genre search: '$genre'\")\n                filter { song ->\n                    song.genre == genre\n                }\n            }\n            MediaStore.Audio.Artists.ENTRY_CONTENT_TYPE -> {\n                // For an Artist focused search, only the artist is set.\n                val artist = extras[MediaStore.EXTRA_MEDIA_ARTIST]\n                Log.d(TAG, \"Focused artist search: '$artist'\")\n                filter { song ->\n                    (song.artist == artist || song.albumArtist == artist)\n                }\n            }\n            MediaStore.Audio.Albums.ENTRY_CONTENT_TYPE -> {\n                // For an Album focused search, album and artist are set.\n                val artist = extras[MediaStore.EXTRA_MEDIA_ARTIST]\n                val album = extras[MediaStore.EXTRA_MEDIA_ALBUM]\n                Log.d(TAG, \"Focused album search: album='$album' artist='$artist\")\n                filter { song ->\n                    (song.artist == artist || song.albumArtist == artist) && song.album == album\n                }\n            }\n            MediaStore.Audio.Media.ENTRY_CONTENT_TYPE -> {\n                // For a Song (aka Media) focused search, title, album, and artist are set.\n                val title = extras[MediaStore.EXTRA_MEDIA_TITLE]\n                val album = extras[MediaStore.EXTRA_MEDIA_ALBUM]\n                val artist = extras[MediaStore.EXTRA_MEDIA_ARTIST]\n                Log.d(TAG, \"Focused media search: title='$title' album='$album' artist='$artist\")\n                filter { song ->\n                    (song.artist == artist || song.albumArtist == artist) && song.album == album\n                            && song.title == title\n                }\n            }\n            else -> {\n                // There isn't a focus, so no results yet.\n                emptyList()\n            }\n        }\n\n        // If there weren't any results from the focused search (or if there wasn't a focus\n        // to begin with), try to find any matches given the 'query' provided, searching against\n        // a few of the fields.\n        // In this sample, we're just checking a few fields with the provided query, but in a\n        // more complex app, more logic could be used to find fuzzy matches, etc...\n        if (focusSearchResult.isEmpty()) {\n            return if (query.isNotBlank()) {\n                Log.d(TAG, \"Unfocused search for '$query'\")\n                filter { song ->\n                    song.title.containsCaseInsensitive(query)\n                            || song.genre.containsCaseInsensitive(query)\n                }\n            } else {\n                // If the user asked to \"play music\", or something similar, the query will also\n                // be blank. Given the small catalog of songs in the sample, just return them\n                // all, shuffled, as something to play.\n                Log.d(TAG, \"Unfocused search without keyword\")\n                return shuffled()\n            }\n        } else {\n            return focusSearchResult\n        }\n    }\n\n    /**\n     * [MediaStore.EXTRA_MEDIA_GENRE] is missing on API 19. Hide this fact by using our\n     * own version of it.\n     */\n    private val EXTRA_MEDIA_GENRE\n        get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n            MediaStore.EXTRA_MEDIA_GENRE\n        } else {\n            \"android.intent.extra.genre\"\n        }\n}\n\nprivate const val TAG = \"MusicSource\"\n"
  },
  {
    "path": "common/src/main/res/drawable/ic_album.xml",
    "content": "<!--\n  ~ Copyright 2019 Google LLC\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     https://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n-->\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"64dp\"\n    android:height=\"64dp\"\n    android:viewportWidth=\"48.0\"\n    android:viewportHeight=\"48.0\">\n    <path\n        android:fillColor=\"#555\"\n        android:pathData=\"M24,4C12.95,4 4,12.95 4,24s8.95,20 20,20 20,-8.95 20,-20S35.05,4 24,4zM24,33c-4.97,0 -9,-4.03 -9,-9s4.03,-9 9,-9 9,4.03 9,9 -4.03,9 -9,9zM24,22c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z\" />\n</vector>\n"
  },
  {
    "path": "common/src/main/res/drawable/ic_recommended.xml",
    "content": "<!--\n  ~ Copyright 2019 Google LLC\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     https://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n-->\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"64dp\"\n    android:height=\"64dp\"\n    android:viewportWidth=\"48.0\"\n    android:viewportHeight=\"48.0\">\n    <path\n        android:fillColor=\"#555\"\n        android:pathData=\"M24,2C14.06,2 6,10.06 6,20v14c0,3.31 2.69,6 6,6h6V24h-8v-4c0,-7.73 6.27,-14 14,-14s14,6.27 14,14v4h-8v16h6c3.31,0 6,-2.69 6,-6V20c0,-9.94 -8.06,-18 -18,-18z\" />\n</vector>\n"
  },
  {
    "path": "common/src/main/res/menu/main_activity_menu.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2017 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n     See the License for the specific language governing permissions and\n     limitations under the License.\n-->\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <item\n        android:id=\"@+id/media_route_menu_item\"\n        android:title=\"@string/media_route_menu_title\"\n        app:actionProviderClass=\"androidx.mediarouter.app.MediaRouteActionProvider\"\n        app:showAsAction=\"always\" />\n\n</menu>"
  },
  {
    "path": "common/src/main/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2018 Google Inc. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n-->\n<resources>\n    <!-- Notificaiton Channel -->\n    <string name=\"notification_channel\">Now Playing</string>\n    <string name=\"notification_channel_description\">Shows what music is currently playing in UAMP</string>\n\n    <!-- Notification actions -->\n    <string name=\"notification_skip_to_previous\">Skip to previous track</string>\n    <string name=\"notification_skip_to_next\">Skip to next track</string>\n    <string name=\"notification_play\">Play</string>\n    <string name=\"notification_pause\">Pause</string>\n\n    <!-- Error messages -->\n    <string name=\"error_media_not_found\">Could not locate requested media.</string>\n    <string name=\"generic_error\">The application has encountered an unexpected error.</string>\n    <!-- Allowed caller template -->\n    <string name=\"allowed_caller_log\">\n        Caller has a valid certificate, but its package doesn\\'t match any expected package for the\n        given certificate. To allow this caller, add the following to the allowed callers list:\\n\n        <![CDATA[\n        <signature name=\\\"%1$s\\\" package=\\\"%2$s\\\">\\n\\t<key>%3$s</key>\\n</signature>\\n\n        ]]>\n    </string>\n\n    <!-- Cast menu item -->\n    <string name=\"media_route_menu_title\">Cast</string>\n\n    <!-- Browse Tree -->\n    <string name=\"recommended_title\">Recommended</string>\n    <string name=\"albums_title\">Albums</string>\n</resources>"
  },
  {
    "path": "common/src/main/res/xml/allowed_media_browser_callers.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\nCopyright (C) 2018 The Android Open Source Project\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n   http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n-->\n<!--\n\nFormat (v2):\n\n  The format for allowed callers is as follows:\n\n  <signature name=\"App Name\" package=\"app.package.name\">\n     <key release=\"true\">...sha-256 hash...</key>\n     ...\n  </signature>\n\n  Where:\n  - 'name' is a friendly name for the app; this is only used for documentation purposes and is\n    otherwise ignored.\n  - 'package' is the package name of the app to allow.\n\n  There can be multiple keys for each app included. The value of 'release' must be 'true' or\n  'false', but it also only for documentation. The value inside the 'key' tag is the SHA-256\n  hash of the public key of the signing certificate for the app.\n\nFormat (v1 : legacy support only/deprecated):\n\n  It is still possible to use the 'signing_certificate's of the previous version of UAMP here. In\n  which case the format is:\n\n  <signing_certificate name=\"App Name\" release=\"true\" package=\"app.package.name\">\n     ...base-64 encoded string...\n  </signing_certificate>\n\n  The values are the same as above, but only 1 certificate is included in each tag, and the\n  value is the base-64 encoded string of the public key of the app's signing certificate.\n\nAdding New Keys:\n\n  If you want to add a new signature to allowed_callers and you don't know its signature,\n  PackageValidator will print the caller's package name and signature to logcat (INFO level).\n\n  Spaces and newlines are ignored.\n-->\n<allowed_callers>\n    <signature\n        name=\"Android Auto\"\n        package=\"com.google.android.projection.gearhead\">\n        <key release=\"false\">\n            19:75:b2:f1:71:77:bc:89:a5:df:f3:1f:9e:64:a6:ca:e2:81:a5:3d:c1:d1:d5:9b:1d:14:7f:e1:c8:2a:fa:00\n        </key>\n        <key release=\"false\">\n            70:81:1a:3e:ac:fd:2e:83:e1:8d:a9:bf:ed:e5:2d:f1:6c:e9:1f:2e:69:a4:4d:21:f1:8a:b6:69:91:13:07:71\n        </key>\n        <!-- Android Auto prod release key -->\n        <key release=\"true\">\n            fd:b0:0c:43:db:de:8b:51:cb:31:2a:a8:1d:3b:5f:a1:77:13:ad:b9:4b:28:f5:98:d7:7f:8e:b8:9d:ac:ee:df\n        </key>\n        <!-- Android Auto rotated prod release key -->\n        <key release=\"true\">\n            1c:a8:dc:c0:be:d3:cb:d8:72:d2:cb:79:12:00:c0:29:2c:a9:97:57:68:a8:2d:67:6b:8b:42:4f:b6:5b:52:95\n        </key>\n    </signature>\n\n    <signature\n        name=\"WearOS\"\n        package=\"com.google.android.wearable.app\">\n        <key release=\"false\">\n            69:d0:72:16:9a:2c:6b:2f:5a:cc:59:0c:e4:33:a1:1a:c3:df:55:1a:df:ee:5d:5f:63:c0:83:b7:22:76:2e:19\n        </key>\n        <key release=\"true\">\n            85:cd:59:73:54:1b:e6:f4:77:d8:47:a0:bc:c6:aa:25:27:68:4b:81:9c:d5:96:85:29:66:4c:b0:71:57:b6:fe\n        </key>\n    </signature>\n\n    <signature\n        name=\"Android Auto Simulator\"\n        package=\"com.google.android.autosimulator\">\n        <key release=\"true\">\n            19:75:b2:f1:71:77:bc:89:a5:df:f3:1f:9e:64:a6:ca:e2:81:a5:3d:c1:d1:d5:9b:1d:14:7f:e1:c8:2a:fa:00\n        </key>\n    </signature>\n\n    <signature\n        name=\"Google\"\n        package=\"com.google.android.googlequicksearchbox\">\n        <key release=\"false\">\n            19:75:b2:f1:71:77:bc:89:a5:df:f3:1f:9e:64:a6:ca:e2:81:a5:3d:c1:d1:d5:9b:1d:14:7f:e1:c8:2a:fa:00\n        </key>\n        <key release=\"true\">\n            f0:fd:6c:5b:41:0f:25:cb:25:c3:b5:33:46:c8:97:2f:ae:30:f8:ee:74:11:df:91:04:80:ad:6b:2d:60:db:83\n        </key>\n    </signature>\n\n    <signature\n        name=\"Google Assistant on Android Automotive OS\"\n        package=\"com.google.android.carassistant\">\n        <key release=\"false\">\n            17:E2:81:11:06:2F:97:A8:60:79:7A:83:70:5B:F8:2C:7C:C0:29:35:56:6D:46:22:BC:4E:CF:EE:1B:EB:F8:15\n        </key>\n        <key release=\"true\">\n            74:B6:FB:F7:10:E8:D9:0D:44:D3:40:12:58:89:B4:23:06:A6:2C:43:79:D0:E5:A6:62:20:E3:A6:8A:BF:90:E2\n        </key>\n    </signature>\n</allowed_callers>\n"
  },
  {
    "path": "common/src/test/java/com/example/android/uamp/media/library/MusicSourceTest.kt",
    "content": "/*\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.uamp.media.library\n\nimport android.os.Bundle\nimport android.provider.MediaStore\nimport android.support.v4.media.MediaMetadataCompat\nimport com.example.android.uamp.media.extensions.album\nimport com.example.android.uamp.media.extensions.artist\nimport com.example.android.uamp.media.extensions.genre\nimport com.example.android.uamp.media.extensions.id\nimport com.example.android.uamp.media.extensions.title\nimport org.junit.Assert\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.robolectric.RobolectricTestRunner\n\n/**\n * A small set of Android integration tests for (primarily) [AbstractMusicSource].\n *\n * The tests all use an extension of [AbstractMusicSource] which is defined at the\n * bottom of this file: [TestMusicSource].\n */\n@RunWith(RobolectricTestRunner::class)\nclass MusicSourceTest {\n\n    private val musicList = listOf<MediaMetadataCompat>(\n        MediaMetadataCompat.Builder().apply {\n            id = \"ich_hasse_dich\"\n            title = \"Ich hasse dich\"\n            album = \"Speechless\"\n            artist = \"Jemand\"\n            genre = \"Folk\"\n        }.build(),\n\n        MediaMetadataCompat.Builder().apply {\n            id = \"about_a_guy\"\n            title = \"About a Guy\"\n            album = \"Tales from the Render Farm\"\n            artist = \"7 Developers and a Pastry Chef\"\n            genre = \"Rock\"\n        }.build()\n    )\n\n    /** Variables for testing [MusicSource.whenReady] */\n    var waiting = true\n\n    @Test\n    fun whenReady_waits() {\n        val testSource = TestMusicSource(musicList)\n        waiting = true\n\n        testSource.whenReady { success ->\n            Assert.assertEquals(success, true)\n            waiting = false\n        }\n        Assert.assertEquals(waiting, true)\n        testSource.prepare()\n        Assert.assertEquals(waiting, false)\n    }\n\n    @Test\n    fun whenReady_errorContinues() {\n        val testSource = TestMusicSource(musicList)\n        waiting = true\n\n        testSource.whenReady { success ->\n            Assert.assertEquals(success, false)\n            waiting = false\n        }\n        Assert.assertEquals(waiting, true)\n        testSource.error()\n        Assert.assertEquals(waiting, false)\n    }\n\n    @Test\n    fun searchByGenre_matches() {\n        val testSource = TestMusicSource(musicList)\n        testSource.prepare()\n\n        val searchQuery = \"Rock\"\n        val searchExtras = Bundle().apply {\n            putString(MediaStore.EXTRA_MEDIA_FOCUS, MediaStore.Audio.Genres.ENTRY_CONTENT_TYPE)\n            putString(MediaStore.EXTRA_MEDIA_GENRE, searchQuery)\n        }\n        val result = testSource.search(searchQuery, searchExtras)\n        Assert.assertEquals(result.size, 1)\n        Assert.assertEquals(result[0].id, \"about_a_guy\")\n    }\n\n    @Test\n    fun searchByMedia_matches() {\n        val testSource = TestMusicSource(musicList)\n        testSource.prepare()\n\n        val searchQuery = \"About a Guy\"\n        val searchExtras = Bundle().apply {\n            putString(MediaStore.EXTRA_MEDIA_FOCUS, MediaStore.Audio.Media.ENTRY_CONTENT_TYPE)\n            putString(MediaStore.EXTRA_MEDIA_TITLE, searchQuery)\n            putString(MediaStore.EXTRA_MEDIA_ALBUM, \"Tales from the Render Farm\")\n            putString(MediaStore.EXTRA_MEDIA_ARTIST, \"7 Developers and a Pastry Chef\")\n        }\n        val result = testSource.search(searchQuery, searchExtras)\n        Assert.assertEquals(result.size, 1)\n        Assert.assertEquals(result[0].id, \"about_a_guy\")\n    }\n\n    @Test\n    fun searchByMedia_nomatches() {\n        val testSource = TestMusicSource(musicList)\n        testSource.prepare()\n\n        val searchQuery = \"Kotlin in 31 Days\"\n        val searchExtras = Bundle().apply {\n            putString(MediaStore.EXTRA_MEDIA_FOCUS, MediaStore.Audio.Media.ENTRY_CONTENT_TYPE)\n            putString(MediaStore.EXTRA_MEDIA_TITLE, searchQuery)\n            putString(MediaStore.EXTRA_MEDIA_ALBUM, \"Delegated by Lazy\")\n            putString(MediaStore.EXTRA_MEDIA_ARTIST, \"Brainiest Jet\")\n        }\n        val result = testSource.search(searchQuery, searchExtras)\n        Assert.assertEquals(result.size, 0)\n    }\n\n    @Test\n    fun searchByKeyword_fallback() {\n        val testSource = TestMusicSource(musicList)\n        testSource.prepare()\n\n        val searchQuery = \"hasse\"\n        val searchExtras = Bundle.EMPTY\n        val result = testSource.search(searchQuery, searchExtras)\n        Assert.assertEquals(result.size, 1)\n        Assert.assertEquals(result[0].id, \"ich_hasse_dich\")\n    }\n}\n\nclass TestMusicSource(\n    private val music: List<MediaMetadataCompat>\n) : AbstractMusicSource(), Iterable<MediaMetadataCompat> by music {\n    override suspend fun load() = Unit\n\n    fun prepare() {\n        state = STATE_INITIALIZED\n    }\n\n    fun error() {\n        state = STATE_ERROR\n    }\n}"
  },
  {
    "path": "docs/FAQs.md",
    "content": "# Frequently Asked Questions\n\n## How can I change the music which UAMP plays?\nUAMP reads its [music catalog](https://storage.googleapis.com/androiddevelopers/samples_assets/uamp/catalog.json) from a server. \nThis contains a list of songs and their metadata in JSON format. To change the music you can create your own \nmusic catalog file, host it on a server and update the catalog URL in \n[`MusicService`](https://github.com/android/uamp/blob/6c3ff3779d02f55661c5b9d6032cfac507a8415e/common/src/main/java/com/example/android/uamp/media/MusicService.kt#L127). \n\nAlternatively, you could package your own music catalog and songs inside the app so they can be played without needing an internet connection."
  },
  {
    "path": "docs/FullGuide.md",
    "content": "# Full Guide to UAMP\n\nThe Universal Android Music Player (UAMP) is an example music player app for Android written in [Kotlin](https://kotlinlang.org/). It supports many features including background playback, audio focus handling, multiple platforms (like Wear, TV and Auto) and assistant integration. \n\nIt loads a [music catalog](https://storage.googleapis.com/androiddevelopers/samples_assets/uamp/catalog.json) from a remote server and allows the user to browse the albums and songs. Tapping on a song will play it through connected speakers or headphones. Under the hood it uses [ExoPlayer](https://exoplayer.dev/).\n\nIf your app's primary goal is to play audio, UAMP is a good place to start. \n\n![Screenshot showing UAMP's UI for browsing albums and songs](images/1-browse-albums-screenshot.png \"Browse albums screenshot\")\n![Screenshot showing UAMP's UI for playing a song](images/2-play-song-screenshot.png \"Play song screenshot\")\n\nScreenshots: Browse albums and play a song using UAMP\n\n\n# Architecture overview\n\nUAMP follows the client/server architecture as [described in the \"how to build an audio app\" official documentation](https://developer.android.com/guide/topics/media-apps/audio-app/building-an-audio-app).\n\nHere's an architectural overview:\n\n![Architecture overview diagram](images/3-architecture-overview.png \"Architecture overview\")\n\nDiagram: Overall architecture of UAMP\n\n\n# Server architecture  \n\n## MusicService\n\nThe most important class on the server side is [MusicService](https://github.com/android/uamp/blob/master/common/src/main/java/com/example/android/uamp/media/MusicService.kt) which is a subclass of [MediaBrowserService](https://developer.android.com/reference/android/service/media/MediaBrowserService). MediaBrowserService allows [MediaBrowser](https://developer.android.com/guide/topics/media-apps/audio-app/building-a-mediabrowser-client.html) clients from UAMP and other applications to discover the service, connect to the media session and control playback.   \n\nUAMP actually uses [MediaBrowserServiceCompat](https://developer.android.com/reference/androidx/media/MediaBrowserServiceCompat.html) - the backwards compatible version of MediaBrowserService provided by the [androidx.media library](https://developer.android.com/reference/androidx/media/package-summary) ([more information](https://medium.com/androiddevelopers/mediabrowserservicecompat-and-the-modern-media-playback-app-7959a5196d90)). \n\nMusicService is responsible for: \n\n*   the audio player (provided by [ExoPlayer](https://exoplayer.dev/))\n*   a [media session](https://developer.android.com/guide/topics/media-apps/working-with-a-media-session) and objects to communicate with it \n*   maintaining a [notification](https://developer.android.com/guide/topics/ui/notifiers/notifications) which displays information and controls for the current media\n*   loading [the media catalog](https://storage.googleapis.com/androiddevelopers/samples_assets/uamp/catalog.json) (a JSON file) from a remote URI and providing it to MediaBrowser clients\n\nBy keeping the objects responsible for audio playback inside a service it allows audio to be played in the background, decoupling playback from the app's UI.\n\nHere's a more detailed view of MusicService.\n\n![Detailed view of MusicService](images/4-MusicService.png \"Detailed view of MusicService\")\n\nDiagram: Detailed view of MusicService\n\n\n## The media session and controller\n\nA [MediaSession](https://developer.android.com/guide/topics/media-apps/working-with-a-media-session.html#top_of_page) represents an ongoing media playback session. It provides various mechanisms for controlling playback, receiving status updates and retrieving metadata about the current media. \n\nA [MediaController](https://developer.android.com/reference/android/support/v4/media/session/MediaControllerCompat.html) is used to communicate with the media session. It receives media button events and forwards them to the media session. State and metadata updates from the media session are performed through a [MediaController.Callback](https://developer.android.com/reference/android/support/v4/media/session/MediaControllerCompat.Callback.html).\n\n![Diagram showing how MediaController and MediaSession communicate](images/5-MediaController.png \"Diagram showing how MediaController and MediaSession communicate\")\n\nDiagram (from the [official Android documentation](https://developer.android.com/guide/topics/media-apps/media-apps-overview#mediasession-and-mediacontroller)): Shows how MediaController and MediaSession communicate. \n\nUAMP uses two MediaControllers. One on the client side to communicate with the UI (explained later), and one inside MusicService to listen to media session state and metadata changes. This is then used to update the notification. \n\n\n## Notifying the user\n\n![Screenshot of notification showing information about the current song being played and playback controls](images/6-notification.png \"Notification showing information about the current song being played and playback controls\")\n\nScreenshot: Notification showing information about the current song being played and playback controls\n\nA [notification](https://developer.android.com/guide/topics/ui/notifiers/notifications.html) allows users to see the song being played and to control playback. It's also a mandatory requirement for a [foreground service](https://developer.android.com/guide/components/services#Foreground) and stops MusicService from being killed. \n\nUAMP delegates the display and update of its notification to [`PlayerNotificationManager`](https://exoplayer.dev/doc/reference/index.html?com/google/android/exoplayer2/ui/PlayerNotificationManager.html) provided by ExoPlayer. \n\n## Playing audio\n\nAudio playback is provided by [ExoPlayer](https://exoplayer.dev/). It is responsible for loading media sources via [UampPlaybackPreparer](https://github.com/android/uamp/blob/master/common/src/main/java/com/example/android/uamp/media/UampPlaybackPreparer.kt), playing audio through the available audio hardware (headphones or speakers) and responding to media commands (play, pause, skip etc). \n\n\n## MediaSessionConnector\n\n[MediaSessionConnector](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.html) is a part of the [media session extension for ExoPlayer](https://github.com/google/ExoPlayer/tree/release-v2/extensions/mediasession). It provides the glue between the ExoPlayer and the MediaSession ([full details here](https://medium.com/google-exoplayer/the-mediasession-extension-for-exoplayer-82b9619deb2d)). \n\nIts key responsibilities are:  \n\n\n\n*   Preparing media sources from URIs using UampPlaybackPreparer\n*   Sending playback state updates from ExoPlayer to the media session\n*   Forwarding actions from the media session, such as play, pause and skip, to ExoPlayer\n\n\n# The User Interface\n\n![Screenshot showing UAMP's UI for browsing albums and songs](images/1-browse-albums-screenshot.png \"Browse albums screenshot\")\n![Screenshot showing UAMP's UI for playing a song](images/2-play-song-screenshot.png \"Play song screenshot\")\n\nScreenshot: UAMP user interface screens\n\nThe UAMP user interface allows users to:\n\n*   browse and play songs\n*   play and pause songs\n*   see changes in the underlying player, such as playback duration\n*   view metadata about the currently playing song including album art, title and artist\n\nUAMP achieves this by using a Model-View-ViewModel architecture. This allows a separation of responsibilities between each layer. \n\n![Class diagram showing UAMP's Model-View-ViewModel architecture](images/9-mvvm.png \"Class diagram showing UAMP's Model-View-ViewModel architecture\")\n\nDiagram: Class diagram showing UAMP's Model-View-ViewModel architecture\n\n\n## Views\n\nUAMP has three main view classes - one [Activity](https://developer.android.com/reference/android/app/Activity) and two [Fragments](https://developer.android.com/guide/components/fragments?hl=en): \n\n*   **MainActivity** is responsible for swapping between the two fragments. \n*   **MediaItemFragment** is for browsing the music catalog. It displays a list of media items which can be either albums or songs. Tapping an album will display a new MediaItemFragment containing the songs within that album. Tapping a song will start playing that song and display the NowPlayingFragment.  \n*   **NowPlayingFragment** shows the song that is currently playing. \n\n![Screenshot showing MediaItemFragment](images/1-browse-albums-screenshot.png \"Screenshot showing MediaItemFragment\")\n![Screenshot showing NowPlayingFragment](images/2-play-song-screenshot.png \"Screenshot showing NowPlayingFragment\")\n\nScreenshot: MediaItemFragment and NowPlayingFragment are displayed by the MainActivity\n\n## ViewModels\n\nThe Activity and Fragments are backed by their own [view models](https://developer.android.com/topic/libraries/architecture/viewmodel). The view model represents the underlying state of the corresponding view. When the model changes, such as when a new song is played, the view is updated to reflect the change. This is achieved using [LiveData](https://developer.android.com/topic/libraries/architecture/livedata) types.\n\nThe view models are able to communicate with the MusicService (described in the \"Service\" section above) using a MusicServiceConnection.  \n\n\n## MusicServiceConnection\n\nMusicServiceConnection is a singleton which connects to the MusicService. It is a wrapper for both the [MediaBrowser](https://developer.android.com/reference/android/support/v4/media/MediaBrowserCompat) and [MediaController](https://developer.android.com/reference/android/support/v4/media/session/MediaControllerCompat) classes. \n\nIt is responsible for: \n\n*   Connecting to the MusicService using a MediaBrowser\n*   Receiving a callback from the MusicService to indicate that the MediaBrowser has been successfully connected\n*   Allowing view models to retrieve the list of albums and songs by subscribing to changes in the MusicService's list of media items via [subscribe](https://developer.android.com/reference/android/support/v4/media/MediaBrowserCompat#subscribe(java.lang.String,%20android.os.Bundle,%20android.support.v4.media.MediaBrowserCompat.SubscriptionCallback))\n*   Creating a MediaController for the current media session which can be used by view models for: \n    *   controlling the session via transport controls\n    *   retrieving playback state and metadata changes\n\n\n## Class diagram\n\nThe following diagram shows the most important interactions between the UI classes.\n\n![Diagram showing important interactions between UI classes](images/12-ui-class-diagram.png \"Diagram showing important interactions between UI classes\")\n\nDiagram: Important interactions between UI classes\n\n\n# Summary\n\nThe easiest way to get started with UAMP is to clone it, run it and step through the source code using Android Studio. If you have questions or find any parts confusing please [file an issue](https://github.com/android/uamp/issues).  \n\n\n# Further resources\n\n[How to build an audio app](https://developer.android.com/guide/topics/media-apps/audio-app/building-an-audio-app)\n\n[Best practices in media playback - Google I/O 2016](https://www.youtube.com/watch?v=iIKxyDRjecU)\n\n[ExoPlayer](https://exoplayer.dev/)\n\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "#Thu Jun 18 22:31:29 CEST 2020\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-6.1.1-all.zip\n"
  },
  {
    "path": "gradle.properties",
    "content": "#\n# Copyright 2017 Google Inc. All rights reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n# Project-wide Gradle settings.\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will override*\n# any settings specified in this file.\n# For more details on how to configure your build environment visit\n# http://www.gradle.org/docs/current/userguide/build_environment.html\n# Specifies the JVM arguments used for the daemon process.\n# The setting is particularly useful for tweaking memory settings.\nandroid.enableJetifier=true\nandroid.useAndroidX=true\norg.gradle.jvmargs=-Xmx1536m\n# Required by Robolectric.\n#android.enableUnitTestBinaryResources=true\n# When configured, Gradle will run in incubating parallel mode.\n# This option should only be used with decoupled projects. More details, visit\n# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects\n# org.gradle.parallel=true\n"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env bash\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS=\"\"\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn ( ) {\n    echo \"$*\"\n}\n\ndie ( ) {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\nesac\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >/dev/null\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >/dev/null\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n    JAVACMD=`cygpath --unix \"$JAVACMD\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=$((i+1))\n    done\n    case $i in\n        (0) set -- ;;\n        (1) set -- \"$args0\" ;;\n        (2) set -- \"$args0\" \"$args1\" ;;\n        (3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        (4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        (5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        (6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        (7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        (8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        (9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules\nfunction splitJvmOpts() {\n    JVM_OPTS=(\"$@\")\n}\neval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\nJVM_OPTS[${#JVM_OPTS[*]}]=\"-Dorg.gradle.appname=$APP_BASE_NAME\"\n\nexec \"$JAVACMD\" \"${JVM_OPTS[@]}\" -classpath \"$CLASSPATH\" org.gradle.wrapper.GradleWrapperMain \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@if \"%DEBUG%\" == \"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif \"%ERRORLEVEL%\" == \"0\" goto init\r\n\r\necho.\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto init\r\n\r\necho.\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:init\r\n@rem Get command-line arguments, handling Windowz variants\r\n\r\nif not \"%OS%\" == \"Windows_NT\" goto win9xME_args\r\nif \"%@eval[2+2]\" == \"4\" goto 4NT_args\r\n\r\n:win9xME_args\r\n@rem Slurp the command line arguments.\r\nset CMD_LINE_ARGS=\r\nset _SKIP=2\r\n\r\n:win9xME_args_slurp\r\nif \"x%~1\" == \"x\" goto execute\r\n\r\nset CMD_LINE_ARGS=%*\r\ngoto execute\r\n\r\n:4NT_args\r\n@rem Get arguments from the 4NT Shell from JP Software\r\nset CMD_LINE_ARGS=%$\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\r\nexit /b 1\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "settings.gradle",
    "content": "/*\n * Copyright 2017 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n * Allow for customization of the modules to include through an extra file that is not included\n * in source control.\n *\n * *** THERE IS USUALLY NO REASON TO USE THIS ***\n *\n * The purpose of \"extra-settings.gradle\" is to allow the use of a local version of the ExoPlayer\n * library to be used to build the project. This is useful for testing experimental features\n * which have not yet been released, extending the library yourself, or for debugging issues\n * with either ExoPlayer or the MediaSession extension.\n *\n * To make use of this, create a file named \"extra-settings.gradle\" in the root of the\n * project (along side settings.gradle) with the following contents:\n *\n * gradle.ext.exoplayerRoot = '<path_to_exoplayer>'\n * gradle.ext.exoplayerModulePrefix = 'exoplayer-'\n * apply from: new File(gradle.ext.exoplayerRoot, 'core_settings.gradle')\n *\n * For more information, see: https://github.com/google/ExoPlayer/blob/release-v2/README.md\n */\nFile extraSettings = new File(rootDir, 'extra-settings.gradle')\nif (extraSettings.exists()) {\n    apply from: extraSettings\n}\n\ninclude ':app', ':common', ':automotive'\n"
  }
]