Showing preview only (454K chars total). Download the full file or copy to clipboard to get everything.
Repository: android/uamp
Branch: main
Commit: 9498d991ab84
Files: 129
Total size: 414.6 KB
Directory structure:
gitextract_ek5wbsm9/
├── .github/
│ ├── scripts/
│ │ └── gradlew_recursive.sh
│ └── workflows/
│ ├── android.yml
│ └── copy-branch.yml
├── .gitignore
├── .google/
│ └── packaging.yaml
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── TODO.md
├── app/
│ ├── build.gradle
│ ├── proguard-rules.pro
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ ├── java/
│ │ └── com/
│ │ └── example/
│ │ └── android/
│ │ └── uamp/
│ │ ├── MainActivity.kt
│ │ ├── MediaItemAdapter.kt
│ │ ├── MediaItemData.kt
│ │ ├── cast/
│ │ │ └── UampCastOptionsProvider.kt
│ │ ├── fragments/
│ │ │ ├── MediaItemFragment.kt
│ │ │ └── NowPlayingFragment.kt
│ │ ├── utils/
│ │ │ ├── Event.kt
│ │ │ └── InjectorUtils.kt
│ │ └── viewmodels/
│ │ ├── MainActivityViewModel.kt
│ │ ├── MediaItemFragmentViewModel.kt
│ │ └── NowPlayingFragmentViewModel.kt
│ └── res/
│ ├── drawable/
│ │ ├── ic_album_black_24dp.xml
│ │ ├── ic_launcher_background.xml
│ │ ├── ic_pause_black_24dp.xml
│ │ ├── ic_play_arrow_black_24dp.xml
│ │ ├── ic_signal_wifi_off_black_24dp.xml
│ │ ├── media_item_background.xml
│ │ ├── media_item_mask.xml
│ │ └── media_overlay_background.xml
│ ├── drawable-v24/
│ │ └── ic_launcher_foreground.xml
│ ├── layout/
│ │ ├── activity_main.xml
│ │ ├── cast_context_error.xml
│ │ ├── fragment_mediaitem.xml
│ │ ├── fragment_mediaitem_list.xml
│ │ └── fragment_nowplaying.xml
│ ├── mipmap-anydpi-v26/
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ ├── values/
│ │ ├── colors.xml
│ │ ├── dimens.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ └── xml/
│ └── automotive_app_desc.xml
├── automotive/
│ ├── build.gradle
│ ├── proguard-rules.pro
│ └── src/
│ ├── androidTest/
│ │ └── java/
│ │ └── com/
│ │ └── example/
│ │ └── android/
│ │ └── uamp/
│ │ └── automotive/
│ │ └── ExampleInstrumentedTest.java
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── example/
│ │ │ └── android/
│ │ │ └── uamp/
│ │ │ └── automotive/
│ │ │ ├── AutomotiveMusicService.kt
│ │ │ ├── PhoneSignInFragment.kt
│ │ │ ├── PinCodeSignInFragment.kt
│ │ │ ├── QrCodeSignInFragment.kt
│ │ │ ├── SettingsActivity.kt
│ │ │ ├── SettingsFragment.kt
│ │ │ ├── SignInActivity.kt
│ │ │ ├── SignInActivityViewModel.kt
│ │ │ ├── SignInLandingPageFragment.kt
│ │ │ └── UsernameAndPasswordSignInFragment.kt
│ │ └── res/
│ │ ├── color/
│ │ │ ├── car_text_dark.xml
│ │ │ └── car_text_light.xml
│ │ ├── drawable/
│ │ │ ├── default_button_background.xml
│ │ │ ├── google_sign_in_button_background.xml
│ │ │ ├── google_sign_in_button_logo.xml
│ │ │ ├── ic_launcher_background.xml
│ │ │ ├── pin_background.xml
│ │ │ ├── sign_in_button_background.xml
│ │ │ ├── sign_in_toolbar_back_icon.xml
│ │ │ └── sign_in_toolbar_back_ripple_background.xml
│ │ ├── drawable-v24/
│ │ │ └── ic_launcher_foreground.xml
│ │ ├── layout/
│ │ │ ├── activity_login.xml
│ │ │ ├── activity_settings.xml
│ │ │ ├── activity_sign_in.xml
│ │ │ ├── phone_sign_in.xml
│ │ │ ├── pin_item.xml
│ │ │ ├── pin_sign_in.xml
│ │ │ ├── preference.xml
│ │ │ ├── preference_category.xml
│ │ │ ├── qr_sign_in.xml
│ │ │ ├── sign_in_landing_page.xml
│ │ │ ├── sign_in_landing_page_with_username_and_password.xml
│ │ │ └── username_and_password_sign_in.xml
│ │ ├── layout-h900dp/
│ │ │ ├── phone_sign_in.xml
│ │ │ ├── pin_sign_in.xml
│ │ │ ├── qr_sign_in.xml
│ │ │ ├── sign_in_landing_page.xml
│ │ │ ├── sign_in_landing_page_with_username_and_password.xml
│ │ │ └── username_and_password_sign_in.xml
│ │ ├── mipmap-anydpi-v26/
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ ├── values/
│ │ │ ├── colors.xml
│ │ │ ├── dimens.xml
│ │ │ ├── strings.xml
│ │ │ └── styles.xml
│ │ ├── values-h1060dp/
│ │ │ └── dimens.xml
│ │ └── xml/
│ │ ├── automotive_app_desc.xml
│ │ └── preferences.xml
│ └── test/
│ └── java/
│ └── com/
│ └── example/
│ └── android/
│ └── uamp/
│ └── automotive/
│ └── ExampleUnitTest.java
├── build.gradle
├── common/
│ ├── build.gradle
│ ├── proguard-rules.pro
│ └── src/
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── example/
│ │ │ └── android/
│ │ │ └── uamp/
│ │ │ ├── common/
│ │ │ │ └── MusicServiceConnection.kt
│ │ │ └── media/
│ │ │ ├── CastMediaItemConverter.kt
│ │ │ ├── MusicService.kt
│ │ │ ├── PackageValidator.kt
│ │ │ ├── PersistentStorage.kt
│ │ │ ├── UampNotificationManager.kt
│ │ │ ├── extensions/
│ │ │ │ ├── FileExt.kt
│ │ │ │ ├── JavaLangExt.kt
│ │ │ │ ├── MediaMetadataCompatExt.kt
│ │ │ │ └── PlaybackStateCompatExt.kt
│ │ │ └── library/
│ │ │ ├── AlbumArtContentProvider.kt
│ │ │ ├── BrowseTree.kt
│ │ │ ├── JsonSource.kt
│ │ │ └── MusicSource.kt
│ │ └── res/
│ │ ├── drawable/
│ │ │ ├── ic_album.xml
│ │ │ └── ic_recommended.xml
│ │ ├── menu/
│ │ │ └── main_activity_menu.xml
│ │ ├── values/
│ │ │ └── strings.xml
│ │ └── xml/
│ │ └── allowed_media_browser_callers.xml
│ └── test/
│ └── java/
│ └── com/
│ └── example/
│ └── android/
│ └── uamp/
│ └── media/
│ └── library/
│ └── MusicSourceTest.kt
├── docs/
│ ├── FAQs.md
│ └── FullGuide.md
├── gradle/
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/scripts/gradlew_recursive.sh
================================================
#!/bin/bash
# Copyright (C) 2020 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
set -xe
# Default Gradle settings are not optimal for Android builds, override them
# here to make the most out of the GitHub Actions build servers
GRADLE_OPTS="$GRADLE_OPTS -Xms4g -Xmx4g"
GRADLE_OPTS="$GRADLE_OPTS -XX:+HeapDumpOnOutOfMemoryError"
GRADLE_OPTS="$GRADLE_OPTS -Dorg.gradle.daemon=false"
GRADLE_OPTS="$GRADLE_OPTS -Dorg.gradle.workers.max=2"
GRADLE_OPTS="$GRADLE_OPTS -Dkotlin.incremental=false"
GRADLE_OPTS="$GRADLE_OPTS -Dkotlin.compiler.execution.strategy=in-process"
GRADLE_OPTS="$GRADLE_OPTS -Dfile.encoding=UTF-8"
export GRADLE_OPTS
# Crawl all gradlew files which indicate an Android project
# You may edit this if your repo has a different project structure
for GRADLEW in `find . -name "gradlew"` ; do
SAMPLE=$(dirname "${GRADLEW}")
# Tell Gradle that this is a CI environment and disable parallel compilation
bash "$GRADLEW" -p "$SAMPLE" -Pci --no-parallel --stacktrace $@
done
================================================
FILE: .github/workflows/android.yml
================================================
# Copyright (C) 2020 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: Android CI
on:
workflow_dispatch:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
name: Build
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v1
- name: set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Build project
run: .github/scripts/gradlew_recursive.sh assembleDebug
- name: Zip artifacts
run: zip -r assemble.zip . -i '**/build/*.apk' '**/build/*.aab' '**/build/*.aar' '**/build/*.so'
- name: Upload artifacts
uses: actions/upload-artifact@v1
with:
name: assemble
path: assemble.zip
================================================
FILE: .github/workflows/copy-branch.yml
================================================
# Duplicates default main branch to the old master branch
name: Duplicates main to old master branch
# Controls when the action will run. Triggers the workflow on push or pull request
# events but only for the main branch
on:
workflow_dispatch:
push:
branches: [ main ]
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "copy-branch"
copy-branch:
# The type of runner that the job will run on
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it,
# but specifies master branch (old default).
- uses: actions/checkout@v2
with:
fetch-depth: 0
ref: master
- run: |
git config user.name github-actions
git config user.email github-actions@github.com
git merge origin/main
git push
================================================
FILE: .gitignore
================================================
*.iml
.gradle
/local.properties
/.idea
.DS_Store
/captures
.externalNativeBuild
# Generated files
build/
# Extra (custom) settings
extra-settings.gradle
================================================
FILE: .google/packaging.yaml
================================================
# GOOGLE SAMPLE PACKAGING DATA
#
# This file is used by Google as part of our samples packaging process.
# End users may safely ignore this file. It has no relevance to other systems.
---
status: PUBLISHED
technologies: [Android, Android Auto, Android Automotive OS, Android Wear]
categories: [Getting Started, Media, UI]
languages: [Kotlin]
solutions: [Mobile]
github: googlesamples/android-UniversalMusicPlayer
level: INTERMEDIATE
icon: screenshots/icon-web.png
apiRefs:
- android:android.support.v4.media.session.MediaSessionCompat
- android:android.support.v4.media.session.MediaControllerCompat
- androidx.media.MediaBrowserServiceCompat
- android:android.support.v4.media.MediaBrowserCompat
- androidx.media.app.NotificationCompat.MediaStyle
- android:com.google.android.exoplayer2.SimpleExoPlayer
- android:com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
license: apache2-android
================================================
FILE: CONTRIBUTING.md
================================================
# How to become a contributor and submit your own code
## Contributor License Agreements
We'd love to accept your sample apps and patches! Before we can take them, we
have to jump a couple of legal hurdles.
Please fill out either the individual or corporate Contributor License Agreement (CLA).
* If you are an individual writing original source code and you're sure you
own the intellectual property, then you'll need to sign an [individual CLA]
(https://developers.google.com/open-source/cla/individual).
* If you work for a company that wants to allow you to contribute your work,
then you'll need to sign a [corporate CLA]
(https://developers.google.com/open-source/cla/corporate).
Follow either of the two links above to access the appropriate CLA and
instructions for how to sign and return it. Once we receive it, we'll be able to
accept your pull requests.
## Contributing A Patch
1. Submit an issue describing your proposed change to the repo in question.
1. The repo owner will respond to your issue promptly.
1. If your proposed change is accepted, and you haven't already done so, sign a
Contributor License Agreement (see details above).
1. Fork the desired repo, develop and test your code changes.
1. Ensure that your code adheres to the existing style in the sample to which
you are contributing. Refer to the
[Android Code Style Guide]
(https://source.android.com/source/code-style.html) for the
recommended coding standards for this organization.
1. Ensure that your code has an appropriate set of unit tests which all pass.
1. Submit a pull request.
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2014 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
> **Warning**
> This sample has been deprecated and is no longer being maintained.
>
> To find other samples that may be of interest, see [https://developer.android.com/samples](https://developer.android.com/samples).
Universal Android Music Player Sample
=====================================
The goal of this sample is to show how to implement an audio media app that works
across multiple form factors and provides a consistent user experience
on Android phones, tablets, Android Auto, Android Wear, Android TV, Google Cast devices,
and with the Google Assistant.
To get started with UAMP please read the [full guide](docs/FullGuide.md).


Pre-requisites
--------------
- Android Studio 3.x
Getting Started
---------------
This sample uses the Gradle build system. To build this project, use the
"gradlew build" command or use "Import Project" in Android Studio.
Support
-------
- Check out the [FAQs page](docs/FAQs.md)
- Stack Overflow: http://stackoverflow.com/questions/tagged/android
If you've found an error in this sample, please
[file an issue](https://github.com/android/UAMP/issues)
Patches are encouraged and may be submitted by forking this project and
submitting a pull request through GitHub. Please see [CONTRIBUTING.md](CONTRIBUTING.md) for more
details.
Audio
-----
Music provided by the [Free Music Archive](http://freemusicarchive.org/).
- [Wake Up](http://freemusicarchive.org/music/The_Kyoto_Connection/Wake_Up_1957/) by
[The Kyoto Connection](http://freemusicarchive.org/music/The_Kyoto_Connection/).
Recordings provided by the [Ambisonic Sound Library](https://library.soundfield.com/).
- [Pre Game Marching Band](https://library.soundfield.com/track/163) by Watson Wu
- [Chickens on a Farm](https://library.soundfield.com/track/129) by Watson Wu
- [Rural Market Busker](https://library.soundfield.com/track/55) by Stephan Schutze
- [Steamtrain Interior](https://library.soundfield.com/track/65) by Stephan Schutze
- [Rural Road Car Pass](https://library.soundfield.com/track/57) by Stephan Schutze
- [10 Feet from Shore](https://library.soundfield.com/track/114) by Watson Wu
License
-------
Copyright 2025 Google Inc.
Licensed to the Apache Software Foundation (ASF) under one or more contributor
license agreements. See the NOTICE file distributed with this work for
additional information regarding copyright ownership. The ASF licenses this
file to you under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance with the License. You may obtain a copy of
the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations under
the License.
================================================
FILE: TODO.md
================================================
TODOs
=====
This file captures the high level goals of the project. This provides guidance for anyone who wants
to contribute. If you see something in the list that you'd like to work on,
the best approach would be to [create an
issue](https://github.com/googlesamples/android-UniversalMusicPlayer/issues) first,
and then provide a pull request once completed to have your work merged into the project.
Service Side Tasks
------------------
- Implement rating (ideally "favorite" vs "thumbs up/down").
- Improve integration with the Google Assistant.
UI Tasks
--------
- Implement a "now playing" UI with current position and skip forward/back 30s ([BottomSheet](https://material.io/guidelines/components/bottom-sheets.html#bottom-sheets-persistent-bottom-sheets)).
================================================
FILE: app/build.gradle
================================================
/*
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion rootProject.compileSdkVersion
defaultConfig {
applicationId "com.example.android.uamp.next"
versionCode 1
versionName "1.0"
minSdkVersion rootProject.minSdkVersion
targetSdkVersion rootProject.targetSdkVersion
multiDexEnabled true
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
vectorDrawables {
useSupportLibrary true
}
}
buildFeatures {
viewBinding true
dataBinding true
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation "com.android.support:multidex:$multidex_version"
implementation project(':common')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "androidx.appcompat:appcompat:$androidx_app_compat_version"
implementation "androidx.fragment:fragment-ktx:$fragment_version"
implementation "androidx.recyclerview:recyclerview:$recycler_view_version"
implementation "androidx.constraintlayout:constraintlayout:$constraint_layout_version"
implementation "androidx.lifecycle:lifecycle-extensions:$arch_lifecycle_version"
// Glide dependencies
implementation "com.github.bumptech.glide:glide:$glide_version"
kapt "com.github.bumptech.glide:compiler:$glide_version"
}
================================================
FILE: app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: app/src/main/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2017 Google Inc. All rights reserved.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:dist="http://schemas.android.com/apk/distribution"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.android.uamp">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:targetSandboxVersion="2"
android:theme="@style/AppTheme">
<!-- Enable instant app support -->
<dist:module dist:instant="true" />
<!-- Declare that UAMP supports Android Auto. -->
<meta-data
android:name="com.google.android.gms.car.application"
android:resource="@xml/automotive_app_desc" />
<!-- Declare that UAMP supports Cast. -->
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="com.example.android.uamp.cast.UampCastOptionsProvider"/>
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- App links for http -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="example.android.com"
android:pathPattern="/uamp"
android:scheme="http" />
</intent-filter>
<!-- App links for https -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="example.android.com"
android:pathPattern="/uamp"
android:scheme="https" />
</intent-filter>
<intent-filter>
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
</intent-filter>
</activity>
<!--
Declare the common MediaBrowserService for use in the mobile app, including
with the Android Auto app.
-->
<service
android:name=".media.MusicService"
android:enabled="true"
android:exported="true"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
</application>
</manifest>
================================================
FILE: app/src/main/java/com/example/android/uamp/MainActivity.kt
================================================
/*
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.uamp
import android.media.AudioManager
import android.os.Bundle
import android.util.Log
import android.view.Menu
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import com.example.android.uamp.fragments.MediaItemFragment
import com.example.android.uamp.media.MusicService
import com.example.android.uamp.utils.Event
import com.example.android.uamp.utils.InjectorUtils
import com.example.android.uamp.viewmodels.MainActivityViewModel
import com.google.android.gms.cast.framework.CastButtonFactory
import com.google.android.gms.cast.framework.CastContext
class MainActivity : AppCompatActivity() {
private val viewModel by viewModels<MainActivityViewModel> {
InjectorUtils.provideMainActivityViewModel(this)
}
private var castContext: CastContext? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Initialize the Cast context. This is required so that the media route button can be
// created in the AppBar
castContext = CastContext.getSharedInstance(this)
setContentView(R.layout.activity_main)
// Since UAMP is a music player, the volume controls should adjust the music volume while
// in the app.
volumeControlStream = AudioManager.STREAM_MUSIC
/**
* Observe [MainActivityViewModel.navigateToFragment] for [Event]s that request a
* fragment swap.
*/
viewModel.navigateToFragment.observe(this, Observer {
it?.getContentIfNotHandled()?.let { fragmentRequest ->
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(
R.id.fragmentContainer, fragmentRequest.fragment, fragmentRequest.tag
)
if (fragmentRequest.backStack) transaction.addToBackStack(null)
transaction.commit()
}
})
/**
* Observe changes to the [MainActivityViewModel.rootMediaId]. When the app starts,
* and the UI connects to [MusicService], this will be updated and the app will show
* the initial list of media items.
*/
viewModel.rootMediaId.observe(this,
Observer<String> { rootMediaId ->
rootMediaId?.let { navigateToMediaItem(it) }
})
/**
* Observe [MainActivityViewModel.navigateToMediaItem] for [Event]s indicating
* the user has requested to browse to a different [MediaItemData].
*/
viewModel.navigateToMediaItem.observe(this, Observer {
it?.getContentIfNotHandled()?.let { mediaId ->
navigateToMediaItem(mediaId)
}
})
}
@Override
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
super.onCreateOptionsMenu(menu)
menuInflater.inflate(R.menu.main_activity_menu, menu)
/**
* Set up a MediaRouteButton to allow the user to control the current media playback route
*/
CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item)
return true
}
private fun navigateToMediaItem(mediaId: String) {
var fragment: MediaItemFragment? = getBrowseFragment(mediaId)
if (fragment == null) {
fragment = MediaItemFragment.newInstance(mediaId)
// If this is not the top level media (root), we add it to the fragment
// back stack, so that actionbar toggle and Back will work appropriately:
viewModel.showFragment(fragment, !isRootId(mediaId), mediaId)
}
}
private fun isRootId(mediaId: String) = mediaId == viewModel.rootMediaId.value
private fun getBrowseFragment(mediaId: String): MediaItemFragment? {
return supportFragmentManager.findFragmentByTag(mediaId) as? MediaItemFragment
}
}
================================================
FILE: app/src/main/java/com/example/android/uamp/MediaItemAdapter.kt
================================================
/*
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.uamp
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.example.android.uamp.MediaItemData.Companion.PLAYBACK_RES_CHANGED
import com.example.android.uamp.databinding.FragmentMediaitemBinding
import com.example.android.uamp.fragments.MediaItemFragment
/**
* [RecyclerView.Adapter] of [MediaItemData]s used by the [MediaItemFragment].
*/
class MediaItemAdapter(
private val itemClickedListener: (MediaItemData) -> Unit
) : ListAdapter<MediaItemData, MediaViewHolder>(MediaItemData.diffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = FragmentMediaitemBinding.inflate(inflater, parent, false)
return MediaViewHolder(binding, itemClickedListener)
}
override fun onBindViewHolder(
holder: MediaViewHolder,
position: Int,
payloads: MutableList<Any>
) {
val mediaItem = getItem(position)
var fullRefresh = payloads.isEmpty()
if (payloads.isNotEmpty()) {
payloads.forEach { payload ->
when (payload) {
PLAYBACK_RES_CHANGED -> {
holder.playbackState.setImageResource(mediaItem.playbackRes)
}
// If the payload wasn't understood, refresh the full item (to be safe).
else -> fullRefresh = true
}
}
}
// Normally we only fully refresh the list item if it's being initially bound, but
// we might also do it if there was a payload that wasn't understood, just to ensure
// there isn't a stale item.
if (fullRefresh) {
holder.item = mediaItem
holder.titleView.text = mediaItem.title
holder.subtitleView.text = mediaItem.subtitle
holder.playbackState.setImageResource(mediaItem.playbackRes)
Glide.with(holder.albumArt)
.load(mediaItem.albumArtUri)
.placeholder(R.drawable.default_art)
.into(holder.albumArt)
}
}
override fun onBindViewHolder(holder: MediaViewHolder, position: Int) {
onBindViewHolder(holder, position, mutableListOf())
}
}
class MediaViewHolder(
binding: FragmentMediaitemBinding,
itemClickedListener: (MediaItemData) -> Unit
) : RecyclerView.ViewHolder(binding.root) {
val titleView: TextView = binding.title
val subtitleView: TextView = binding.subtitle
val albumArt: ImageView = binding.albumArt
val playbackState: ImageView = binding.itemState
var item: MediaItemData? = null
init {
binding.root.setOnClickListener {
item?.let { itemClickedListener(it) }
}
}
}
================================================
FILE: app/src/main/java/com/example/android/uamp/MediaItemData.kt
================================================
/*
* Copyright 2018 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.uamp
import android.net.Uri
import android.support.v4.media.MediaBrowserCompat
import android.support.v4.media.MediaBrowserCompat.MediaItem
import androidx.recyclerview.widget.DiffUtil
import com.example.android.uamp.viewmodels.MediaItemFragmentViewModel
/**
* Data class to encapsulate properties of a [MediaItem].
*
* If an item is [browsable] it means that it has a list of child media items that
* can be retrieved by passing the mediaId to [MediaBrowserCompat.subscribe].
*
* Objects of this class are built from [MediaItem]s in
* [MediaItemFragmentViewModel.subscriptionCallback].
*/
data class MediaItemData(
val mediaId: String,
val title: String,
val subtitle: String,
val albumArtUri: Uri,
val browsable: Boolean,
var playbackRes: Int
) {
companion object {
/**
* Indicates [playbackRes] has changed.
*/
const val PLAYBACK_RES_CHANGED = 1
/**
* [DiffUtil.ItemCallback] for a [MediaItemData].
*
* Since all [MediaItemData]s have a unique ID, it's easiest to check if two
* items are the same by simply comparing that ID.
*
* To check if the contents are the same, we use the same ID, but it may be the
* case that it's only the play state itself which has changed (from playing to
* paused, or perhaps a different item is the active item now). In this case
* we check both the ID and the playback resource.
*
* To calculate the payload, we use the simplest method possible:
* - Since the title, subtitle, and albumArtUri are constant (with respect to mediaId),
* there's no reason to check if they've changed. If the mediaId is the same, none of
* those properties have changed.
* - If the playback resource (playbackRes) has changed to reflect the change in playback
* state, that's all that needs to be updated. We return [PLAYBACK_RES_CHANGED] as
* the payload in this case.
* - If something else changed, then refresh the full item for simplicity.
*/
val diffCallback = object : DiffUtil.ItemCallback<MediaItemData>() {
override fun areItemsTheSame(
oldItem: MediaItemData,
newItem: MediaItemData
): Boolean =
oldItem.mediaId == newItem.mediaId
override fun areContentsTheSame(oldItem: MediaItemData, newItem: MediaItemData) =
oldItem.mediaId == newItem.mediaId && oldItem.playbackRes == newItem.playbackRes
override fun getChangePayload(oldItem: MediaItemData, newItem: MediaItemData) =
if (oldItem.playbackRes != newItem.playbackRes) {
PLAYBACK_RES_CHANGED
} else null
}
}
}
================================================
FILE: app/src/main/java/com/example/android/uamp/cast/UampCastOptionsProvider.kt
================================================
/*
* Copyright 2020 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.uamp.cast
import android.content.Context
import com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider.APP_ID_DEFAULT_RECEIVER_WITH_DRM
import com.google.android.gms.cast.framework.CastOptions
import com.google.android.gms.cast.framework.OptionsProvider
import com.google.android.gms.cast.framework.SessionProvider
import com.google.android.gms.cast.framework.media.CastMediaOptions
class UampCastOptionsProvider : OptionsProvider {
override fun getCastOptions(context: Context?): CastOptions? {
return CastOptions.Builder()
// Use the Default Media Receiver with DRM support.
.setReceiverApplicationId(APP_ID_DEFAULT_RECEIVER_WITH_DRM)
.setCastMediaOptions(
CastMediaOptions.Builder()
// We manage the media session and the notifications ourselves.
.setMediaSessionEnabled(false)
.setNotificationOptions(null)
.build()
)
.setStopReceiverApplicationWhenEndingSession(true).build()
}
override fun getAdditionalSessionProviders(context: Context?): List<SessionProvider?>? {
return null
}
}
================================================
FILE: app/src/main/java/com/example/android/uamp/fragments/MediaItemFragment.kt
================================================
/*
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.uamp.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import com.example.android.uamp.MediaItemAdapter
import com.example.android.uamp.databinding.FragmentMediaitemListBinding
import com.example.android.uamp.utils.InjectorUtils
import com.example.android.uamp.viewmodels.MainActivityViewModel
import com.example.android.uamp.viewmodels.MediaItemFragmentViewModel
/**
* A fragment representing a list of MediaItems.
*/
class MediaItemFragment : Fragment() {
private val mainActivityViewModel by activityViewModels<MainActivityViewModel> {
InjectorUtils.provideMainActivityViewModel(requireContext())
}
private val mediaItemFragmentViewModel by viewModels<MediaItemFragmentViewModel> {
InjectorUtils.provideMediaItemFragmentViewModel(requireContext(), mediaId)
}
private lateinit var mediaId: String
private lateinit var binding: FragmentMediaitemListBinding
private val listAdapter = MediaItemAdapter { clickedItem ->
mainActivityViewModel.mediaItemClicked(clickedItem)
}
companion object {
fun newInstance(mediaId: String): MediaItemFragment {
return MediaItemFragment().apply {
arguments = Bundle().apply {
putString(MEDIA_ID_ARG, mediaId)
}
}
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentMediaitemListBinding.inflate(inflater, container, false)
return binding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
// Always true, but lets lint know that as well.
mediaId = arguments?.getString(MEDIA_ID_ARG) ?: return
mediaItemFragmentViewModel.mediaItems.observe(viewLifecycleOwner,
Observer { list ->
binding.loadingSpinner.visibility =
if (list?.isNotEmpty() == true) View.GONE else View.VISIBLE
listAdapter.submitList(list)
})
mediaItemFragmentViewModel.networkError.observe(viewLifecycleOwner,
Observer { error ->
if (error) {
binding.loadingSpinner.visibility = View.GONE
binding.networkError.visibility = View.VISIBLE
} else {
binding.networkError.visibility = View.GONE
}
})
// Set the adapter
binding.list.adapter = listAdapter
}
}
private const val MEDIA_ID_ARG = "com.example.android.uamp.fragments.MediaItemFragment.MEDIA_ID"
================================================
FILE: app/src/main/java/com/example/android/uamp/fragments/NowPlayingFragment.kt
================================================
/*
* Copyright 2019 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.uamp.fragments
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import com.bumptech.glide.Glide
import com.example.android.uamp.R
import com.example.android.uamp.databinding.FragmentNowplayingBinding
import com.example.android.uamp.utils.InjectorUtils
import com.example.android.uamp.viewmodels.MainActivityViewModel
import com.example.android.uamp.viewmodels.NowPlayingFragmentViewModel
import com.example.android.uamp.viewmodels.NowPlayingFragmentViewModel.NowPlayingMetadata
/**
* A fragment representing the current media item being played.
*/
class NowPlayingFragment : Fragment() {
private val mainActivityViewModel by activityViewModels<MainActivityViewModel> {
InjectorUtils.provideMainActivityViewModel(requireContext())
}
private val nowPlayingViewModel by viewModels<NowPlayingFragmentViewModel> {
InjectorUtils.provideNowPlayingFragmentViewModel(requireContext())
}
lateinit var binding: FragmentNowplayingBinding
companion object {
fun newInstance() = NowPlayingFragment()
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentNowplayingBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Always true, but lets lint know that as well.
val context = activity ?: return
// Attach observers to the LiveData coming from this ViewModel
nowPlayingViewModel.mediaMetadata.observe(viewLifecycleOwner,
Observer { mediaItem -> updateUI(view, mediaItem) })
nowPlayingViewModel.mediaButtonRes.observe(viewLifecycleOwner,
Observer { res ->
binding.mediaButton.setImageResource(res)
})
nowPlayingViewModel.mediaPosition.observe(viewLifecycleOwner,
Observer { pos ->
binding.position.text = NowPlayingMetadata.timestampToMSS(context, pos)
})
// Setup UI handlers for buttons
binding.mediaButton.setOnClickListener {
nowPlayingViewModel.mediaMetadata.value?.let { mainActivityViewModel.playMediaId(it.id) }
}
// Initialize playback duration and position to zero
binding.duration.text = NowPlayingMetadata.timestampToMSS(context, 0L)
binding.position.text = NowPlayingMetadata.timestampToMSS(context, 0L)
}
/**
* Internal function used to update all UI elements except for the current item playback
*/
private fun updateUI(view: View, metadata: NowPlayingMetadata) = with(binding) {
if (metadata.albumArtUri == Uri.EMPTY) {
albumArt.setImageResource(R.drawable.ic_album_black_24dp)
} else {
Glide.with(view)
.load(metadata.albumArtUri)
.into(albumArt)
}
title.text = metadata.title
subtitle.text = metadata.subtitle
duration.text = metadata.duration
}
}
================================================
FILE: app/src/main/java/com/example/android/uamp/utils/Event.kt
================================================
/*
* Copyright 2018 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.uamp.utils
/**
* Used as a wrapper for data that is exposed via a LiveData that represents an event.
*
* For more information, see:
* https://medium.com/google-developers/livedata-with-events-ac2622673150
*/
class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set // Allow external read but not write
/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}
================================================
FILE: app/src/main/java/com/example/android/uamp/utils/InjectorUtils.kt
================================================
/*
* Copyright 2018 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.uamp.utils
import android.app.Application
import android.content.ComponentName
import android.content.Context
import com.example.android.uamp.common.MusicServiceConnection
import com.example.android.uamp.media.MusicService
import com.example.android.uamp.viewmodels.MainActivityViewModel
import com.example.android.uamp.viewmodels.MediaItemFragmentViewModel
import com.example.android.uamp.viewmodels.NowPlayingFragmentViewModel
/**
* Static methods used to inject classes needed for various Activities and Fragments.
*/
object InjectorUtils {
private fun provideMusicServiceConnection(context: Context): MusicServiceConnection {
return MusicServiceConnection.getInstance(
context,
ComponentName(context, MusicService::class.java)
)
}
fun provideMainActivityViewModel(context: Context): MainActivityViewModel.Factory {
val applicationContext = context.applicationContext
val musicServiceConnection = provideMusicServiceConnection(applicationContext)
return MainActivityViewModel.Factory(musicServiceConnection)
}
fun provideMediaItemFragmentViewModel(context: Context, mediaId: String)
: MediaItemFragmentViewModel.Factory {
val applicationContext = context.applicationContext
val musicServiceConnection = provideMusicServiceConnection(applicationContext)
return MediaItemFragmentViewModel.Factory(mediaId, musicServiceConnection)
}
fun provideNowPlayingFragmentViewModel(context: Context)
: NowPlayingFragmentViewModel.Factory {
val applicationContext = context.applicationContext
val musicServiceConnection = provideMusicServiceConnection(applicationContext)
return NowPlayingFragmentViewModel.Factory(
applicationContext as Application, musicServiceConnection
)
}
}
================================================
FILE: app/src/main/java/com/example/android/uamp/viewmodels/MainActivityViewModel.kt
================================================
/*
* Copyright 2018 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.uamp.viewmodels
import android.support.v4.media.MediaBrowserCompat
import android.util.Log
import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.example.android.uamp.MainActivity
import com.example.android.uamp.MediaItemData
import com.example.android.uamp.common.MusicServiceConnection
import com.example.android.uamp.fragments.NowPlayingFragment
import com.example.android.uamp.media.extensions.id
import com.example.android.uamp.media.extensions.isPlayEnabled
import com.example.android.uamp.media.extensions.isPlaying
import com.example.android.uamp.media.extensions.isPrepared
import com.example.android.uamp.utils.Event
/**
* Small [ViewModel] that watches a [MusicServiceConnection] to become connected
* and provides the root/initial media ID of the underlying [MediaBrowserCompat].
*/
class MainActivityViewModel(
private val musicServiceConnection: MusicServiceConnection
) : ViewModel() {
val rootMediaId: LiveData<String> =
Transformations.map(musicServiceConnection.isConnected) { isConnected ->
if (isConnected) {
musicServiceConnection.rootMediaId
} else {
null
}
}
/**
* [navigateToMediaItem] acts as an "event", rather than state. [Observer]s
* are notified of the change as usual with [LiveData], but only one [Observer]
* will actually read the data. For more information, check the [Event] class.
*/
val navigateToMediaItem: LiveData<Event<String>> get() = _navigateToMediaItem
private val _navigateToMediaItem = MutableLiveData<Event<String>>()
/**
* This [LiveData] object is used to notify the MainActivity that the main
* content fragment needs to be swapped. Information about the new fragment
* is conveniently wrapped by the [Event] class.
*/
val navigateToFragment: LiveData<Event<FragmentNavigationRequest>> get() = _navigateToFragment
private val _navigateToFragment = MutableLiveData<Event<FragmentNavigationRequest>>()
/**
* This method takes a [MediaItemData] and routes it depending on whether it's
* browsable (i.e.: it's the parent media item of a set of other media items,
* such as an album), or not.
*
* If the item is browsable, handle it by sending an event to the Activity to
* browse to it, otherwise play it.
*/
fun mediaItemClicked(clickedItem: MediaItemData) {
if (clickedItem.browsable) {
browseToItem(clickedItem)
} else {
playMedia(clickedItem, pauseAllowed = false)
showFragment(NowPlayingFragment.newInstance())
}
}
/**
* Convenience method used to swap the fragment shown in the main activity
*
* @param fragment the fragment to show
* @param backStack if true, add this transaction to the back stack
* @param tag the name to use for this fragment in the stack
*/
fun showFragment(fragment: Fragment, backStack: Boolean = true, tag: String? = null) {
_navigateToFragment.value = Event(FragmentNavigationRequest(fragment, backStack, tag))
}
/**
* This posts a browse [Event] that will be handled by the
* observer in [MainActivity].
*/
private fun browseToItem(mediaItem: MediaItemData) {
_navigateToMediaItem.value = Event(mediaItem.mediaId)
}
/**
* This method takes a [MediaItemData] and does one of the following:
* - If the item is *not* the active item, then play it directly.
* - If the item *is* the active item, check whether "pause" is a permitted command. If it is,
* then pause playback, otherwise send "play" to resume playback.
*/
fun playMedia(mediaItem: MediaItemData, pauseAllowed: Boolean = true) {
val nowPlaying = musicServiceConnection.nowPlaying.value
val transportControls = musicServiceConnection.transportControls
val isPrepared = musicServiceConnection.playbackState.value?.isPrepared ?: false
if (isPrepared && mediaItem.mediaId == nowPlaying?.id) {
musicServiceConnection.playbackState.value?.let { playbackState ->
when {
playbackState.isPlaying ->
if (pauseAllowed) transportControls.pause() else Unit
playbackState.isPlayEnabled -> transportControls.play()
else -> {
Log.w(
TAG, "Playable item clicked but neither play nor pause are enabled!" +
" (mediaId=${mediaItem.mediaId})"
)
}
}
}
} else {
transportControls.playFromMediaId(mediaItem.mediaId, null)
}
}
fun playMediaId(mediaId: String) {
val nowPlaying = musicServiceConnection.nowPlaying.value
val transportControls = musicServiceConnection.transportControls
val isPrepared = musicServiceConnection.playbackState.value?.isPrepared ?: false
if (isPrepared && mediaId == nowPlaying?.id) {
musicServiceConnection.playbackState.value?.let { playbackState ->
when {
playbackState.isPlaying -> transportControls.pause()
playbackState.isPlayEnabled -> transportControls.play()
else -> {
Log.w(
TAG, "Playable item clicked but neither play nor pause are enabled!" +
" (mediaId=$mediaId)"
)
}
}
}
} else {
transportControls.playFromMediaId(mediaId, null)
}
}
class Factory(
private val musicServiceConnection: MusicServiceConnection
) : ViewModelProvider.NewInstanceFactory() {
@Suppress("unchecked_cast")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return MainActivityViewModel(musicServiceConnection) as T
}
}
}
/**
* Helper class used to pass fragment navigation requests between MainActivity
* and its corresponding ViewModel.
*/
data class FragmentNavigationRequest(
val fragment: Fragment,
val backStack: Boolean = false,
val tag: String? = null
)
private const val TAG = "MainActivitytVM"
================================================
FILE: app/src/main/java/com/example/android/uamp/viewmodels/MediaItemFragmentViewModel.kt
================================================
/*
* Copyright 2018 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.uamp.viewmodels
import android.support.v4.media.MediaBrowserCompat
import android.support.v4.media.MediaBrowserCompat.MediaItem
import android.support.v4.media.MediaBrowserCompat.SubscriptionCallback
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.PlaybackStateCompat
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.example.android.uamp.MediaItemData
import com.example.android.uamp.R
import com.example.android.uamp.common.EMPTY_PLAYBACK_STATE
import com.example.android.uamp.common.MusicServiceConnection
import com.example.android.uamp.common.NOTHING_PLAYING
import com.example.android.uamp.fragments.MediaItemFragment
import com.example.android.uamp.media.extensions.id
import com.example.android.uamp.media.extensions.isPlaying
/**
* [ViewModel] for [MediaItemFragment].
*/
class MediaItemFragmentViewModel(
private val mediaId: String,
musicServiceConnection: MusicServiceConnection
) : ViewModel() {
/**
* Use a backing property so consumers of mediaItems only get a [LiveData] instance so
* they don't inadvertently modify it.
*/
private val _mediaItems = MutableLiveData<List<MediaItemData>>()
val mediaItems: LiveData<List<MediaItemData>> = _mediaItems
/**
* Pass the status of the [MusicServiceConnection.networkFailure] through.
*/
val networkError = Transformations.map(musicServiceConnection.networkFailure) { it }
private val subscriptionCallback = object : SubscriptionCallback() {
override fun onChildrenLoaded(parentId: String, children: List<MediaItem>) {
val itemsList = children.map { child ->
val subtitle = child.description.subtitle ?: ""
MediaItemData(
child.mediaId!!,
child.description.title.toString(),
subtitle.toString(),
child.description.iconUri!!,
child.isBrowsable,
getResourceForMediaId(child.mediaId!!)
)
}
_mediaItems.postValue(itemsList)
}
}
/**
* When the session's [PlaybackStateCompat] changes, the [mediaItems] need to be updated
* so the correct [MediaItemData.playbackRes] is displayed on the active item.
* (i.e.: play/pause button or blank)
*/
private val playbackStateObserver = Observer<PlaybackStateCompat> {
val playbackState = it ?: EMPTY_PLAYBACK_STATE
val metadata = musicServiceConnection.nowPlaying.value ?: NOTHING_PLAYING
if (metadata.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID) != null) {
_mediaItems.postValue(updateState(playbackState, metadata))
}
}
/**
* When the session's [MediaMetadataCompat] changes, the [mediaItems] need to be updated
* as it means the currently active item has changed. As a result, the new, and potentially
* old item (if there was one), both need to have their [MediaItemData.playbackRes]
* changed. (i.e.: play/pause button or blank)
*/
private val mediaMetadataObserver = Observer<MediaMetadataCompat> {
val playbackState = musicServiceConnection.playbackState.value ?: EMPTY_PLAYBACK_STATE
val metadata = it ?: NOTHING_PLAYING
if (metadata.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID) != null) {
_mediaItems.postValue(updateState(playbackState, metadata))
}
}
/**
* Because there's a complex dance between this [ViewModel] and the [MusicServiceConnection]
* (which is wrapping a [MediaBrowserCompat] object), the usual guidance of using
* [Transformations] doesn't quite work.
*
* Specifically there's three things that are watched that will cause the single piece of
* [LiveData] exposed from this class to be updated.
*
* [subscriptionCallback] (defined above) is called if/when the children of this
* ViewModel's [mediaId] changes.
*
* [MusicServiceConnection.playbackState] changes state based on the playback state of
* the player, which can change the [MediaItemData.playbackRes]s in the list.
*
* [MusicServiceConnection.nowPlaying] changes based on the item that's being played,
* which can also change the [MediaItemData.playbackRes]s in the list.
*/
private val musicServiceConnection = musicServiceConnection.also {
it.subscribe(mediaId, subscriptionCallback)
it.playbackState.observeForever(playbackStateObserver)
it.nowPlaying.observeForever(mediaMetadataObserver)
}
/**
* Since we use [LiveData.observeForever] above (in [musicServiceConnection]), we want
* to call [LiveData.removeObserver] here to prevent leaking resources when the [ViewModel]
* is not longer in use.
*
* For more details, see the kdoc on [musicServiceConnection] above.
*/
override fun onCleared() {
super.onCleared()
// Remove the permanent observers from the MusicServiceConnection.
musicServiceConnection.playbackState.removeObserver(playbackStateObserver)
musicServiceConnection.nowPlaying.removeObserver(mediaMetadataObserver)
// And then, finally, unsubscribe the media ID that was being watched.
musicServiceConnection.unsubscribe(mediaId, subscriptionCallback)
}
private fun getResourceForMediaId(mediaId: String): Int {
val isActive = mediaId == musicServiceConnection.nowPlaying.value?.id
val isPlaying = musicServiceConnection.playbackState.value?.isPlaying ?: false
return when {
!isActive -> NO_RES
isPlaying -> R.drawable.ic_pause_black_24dp
else -> R.drawable.ic_play_arrow_black_24dp
}
}
private fun updateState(
playbackState: PlaybackStateCompat,
mediaMetadata: MediaMetadataCompat
): List<MediaItemData> {
val newResId = when (playbackState.isPlaying) {
true -> R.drawable.ic_pause_black_24dp
else -> R.drawable.ic_play_arrow_black_24dp
}
return mediaItems.value?.map {
val useResId = if (it.mediaId == mediaMetadata.id) newResId else NO_RES
it.copy(playbackRes = useResId)
} ?: emptyList()
}
class Factory(
private val mediaId: String,
private val musicServiceConnection: MusicServiceConnection
) : ViewModelProvider.NewInstanceFactory() {
@Suppress("unchecked_cast")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return MediaItemFragmentViewModel(mediaId, musicServiceConnection) as T
}
}
}
private const val TAG = "MediaItemFragmentVM"
private const val NO_RES = 0
================================================
FILE: app/src/main/java/com/example/android/uamp/viewmodels/NowPlayingFragmentViewModel.kt
================================================
/*
* Copyright 2019 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.uamp.viewmodels
import android.app.Application
import android.content.Context
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.support.v4.media.MediaBrowserCompat
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.PlaybackStateCompat
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.example.android.uamp.R
import com.example.android.uamp.common.EMPTY_PLAYBACK_STATE
import com.example.android.uamp.common.MusicServiceConnection
import com.example.android.uamp.common.NOTHING_PLAYING
import com.example.android.uamp.fragments.NowPlayingFragment
import com.example.android.uamp.media.extensions.albumArtUri
import com.example.android.uamp.media.extensions.currentPlayBackPosition
import com.example.android.uamp.media.extensions.displaySubtitle
import com.example.android.uamp.media.extensions.duration
import com.example.android.uamp.media.extensions.id
import com.example.android.uamp.media.extensions.isPlaying
import com.example.android.uamp.media.extensions.title
/**
* [ViewModel] for [NowPlayingFragment] which displays the album art in full size.
* It extends AndroidViewModel and uses the [Application]'s context to be able to reference string
* resources.
*/
class NowPlayingFragmentViewModel(
private val app: Application,
musicServiceConnection: MusicServiceConnection
) : AndroidViewModel(app) {
/**
* Utility class used to represent the metadata necessary to display the
* media item currently being played.
*/
data class NowPlayingMetadata(
val id: String,
val albumArtUri: Uri,
val title: String?,
val subtitle: String?,
val duration: String
) {
companion object {
/**
* Utility method to convert milliseconds to a display of minutes and seconds
*/
fun timestampToMSS(context: Context, position: Long): String {
val totalSeconds = Math.floor(position / 1E3).toInt()
val minutes = totalSeconds / 60
val remainingSeconds = totalSeconds - (minutes * 60)
return if (position < 0) context.getString(R.string.duration_unknown)
else context.getString(R.string.duration_format).format(minutes, remainingSeconds)
}
}
}
private var playbackState: PlaybackStateCompat = EMPTY_PLAYBACK_STATE
val mediaMetadata = MutableLiveData<NowPlayingMetadata>()
val mediaPosition = MutableLiveData<Long>().apply {
postValue(0L)
}
val mediaButtonRes = MutableLiveData<Int>().apply {
postValue(R.drawable.ic_album_black_24dp)
}
private var updatePosition = true
private val handler = Handler(Looper.getMainLooper())
/**
* When the session's [PlaybackStateCompat] changes, the [mediaItems] need to be updated
* so the correct [MediaItemData.playbackRes] is displayed on the active item.
* (i.e.: play/pause button or blank)
*/
private val playbackStateObserver = Observer<PlaybackStateCompat> {
playbackState = it ?: EMPTY_PLAYBACK_STATE
val metadata = musicServiceConnection.nowPlaying.value ?: NOTHING_PLAYING
updateState(playbackState, metadata)
}
/**
* When the session's [MediaMetadataCompat] changes, the [mediaItems] need to be updated
* as it means the currently active item has changed. As a result, the new, and potentially
* old item (if there was one), both need to have their [MediaItemData.playbackRes]
* changed. (i.e.: play/pause button or blank)
*/
private val mediaMetadataObserver = Observer<MediaMetadataCompat> {
updateState(playbackState, it)
}
/**
* Because there's a complex dance between this [ViewModel] and the [MusicServiceConnection]
* (which is wrapping a [MediaBrowserCompat] object), the usual guidance of using
* [Transformations] doesn't quite work.
*
* Specifically there's three things that are watched that will cause the single piece of
* [LiveData] exposed from this class to be updated.
*
* [MusicServiceConnection.playbackState] changes state based on the playback state of
* the player, which can change the [MediaItemData.playbackRes]s in the list.
*
* [MusicServiceConnection.nowPlaying] changes based on the item that's being played,
* which can also change the [MediaItemData.playbackRes]s in the list.
*/
private val musicServiceConnection = musicServiceConnection.also {
it.playbackState.observeForever(playbackStateObserver)
it.nowPlaying.observeForever(mediaMetadataObserver)
checkPlaybackPosition()
}
/**
* Internal function that recursively calls itself every [POSITION_UPDATE_INTERVAL_MILLIS] ms
* to check the current playback position and updates the corresponding LiveData object when it
* has changed.
*/
private fun checkPlaybackPosition(): Boolean = handler.postDelayed({
val currPosition = playbackState.currentPlayBackPosition
if (mediaPosition.value != currPosition)
mediaPosition.postValue(currPosition)
if (updatePosition)
checkPlaybackPosition()
}, POSITION_UPDATE_INTERVAL_MILLIS)
/**
* Since we use [LiveData.observeForever] above (in [musicServiceConnection]), we want
* to call [LiveData.removeObserver] here to prevent leaking resources when the [ViewModel]
* is not longer in use.
*
* For more details, see the kdoc on [musicServiceConnection] above.
*/
override fun onCleared() {
super.onCleared()
// Remove the permanent observers from the MusicServiceConnection.
musicServiceConnection.playbackState.removeObserver(playbackStateObserver)
musicServiceConnection.nowPlaying.removeObserver(mediaMetadataObserver)
// Stop updating the position
updatePosition = false
}
private fun updateState(
playbackState: PlaybackStateCompat,
mediaMetadata: MediaMetadataCompat
) {
// Only update media item once we have duration available
if (mediaMetadata.duration != 0L && mediaMetadata.id != null) {
val nowPlayingMetadata = NowPlayingMetadata(
mediaMetadata.id!!,
mediaMetadata.albumArtUri,
mediaMetadata.title?.trim(),
mediaMetadata.displaySubtitle?.trim(),
NowPlayingMetadata.timestampToMSS(app, mediaMetadata.duration)
)
this.mediaMetadata.postValue(nowPlayingMetadata)
}
// Update the media button resource ID
mediaButtonRes.postValue(
when (playbackState.isPlaying) {
true -> R.drawable.ic_pause_black_24dp
else -> R.drawable.ic_play_arrow_black_24dp
}
)
}
class Factory(
private val app: Application,
private val musicServiceConnection: MusicServiceConnection
) : ViewModelProvider.NewInstanceFactory() {
@Suppress("unchecked_cast")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return NowPlayingFragmentViewModel(app, musicServiceConnection) as T
}
}
}
private const val TAG = "NowPlayingFragmentVM"
private const val POSITION_UPDATE_INTERVAL_MILLIS = 100L
================================================
FILE: app/src/main/res/drawable/ic_album_black_24dp.xml
================================================
<!--
~ Copyright 2017 Google Inc. All rights reserved.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="36dp"
android:height="36dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,16.5c-2.49,0 -4.5,-2.01 -4.5,-4.5S9.51,7.5 12,7.5s4.5,2.01 4.5,4.5 -2.01,4.5 -4.5,4.5zM12,11c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1z" />
</vector>
================================================
FILE: app/src/main/res/drawable/ic_launcher_background.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2018 Google Inc. All rights reserved.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
================================================
FILE: app/src/main/res/drawable/ic_pause_black_24dp.xml
================================================
<!--
~ Copyright 2017 Google Inc. All rights reserved.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z" />
</vector>
================================================
FILE: app/src/main/res/drawable/ic_play_arrow_black_24dp.xml
================================================
<!--
~ Copyright 2017 Google Inc. All rights reserved.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M8,5v14l11,-7z" />
</vector>
================================================
FILE: app/src/main/res/drawable/ic_signal_wifi_off_black_24dp.xml
================================================
<!--
~ Copyright 2018 Google Inc. All rights reserved.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M23.64,7c-0.45,-0.34 -4.93,-4 -11.64,-4 -1.5,0 -2.89,0.19 -4.15,0.48L18.18,13.8 23.64,7zM17.04,15.22L3.27,1.44 2,2.72l2.05,2.06C1.91,5.76 0.59,6.82 0.36,7l11.63,14.49 0.01,0.01 0.01,-0.01 3.9,-4.86 3.32,3.32 1.27,-1.27 -3.46,-3.46z" />
</vector>
================================================
FILE: app/src/main/res/drawable/media_item_background.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2018 Google Inc. All rights reserved.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:bottom="@dimen/media_item_negative_half_radius"
android:left="@dimen/media_item_negative_half_radius"
android:right="@dimen/media_item_negative_half_radius"
android:top="@dimen/media_item_negative_half_radius">
<shape android:shape="rectangle">
<stroke
android:width="@dimen/media_item_half_radius"
android:color="@color/mediaListBackground" />
<corners android:radius="@dimen/media_item_radius" />
<solid android:color="#ffffff" />
</shape>
</item>
</layer-list>
================================================
FILE: app/src/main/res/drawable/media_item_mask.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2018 Google Inc. All rights reserved.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:bottom="@dimen/media_item_negative_half_radius"
android:left="@dimen/media_item_negative_half_radius"
android:right="@dimen/media_item_negative_half_radius"
android:top="@dimen/media_item_negative_half_radius">
<shape android:shape="rectangle">
<stroke
android:width="@dimen/media_item_half_radius"
android:color="@color/mediaListBackground" />
<corners
android:bottomLeftRadius="@dimen/media_item_radius"
android:bottomRightRadius="0dp"
android:topLeftRadius="@dimen/media_item_radius"
android:topRightRadius="0dp" />
</shape>
</item>
</layer-list>
================================================
FILE: app/src/main/res/drawable/media_overlay_background.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2019 Google Inc. All rights reserved.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="90"
android:centerColor="#AAFFFFFF"
android:endColor="#00FFFFFF"
android:startColor="#CCFFFFFF" />
</shape>
================================================
FILE: app/src/main/res/drawable-v24/ic_launcher_foreground.xml
================================================
<!--
~ Copyright 2017 Google Inc. All rights reserved.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeWidth="1"
android:strokeColor="#00000000">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
================================================
FILE: app/src/main/res/layout/activity_main.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2017 Google Inc. All rights reserved.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/fragmentContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.android.uamp.MainActivity" />
================================================
FILE: app/src/main/res/layout/cast_context_error.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2018 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:textSize="20sp"
android:text="@string/cast_context_error"/>
================================================
FILE: app/src/main/res/layout/fragment_mediaitem.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2017 Google Inc. All rights reserved.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="2dp"
android:background="@drawable/media_item_background">
<ImageView
android:id="@+id/albumArt"
android:layout_width="@dimen/media_item_art"
android:layout_height="@dimen/media_item_art"
android:scaleType="centerCrop"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_album_black_24dp"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/item_state"
style="@style/MediaStateIcon"
android:layout_width="0dp"
android:layout_height="0dp"
android:scaleType="center"
app:layout_constraintBottom_toBottomOf="@+id/albumArt"
app:layout_constraintLeft_toLeftOf="@+id/albumArt"
app:layout_constraintRight_toRightOf="@+id/albumArt"
app:layout_constraintTop_toTopOf="@+id/albumArt"
tools:ignore="ContentDescription" />
<ImageView
android:layout_width="0dp"
android:layout_height="0dp"
android:src="@drawable/media_item_mask"
app:layout_constraintBottom_toBottomOf="@+id/albumArt"
app:layout_constraintLeft_toLeftOf="@+id/albumArt"
app:layout_constraintRight_toRightOf="@+id/albumArt"
app:layout_constraintTop_toTopOf="@+id/albumArt"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/text_margin"
android:layout_marginEnd="@dimen/text_margin"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.Uamp.Title"
app:layout_constraintBottom_toTopOf="@+id/center_guideline"
app:layout_constraintLeft_toRightOf="@+id/albumArt"
app:layout_constraintRight_toRightOf="parent"
tools:text="Song Title" />
<TextView
android:id="@+id/subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/text_margin"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.Uamp.Subtitle"
app:layout_constraintLeft_toLeftOf="@+id/title"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/center_guideline"
tools:text="Artist" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/center_guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.5" />
</androidx.constraintlayout.widget.ConstraintLayout>
================================================
FILE: app/src/main/res/layout/fragment_mediaitem_list.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2017 Google Inc. All rights reserved.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:name="com.example.android.uamp.MediaItemFragment"
style="@style/MediaItemList"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:context="com.example.android.uamp.fragments.MediaItemFragment"
tools:listitem="@layout/fragment_mediaitem" />
<ProgressBar
android:id="@+id/loadingSpinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<ImageView
android:id="@+id/networkError"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_gravity="center"
android:tint="@color/colorAccent"
android:visibility="gone"
app:srcCompat="@drawable/ic_signal_wifi_off_black_24dp" />
</FrameLayout>
================================================
FILE: app/src/main/res/layout/fragment_nowplaying.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2019 Google Inc. All rights reserved.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="2dp"
android:background="@drawable/media_item_background">
<ImageView
android:id="@+id/albumArt"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/album_art_alt"
android:scaleType="centerCrop"
app:srcCompat="@android:color/transparent" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/divider"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_end="64dp" />
<View
android:layout_width="match_parent"
android:layout_height="96dp"
android:layout_marginTop="-24dp"
android:background="@drawable/media_overlay_background"
app:layout_constraintBottom_toBottomOf="@+id/albumArt"
app:layout_constraintTop_toBottomOf="@id/divider" />
<ImageButton
android:id="@+id/media_button"
android:layout_width="@dimen/exo_media_button_width"
android:layout_height="@dimen/exo_media_button_height"
android:background="?attr/selectableItemBackground"
android:scaleType="centerInside"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/divider"
app:srcCompat="@drawable/ic_play_arrow_black_24dp"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/text_margin"
android:layout_marginTop="8dp"
android:layout_marginEnd="@dimen/text_margin"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.Uamp.Title"
app:layout_constraintLeft_toRightOf="@id/media_button"
app:layout_constraintRight_toLeftOf="@id/position"
app:layout_constraintTop_toBottomOf="@id/divider"
tools:text="Song Title" />
<TextView
android:id="@+id/subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/text_margin"
android:layout_marginEnd="@dimen/text_margin"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.Uamp.Subtitle"
app:layout_constraintLeft_toRightOf="@id/media_button"
app:layout_constraintRight_toLeftOf="@id/position"
app:layout_constraintTop_toBottomOf="@+id/title"
tools:text="Artist" />
<TextView
android:id="@+id/position"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/text_margin"
android:layout_marginTop="8dp"
android:layout_marginEnd="@dimen/text_margin"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.Uamp.Title"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/divider"
tools:text="0:00" />
<TextView
android:id="@+id/duration"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/text_margin"
android:layout_marginEnd="@dimen/text_margin"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.Uamp.Subtitle"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/position"
tools:text="0:00" />
</androidx.constraintlayout.widget.ConstraintLayout>
================================================
FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2018 Google Inc. All rights reserved.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
</adaptive-icon>
================================================
FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2018 Google Inc. All rights reserved.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
</adaptive-icon>
================================================
FILE: app/src/main/res/values/colors.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2017 Google Inc. All rights reserved.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<resources>
<color name="colorPrimary">#840255</color>
<color name="colorPrimaryDark">#710144</color>
<color name="colorAccent">#14A5A1</color>
<color name="transparent">#00000000</color>
<color name="mediaListBackground">#F1F1F1</color>
<color name="mediaControlColor">#f8f8f8</color>
<color name="nowPlayingBackground">#eff0f0</color>
<color name="nowPlayingWhiteBackground">#fdfdfd</color>
<color name="menu_icon_tint">#ffffff</color>
</resources>
================================================
FILE: app/src/main/res/values/dimens.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2017 Google Inc. All rights reserved.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<resources>
<dimen name="text_margin">16dp</dimen>
<dimen name="media_item_height">72dp</dimen>
<dimen name="media_item_control_margin">4dp</dimen>
<dimen name="media_item_icon_margin_start">12dp</dimen>
<dimen name="media_item_text_margin_start">4dp</dimen>
<dimen name="media_item_art">72dp</dimen>
<dimen name="media_item_radius">4dp</dimen>
<dimen name="media_item_half_radius">2dp</dimen>
<dimen name="media_item_negative_half_radius">-2dp</dimen>
<dimen name="now_playing_album_art_margin">72dp</dimen>
<dimen name="now_playing_skip_top_margin">42dp</dimen>
<dimen name="now_playing_skip_margin">8dp</dimen>
<dimen name="now_playing_play_pause_top_margin">42dp</dimen>
<dimen name="now_playing_play_pause_bottom_margin">52dp</dimen>
<dimen name="now_playing_title_top_margin">32dp</dimen>
<dimen name="now_playing_title_bottom_margin">8dp</dimen>
<dimen name="now_playing_artist_bottom_margin">32dp</dimen>
<dimen name="now_playing_text_padding">16dp</dimen>
<dimen name="now_playing_title_size">32sp</dimen>
<dimen name="now_playing_artist_size">18sp</dimen>
</resources>
================================================
FILE: app/src/main/res/values/strings.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2017 Google Inc. All rights reserved.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<resources>
<string name="app_name">UAMP</string>
<string name="skip_back_10s">Skip back 10s</string>
<string name="skip_forward_10s">Skip forward 10s</string>
<string name="play">Play</string>
<string name="pause">Pause</string>
<string name="menu_music_queue">Queue</string>
<string name="album_art_alt">Album art</string>
<string name="duration_unknown">--:--</string>
<string name="duration_format">%d:%02d</string>
<string name="cast_context_error">Failed to get Cast context. Try updating Google Play Services and restart the app.</string>
</resources>
================================================
FILE: app/src/main/res/values/styles.xml
================================================
<!--
~ Copyright 2017 Google Inc. All rights reserved.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="TextAppearance.Uamp.Title" parent="@style/TextAppearance.AppCompat.Title">
<item name="android:textColor">?android:textColorSecondary</item>
</style>
<style name="TextAppearance.Uamp.Subtitle" parent="@style/TextAppearance.AppCompat.Small">
<item name="android:textColor">?android:textColorSecondary</item>
</style>
<style name="MediaStateIcon">
<item name="tint">@color/mediaControlColor</item>
<item name="android:layout_margin">@dimen/media_item_control_margin</item>
</style>
<style name="MediaItemList">
<item name="android:background">@color/mediaListBackground</item>
<item name="android:paddingBottom">2dp</item>
<!-- In order to account for optical illusions,
make the end padding a bit larger so it visually looks the same. -->
<item name="android:paddingEnd">3dp</item>
<item name="android:paddingStart">2dp</item>
<item name="android:paddingTop">2dp</item>
</style>
</resources>
================================================
FILE: app/src/main/res/xml/automotive_app_desc.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2018 Google Inc. All rights reserved.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<automotiveApp>
<!--
~ This lets Android Auto know that UAMP can act as a media app.
~ See: https://developer.android.com/training/auto/audio/ for more info.
-->
<uses name="media" />
</automotiveApp>
================================================
FILE: automotive/build.gradle
================================================
/*
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion rootProject.compileSdkVersion
defaultConfig {
applicationId "com.example.android.uamp.next"
minSdkVersion 21
targetSdkVersion rootProject.targetSdkVersion
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildFeatures {
viewBinding true
}
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation project(':common')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "androidx.core:core-ktx:$androidx_core_ktx_version"
implementation "androidx.preference:preference:$androidx_preference_version"
implementation "androidx.car:car:$androidx_car_version"
implementation "androidx.constraintlayout:constraintlayout:$constraint_layout_version"
implementation "androidx.appcompat:appcompat:$androidx_app_compat_version"
implementation "androidx.lifecycle:lifecycle-extensions:$arch_lifecycle_version"
implementation "com.google.android.gms:play-services-auth:$play_services_auth_version"
testImplementation "junit:junit:$junit_version"
androidTestImplementation "androidx.test:runner:$androidx_test_runner_version"
androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version"
}
================================================
FILE: automotive/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: automotive/src/androidTest/java/com/example/android/uamp/automotive/ExampleInstrumentedTest.java
================================================
/*
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.uamp.automotive;
import android.content.Context;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getTargetContext();
assertEquals("com.example.automotive", appContext.getPackageName());
}
}
================================================
FILE: automotive/src/main/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2019 Google LLC
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.android.uamp.automotive">
<!--
Since this module contains code exclusive to Android Automotive, require the feature here.
If you were mixing projected and automotive code, then the feature should not be marked
as required.
-->
<uses-feature
android:name="android.hardware.type.automotive"
android:required="true" />
<uses-sdk tools:overrideLibrary="androidx.car" />
<uses-feature
android:name="android.hardware.wifi"
android:required="false" />
<uses-feature
android:name="android.hardware.screen.portrait"
android:required="false" />
<uses-feature
android:name="android.hardware.screen.landscape"
android:required="false" />
<application
android:allowBackup="true"
android:appCategory="audio"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:ignore="GoogleAppIndexingWarning">
<meta-data
android:name="com.android.automotive"
android:resource="@xml/automotive_app_desc" />
<activity
android:name=".SignInActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.ACTION_SIGN_IN" />
</intent-filter>
</activity>
<!-- Car compatible theme must use minSDK24 -->
<activity
android:name=".SettingsActivity"
android:label="@string/settings_label"
android:theme="@style/AppTheme.Drawer">
<intent-filter>
<action android:name="android.intent.action.APPLICATION_PREFERENCES" />
</intent-filter>
</activity>
<!--
Declare the MediaBrowserService which supports Android Automotive specific
extensions.
-->
<service
android:name=".AutomotiveMusicService"
android:enabled="true"
android:exported="true"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
</application>
</manifest>
================================================
FILE: automotive/src/main/java/com/example/android/uamp/automotive/AutomotiveMusicService.kt
================================================
/*
* Copyright 2019 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.uamp.automotive
import android.accounts.AccountManager
import android.app.Activity
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.ResultReceiver
import android.support.v4.media.session.PlaybackStateCompat
import android.util.Log
import androidx.core.content.edit
import com.example.android.uamp.media.MusicService
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
import kotlinx.coroutines.ExperimentalCoroutinesApi
/** UAMP specific command for logging into the service. */
const val LOGIN = "com.example.android.uamp.automotive.COMMAND.LOGIN"
/** UAMP specific command for logging out of the service. */
const val LOGOUT = "com.example.android.uamp.automotive.COMMAND.LOGOUT"
const val LOGIN_EMAIL = "com.example.android.uamp.automotive.ARGS.LOGIN_EMAIL"
const val LOGIN_PASSWORD = "com.example.android.uamp.automotive.ARGS.LOGIN_PASSWORD"
typealias CommandHandler = (parameters: Bundle, callback: ResultReceiver?) -> Boolean
/**
* Android Automotive specific extensions for [MusicService].
*
* UAMP for Android Automotive OS requires the user to login in order to demonstrate
* how authentication works on the system. If this doesn't apply to your application,
* this class can be skipped in favor of its parent, [MusicService].
*
* If you'd like to support authentication, but not prevent using the system,
* comment out the calls to [requireLogin].
*/
class AutomotiveMusicService : MusicService() {
@ExperimentalCoroutinesApi
override fun onCreate() {
super.onCreate()
// Register to handle login/logout commands.
mediaSessionConnector.registerCustomCommandReceiver(AutomotiveCommandReceiver())
// Require the user to be logged in for demonstration purposes.
if (!isAuthenticated()) {
requireLogin()
}
}
private fun onLogin(email: String, password: String): Boolean {
Log.i(TAG, "User logged in: $email")
getSharedPreferences(AutomotiveMusicService::class.java.name, Context.MODE_PRIVATE).edit {
putString(USER_TOKEN, "$email:${password.hashCode()}")
}
return true
}
private fun onLogout(): Boolean {
Log.i(TAG, "User logged out")
getSharedPreferences(AutomotiveMusicService::class.java.name, Context.MODE_PRIVATE).edit {
remove(USER_TOKEN)
}
return false
}
/**
* Verifies if the user has logged into the system.
* In a real system, credentials should probably be handled by the
* [AccountManager] APIs.
*/
private fun isAuthenticated() =
getSharedPreferences(AutomotiveMusicService::class.java.name, Context.MODE_PRIVATE)
.contains(USER_TOKEN)
/**
* Sets [PlaybackStateCompat] values to indicate the user must login to continue.
*
* This routine sets the playback state and provides the resolution [PendingIntent]
* that Android Automotive OS requires.
*/
private fun requireLogin() {
val loginIntent = Intent(this, SignInActivity::class.java)
val loginActivityPendingIntent = PendingIntent.getActivity(this, 0, loginIntent, 0)
val extras = Bundle().apply {
putString(ERROR_RESOLUTION_ACTION_LABEL, getString(R.string.error_login_button))
putParcelable(ERROR_RESOLUTION_ACTION_INTENT, loginActivityPendingIntent)
}
mediaSessionConnector.setCustomErrorMessage(
getString(R.string.error_require_login),
PlaybackStateCompat.ERROR_CODE_AUTHENTICATION_EXPIRED,
extras
)
}
/**
* This is the entry point for custom commands received by ExoPlayer's
* [MediaSessionConnector.customCommandReceivers].
*
* The extension will call each [CommandReceiver] in turn. If the [CommandReceiver] can
* handle the command, it returns `true` to indicate the command's been handled and
* processing should stop. If the [CommandReceiver] cannot/doesn't want to handle the
* command, it should return `false`.
*
* We simplify this a bit by having our own [CommandHandler] that works with a single
* command (either "log in" or "log out"). Each of these returns true at the end of its
* processing.
*
* If the command received isn't either of our commands, we just return `false`.
*
* Suppress the warning because the original name, `cb` is not as clear as to its purpose.
*/
private inner class AutomotiveCommandReceiver : MediaSessionConnector.CommandReceiver {
@Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE")
override fun onCommand(
player: Player,
command: String,
extras: Bundle?,
callback: ResultReceiver?
): Boolean =
when (command) {
LOGIN -> loginCommand(extras ?: Bundle.EMPTY, callback)
LOGOUT -> logoutCommand(extras ?: Bundle.EMPTY, callback)
else -> false
}
}
private val loginCommand: CommandHandler = { extras, callback ->
val email = extras.getString(LOGIN_EMAIL) ?: ""
val password = extras.getString(LOGIN_PASSWORD) ?: ""
if (onLogin(email, password)) {
// Updated state (including clearing the error) now that the user has logged in.
mediaSessionConnector.setCustomErrorMessage(null)
mediaSessionConnector.invalidateMediaSessionPlaybackState()
callback?.send(Activity.RESULT_OK, Bundle.EMPTY)
} else {
// Login is required - note this.
requireLogin()
callback?.send(Activity.RESULT_CANCELED, Bundle.EMPTY)
}
true
}
private val logoutCommand: CommandHandler = { _, callback ->
// Log the user out.
onLogout()
// Login is required - note this.
requireLogin()
callback?.send(Activity.RESULT_OK, Bundle.EMPTY)
true
}
}
private const val TAG = "AutomotiveMusicService"
private const val ERROR_RESOLUTION_ACTION_LABEL =
"android.media.extras.ERROR_RESOLUTION_ACTION_LABEL"
private const val ERROR_RESOLUTION_ACTION_INTENT =
"android.media.extras.ERROR_RESOLUTION_ACTION_INTENT"
private const val USER_TOKEN = "com.example.android.uamp.automotive.PREFS.USER_TOKEN"
================================================
FILE: automotive/src/main/java/com/example/android/uamp/automotive/PhoneSignInFragment.kt
================================================
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.uamp.automotive
import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.view.View
import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat
import androidx.fragment.app.Fragment
import com.example.android.uamp.automotive.databinding.PhoneSignInBinding
/**
* Fragment that is used to facilitate phone sign-in. The fragment allows users to choose between
* either the PIN or QR code sign-in flow.
*/
class PhoneSignInFragment : Fragment(R.layout.phone_sign_in) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val context = requireContext()
val binding = PhoneSignInBinding.bind(view)
binding.toolbar.setNavigationOnClickListener {
requireActivity().supportFragmentManager.popBackStack()
}
// Set up PIN sign in button.
binding.appIcon.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.aural_logo))
binding.primaryMessage.text = getString(R.string.phone_sign_in_primary_text)
binding.pinSignInButton.text = getString(R.string.pin_sign_in_button_label)
binding.pinSignInButton.setOnClickListener {
requireActivity().supportFragmentManager.beginTransaction()
.replace(R.id.sign_in_container, PinCodeSignInFragment())
.addToBackStack("landingPage")
.commit()
}
// Set up QR code sign in button.
binding.qrSignInButton.text = getString(R.string.qr_sign_in_button_label)
binding.qrSignInButton.setOnClickListener {
requireActivity().supportFragmentManager.beginTransaction()
.replace(R.id.sign_in_container, QrCodeSignInFragment())
.addToBackStack("landingPage")
.commit()
}
// Links in footer text should be clickable.
binding.footer.text = HtmlCompat.fromHtml(
context.getString(R.string.sign_in_footer),
HtmlCompat.FROM_HTML_MODE_LEGACY
)
binding.footer.movementMethod = LinkMovementMethod.getInstance()
}
}
================================================
FILE: automotive/src/main/java/com/example/android/uamp/automotive/PinCodeSignInFragment.kt
================================================
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.uamp.automotive
import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.example.android.uamp.automotive.databinding.PinSignInBinding
/**
* Fragment that is used to facilitate PIN code sign-in. This fragment displayed a configurable
* PIN code that users enter in a secondary device to perform sign-in.
*
*<p>This screen serves as a demo for UI best practices for PIN code sign in. Sign in implementation
* will be app specific and is not included.
*/
class PinCodeSignInFragment : Fragment(R.layout.pin_sign_in) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val context = requireContext()
val binding = PinSignInBinding.bind(view)
binding.toolbar.setNavigationOnClickListener {
requireActivity().supportFragmentManager.popBackStack()
}
binding.appIcon.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.aural_logo))
binding.primaryMessage.text = getString(R.string.pin_sign_in_primary_text)
binding.secondaryMessage.text = getString(R.string.pin_sign_in_secondary_text)
// Links in footer text should be clickable.
binding.footer.text = HtmlCompat.fromHtml(
context.getString(R.string.sign_in_footer),
HtmlCompat.FROM_HTML_MODE_LEGACY
)
binding.footer.movementMethod = LinkMovementMethod.getInstance()
val pin = ViewModelProvider(requireActivity())
.get(SignInActivityViewModel::class.java)
.generatePin()
// Remove existing PIN characters.
if (binding.pinCodeContainer.childCount > 0) {
binding.pinCodeContainer.removeAllViews()
}
for (element in pin) {
val pinItem = LayoutInflater.from(context).inflate(
R.layout.pin_item,
binding.pinCodeContainer,
false
) as TextView
pinItem.text = element.toString()
binding.pinCodeContainer.addView(pinItem)
}
}
}
================================================
FILE: automotive/src/main/java/com/example/android/uamp/automotive/QrCodeSignInFragment.kt
================================================
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.uamp.automotive
import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.view.View
import androidx.core.content.ContextCompat.getDrawable
import androidx.core.text.HtmlCompat
import androidx.fragment.app.Fragment
import com.bumptech.glide.Glide
import com.example.android.uamp.automotive.databinding.QrSignInBinding
/**
* Fragment that is used to facilitate QR code sign-in. Users scan a QR code rendered by this
* fragment with their phones, which performs the authentication required for sign-in
*
* <p>This screen serves as a demo for UI best practices for QR code sign in. Sign in implementation
* will be app specific and is not included.
*/
class QrCodeSignInFragment : Fragment(R.layout.qr_sign_in) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = QrSignInBinding.bind(view)
binding.toolbar.setNavigationOnClickListener {
requireActivity().supportFragmentManager.popBackStack()
}
binding.appIcon.setImageDrawable(getDrawable(requireContext(), R.drawable.aural_logo))
binding.primaryMessage.text = getString(R.string.qr_sign_in_primary_text)
binding.secondaryMessage.text = getString(R.string.qr_sign_in_secondary_text)
// Links in footer text should be clickable.
binding.footer.text = HtmlCompat.fromHtml(
requireContext().getString(R.string.sign_in_footer),
HtmlCompat.FROM_HTML_MODE_LEGACY
)
binding.footer.movementMethod = LinkMovementMethod.getInstance()
Glide.with(this).load(getString(R.string.qr_code_url)).into(binding.qrCode)
}
}
================================================
FILE: automotive/src/main/java/com/example/android/uamp/automotive/SettingsActivity.kt
================================================
/*
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.uamp.automotive
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.example.android.uamp.automotive.databinding.ActivitySettingsBinding
/**
* This class exposes application settings
* for integration with MediaCenter in Android Automotive.
*/
class SettingsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivitySettingsBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
supportActionBar?.setHomeButtonEnabled(true)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportFragmentManager
.beginTransaction()
.replace(R.id.settings_container, SettingsFragment())
.commit()
}
override fun onBackPressed() {
super.onBackPressed()
finish()
}
override fun onSupportNavigateUp(): Boolean {
onBackPressed()
return true
}
}
================================================
FILE: automotive/src/main/java/com/example/android/uamp/automotive/SettingsFragment.kt
================================================
/*
* Copyright 2019 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.uamp.automotive
import android.app.Application
import android.content.ComponentName
import android.os.Bundle
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.example.android.uamp.common.MusicServiceConnection
/**
* Preference fragment hosted by [SettingsActivity]. Handles events to various preference changes.
*/
class SettingsFragment : PreferenceFragmentCompat() {
private lateinit var viewModel: SettingsFragmentViewModel
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.preferences, rootKey)
viewModel = ViewModelProvider(this)
.get(SettingsFragmentViewModel::class.java)
}
override fun onPreferenceTreeClick(preference: Preference?): Boolean {
return when (preference?.key) {
"logout" -> {
viewModel.logout()
requireActivity().finish()
true
}
else -> {
super.onPreferenceTreeClick(preference)
}
}
}
}
/**
* Basic ViewModel for [SettingsFragment].
*/
class SettingsFragmentViewModel(application: Application) : AndroidViewModel(application) {
private val applicationContext = application.applicationContext
private val musicServiceConnection = MusicServiceConnection(
applicationContext,
ComponentName(applicationContext, AutomotiveMusicService::class.java)
)
fun logout() {
// Logout is fire and forget.
musicServiceConnection.sendCommand(LOGOUT, null)
}
}
================================================
FILE: automotive/src/main/java/com/example/android/uamp/automotive/SignInActivity.kt
================================================
/*
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.uamp.automotive
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
class SignInActivity : AppCompatActivity() {
private lateinit var viewModel: SignInActivityViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_sign_in)
viewModel = ViewModelProvider(this)
.get(SignInActivityViewModel::class.java)
viewModel.loggedIn.observe(this, Observer { loggedIn ->
if (loggedIn == true) {
Toast.makeText(this, R.string.sign_in_success_message, Toast.LENGTH_SHORT).show()
finish()
}
})
supportFragmentManager.beginTransaction()
.add(R.id.sign_in_container, SignInLandingPageFragment())
.commit()
}
}
================================================
FILE: automotive/src/main/java/com/example/android/uamp/automotive/SignInActivityViewModel.kt
================================================
/*
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.uamp.automotive
import android.app.Activity
import android.app.Application
import android.content.ComponentName
import android.os.Bundle
import android.text.TextUtils
import android.widget.Toast
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.example.android.uamp.common.MusicServiceConnection
import java.util.Random
/**
* Basic ViewModel for [SignInActivity].
*/
class SignInActivityViewModel(application: Application) : AndroidViewModel(application) {
private val applicationContext = application.applicationContext
private val musicServiceConnection = MusicServiceConnection(
applicationContext,
ComponentName(applicationContext, AutomotiveMusicService::class.java)
)
private val _loggedIn = MutableLiveData<Boolean>()
val loggedIn: LiveData<Boolean> = _loggedIn
fun login(email: String, password: String) {
if (TextUtils.isEmpty(email) or TextUtils.isEmpty(password)) {
Toast.makeText(
applicationContext,
applicationContext.getString(R.string.missing_fields_error),
Toast.LENGTH_SHORT
).show()
} else {
val loginParams = Bundle().apply {
putString(LOGIN_EMAIL, email)
putString(LOGIN_PASSWORD, password)
}
musicServiceConnection.sendCommand(LOGIN, loginParams) { resultCode, _ ->
_loggedIn.postValue(resultCode == Activity.RESULT_OK)
}
}
}
fun generatePin(): CharSequence {
return String.format("%08d", Random().nextInt(99999999))
}
}
================================================
FILE: automotive/src/main/java/com/example/android/uamp/automotive/SignInLandingPageFragment.kt
================================================
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.uamp.automotive
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.text.InputType
import android.text.TextUtils
import android.text.method.LinkMovementMethod
import android.view.LayoutInflater
import android.view.View
import android.view.View.AUTOFILL_HINT_USERNAME
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.widget.Toolbar
import androidx.core.text.HtmlCompat
import androidx.fragment.app.Fragment
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.gms.common.api.ApiException
import com.google.android.gms.tasks.Task
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
const val RC_SIGN_IN = 9001
const val PLAY_SERVICES_RESOLUTION_REQUEST = 9000
// Control the supported sign in flows by toggling the constants below.
const val ENABLE_PIN_SIGN_IN = true
const val ENABLE_QR_CODE_SIGN_IN = true
const val ENABLE_GOOGLE_SIGN_IN = true
const val ENABLE_USERNAME_PASSWORD_SIGN_IN = true
/**
* A fragment that renders the landing screen for a sign-in flow. This screen can be configured
* to display third-party sign-in, PIN sign-in, QR-code sign-in and/or Google sign-in.
*/
class SignInLandingPageFragment : Fragment() {
companion object {
internal const val CAR_SIGN_IN_IDENTIFIER_KEY = "userID"
}
private lateinit var toolbar: Toolbar
private lateinit var appIcon: ImageView
private lateinit var phoneSignInButton: Button
private lateinit var googleSignInButton: Button
private lateinit var usernameAndPasswordSignInButton: Button
private lateinit var primaryTextView: TextView
private lateinit var identifierContainer: TextInputLayout
private lateinit var identifierInput: TextInputEditText
private lateinit var footerTextView: TextView
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val layout = if (ENABLE_USERNAME_PASSWORD_SIGN_IN)
R.layout.sign_in_landing_page_with_username_and_password
else R.layout.sign_in_landing_page
return inflater.inflate(layout, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val context = requireContext()
toolbar = view.findViewById(R.id.toolbar)
appIcon = view.findViewById(R.id.app_icon)
primaryTextView = view.findViewById(R.id.primary_message)
footerTextView = view.findViewById(R.id.footer)
phoneSignInButton = view.findViewById(R.id.phone_sign_in_button)
googleSignInButton = view.findViewById(R.id.google_sign_in_button)
if (ENABLE_USERNAME_PASSWORD_SIGN_IN) {
usernameAndPasswordSignInButton = view.findViewById(R.id.sign_in_button)
identifierContainer = view.findViewById(R.id.identifier_container)
identifierInput = view.findViewById(R.id.identifier_input)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
identifierInput.setAutofillHints(AUTOFILL_HINT_USERNAME)
}
}
toolbar.setNavigationOnClickListener { requireActivity().finish() }
appIcon.setImageDrawable(context.getDrawable(R.drawable.aural_logo))
primaryTextView.text = getString(R.string.sign_in_primary_text)
// Links in footer text should be clickable.
footerTextView.text = HtmlCompat.fromHtml(
context.getString(R.string.sign_in_footer),
HtmlCompat.FROM_HTML_MODE_LEGACY
)
footerTextView.movementMethod = LinkMovementMethod.getInstance()
configureUsernameAndPasswordSignIn()
configurePhoneSignIn()
configureGoogleSignIn()
}
private fun configureUsernameAndPasswordSignIn() {
if (!ENABLE_USERNAME_PASSWORD_SIGN_IN) {
return
}
identifierContainer.hint = getString(R.string.sign_in_user_id_hint)
identifierInput.inputType = InputType.TYPE_CLASS_TEXT
usernameAndPasswordSignInButton.text = getString(R.string.sign_in_next_button_label)
usernameAndPasswordSignInButton.setOnClickListener {
val identifier = identifierInput.text
if (TextUtils.isEmpty(identifier)) {
identifierInput.error = getString(R.string.sign_in_username_error)
} else {
val args = Bundle()
args.putString(CAR_SIGN_IN_IDENTIFIER_KEY, identifierInput.text.toString())
val fragment = UsernameAndPasswordSignInFragment()
fragment.arguments = args
requireActivity().supportFragmentManager.beginTransaction()
.replace(R.id.sign_in_container, fragment)
.addToBackStack("landingPage")
.commit()
}
}
}
private fun configurePhoneSignIn() {
if (!ENABLE_QR_CODE_SIGN_IN && !ENABLE_PIN_SIGN_IN) {
phoneSignInButton.visibility = View.GONE
return
}
lateinit var phoneSignInFragment: Fragment
if (ENABLE_QR_CODE_SIGN_IN && ENABLE_PIN_SIGN_IN) {
// Reduce the number of choices displayed to the user in a single screen. If both PIN
// and QR code sign in is enabled, separate the choice between the two options to a
// new screen.
phoneSignInFragment = PhoneSignInFragment()
} else if (ENABLE_PIN_SIGN_IN) {
phoneSignInFragment = PinCodeSignInFragment()
} else if (ENABLE_QR_CODE_SIGN_IN) {
phoneSignInFragment = QrCodeSignInFragment()
}
phoneSignInButton.text = getString(R.string.phone_sign_in_button_label)
phoneSignInButton.setOnClickListener {
requireActivity().supportFragmentManager.beginTransaction()
.replace(R.id.sign_in_container, phoneSignInFragment)
.addToBackStack("landingPage")
.commit()
}
}
/**
* Configure the Google sign in option on the landing page.
*
* <p>https://developers.google.com/identity/sign-in/android/start provides additional
* information on integrating Google sign in into your Android app.
*/
private fun configureGoogleSignIn() {
if (!ENABLE_GOOGLE_SIGN_IN or !checkPlayServices()) {
googleSignInButton.visibility = View.GONE
return
}
val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(getString(R.string.server_client_id))
.requestEmail()
.build()
googleSignInButton.text = getString(R.string.google_sign_in_button_label)
googleSignInButton.setOnClickListener {
val mGoogleSignInClient = GoogleSignIn.getClient(requireContext(), gso)
val signInIntent = mGoogleSignInClient.signInIntent
startActivityForResult(signInIntent, RC_SIGN_IN)
}
}
private fun checkPlayServices(): Boolean {
val apiAvailability = GoogleApiAvailability.getInstance();
val resultCode = apiAvailability.isGooglePlayServicesAvailable(context);
if (resultCode != ConnectionResult.SUCCESS) {
if (apiAvailability.isUserResolvableError(resultCode)) {
apiAvailability.getErrorDialog(
activity, resultCode, PLAY_SERVICES_RESOLUTION_REQUEST
).show();
}
return false;
}
return true;
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == RC_SIGN_IN) {
val task = GoogleSignIn.getSignedInAccountFromIntent(data)
handleGoogleSignIn(task)
}
}
private fun handleGoogleSignIn(completedTask: Task<GoogleSignInAccount>) {
try {
val account = completedTask.getResult(ApiException::class.java)
@Suppress("unused_variable") val idToken = account?.idToken
// Send ID Token to server and validate.
} catch (e: ApiException) {
// The ApiException status code indicates the detailed failure reason.
// Please refer to the GoogleSignInStatusCodes class reference for more information.
Toast.makeText(
requireContext(), getString(R.string.sign_in_failed_message, e.statusCode),
Toast.LENGTH_SHORT
)
.show()
}
}
}
================================================
FILE: automotive/src/main/java/com/example/android/uamp/automotive/UsernameAndPasswordSignInFragment.kt
================================================
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.uamp.automotive
import android.os.Build
import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
/**
* Fragment that is used to facilitates username and password sign-in.
*/
class UsernameAndPasswordSignInFragment : Fragment() {
private lateinit var toolbar: Toolbar
private lateinit var appIcon: ImageView
private lateinit var primaryTextView: TextView
private lateinit var passwordContainer: TextInputLayout
private lateinit var passwordInput: TextInputEditText
private lateinit var submitButton: Button
private lateinit var footerTextView: TextView
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.username_and_password_sign_in, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val context = requireContext()
toolbar = view.findViewById(R.id.toolbar)
appIcon = view.findViewById(R.id.app_icon)
primaryTextView = view.findViewById(R.id.primary_message)
passwordContainer = view.findViewById(R.id.password_container)
passwordInput = view.findViewById(R.id.password_input)
submitButton = view.findViewById(R.id.submit_button)
footerTextView = view.findViewById(R.id.footer)
toolbar.setNavigationOnClickListener {
requireActivity().supportFragmentManager.popBackStack()
}
appIcon.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.aural_logo))
primaryTextView.text = getString(R.string.username_and_password_sign_in_primary_text)
passwordContainer.hint = getString(R.string.password_hint)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
passwordInput.setAutofillHints(View.AUTOFILL_HINT_PASSWORD)
}
// Links in footer text should be clickable.
footerTextView.text = HtmlCompat.fromHtml(
context.getString(R.string.sign_in_footer),
HtmlCompat.FROM_HTML_MODE_LEGACY
)
footerTextView.movementMethod = LinkMovementMethod.getInstance()
// Get user identifier from previous screen.
val userId = arguments?.getString(SignInLandingPageFragment.CAR_SIGN_IN_IDENTIFIER_KEY)
submitButton.text = getString(R.string.sign_in_submit_button_label)
submitButton.setOnClickListener {
onSignIn(userId!!, passwordInput.text.toString())
}
}
private fun onSignIn(userIdentifier: CharSequence, password: CharSequence) {
ViewModelProvider(requireActivity())
.get(SignInActivityViewModel::class.java)
.login(userIdentifier.toString(), password.toString())
}
}
================================================
FILE: automotive/src/main/res/color/car_text_dark.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2019 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- Default text colors for car buttons when enabled/disabled. -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:alpha="0.40" android:color="@android:color/white" android:state_enabled="false" />
<item android:color="@android:color/black" />
</selector>
================================================
FILE: automotive/src/main/res/color/car_text_light.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2019 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- Default text colors for car buttons when enabled/disabled. -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:alpha="0.36" android:color="@android:color/white" android:state_enabled="false" />
<item android:alpha="0.72" android:color="@android:color/white" />
</selector>
================================================
FILE: automotive/src/main/res/drawable/default_button_background.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2019 Google Inc. All rights reserved.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<!-- Default background styles for car buttons when enabled/disabled. -->
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?attr/colorControlHighlight">
<item>
<shape android:shape="rectangle">
<corners android:radius="@dimen/car_button_radius" />
<solid android:color="@color/colorAccent" />
</shape>
</item>
</ripple>
================================================
FILE: automotive/src/main/res/drawable/google_sign_in_button_background.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2019 Google Inc. All rights reserved.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="@dimen/sign_in_button_corner_radius" />
<solid android:color="@color/google_sign_in_button_background_color" />
</shape>
================================================
FILE: automotive/src/main/res/drawable/google_sign_in_button_logo.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2019 Google Inc. All rights reserved.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:width="24dp"
android:height="24dp"
android:drawable="@drawable/google_logo" />
</layer-list>
================================================
FILE: automotive/src/main/res/drawable/ic_launcher_background.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2019 Google LLC
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
================================================
FILE: automotive/src/main/res/drawable/pin_background.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2019 Google Inc. All rights reserved.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="@dimen/sign_in_pin_container_corner_radius" />
<stroke
android:width="1dp"
android:color="@color/grey_300" />
</shape>
================================================
FILE: automotive/src/main/res/drawable/sign_in_button_background.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2019 Google Inc. All rights reserved.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?attr/colorControlHighlight">
<item>
<shape android:shape="rectangle">
<corners android:radius="@dimen/sign_in_button_corner_radius" />
<solid android:color="?attr/colorAccent" />
</shape>
</item>
</ripple>
================================================
FILE: automotive/src/main/res/drawable/sign_in_toolbar_back_icon.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2019 Google Inc. All rights reserved.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="@dimen/sign_in_toolbar_nav_button_size"
android:height="@dimen/sign_in_toolbar_nav_button_size"
android:viewportWidth="48"
android:viewportHeight="48">
<path android:pathData="M0 0h48v48H0z" />
<path
android:fillColor="#000000"
android:pathData="M40 22H15.66l11.17-11.17L24 8 8 24l16 16 2.83-2.83L15.66 26H40v-4z" />
</vector>
================================================
FILE: automotive/src/main/res/drawable/sign_in_toolbar_back_ripple_background.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2019 Google Inc. All rights reserved.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?attr/colorControlHighlight"
android:radius="@dimen/sign_in_toolbar_nav_button_size" />
================================================
FILE: automotive/src/main/res/drawable-v24/ic_launcher_foreground.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2019 Google LLC
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,1c-4.97,0 -9,4.03 -9,9v7c0,1.66 1.34,3 3,3h3v-8H5v-2c0,-3.87 3.13,-7 7,-7s7,3.13 7,7v2h-4v8h3c1.66,0 3,-1.34 3,-3v-7c0,-4.97 -4.03,-9 -9,-9z" />
</vector>
================================================
FILE: automotive/src/main/res/layout/activity_login.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2019 Google Inc. All rights reserved.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="0dp"
android:layout_height="0dp"
android:src="@drawable/ic_launcher_foreground"
android:tint="@color/colorPrimary"
app:layout_constraintBottom_toTopOf="@+id/email"
app:layout_constraintLeft_toLeftOf="@id/email"
app:layout_constraintRight_toRightOf="@id/email"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/email"
android:layout_width="400dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:hint="@string/email_hint"
android:inputType="textEmailAddress"
android:maxLines="1"
android:singleLine="true"
app:layout_constraintBottom_toTopOf="@+id/password"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<EditText
android:id="@+id/password"
android:layout_width="400dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:focusableInTouchMode="true"
android:hint="@string/password_hint"
android:imeActionLabel="@+id/login"
android:imeOptions="actionUnspecified"
android:inputType="textPassword"
android:maxLines="1"
android:singleLine="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/sign_in_button"
style="?android:textAppearanceSmall"
android:layout_width="400dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="16dp"
android:background="@color/colorAccent"
android:text="@string/login_button_label"
android:textColor="@android:color/black"
android:textStyle="bold"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/password" />
</androidx.constraintlayout.widget.ConstraintLayout>
================================================
FILE: automotive/src/main/res/layout/activity_settings.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2019 Google Inc. All rights reserved.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/settings_activity"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary" />
</com.google.android.material.appbar.AppBarLayout>
<FrameLayout
android:id="@+id/settings_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/darkBackground" />
</LinearLayout>
================================================
FILE: automotive/src/main/res/layout/activity_sign_in.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2019 Google Inc. All rights reserved.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/sign_in_container"
android:layout_width="match_parent"
android:layout_height="match_parent"></FrameLayout>
================================================
FILE: automotive/src/main/res/layout/phone_sign_in.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2019 Google Inc. All rights reserved.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/content_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/sign_in_toolbar_height">
<ImageView
android:id="@+id/app_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/sign_in_app_icon_top_margin"
android:adjustViewBounds="true"
android:maxHeight="@dimen/sign_in_app_icon_max_height"
android:scaleType="fitXY"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/primary_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/sign_in_primary_message_top_margin"
android:adjustViewBounds="true"
android:maxWidth="@dimen/sign_in_text_max_width"
android:textAppearance="@style/PrimaryText"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/app_icon" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/start_guideline"
android:layout_width="1dp"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="@dimen/sign_in_horizontal_start_guideline" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/end_guideline"
android:layout_width="1dp"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="@dimen/sign_in_horizontal_end_guideline" />
<Button
android:id="@+id/pin_sign_in_button"
style="@style/CarButton.SignIn"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/sign_in_button1_top_margin_no_input"
app:layout_constraintEnd_toEndOf="@id/end_guideline"
app:layout_constraintStart_toStartOf="@id/start_guideline"
app:layout_constraintTop_toBottomOf="@id/primary_message"
app:layout_constraintWidth_max="@dimen/sign_in_button_max_width" />
<Button
android:id="@+id/qr_sign_in_button"
style="@style/CarButton.SignIn"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/sign_in_button2_top_margin_no_input"
app:layout_constraintEnd_toEndOf="@id/end_guideline"
app:layout_constraintStart_toStartOf="@id/start_guideline"
app:layout_constraintTop_toBottomOf="@id/pin_sign_in_button"
app:layout_constraintWidth_max="@dimen/sign_in_button_max_width" />
<TextView
android:id="@+id/footer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/sign_in_footer_top_margin"
android:layout_marginBottom="@dimen/sign_in_footer_bottom_margin"
android:adjustViewBounds="true"
android:gravity="center"
android:maxWidth="@dimen/sign_in_text_max_width"
android:textAppearance="@style/SecondaryText"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/qr_sign_in_button" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent">
</androidx.appcompat.widget.Toolbar>
</FrameLayout>
================================================
FILE: automotive/src/main/res/layout/pin_item.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2019 Google Inc. All rights reserved.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/sign_in_pin_container_margin"
android:layout_marginEnd="@dimen/sign_in_pin_container_margin"
android:background="@drawable/pin_background"
android:paddingStart="@dimen/sign_in_pin_container_padding"
android:paddingEnd="@dimen/sign_in_pin_container_padding"
android:textAppearance="@style/DisplayText"
android:textColor="@color/colorAccent" />
================================================
FILE: automotive/src/main/res/layout/pin_sign_in.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2019 Google Inc. All rights reserved.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in complia
gitextract_ek5wbsm9/ ├── .github/ │ ├── scripts/ │ │ └── gradlew_recursive.sh │ └── workflows/ │ ├── android.yml │ └── copy-branch.yml ├── .gitignore ├── .google/ │ └── packaging.yaml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── TODO.md ├── app/ │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── com/ │ │ └── example/ │ │ └── android/ │ │ └── uamp/ │ │ ├── MainActivity.kt │ │ ├── MediaItemAdapter.kt │ │ ├── MediaItemData.kt │ │ ├── cast/ │ │ │ └── UampCastOptionsProvider.kt │ │ ├── fragments/ │ │ │ ├── MediaItemFragment.kt │ │ │ └── NowPlayingFragment.kt │ │ ├── utils/ │ │ │ ├── Event.kt │ │ │ └── InjectorUtils.kt │ │ └── viewmodels/ │ │ ├── MainActivityViewModel.kt │ │ ├── MediaItemFragmentViewModel.kt │ │ └── NowPlayingFragmentViewModel.kt │ └── res/ │ ├── drawable/ │ │ ├── ic_album_black_24dp.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_pause_black_24dp.xml │ │ ├── ic_play_arrow_black_24dp.xml │ │ ├── ic_signal_wifi_off_black_24dp.xml │ │ ├── media_item_background.xml │ │ ├── media_item_mask.xml │ │ └── media_overlay_background.xml │ ├── drawable-v24/ │ │ └── ic_launcher_foreground.xml │ ├── layout/ │ │ ├── activity_main.xml │ │ ├── cast_context_error.xml │ │ ├── fragment_mediaitem.xml │ │ ├── fragment_mediaitem_list.xml │ │ └── fragment_nowplaying.xml │ ├── mipmap-anydpi-v26/ │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ ├── values/ │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── xml/ │ └── automotive_app_desc.xml ├── automotive/ │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── com/ │ │ └── example/ │ │ └── android/ │ │ └── uamp/ │ │ └── automotive/ │ │ └── ExampleInstrumentedTest.java │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── com/ │ │ │ └── example/ │ │ │ └── android/ │ │ │ └── uamp/ │ │ │ └── automotive/ │ │ │ ├── AutomotiveMusicService.kt │ │ │ ├── PhoneSignInFragment.kt │ │ │ ├── PinCodeSignInFragment.kt │ │ │ ├── QrCodeSignInFragment.kt │ │ │ ├── SettingsActivity.kt │ │ │ ├── SettingsFragment.kt │ │ │ ├── SignInActivity.kt │ │ │ ├── SignInActivityViewModel.kt │ │ │ ├── SignInLandingPageFragment.kt │ │ │ └── UsernameAndPasswordSignInFragment.kt │ │ └── res/ │ │ ├── color/ │ │ │ ├── car_text_dark.xml │ │ │ └── car_text_light.xml │ │ ├── drawable/ │ │ │ ├── default_button_background.xml │ │ │ ├── google_sign_in_button_background.xml │ │ │ ├── google_sign_in_button_logo.xml │ │ │ ├── ic_launcher_background.xml │ │ │ ├── pin_background.xml │ │ │ ├── sign_in_button_background.xml │ │ │ ├── sign_in_toolbar_back_icon.xml │ │ │ └── sign_in_toolbar_back_ripple_background.xml │ │ ├── drawable-v24/ │ │ │ └── ic_launcher_foreground.xml │ │ ├── layout/ │ │ │ ├── activity_login.xml │ │ │ ├── activity_settings.xml │ │ │ ├── activity_sign_in.xml │ │ │ ├── phone_sign_in.xml │ │ │ ├── pin_item.xml │ │ │ ├── pin_sign_in.xml │ │ │ ├── preference.xml │ │ │ ├── preference_category.xml │ │ │ ├── qr_sign_in.xml │ │ │ ├── sign_in_landing_page.xml │ │ │ ├── sign_in_landing_page_with_username_and_password.xml │ │ │ └── username_and_password_sign_in.xml │ │ ├── layout-h900dp/ │ │ │ ├── phone_sign_in.xml │ │ │ ├── pin_sign_in.xml │ │ │ ├── qr_sign_in.xml │ │ │ ├── sign_in_landing_page.xml │ │ │ ├── sign_in_landing_page_with_username_and_password.xml │ │ │ └── username_and_password_sign_in.xml │ │ ├── mipmap-anydpi-v26/ │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ ├── values/ │ │ │ ├── colors.xml │ │ │ ├── dimens.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ ├── values-h1060dp/ │ │ │ └── dimens.xml │ │ └── xml/ │ │ ├── automotive_app_desc.xml │ │ └── preferences.xml │ └── test/ │ └── java/ │ └── com/ │ └── example/ │ └── android/ │ └── uamp/ │ └── automotive/ │ └── ExampleUnitTest.java ├── build.gradle ├── common/ │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── com/ │ │ │ └── example/ │ │ │ └── android/ │ │ │ └── uamp/ │ │ │ ├── common/ │ │ │ │ └── MusicServiceConnection.kt │ │ │ └── media/ │ │ │ ├── CastMediaItemConverter.kt │ │ │ ├── MusicService.kt │ │ │ ├── PackageValidator.kt │ │ │ ├── PersistentStorage.kt │ │ │ ├── UampNotificationManager.kt │ │ │ ├── extensions/ │ │ │ │ ├── FileExt.kt │ │ │ │ ├── JavaLangExt.kt │ │ │ │ ├── MediaMetadataCompatExt.kt │ │ │ │ └── PlaybackStateCompatExt.kt │ │ │ └── library/ │ │ │ ├── AlbumArtContentProvider.kt │ │ │ ├── BrowseTree.kt │ │ │ ├── JsonSource.kt │ │ │ └── MusicSource.kt │ │ └── res/ │ │ ├── drawable/ │ │ │ ├── ic_album.xml │ │ │ └── ic_recommended.xml │ │ ├── menu/ │ │ │ └── main_activity_menu.xml │ │ ├── values/ │ │ │ └── strings.xml │ │ └── xml/ │ │ └── allowed_media_browser_callers.xml │ └── test/ │ └── java/ │ └── com/ │ └── example/ │ └── android/ │ └── uamp/ │ └── media/ │ └── library/ │ └── MusicSourceTest.kt ├── docs/ │ ├── FAQs.md │ └── FullGuide.md ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat └── settings.gradle
SYMBOL INDEX (4 symbols across 2 files)
FILE: automotive/src/androidTest/java/com/example/android/uamp/automotive/ExampleInstrumentedTest.java
class ExampleInstrumentedTest (line 34) | @RunWith(AndroidJUnit4.class)
method useAppContext (line 36) | @Test
FILE: automotive/src/test/java/com/example/android/uamp/automotive/ExampleUnitTest.java
class ExampleUnitTest (line 28) | public class ExampleUnitTest {
method addition_isCorrect (line 29) | @Test
Condensed preview — 129 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (451K chars).
[
{
"path": ".github/scripts/gradlew_recursive.sh",
"chars": 1529,
"preview": "#!/bin/bash\n\n# Copyright (C) 2020 The Android Open Source Project\n#\n# Licensed under the Apache License, Version 2.0 (th"
},
{
"path": ".github/workflows/android.yml",
"chars": 1291,
"preview": "# Copyright (C) 2020 The Android Open Source Project\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");"
},
{
"path": ".github/workflows/copy-branch.yml",
"chars": 1014,
"preview": "# Duplicates default main branch to the old master branch\n\nname: Duplicates main to old master branch\n\n# Controls when t"
},
{
"path": ".gitignore",
"chars": 155,
"preview": "*.iml\n.gradle\n/local.properties\n/.idea\n.DS_Store\n/captures\n.externalNativeBuild\n\n# Generated files\nbuild/\n\n# Extra (cust"
},
{
"path": ".google/packaging.yaml",
"chars": 954,
"preview": "# GOOGLE SAMPLE PACKAGING DATA\n#\n# This file is used by Google as part of our samples packaging process.\n# End users may"
},
{
"path": "CONTRIBUTING.md",
"chars": 1612,
"preview": "# How to become a contributor and submit your own code\n\n## Contributor License Agreements\n\nWe'd love to accept your samp"
},
{
"path": "LICENSE",
"chars": 11361,
"preview": " Apache License\n Version 2.0, January 2004\n "
},
{
"path": "README.md",
"chars": 3167,
"preview": "> **Warning**\n> This sample has been deprecated and is no longer being maintained.\n> \n> To find other samples that may b"
},
{
"path": "TODO.md",
"chars": 771,
"preview": "TODOs\n=====\n\nThis file captures the high level goals of the project. This provides guidance for anyone who wants\nto cont"
},
{
"path": "app/build.gradle",
"chars": 2389,
"preview": "/*\n * Copyright 2017 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "app/proguard-rules.pro",
"chars": 751,
"preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
},
{
"path": "app/src/main/AndroidManifest.xml",
"chars": 3649,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2017 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "app/src/main/java/com/example/android/uamp/MainActivity.kt",
"chars": 4594,
"preview": "/*\n * Copyright 2017 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "app/src/main/java/com/example/android/uamp/MediaItemAdapter.kt",
"chars": 3649,
"preview": "/*\n * Copyright 2017 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "app/src/main/java/com/example/android/uamp/MediaItemData.kt",
"chars": 3483,
"preview": "/*\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "app/src/main/java/com/example/android/uamp/cast/UampCastOptionsProvider.kt",
"chars": 1836,
"preview": "/*\n * Copyright 2020 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "app/src/main/java/com/example/android/uamp/fragments/MediaItemFragment.kt",
"chars": 3559,
"preview": "/*\n * Copyright 2017 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "app/src/main/java/com/example/android/uamp/fragments/NowPlayingFragment.kt",
"chars": 4010,
"preview": "/*\n * Copyright 2019 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "app/src/main/java/com/example/android/uamp/utils/Event.kt",
"chars": 1361,
"preview": "/*\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "app/src/main/java/com/example/android/uamp/utils/InjectorUtils.kt",
"chars": 2508,
"preview": "/*\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "app/src/main/java/com/example/android/uamp/viewmodels/MainActivityViewModel.kt",
"chars": 7247,
"preview": "/*\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "app/src/main/java/com/example/android/uamp/viewmodels/MediaItemFragmentViewModel.kt",
"chars": 7597,
"preview": "/*\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "app/src/main/java/com/example/android/uamp/viewmodels/NowPlayingFragmentViewModel.kt",
"chars": 8211,
"preview": "/*\n * Copyright 2019 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "app/src/main/res/drawable/ic_album_black_24dp.xml",
"chars": 1134,
"preview": "<!--\n ~ Copyright 2017 Google Inc. All rights reserved.\n ~\n ~ Licensed under the Apache License, Version 2.0 (the \"Li"
},
{
"path": "app/src/main/res/drawable/ic_launcher_background.xml",
"chars": 6240,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2018 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "app/src/main/res/drawable/ic_pause_black_24dp.xml",
"chars": 948,
"preview": "<!--\n ~ Copyright 2017 Google Inc. All rights reserved.\n ~\n ~ Licensed under the Apache License, Version 2.0 (the \"Li"
},
{
"path": "app/src/main/res/drawable/ic_play_arrow_black_24dp.xml",
"chars": 923,
"preview": "<!--\n ~ Copyright 2017 Google Inc. All rights reserved.\n ~\n ~ Licensed under the Apache License, Version 2.0 (the \"Li"
},
{
"path": "app/src/main/res/drawable/ic_signal_wifi_off_black_24dp.xml",
"chars": 1140,
"preview": "<!--\n ~ Copyright 2018 Google Inc. All rights reserved.\n ~\n ~ Licensed under the Apache License, Version 2.0 (the \"Li"
},
{
"path": "app/src/main/res/drawable/media_item_background.xml",
"chars": 1345,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2018 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "app/src/main/res/drawable/media_item_mask.xml",
"chars": 1483,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2018 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "app/src/main/res/drawable/media_overlay_background.xml",
"chars": 938,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2019 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "app/src/main/res/drawable-v24/ic_launcher_foreground.xml",
"chars": 2514,
"preview": "<!--\n ~ Copyright 2017 Google Inc. All rights reserved.\n ~\n ~ Licensed under the Apache License, Version 2.0 (the \"Li"
},
{
"path": "app/src/main/res/layout/activity_main.xml",
"chars": 976,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2017 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "app/src/main/res/layout/cast_context_error.xml",
"chars": 956,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2018 The Android Open Source Project\n\n Licensed under the "
},
{
"path": "app/src/main/res/layout/fragment_mediaitem.xml",
"chars": 3792,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2017 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "app/src/main/res/layout/fragment_mediaitem_list.xml",
"chars": 1971,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2017 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "app/src/main/res/layout/fragment_nowplaying.xml",
"chars": 4742,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2019 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
"chars": 904,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2018 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
"chars": 904,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2018 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "app/src/main/res/values/colors.xml",
"chars": 1164,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2017 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "app/src/main/res/values/dimens.xml",
"chars": 1837,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2017 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "app/src/main/res/values/strings.xml",
"chars": 1274,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2017 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "app/src/main/res/values/styles.xml",
"chars": 2031,
"preview": "<!--\n ~ Copyright 2017 Google Inc. All rights reserved.\n ~\n ~ Licensed under the Apache License, Version 2.0 (the \"Li"
},
{
"path": "app/src/main/res/xml/automotive_app_desc.xml",
"chars": 898,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2018 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "automotive/build.gradle",
"chars": 2392,
"preview": "/*\n * Copyright 2019 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
},
{
"path": "automotive/proguard-rules.pro",
"chars": 751,
"preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
},
{
"path": "automotive/src/androidTest/java/com/example/android/uamp/automotive/ExampleInstrumentedTest.java",
"chars": 1335,
"preview": "/*\n * Copyright 2019 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
},
{
"path": "automotive/src/main/AndroidManifest.xml",
"chars": 3156,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n ~ Copyright 2019 Google LLC\n ~\n ~ Licensed under the Apache License, Vers"
},
{
"path": "automotive/src/main/java/com/example/android/uamp/automotive/AutomotiveMusicService.kt",
"chars": 7103,
"preview": "/*\n * Copyright 2019 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "automotive/src/main/java/com/example/android/uamp/automotive/PhoneSignInFragment.kt",
"chars": 2809,
"preview": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "automotive/src/main/java/com/example/android/uamp/automotive/PinCodeSignInFragment.kt",
"chars": 2997,
"preview": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "automotive/src/main/java/com/example/android/uamp/automotive/QrCodeSignInFragment.kt",
"chars": 2357,
"preview": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "automotive/src/main/java/com/example/android/uamp/automotive/SettingsActivity.kt",
"chars": 1665,
"preview": "/*\n * Copyright 2019 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
},
{
"path": "automotive/src/main/java/com/example/android/uamp/automotive/SettingsFragment.kt",
"chars": 2352,
"preview": "/*\n * Copyright 2019 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "automotive/src/main/java/com/example/android/uamp/automotive/SignInActivity.kt",
"chars": 1577,
"preview": "/*\n * Copyright 2019 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
},
{
"path": "automotive/src/main/java/com/example/android/uamp/automotive/SignInActivityViewModel.kt",
"chars": 2306,
"preview": "/*\n * Copyright 2019 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
},
{
"path": "automotive/src/main/java/com/example/android/uamp/automotive/SignInLandingPageFragment.kt",
"chars": 9710,
"preview": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "automotive/src/main/java/com/example/android/uamp/automotive/UsernameAndPasswordSignInFragment.kt",
"chars": 3991,
"preview": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "automotive/src/main/res/color/car_text_dark.xml",
"chars": 963,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright (C) 2019 The Android Open Source Project\n\n Licensed under t"
},
{
"path": "automotive/src/main/res/color/car_text_light.xml",
"chars": 984,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright (C) 2019 The Android Open Source Project\n\n Licensed under t"
},
{
"path": "automotive/src/main/res/drawable/default_button_background.xml",
"chars": 1077,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2019 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "automotive/src/main/res/drawable/google_sign_in_button_background.xml",
"chars": 923,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2019 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "automotive/src/main/res/drawable/google_sign_in_button_logo.xml",
"chars": 878,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2019 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "automotive/src/main/res/drawable/ic_launcher_background.xml",
"chars": 6218,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2019 Google LLC\n ~\n ~ Licensed under the Apache License, Ver"
},
{
"path": "automotive/src/main/res/drawable/pin_background.xml",
"chars": 937,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2019 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "automotive/src/main/res/drawable/sign_in_button_background.xml",
"chars": 1013,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2019 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "automotive/src/main/res/drawable/sign_in_toolbar_back_icon.xml",
"chars": 1122,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2019 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "automotive/src/main/res/drawable/sign_in_toolbar_back_ripple_background.xml",
"chars": 850,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2019 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "automotive/src/main/res/drawable-v24/ic_launcher_foreground.xml",
"chars": 1071,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2019 Google LLC\n ~\n ~ Licensed under the Apache License, Ver"
},
{
"path": "automotive/src/main/res/layout/activity_login.xml",
"chars": 3214,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2019 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "automotive/src/main/res/layout/activity_settings.xml",
"chars": 1565,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2019 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "automotive/src/main/res/layout/activity_sign_in.xml",
"chars": 880,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2019 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "automotive/src/main/res/layout/phone_sign_in.xml",
"chars": 5548,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2019 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "automotive/src/main/res/layout/pin_item.xml",
"chars": 1228,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2019 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "automotive/src/main/res/layout/pin_sign_in.xml",
"chars": 4810,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2019 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "automotive/src/main/res/layout/preference.xml",
"chars": 1158,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2019 Google LLC\n ~\n ~ Licensed under the Apache License, Ver"
},
{
"path": "automotive/src/main/res/layout/preference_category.xml",
"chars": 1157,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2019 Google LLC\n ~\n ~ Licensed under the Apache License, Ver"
},
{
"path": "automotive/src/main/res/layout/qr_sign_in.xml",
"chars": 4816,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2019 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "automotive/src/main/res/layout/sign_in_landing_page.xml",
"chars": 6398,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2019 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "automotive/src/main/res/layout/sign_in_landing_page_with_username_and_password.xml",
"chars": 7929,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2019 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "automotive/src/main/res/layout/username_and_password_sign_in.xml",
"chars": 6599,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2019 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "automotive/src/main/res/layout-h900dp/phone_sign_in.xml",
"chars": 5182,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2019 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "automotive/src/main/res/layout-h900dp/pin_sign_in.xml",
"chars": 4499,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2019 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "automotive/src/main/res/layout-h900dp/qr_sign_in.xml",
"chars": 4497,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2019 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "automotive/src/main/res/layout-h900dp/sign_in_landing_page.xml",
"chars": 5916,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2019 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "automotive/src/main/res/layout-h900dp/sign_in_landing_page_with_username_and_password.xml",
"chars": 7438,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2019 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "automotive/src/main/res/layout-h900dp/username_and_password_sign_in.xml",
"chars": 6041,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2019 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "automotive/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
"chars": 904,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2018 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "automotive/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
"chars": 882,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2019 Google LLC\n ~\n ~ Licensed under the Apache License, Ver"
},
{
"path": "automotive/src/main/res/values/colors.xml",
"chars": 1267,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2019 Google LLC\n ~\n ~ Licensed under the Apache License, Ver"
},
{
"path": "automotive/src/main/res/values/dimens.xml",
"chars": 2560,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2019 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "automotive/src/main/res/values/strings.xml",
"chars": 3053,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2019 Google LLC\n ~\n ~ Licensed under the Apache License, Ver"
},
{
"path": "automotive/src/main/res/values/styles.xml",
"chars": 4539,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2019 Google LLC\n ~\n ~ Licensed under the Apache License, Ver"
},
{
"path": "automotive/src/main/res/values-h1060dp/dimens.xml",
"chars": 890,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2019 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "automotive/src/main/res/xml/automotive_app_desc.xml",
"chars": 899,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2018 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "automotive/src/main/res/xml/preferences.xml",
"chars": 1089,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2019 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "automotive/src/test/java/com/example/android/uamp/automotive/ExampleUnitTest.java",
"chars": 1003,
"preview": "/*\n * Copyright 2019 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
},
{
"path": "build.gradle",
"chars": 2429,
"preview": "/*\n * Copyright 2017 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "common/build.gradle",
"chars": 2942,
"preview": "/*\n * Copyright 2017 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "common/proguard-rules.pro",
"chars": 751,
"preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
},
{
"path": "common/src/main/AndroidManifest.xml",
"chars": 1820,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2017 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "common/src/main/java/com/example/android/uamp/common/MusicServiceConnection.kt",
"chars": 7813,
"preview": "/*\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "common/src/main/java/com/example/android/uamp/media/CastMediaItemConverter.kt",
"chars": 3591,
"preview": "package com.example.android.uamp.media\n\nimport android.net.Uri\nimport android.support.v4.media.MediaMetadataCompat\nimpor"
},
{
"path": "common/src/main/java/com/example/android/uamp/media/MusicService.kt",
"chars": 27462,
"preview": "/*\n * Copyright 2017 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "common/src/main/java/com/example/android/uamp/media/PackageValidator.kt",
"chars": 15057,
"preview": "/*\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "common/src/main/java/com/example/android/uamp/media/PersistentStorage.kt",
"chars": 4266,
"preview": "/*\n * Copyright 2020 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "common/src/main/java/com/example/android/uamp/media/UampNotificationManager.kt",
"chars": 5472,
"preview": "/*\n * Copyright 2020 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "common/src/main/java/com/example/android/uamp/media/extensions/FileExt.kt",
"chars": 1143,
"preview": "/*\n * Copyright 2019 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "common/src/main/java/com/example/android/uamp/media/extensions/JavaLangExt.kt",
"chars": 1730,
"preview": "/*\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "common/src/main/java/com/example/android/uamp/media/extensions/MediaMetadataCompatExt.kt",
"chars": 11503,
"preview": "/*\n * Copyright 2017 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "common/src/main/java/com/example/android/uamp/media/extensions/PlaybackStateCompatExt.kt",
"chars": 3061,
"preview": "/*\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "common/src/main/java/com/example/android/uamp/media/library/AlbumArtContentProvider.kt",
"chars": 3059,
"preview": "/*\n * Copyright 2019 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "common/src/main/java/com/example/android/uamp/media/library/BrowseTree.kt",
"chars": 6720,
"preview": "/*\n * Copyright 2019 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "common/src/main/java/com/example/android/uamp/media/library/JsonSource.kt",
"chars": 8790,
"preview": "/*\n * Copyright 2017 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "common/src/main/java/com/example/android/uamp/media/library/MusicSource.kt",
"chars": 7879,
"preview": "/*\n * Copyright 2017 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "common/src/main/res/drawable/ic_album.xml",
"chars": 1079,
"preview": "<!--\n ~ Copyright 2019 Google LLC\n ~\n ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n ~ you may n"
},
{
"path": "common/src/main/res/drawable/ic_recommended.xml",
"chars": 1036,
"preview": "<!--\n ~ Copyright 2019 Google LLC\n ~\n ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n ~ you may n"
},
{
"path": "common/src/main/res/menu/main_activity_menu.xml",
"chars": 1035,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2017 The Android Open Source Project\n\n Licensed under the "
},
{
"path": "common/src/main/res/values/strings.xml",
"chars": 2018,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2018 Google Inc. All rights reserved.\n ~\n ~ Licensed under t"
},
{
"path": "common/src/main/res/xml/allowed_media_browser_callers.xml",
"chars": 4418,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\nCopyright (C) 2018 The Android Open Source Project\n\nLicensed under the Apach"
},
{
"path": "common/src/test/java/com/example/android/uamp/media/library/MusicSourceTest.kt",
"chars": 5420,
"preview": "/*\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "docs/FAQs.md",
"chars": 703,
"preview": "# Frequently Asked Questions\n\n## How can I change the music which UAMP plays?\nUAMP reads its [music catalog](https://sto"
},
{
"path": "docs/FullGuide.md",
"chars": 11286,
"preview": "# Full Guide to UAMP\n\nThe Universal Android Music Player (UAMP) is an example music player app for Android written in [K"
},
{
"path": "gradle/wrapper/gradle-wrapper.properties",
"chars": 233,
"preview": "#Thu Jun 18 22:31:29 CEST 2020\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER"
},
{
"path": "gradle.properties",
"chars": 1450,
"preview": "#\n# Copyright 2017 Google Inc. All rights reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n"
},
{
"path": "gradlew",
"chars": 4971,
"preview": "#!/usr/bin/env bash\n\n##############################################################################\n##\n## Gradle start "
},
{
"path": "gradlew.bat",
"chars": 2404,
"preview": "@if \"%DEBUG%\" == \"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@r"
},
{
"path": "settings.gradle",
"chars": 1740,
"preview": "/*\n * Copyright 2017 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the android/uamp GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 129 files (414.6 KB), approximately 98.7k tokens, and a symbol index with 4 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.