first commit

This commit is contained in:
Volkova2007. 2026-06-26 03:26:01 +04:00
commit 8750da4c24
922 changed files with 70197 additions and 0 deletions

6
.editorconfig Normal file
View File

@ -0,0 +1,6 @@
# https://editorconfig.org/
# This configuration is used by ktlint when spotless invokes it
[*.{kt,kts}]
ij_kotlin_allow_trailing_comma=true
ij_kotlin_allow_trailing_comma_on_call_site=true

56
.gitignore vendored Normal file
View File

@ -0,0 +1,56 @@
# Gradle
.gradle
build/
captures
**/local.properties
# IntelliJ .idea folder
.idea/workspace.xml
.idea/libraries
.idea/caches
.idea/navEditor.xml
.idea/tasks.xml
.idea/modules.xml
.idea/compiler.xml
.idea/jarRepositories.xml
.idea/deploymentTargetDropDown.xml
.idea/deploymentTargetSelector.xml
.idea/misc.xml
.idea/androidTestResultsUserPreferences.xml
.idea/inspectionProfiles
.idea/vcs.xml
# Ignore all auto-generated configs except for app.xml. It's needed as template for the build
.idea/runConfigurations
!.idea/runConfigurations/app.xml
gradle.xml
*.iml
# General
.DS_Store
.externalNativeBuild
# Do not commit plain-text signing info
release/*.properties
release/*.gpg
# VS Code config
org.eclipse.buildship.core.prefs
.classpath
.project
# Temporary API docs
docs/api
package-list-coil-base
# Mkdocs temporary serving folder
docs-gen
site
*.bak
.idea/appInsightsSettings.xml
#TFLite
*.tflite

41
.google/packaging.yaml Normal file
View File

@ -0,0 +1,41 @@
# Copyright (C) 2022 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.
#
# 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, Platform]
categories:
- Permissions
- Camera
- Accessibility
- Connectivity
- Location
- Privacy
- UserInterface
- AndroidPlatform
- JetpackComposeA11y
- JetpackComposePermissions
solutions:
- Mobile
- Platform
- SDK
- Jetpack
languages: [Kotlin]
github: android/platform-samples
level: INTERMEDIATE
license: apache2

3
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

1
.idea/.name Normal file
View File

@ -0,0 +1 @@
Platform Samples

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="WizardSettings">
<option name="children">
<map>
<entry key="vectorWizard">
<value>
<PersistentState>
<option name="children">
<map>
<entry key="vectorAssetStep">
<value>
<PersistentState>
<option name="children">
<map>
<entry key="clipartAsset">
<value>
<PersistentState>
<option name="values">
<map>
<entry key="url" value="file:$USER_HOME$/Library/Android/sdk/icons/material/materialicons/search/baseline_search_24.xml" />
</map>
</option>
</PersistentState>
</value>
</entry>
</map>
</option>
<option name="values">
<map>
<entry key="outputName" value="ic_search" />
</map>
</option>
</PersistentState>
</value>
</entry>
</map>
</option>
</PersistentState>
</value>
</entry>
</map>
</option>
</component>
</project>

View File

@ -0,0 +1,124 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="ALLOW_TRAILING_COMMA" value="true" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>

View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

6
.idea/copyright/AOSP.xml Normal file
View File

@ -0,0 +1,6 @@
<component name="CopyrightManager">
<copyright>
<option name="notice" value="Copyright 2023 The Android Open Source Project&#10; &#10;Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);&#10;you may not use this file except in compliance with the License.&#10;You may obtain a copy of the License at&#10;&#10; https://www.apache.org/licenses/LICENSE-2.0&#10;&#10;Unless required by applicable law or agreed to in writing, software&#10;distributed under the License is distributed on an &quot;AS IS&quot; BASIS,&#10;WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.&#10;See the License for the specific language governing permissions and&#10;limitations under the License." />
<option name="myName" value="AOSP" />
</copyright>
</component>

View File

@ -0,0 +1,7 @@
<component name="CopyrightManager">
<settings default="AOSP">
<module2copyright>
<element module="All" copyright="AOSP" />
</module2copyright>
</settings>
</component>

6
.idea/kotlinc.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="2.1.10" />
</component>
</project>

8
.idea/markdown.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MarkdownSettings">
<option name="previewPanelProviderInfo">
<ProviderInfo name="Compose (experimental)" className="com.intellij.markdown.compose.preview.ComposePanelProvider" />
</option>
</component>
</project>

10
.idea/migrations.xml Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>

1
CODEOWNERS Normal file
View File

@ -0,0 +1 @@
* @yrezgui @ashnohe @madebymozart @alabiaga

63
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,63 @@
# Google Open Source Community Guidelines
At Google, we recognize and celebrate the creativity and collaboration of open
source contributors and the diversity of skills, experiences, cultures, and
opinions they bring to the projects and communities they participate in.
Every one of Google's open source projects and communities are inclusive
environments, based on treating all individuals respectfully, regardless of
gender identity and expression, sexual orientation, disabilities,
neurodiversity, physical appearance, body size, ethnicity, nationality, race,
age, religion, or similar personal characteristic.
We value diverse opinions, but we value respectful behavior more.
Respectful behavior includes:
* Being considerate, kind, constructive, and helpful.
* Not engaging in demeaning, discriminatory, harassing, hateful, sexualized, or
physically threatening behavior, speech, and imagery.
* Not engaging in unwanted physical contact.
Some Google open source projects [may adopt][] an explicit project code of
conduct, which may have additional detailed expectations for participants. Most
of those projects will use our [modified Contributor Covenant][].
[may adopt]: https://opensource.google/docs/releasing/preparing/#conduct
[modified Contributor Covenant]: https://opensource.google/docs/releasing/template/CODE_OF_CONDUCT/
## Resolve peacefully
We do not believe that all conflict is necessarily bad; healthy debate and
disagreement often yields positive results. However, it is never okay to be
disrespectful.
If you see someone behaving disrespectfully, you are encouraged to address the
behavior directly with those involved. Many issues can be resolved quickly and
easily, and this gives people more control over the outcome of their dispute.
If you are unable to resolve the matter for any reason, or if the behavior is
threatening or harassing, report it. We are dedicated to providing an
environment where participants feel welcome and safe.
## Reporting problems
Some Google open source projects may adopt a project-specific code of conduct.
In those cases, a Google employee will be identified as the Project Steward,
who will receive and handle reports of code of conduct violations. In the event
that a project hasnt identified a Project Steward, you can report problems by
emailing opensource@google.com.
We will investigate every complaint, but you may not receive a direct response.
We will use our discretion in determining when and how to follow up on reported
incidents, which may range from not taking action to permanent expulsion from
the project and project-sponsored spaces. We will notify the accused of the
report and provide them an opportunity to discuss it before any action is
taken. The identity of the reporter will be omitted from the details of the
report supplied to the accused. In potentially harmful situations, such as
ongoing harassment or threats to anyone's safety, we may take action without
notice.
*This document was adapted from the [IndieWeb Code of Conduct][] and can also
be found at <https://opensource.google/conduct/>.*
[IndieWeb Code of Conduct]: https://indieweb.org/code-of-conduct

28
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,28 @@
# How to Contribute
We'd love to accept your patches and contributions to this project. There are
just a few small guidelines you need to follow.
## Contributor License Agreement
Contributions to this project must be accompanied by a Contributor License
Agreement. You (or your employer) retain the copyright to your contribution;
this simply gives us permission to use and redistribute your contributions as
part of the project. Head over to <https://cla.developers.google.com/> to see
your current agreements on file or to sign a new one.
You generally only need to submit a CLA once, so if you've already submitted one
(even if it was for a different project), you probably don't need to do it
again.
## Code reviews
All submissions, including submissions by project members, require review. We
use GitHub pull requests for this purpose. Consult
[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
information on using pull requests.
## Community Guidelines
This project follows
[Google's Open Source Community Guidelines](https://opensource.google.com/conduct/).

201
LICENSE Normal file
View File

@ -0,0 +1,201 @@
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 [yyyy] [name of copyright owner]
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.

68
README.md Normal file
View File

@ -0,0 +1,68 @@
![Build](https://github.com/android/platform-samples/actions/workflows/build.yml/badge.svg)
# Android Platform Samples
This repository contains a collection of samples that demonstrate the use of different Android OS platform APIs. The samples are organized into folders by topic, and each folder contains a README file that provides more information about the samples in that folder.
> **Note:** These samples are intended to showcase specific functionality in isolation, and they may use
> simplified code. They are not intended to be used as production-ready code.
> For best practices follow our documentation and check
> [Now In Android](https://github.com/android/nowinandroid)
Browse the samples inside each topic samples folder:
- [Accessibility](https://github.com/android/platform-samples/tree/main/samples/accessibility)
- [Camera](https://github.com/android/platform-samples/tree/main/samples/camera)
- [Connectivity](https://github.com/android/platform-samples/tree/main/samples/connectivity)
- [Graphics](https://github.com/android/platform-samples/tree/main/samples/graphics)
- [Location](https://github.com/android/platform-samples/tree/main/samples/location)
- [Privacy](https://github.com/android/platform-samples/tree/main/samples/privacy)
- [Storage](https://github.com/android/platform-samples/tree/main/samples/storage)
- [User-interface](https://github.com/android/platform-samples/tree/main/samples/user-interface)
- More to come...
We are constantly adding new samples to this repository. You can find a list of all the available samples [here](https://github.com/android/platform-samples/tree/main/samples).
## How to run
1. Clone the repository
2. Open the whole project in Android Studio.
3. Sync & Run `app` configuration
The app will open with the samples list screen that allows you to navigate throughout the different
categories and available samples.
> **Note:** the `app` module is required to bring together all the samples but it's not relevant
> for their functionality, you can simply ignore it. The wiring is done under the hood and an
> implementation detail not needed to understand any sample functionality.
## Reporting Issues
You can report an [issue with a sample](https://github.com/android/platform-samples/issues) using
this repository. When doing so, make sure to specify which sample you are referring to.
## Contributions
Please contribute! We will gladly review any pull requests.
Make sure to read the [Contributing](CONTRIBUTING.md) page first though.
> Note: make sure to run `./gradlew --init-script gradle/spotless-init.gradle.kts spotlessApply` before
> submitting PRs.
## License
```
Copyright 2023 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.
```

1
app/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

24
app/README.md Normal file
View File

@ -0,0 +1,24 @@
# Platform Samples app
This module contains the necessary infrastructure using the [casa-android framework](https://github.com/google/casa-android)
to wire all the samples together and offer a launch point.
**You can ignore the code in this modules since it's not relevant for the samples**
## License
```
Copyright 2023 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.
```

113
app/build.gradle.kts Normal file
View File

@ -0,0 +1,113 @@
/*
* Copyright 2022 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.
*/
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
kotlin("plugin.serialization") version "2.1.10"
}
android {
namespace = "com.example.platform"
compileSdk = 37
defaultConfig {
applicationId = "com.example.platform"
minSdk = 24
targetSdk = 37
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
buildFeatures {
compose = true
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.androidx.navigation.compose)
implementation(libs.kotlinx.serialization.json)
implementation(libs.androidx.fragment.compose)
implementation(project(":shared"))
implementation(project(":samples:accessibility"))
implementation(project(":samples:camera:camera2"))
implementation(project(":samples:camera:camerax"))
implementation(project(":samples:connectivity:audio"))
implementation(project(":samples:connectivity:bluetooth:ble"))
implementation(project(":samples:connectivity:bluetooth:companion"))
implementation(project(":samples:connectivity:callnotification"))
implementation(project(":samples:connectivity:telecom"))
implementation(project(":samples:connectivity:UwbRanging"))
implementation(project(":samples:graphics:pdf"))
implementation(project(":samples:graphics:ultrahdr"))
implementation(project(":samples:location"))
implementation(project(":samples:media:ultrahdr"))
implementation(project(":samples:media:video"))
implementation(project(":samples:privacy:data"))
implementation(project(":samples:privacy:permissions"))
implementation(project(":samples:privacy:transparency"))
implementation(project(":samples:storage"))
implementation(project(":samples:user-interface:appwidgets"))
implementation(project(":samples:user-interface:constraintlayout"))
implementation(project(":samples:user-interface:draganddrop"))
implementation(project(":samples:user-interface:haptics"))
implementation(project(":samples:user-interface:live-updates"))
implementation(project(":samples:user-interface:picture-in-picture"))
implementation(project(":samples:user-interface:predictiveback"))
implementation(project(":samples:user-interface:quicksettings"))
implementation(project(":samples:user-interface:share"))
implementation(project(":samples:user-interface:text"))
implementation(project(":samples:user-interface:window-insets"))
implementation(project(":samples:user-interface:windowmanager"))
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
}

21
app/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# 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

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2023 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!--
Modules/samples below use a lib that requires a minSdk higher than that of the.
main app. Rather than updating the main app's minSdk, override those libraries minSdk.
Removing this would otherwise cause a Manifest merger issue.
See https://developer.android.com/build/manage-manifests#merge_conflict_heuristics
-->
<uses-sdk tools:overrideLibrary="com.example.platform.storage, androidx.photopicker.compose, com.example.platform.connectivity.uwb" />
<application
android:enableOnBackInvokedCallback="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true">
<activity
android:name=".app.MainActivity"
android:exported="true"
android:theme="@style/Theme.PlatformSamples">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.settings.APP_NOTIFICATION_PROMOTION_SETTINGS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<!--required for TFLite/LiteRT style transfer demo -->
<uses-library
android:name="libOpenCL.so"
android:required="false" />
<uses-library
android:name="libOpenCL-pixel.so"
android:required="false" />
</application>
</manifest>

View File

@ -0,0 +1,238 @@
/*
* Copyright 2023 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.
*/
package com.example.platform.app
import kotlinx.serialization.Serializable
@Serializable
data class ApiSurface(override val id: String, override val name: String, override val description: String? = null) : CatalogItem
val AccessiblityApiSurface = ApiSurface(
"accessiblity",
"Accessibility",
null,
)
val CameraCamera2ApiSurface = ApiSurface(
"camera-camera2",
"Camera2",
null,
)
val CameraCameraXApiSurface = ApiSurface(
"camera-camerax",
"CameraX",
null,
)
val ConnectivityAudioApiSurface = ApiSurface(
"connectivity-audio",
"Connectivity Audio",
null,
)
val ConnectivityBluetoothBleApiSurface = ApiSurface(
"connectivity-bluetooth-ble",
"Connectivity Bluetooth BLE",
null,
)
val ConnectivityBluetoothCompanionApiSurface = ApiSurface(
"connectivity-bluetooth-companion",
"Connectivity Bluetooth Companion",
null,
)
val ConnectivityCallNotificationApiSurface = ApiSurface(
"connectivity-call-notification",
"Connectivity Call Notification",
null,
)
val ConnectivityTelecomApiSurface = ApiSurface(
"connectivity-telecom",
"Connectivity Telecom",
null,
)
val GraphicsPdfApiSurface = ApiSurface(
"graphics-pdf",
"Graphics PDF",
null,
)
val GraphicsUltraHdrApiSurface = ApiSurface(
"graphics-ultrahdr",
"Graphics UltraHDR",
null,
)
val LocationApiSurface = ApiSurface(
"location",
"Location",
null,
)
val MediaUltraHdrApiSurface = ApiSurface(
"media-ultrahdr",
"Media UltraHDR",
null,
)
val MediaVideoApiSurface = ApiSurface(
"media-video",
"Media Video",
null,
)
val PrivacyDataApiSurface = ApiSurface(
"privacy-data",
"Privacy Data",
null,
)
val PrivacyPermissionsApiSurface = ApiSurface(
"privacy-permissions",
"Privacy Permissions",
null,
)
val PrivacyTransparencyApiSurface = ApiSurface(
"privacy-transparency",
"Privacy Transparency",
null,
)
val StorageApiSurface = ApiSurface(
"storage",
"Storage",
"""Android photo library access capabilities.
Photo Picker for unified device and cloud photo access, and MediaStore for detailed local media querying.
WARNING: These samples only work on devices with Android 14 or higher.""".trimMargin()
)
val UserInterfaceAppWidgetsApiSurface = ApiSurface(
"user-interface-app-widgets",
"User Interface - App Widgets",
null,
)
val UserInterfaceConstraintLayoutApiSurface = ApiSurface(
"user-interface-constraint-layout",
"User Interface - Constraint Layout",
null,
)
val UserInterfaceDragAndDropApiSurface = ApiSurface(
"user-interface-draganddrop",
"User Interface - Drag and Drop",
null,
)
val UserInterfaceHapticsApiSurface = ApiSurface(
"user-interface-haptics",
"User Interface - Haptics",
null,
)
val UserInterfaceLiveUpdatesApiSurface = ApiSurface(
"live-updates",
"User Interface - Live Updates",
null,
)
val UserInterfacePictureInPictureApiSurface = ApiSurface(
"user-interface-picture-in-picture",
"User Interface - Picture In Picture",
null,
)
val UserInterfacePredictiveBackApiSurface = ApiSurface(
"user-interface-predictive-back",
"User Interface - Predictive Back",
null,
)
val UserInterfaceQuickSettingsApiSurface = ApiSurface(
"user-interface-quick-settings",
"User Interface - Quick Settings",
null,
)
val UserInterfaceShareApiSurface = ApiSurface(
"user-interface-share",
"User Interface - Share",
null,
)
val UserInterfaceTextApiSurface = ApiSurface(
"user-interface-text",
"User Interface - Text",
null,
)
val UserInterfaceWindowInsetsApiSurface = ApiSurface(
"user-interface-window-insets",
"User Interface - Window Insets",
null,
)
val UserInterfaceWindowManagerApiSurface = ApiSurface(
"user-interface-window-manager",
"User Interface - Window Manager",
null,
)
val ConnectivityUwbRangingApiSurface = ApiSurface(
"connectivity-uwb-ranging",
"Connectivity UWB Ranging",
null,
)
val API_SURFACES = listOf(
AccessiblityApiSurface,
CameraCamera2ApiSurface,
CameraCameraXApiSurface,
ConnectivityAudioApiSurface,
ConnectivityBluetoothBleApiSurface,
ConnectivityBluetoothCompanionApiSurface,
ConnectivityCallNotificationApiSurface,
ConnectivityTelecomApiSurface,
ConnectivityUwbRangingApiSurface,
GraphicsPdfApiSurface,
GraphicsUltraHdrApiSurface,
LocationApiSurface,
MediaUltraHdrApiSurface,
MediaVideoApiSurface,
PrivacyDataApiSurface,
PrivacyPermissionsApiSurface,
PrivacyTransparencyApiSurface,
StorageApiSurface,
UserInterfaceAppWidgetsApiSurface,
UserInterfaceConstraintLayoutApiSurface,
UserInterfaceDragAndDropApiSurface,
UserInterfaceHapticsApiSurface,
UserInterfaceLiveUpdatesApiSurface,
UserInterfacePictureInPictureApiSurface,
UserInterfacePredictiveBackApiSurface,
UserInterfaceQuickSettingsApiSurface,
UserInterfaceShareApiSurface,
UserInterfaceTextApiSurface,
UserInterfaceWindowInsetsApiSurface,
UserInterfaceWindowManagerApiSurface
).associateBy { it.id }

View File

@ -0,0 +1,122 @@
/*
* Copyright 2023 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.
*/
package com.example.platform.app
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
/**
* Represents an API surface or sample.
*/
interface CatalogItem {
val id: String
val name: String
val description: String?
}
/**
* Screen rendering list of items with their descriptions (API surfaces or samples)
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CatalogScreen(
item: CatalogItem,
subItems: Map<String, CatalogItem>,
onNavigateToSubItem: (CatalogItem) -> Unit = {},
) {
val list = remember { subItems.values.toList() }
Scaffold(
topBar = {
TopAppBar(
title = { Text(item.name) },
)
},
) { paddingValues ->
LazyColumn(
modifier = Modifier
.padding(paddingValues)
.padding(8.dp),
) {
item {
Text(
text = "Samples",
style = MaterialTheme.typography.bodyLarge,
)
Spacer(modifier = Modifier.height(16.dp))
}
items(list) { subItem ->
CatalogScreenItem(item = subItem, onClick = onNavigateToSubItem)
Spacer(modifier = Modifier.height(16.dp))
}
}
}
}
@Composable
fun CatalogScreenItem(
item: CatalogItem,
onClick: (CatalogItem) -> Unit,
) {
OutlinedCard(onClick = { onClick(item) }, modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(text = item.name, style = MaterialTheme.typography.titleSmall)
if (item.description != null) {
Text(
text = item.description!!,
style = MaterialTheme.typography.bodySmall,
)
}
}
Icon(
imageVector = Icons.AutoMirrored.Default.KeyboardArrowRight,
contentDescription = null,
modifier = Modifier.align(Alignment.CenterVertically),
)
}
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright 2023 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.
*/
package com.example.platform.app
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.fragment.app.FragmentActivity
import com.example.platform.shared.theme.CatalogTheme
class MainActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
CatalogTheme {
NavGraph()
}
}
}
}

View File

@ -0,0 +1,19 @@
import android.app.Application
/*
* Copyright 2023 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.
*/
class MainApp : Application()

View File

@ -0,0 +1,104 @@
/*
* Copyright 2023 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.
*/
package com.example.platform.app
import android.content.Intent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import androidx.navigation.toRoute
import kotlinx.serialization.Serializable
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NavGraph() {
val context = LocalContext.current
val navController = rememberNavController()
NavHost(navController = navController, startDestination = MainScreen) {
composable<MainScreen> {
CatalogScreen(
item = MainScreen,
subItems = API_SURFACES,
onNavigateToSubItem = { apiSurface -> navController.navigate(apiSurface as ApiSurface) },
)
}
composable<ApiSurface> { backStackEntry ->
val apiSurface = backStackEntry.toRoute<ApiSurface>()
val subItems = remember { SAMPLE_DEMOS.filter { it.value.apiSurface == apiSurface } }
CatalogScreen(
item = apiSurface,
subItems = subItems,
onNavigateToSubItem = { sampleDemo ->
if (sampleDemo is ActivitySampleDemo) {
context.startActivity(Intent(context, sampleDemo.content))
} else {
navController.navigate("$SAMPLE_DEMO_ROUTE/${sampleDemo.id}")
}
},
)
}
composable(
"$SAMPLE_DEMO_ROUTE/{$SAMPLE_DEMO_ID}",
listOf(navArgument(SAMPLE_DEMO_ID) { type = NavType.StringType }),
) { backStackEntry ->
val arguments = requireNotNull(backStackEntry.arguments)
val sampleDemoId = requireNotNull(arguments.getString(SAMPLE_DEMO_ID))
val sampleDemo = SAMPLE_DEMOS.getValue(sampleDemoId) as ComposableSampleDemo
Scaffold(
topBar = { TopAppBar(title = { Text(sampleDemo.name) }) },
) { paddingValues ->
Box(
modifier = Modifier
.padding(paddingValues)
.padding(8.dp)
.fillMaxSize(),
) {
sampleDemo.content()
}
}
}
}
}
@Serializable
data object MainScreen : CatalogItem {
override val id = "main"
override val name = "Platform Samples"
override val description = null
}
const val SAMPLE_DEMO_ROUTE = "sampledemo"
const val SAMPLE_DEMO_ID = "id"

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2023 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.
-->
<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:pathData="M0,0h108v108h-108z">
<aapt:attr name="android:fillColor">
<gradient
android:centerX="-1.25"
android:centerY="121.75"
android:gradientRadius="166.55"
android:type="radial">
<item
android:color="#FFD7EFFE"
android:offset="0.02" />
<item
android:color="#FFEFF7CF"
android:offset="0.99" />
</gradient>
</aapt:attr>
</path>
</vector>

View File

@ -0,0 +1,237 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2023 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.
-->
<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:fillColor="#C2A772"
android:pathData="M48.75,21.27C51.81,18.78 56.19,18.78 59.25,21.27V21.27C61.09,22.77 63.48,23.41 65.82,23.03V23.03C69.71,22.41 73.51,24.6 74.91,28.28V28.28C75.76,30.5 77.5,32.24 79.72,33.09V33.09C83.4,34.49 85.59,38.29 84.97,42.18V42.18C84.59,44.52 85.23,46.91 86.73,48.75V48.75C89.22,51.81 89.22,56.19 86.73,59.25V59.25C85.23,61.09 84.59,63.48 84.97,65.82V65.82C85.59,69.71 83.4,73.51 79.72,74.91V74.91C77.5,75.76 75.76,77.5 74.91,79.72V79.72C73.51,83.4 69.71,85.59 65.82,84.97V84.97C63.48,84.59 61.09,85.23 59.25,86.73V86.73C56.19,89.22 51.81,89.22 48.75,86.73V86.73C46.91,85.23 44.52,84.59 42.18,84.97V84.97C38.29,85.59 34.49,83.4 33.09,79.72V79.72C32.24,77.5 30.5,75.76 28.28,74.91V74.91C24.6,73.51 22.41,69.71 23.03,65.82V65.82C23.41,63.48 22.77,61.09 21.27,59.25V59.25C18.78,56.19 18.78,51.81 21.27,48.75V48.75C22.77,46.91 23.41,44.52 23.03,42.18V42.18C22.41,38.29 24.6,34.49 28.28,33.09V33.09C30.5,32.24 32.24,30.5 33.09,28.28V28.28C34.49,24.6 38.29,22.41 42.18,23.03V23.03C44.52,23.41 46.91,22.77 48.75,21.27V21.27Z" />
<path
android:fillAlpha="0.8"
android:pathData="M48.75,21.27C51.81,18.78 56.19,18.78 59.25,21.27V21.27C61.09,22.77 63.48,23.41 65.82,23.03V23.03C69.71,22.41 73.51,24.6 74.91,28.28V28.28C75.76,30.5 77.5,32.24 79.72,33.09V33.09C83.4,34.49 85.59,38.29 84.97,42.18V42.18C84.59,44.52 85.23,46.91 86.73,48.75V48.75C89.22,51.81 89.22,56.19 86.73,59.25V59.25C85.23,61.09 84.59,63.48 84.97,65.82V65.82C85.59,69.71 83.4,73.51 79.72,74.91V74.91C77.5,75.76 75.76,77.5 74.91,79.72V79.72C73.51,83.4 69.71,85.59 65.82,84.97V84.97C63.48,84.59 61.09,85.23 59.25,86.73V86.73C56.19,89.22 51.81,89.22 48.75,86.73V86.73C46.91,85.23 44.52,84.59 42.18,84.97V84.97C38.29,85.59 34.49,83.4 33.09,79.72V79.72C32.24,77.5 30.5,75.76 28.28,74.91V74.91C24.6,73.51 22.41,69.71 23.03,65.82V65.82C23.41,63.48 22.77,61.09 21.27,59.25V59.25C18.78,56.19 18.78,51.81 21.27,48.75V48.75C22.77,46.91 23.41,44.52 23.03,42.18V42.18C22.41,38.29 24.6,34.49 28.28,33.09V33.09C30.5,32.24 32.24,30.5 33.09,28.28V28.28C34.49,24.6 38.29,22.41 42.18,23.03V23.03C44.52,23.41 46.91,22.77 48.75,21.27V21.27Z">
<aapt:attr name="android:fillColor">
<gradient
android:centerX="83.29"
android:centerY="45.73"
android:gradientRadius="35.08"
android:type="radial">
<item
android:color="#FFBA9562"
android:offset="0" />
<item
android:color="#21E8CE74"
android:offset="0.43" />
<item
android:color="#00F2ECD6"
android:offset="1" />
</gradient>
</aapt:attr>
</path>
<path
android:fillAlpha="0.8"
android:pathData="M48.75,21.27C51.81,18.78 56.19,18.78 59.25,21.27V21.27C61.09,22.77 63.48,23.41 65.82,23.03V23.03C69.71,22.41 73.51,24.6 74.91,28.28V28.28C75.76,30.5 77.5,32.24 79.72,33.09V33.09C83.4,34.49 85.59,38.29 84.97,42.18V42.18C84.59,44.52 85.23,46.91 86.73,48.75V48.75C89.22,51.81 89.22,56.19 86.73,59.25V59.25C85.23,61.09 84.59,63.48 84.97,65.82V65.82C85.59,69.71 83.4,73.51 79.72,74.91V74.91C77.5,75.76 75.76,77.5 74.91,79.72V79.72C73.51,83.4 69.71,85.59 65.82,84.97V84.97C63.48,84.59 61.09,85.23 59.25,86.73V86.73C56.19,89.22 51.81,89.22 48.75,86.73V86.73C46.91,85.23 44.52,84.59 42.18,84.97V84.97C38.29,85.59 34.49,83.4 33.09,79.72V79.72C32.24,77.5 30.5,75.76 28.28,74.91V74.91C24.6,73.51 22.41,69.71 23.03,65.82V65.82C23.41,63.48 22.77,61.09 21.27,59.25V59.25C18.78,56.19 18.78,51.81 21.27,48.75V48.75C22.77,46.91 23.41,44.52 23.03,42.18V42.18C22.41,38.29 24.6,34.49 28.28,33.09V33.09C30.5,32.24 32.24,30.5 33.09,28.28V28.28C34.49,24.6 38.29,22.41 42.18,23.03V23.03C44.52,23.41 46.91,22.77 48.75,21.27V21.27Z">
<aapt:attr name="android:fillColor">
<gradient
android:centerX="46.71"
android:centerY="42.51"
android:gradientRadius="49.23"
android:type="radial">
<item
android:color="#FFF5E7B4"
android:offset="0" />
<item
android:color="#FFF9E9AF"
android:offset="0.24" />
<item
android:color="#FFF5C872"
android:offset="0.86" />
<item
android:color="#00F5C172"
android:offset="1" />
</gradient>
</aapt:attr>
</path>
<path
android:fillAlpha="0.3"
android:pathData="M48.75,21.27C51.81,18.78 56.19,18.78 59.25,21.27V21.27C61.09,22.77 63.48,23.41 65.82,23.03V23.03C69.71,22.41 73.51,24.6 74.91,28.28V28.28C75.76,30.5 77.5,32.24 79.72,33.09V33.09C83.4,34.49 85.59,38.29 84.97,42.18V42.18C84.59,44.52 85.23,46.91 86.73,48.75V48.75C89.22,51.81 89.22,56.19 86.73,59.25V59.25C85.23,61.09 84.59,63.48 84.97,65.82V65.82C85.59,69.71 83.4,73.51 79.72,74.91V74.91C77.5,75.76 75.76,77.5 74.91,79.72V79.72C73.51,83.4 69.71,85.59 65.82,84.97V84.97C63.48,84.59 61.09,85.23 59.25,86.73V86.73C56.19,89.22 51.81,89.22 48.75,86.73V86.73C46.91,85.23 44.52,84.59 42.18,84.97V84.97C38.29,85.59 34.49,83.4 33.09,79.72V79.72C32.24,77.5 30.5,75.76 28.28,74.91V74.91C24.6,73.51 22.41,69.71 23.03,65.82V65.82C23.41,63.48 22.77,61.09 21.27,59.25V59.25C18.78,56.19 18.78,51.81 21.27,48.75V48.75C22.77,46.91 23.41,44.52 23.03,42.18V42.18C22.41,38.29 24.6,34.49 28.28,33.09V33.09C30.5,32.24 32.24,30.5 33.09,28.28V28.28C34.49,24.6 38.29,22.41 42.18,23.03V23.03C44.52,23.41 46.91,22.77 48.75,21.27V21.27Z">
<aapt:attr name="android:fillColor">
<gradient
android:centerX="45.87"
android:centerY="41.81"
android:gradientRadius="41.97"
android:type="radial">
<item
android:color="#FFF5C872"
android:offset="0" />
<item
android:color="#FFBA9562"
android:offset="1" />
</gradient>
</aapt:attr>
</path>
<path
android:fillAlpha="0.8"
android:pathData="M48.75,21.27C51.81,18.78 56.19,18.78 59.25,21.27V21.27C61.09,22.77 63.48,23.41 65.82,23.03V23.03C69.71,22.41 73.51,24.6 74.91,28.28V28.28C75.76,30.5 77.5,32.24 79.72,33.09V33.09C83.4,34.49 85.59,38.29 84.97,42.18V42.18C84.59,44.52 85.23,46.91 86.73,48.75V48.75C89.22,51.81 89.22,56.19 86.73,59.25V59.25C85.23,61.09 84.59,63.48 84.97,65.82V65.82C85.59,69.71 83.4,73.51 79.72,74.91V74.91C77.5,75.76 75.76,77.5 74.91,79.72V79.72C73.51,83.4 69.71,85.59 65.82,84.97V84.97C63.48,84.59 61.09,85.23 59.25,86.73V86.73C56.19,89.22 51.81,89.22 48.75,86.73V86.73C46.91,85.23 44.52,84.59 42.18,84.97V84.97C38.29,85.59 34.49,83.4 33.09,79.72V79.72C32.24,77.5 30.5,75.76 28.28,74.91V74.91C24.6,73.51 22.41,69.71 23.03,65.82V65.82C23.41,63.48 22.77,61.09 21.27,59.25V59.25C18.78,56.19 18.78,51.81 21.27,48.75V48.75C22.77,46.91 23.41,44.52 23.03,42.18V42.18C22.41,38.29 24.6,34.49 28.28,33.09V33.09C30.5,32.24 32.24,30.5 33.09,28.28V28.28C34.49,24.6 38.29,22.41 42.18,23.03V23.03C44.52,23.41 46.91,22.77 48.75,21.27V21.27Z">
<aapt:attr name="android:fillColor">
<gradient
android:centerX="30.59"
android:centerY="54"
android:gradientRadius="26.67"
android:type="radial">
<item
android:color="#FFFBF8EE"
android:offset="0" />
<item
android:color="#00D1C189"
android:offset="0.97" />
<item
android:color="#00D1C189"
android:offset="1" />
</gradient>
</aapt:attr>
</path>
<path
android:fillAlpha="0.8"
android:pathData="M48.75,21.27C51.81,18.78 56.19,18.78 59.25,21.27V21.27C61.09,22.77 63.48,23.41 65.82,23.03V23.03C69.71,22.41 73.51,24.6 74.91,28.28V28.28C75.76,30.5 77.5,32.24 79.72,33.09V33.09C83.4,34.49 85.59,38.29 84.97,42.18V42.18C84.59,44.52 85.23,46.91 86.73,48.75V48.75C89.22,51.81 89.22,56.19 86.73,59.25V59.25C85.23,61.09 84.59,63.48 84.97,65.82V65.82C85.59,69.71 83.4,73.51 79.72,74.91V74.91C77.5,75.76 75.76,77.5 74.91,79.72V79.72C73.51,83.4 69.71,85.59 65.82,84.97V84.97C63.48,84.59 61.09,85.23 59.25,86.73V86.73C56.19,89.22 51.81,89.22 48.75,86.73V86.73C46.91,85.23 44.52,84.59 42.18,84.97V84.97C38.29,85.59 34.49,83.4 33.09,79.72V79.72C32.24,77.5 30.5,75.76 28.28,74.91V74.91C24.6,73.51 22.41,69.71 23.03,65.82V65.82C23.41,63.48 22.77,61.09 21.27,59.25V59.25C18.78,56.19 18.78,51.81 21.27,48.75V48.75C22.77,46.91 23.41,44.52 23.03,42.18V42.18C22.41,38.29 24.6,34.49 28.28,33.09V33.09C30.5,32.24 32.24,30.5 33.09,28.28V28.28C34.49,24.6 38.29,22.41 42.18,23.03V23.03C44.52,23.41 46.91,22.77 48.75,21.27V21.27Z">
<aapt:attr name="android:fillColor">
<gradient
android:centerX="63.11"
android:centerY="37.18"
android:gradientRadius="18.39"
android:type="radial">
<item
android:color="#FFFBF8EE"
android:offset="0" />
<item
android:color="#00F1DB8D"
android:offset="0.84" />
</gradient>
</aapt:attr>
</path>
<path
android:fillAlpha="0.8"
android:pathData="M48.75,21.27C51.81,18.78 56.19,18.78 59.25,21.27V21.27C61.09,22.77 63.48,23.41 65.82,23.03V23.03C69.71,22.41 73.51,24.6 74.91,28.28V28.28C75.76,30.5 77.5,32.24 79.72,33.09V33.09C83.4,34.49 85.59,38.29 84.97,42.18V42.18C84.59,44.52 85.23,46.91 86.73,48.75V48.75C89.22,51.81 89.22,56.19 86.73,59.25V59.25C85.23,61.09 84.59,63.48 84.97,65.82V65.82C85.59,69.71 83.4,73.51 79.72,74.91V74.91C77.5,75.76 75.76,77.5 74.91,79.72V79.72C73.51,83.4 69.71,85.59 65.82,84.97V84.97C63.48,84.59 61.09,85.23 59.25,86.73V86.73C56.19,89.22 51.81,89.22 48.75,86.73V86.73C46.91,85.23 44.52,84.59 42.18,84.97V84.97C38.29,85.59 34.49,83.4 33.09,79.72V79.72C32.24,77.5 30.5,75.76 28.28,74.91V74.91C24.6,73.51 22.41,69.71 23.03,65.82V65.82C23.41,63.48 22.77,61.09 21.27,59.25V59.25C18.78,56.19 18.78,51.81 21.27,48.75V48.75C22.77,46.91 23.41,44.52 23.03,42.18V42.18C22.41,38.29 24.6,34.49 28.28,33.09V33.09C30.5,32.24 32.24,30.5 33.09,28.28V28.28C34.49,24.6 38.29,22.41 42.18,23.03V23.03C44.52,23.41 46.91,22.77 48.75,21.27V21.27Z">
<aapt:attr name="android:fillColor">
<gradient
android:centerX="50.36"
android:centerY="54.98"
android:gradientRadius="31.98"
android:type="radial">
<item
android:color="#FFFBF8EE"
android:offset="0" />
<item
android:color="#00F1DB8D"
android:offset="1" />
</gradient>
</aapt:attr>
</path>
<path
android:fillType="evenOdd"
android:pathData="M36.61,62.62C36.77,62.24 37.22,62.05 37.61,62.2L40.86,63.46C41.92,62.22 43.3,61.2 44.93,60.52C46.56,59.84 48.25,59.59 49.88,59.7L51.28,56.51C51.45,56.13 51.9,55.95 52.29,56.1C52.67,56.25 52.85,56.68 52.69,57.06L51.43,59.93C54.53,60.62 57.22,62.69 58.5,65.76V65.76C58.55,65.89 58.49,66.03 58.36,66.09L39.41,73.97C39.29,74.03 39.14,73.97 39.09,73.84V73.84C37.81,70.77 38.24,67.4 39.93,64.71L37.01,63.58C36.62,63.43 36.44,63 36.61,62.62Z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="48.89"
android:endY="70.03"
android:startX="42.93"
android:startY="60.31"
android:type="linear">
<item
android:color="#FF8EEC77"
android:offset="0" />
<item
android:color="#E568AD57"
android:offset="1" />
</gradient>
</aapt:attr>
</path>
<path
android:fillAlpha="0.7"
android:fillType="evenOdd"
android:pathData="M36.61,62.62C36.77,62.24 37.22,62.05 37.61,62.2L40.86,63.46C41.92,62.22 43.3,61.2 44.93,60.52C46.56,59.84 48.25,59.59 49.88,59.7L51.28,56.51C51.45,56.13 51.9,55.95 52.29,56.1C52.67,56.25 52.85,56.68 52.69,57.06L51.43,59.93C54.53,60.62 57.22,62.69 58.5,65.76V65.76C58.55,65.89 58.49,66.03 58.36,66.09L39.41,73.97C39.29,74.03 39.14,73.97 39.09,73.84V73.84C37.81,70.77 38.24,67.4 39.93,64.71L37.01,63.58C36.62,63.43 36.44,63 36.61,62.62Z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="47.41"
android:endY="66.05"
android:startX="39.98"
android:startY="62.27"
android:type="linear">
<item
android:color="#E568AD57"
android:offset="0" />
<item
android:color="#FF8EEC77"
android:offset="1" />
</gradient>
</aapt:attr>
</path>
<path android:pathData="M68.36,57.31C68.22,57.29 68.09,57.24 67.96,57.17C67.85,57.08 67.76,56.98 67.71,56.86L63.01,50.07C62.57,49.44 62.25,48.76 62.07,48.01C61.89,47.26 61.86,46.52 61.99,45.8C62.23,44.51 62.83,43.48 63.79,42.74C64.77,41.99 65.82,41.72 66.95,41.92C67.52,42.03 68.06,42.25 68.59,42.6C69.12,42.95 69.57,43.39 69.95,43.92L70.61,44.92L71.52,44.25C72.08,43.86 72.67,43.59 73.29,43.45C73.9,43.31 74.49,43.29 75.06,43.39C76.2,43.6 77.1,44.22 77.76,45.26C78.42,46.29 78.63,47.45 78.39,48.74C78.26,49.48 77.98,50.17 77.54,50.81C77.11,51.43 76.58,51.96 75.94,52.41L69.14,57.12C69.01,57.21 68.88,57.27 68.75,57.31C68.61,57.34 68.49,57.34 68.36,57.31Z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="74.12"
android:endY="59.15"
android:startX="70.8"
android:startY="43.87"
android:type="linear">
<item
android:color="#FF8BFFEA"
android:offset="0" />
<item
android:color="#FF379382"
android:offset="1" />
</gradient>
</aapt:attr>
</path>
<path android:pathData="M44.28,31.93C44.43,31.51 44.99,31.43 45.26,31.78L48.42,35.92C48.53,36.05 48.68,36.13 48.85,36.14L54.06,36.3C54.51,36.31 54.76,36.81 54.51,37.18L51.55,41.47C51.45,41.61 51.43,41.78 51.47,41.95L52.93,46.95C53.05,47.38 52.66,47.77 52.23,47.64L47.24,46.16C47.07,46.11 46.9,46.14 46.76,46.23L42.45,49.16C42.08,49.41 41.59,49.16 41.57,48.72L41.44,43.5C41.44,43.34 41.36,43.18 41.23,43.08L37.11,39.88C36.76,39.61 36.85,39.06 37.26,38.91L42.18,37.18C42.34,37.12 42.46,37 42.52,36.84L44.28,31.93Z">
<aapt:attr name="android:fillColor">
<gradient
android:centerX="52.17"
android:centerY="41.69"
android:gradientRadius="10.8"
android:type="radial">
<item
android:color="#FFDC6EB7"
android:offset="0" />
<item
android:color="#E5F4C9F8"
android:offset="1" />
</gradient>
</aapt:attr>
</path>
<path
android:fillAlpha="0.2"
android:pathData="M44.28,31.93C44.43,31.51 44.99,31.43 45.26,31.78L48.42,35.92C48.53,36.05 48.68,36.13 48.85,36.14L54.06,36.3C54.51,36.31 54.76,36.81 54.51,37.18L51.55,41.47C51.45,41.61 51.43,41.78 51.47,41.95L52.93,46.95C53.05,47.38 52.66,47.77 52.23,47.64L47.24,46.16C47.07,46.11 46.9,46.14 46.76,46.23L42.45,49.16C42.08,49.41 41.59,49.16 41.57,48.72L41.44,43.5C41.44,43.34 41.36,43.18 41.23,43.08L37.11,39.88C36.76,39.61 36.85,39.06 37.26,38.91L42.18,37.18C42.34,37.12 42.46,37 42.52,36.84L44.28,31.93Z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="46.18"
android:endY="38.35"
android:startX="44.92"
android:startY="36.54"
android:type="linear">
<item
android:color="#FFDF63B5"
android:offset="0" />
<item
android:color="#E5F4C9F8"
android:offset="1" />
</gradient>
</aapt:attr>
</path>
</vector>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2023 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.
-->
<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="#1E1E1C"
android:fillType="evenOdd"
android:pathData="M56.02,22.61C54.84,21.67 53.16,21.67 51.98,22.61L50.28,23.98C49.42,24.66 48.27,24.86 47.23,24.51L45.16,23.81C43.73,23.33 42.16,23.91 41.37,25.19L40.23,27.06C39.66,28 38.65,28.58 37.55,28.61L35.37,28.66C33.86,28.69 32.58,29.77 32.28,31.25L31.85,33.39C31.63,34.47 30.88,35.36 29.86,35.76L27.82,36.56C26.42,37.11 25.58,38.56 25.81,40.05L26.14,42.21C26.3,43.3 25.9,44.39 25.08,45.12L23.44,46.56C22.31,47.56 22.01,49.21 22.74,50.53L23.78,52.45C24.31,53.42 24.31,54.58 23.78,55.55L22.74,57.47C22.01,58.79 22.31,60.44 23.44,61.44L25.08,62.88C25.9,63.61 26.3,64.7 26.14,65.79L25.81,67.95C25.58,69.44 26.42,70.89 27.82,71.44L29.86,72.24C30.88,72.64 31.63,73.53 31.85,74.61L32.28,76.75C32.58,78.23 33.86,79.31 35.37,79.34L37.55,79.39C38.65,79.42 39.66,80 40.23,80.94L41.37,82.81C42.16,84.09 43.73,84.67 45.16,84.19L47.23,83.49C48.27,83.14 49.42,83.34 50.28,84.02L51.98,85.39C53.16,86.33 54.84,86.33 56.02,85.39L57.72,84.02C58.58,83.34 59.73,83.14 60.77,83.49L62.84,84.19C64.27,84.67 65.84,84.09 66.63,82.81L67.77,80.94C68.34,80 69.35,79.42 70.45,79.39L72.63,79.34C74.14,79.31 75.42,78.23 75.72,76.75L76.15,74.61C76.37,73.53 77.12,72.64 78.14,72.24L80.18,71.44C81.58,70.89 82.42,69.44 82.19,67.95L81.86,65.79C81.7,64.7 82.1,63.61 82.92,62.88L84.56,61.44C85.69,60.44 85.99,58.79 85.26,57.47L84.21,55.55C83.69,54.58 83.69,53.42 84.21,52.45L85.26,50.53C85.99,49.21 85.69,47.56 84.56,46.56L82.92,45.12C82.1,44.39 81.7,43.3 81.86,42.21L82.19,40.05C82.42,38.56 81.58,37.11 80.18,36.56L78.14,35.76C77.12,35.36 76.37,34.47 76.15,33.39L75.72,31.25C75.42,29.77 74.14,28.69 72.63,28.66L70.45,28.61C69.35,28.58 68.34,28 67.77,27.06L66.63,25.19C65.84,23.91 64.27,23.33 62.84,23.81L60.77,24.51C59.73,24.86 58.58,24.66 57.72,23.98L56.02,22.61ZM39.38,61.34C39.04,61.21 38.64,61.37 38.49,61.71C38.34,62.05 38.5,62.44 38.84,62.57L41.45,63.58C39.94,65.98 39.56,68.98 40.7,71.72C40.75,71.83 40.88,71.88 40.99,71.84L57.89,64.8C58,64.76 58.06,64.63 58.01,64.51C56.87,61.78 54.47,59.93 51.7,59.31L52.83,56.75C52.98,56.41 52.82,56.03 52.47,55.89C52.13,55.76 51.72,55.93 51.58,56.27L50.33,59.11C48.87,59 47.36,59.23 45.91,59.84C44.46,60.44 43.23,61.35 42.28,62.46L39.38,61.34ZM66.83,57.13C66.7,57.1 66.58,57.06 66.47,57C66.37,56.92 66.29,56.83 66.24,56.72L62.03,50.62C61.63,50.06 61.35,49.44 61.18,48.77C61.02,48.1 60.99,47.44 61.11,46.79C61.32,45.62 61.86,44.71 62.72,44.04C63.6,43.37 64.55,43.12 65.56,43.3C66.07,43.4 66.56,43.6 67.03,43.91C67.51,44.23 67.91,44.62 68.25,45.1L68.84,46L69.66,45.4C70.17,45.05 70.69,44.81 71.25,44.68C71.8,44.55 72.33,44.53 72.84,44.62C73.87,44.81 74.67,45.37 75.26,46.3C75.85,47.23 76.04,48.27 75.83,49.43C75.71,50.09 75.46,50.71 75.07,51.29C74.68,51.84 74.2,52.32 73.63,52.72L67.52,56.95C67.41,57.03 67.29,57.09 67.17,57.12C67.05,57.14 66.94,57.15 66.83,57.13ZM46.17,34.13C45.93,33.81 45.44,33.89 45.3,34.26L43.72,38.67C43.67,38.81 43.56,38.92 43.42,38.97L39,40.53C38.63,40.66 38.55,41.15 38.86,41.4L42.56,44.27C42.68,44.36 42.75,44.5 42.75,44.65L42.87,49.33C42.88,49.72 43.33,49.95 43.65,49.73L47.53,47.1C47.65,47.01 47.81,46.99 47.95,47.03L52.44,48.37C52.82,48.48 53.17,48.13 53.06,47.75L51.75,43.25C51.71,43.11 51.73,42.95 51.82,42.83L54.48,38.97C54.7,38.64 54.48,38.2 54.08,38.19L49.4,38.04C49.25,38.04 49.11,37.97 49.02,37.85L46.17,34.13Z" />
</vector>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2023 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.
-->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -0,0 +1,22 @@
<!--
~ Copyright 2023 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.
-->
<resources>
<string name="app_name">Platform Samples</string>
<string name="source_base_url">https://github.com/android/platform-samples/tree/main/samples/%1$s</string>
<string name="documentation_base_url">https://developer.android.com/about</string>
<string name="bug_report_url">https://github.com/android/platform-samples/issues/new?template=bug_report.md&amp;title=%1$s</string>
</resources>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2023 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.
-->
<resources>
<style name="Theme.PlatformSamples" parent="android:Theme.Material.Light.NoActionBar" />
</resources>

22
build.gradle.kts Normal file
View File

@ -0,0 +1,22 @@
/*
* Copyright 2022 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.
*/
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.protobuf) apply false
}

40
checksum.sh Normal file
View File

@ -0,0 +1,40 @@
#!/bin/bash
#
# Copyright 2023 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.
#
RESULT_FILE=$1
if [ -f $RESULT_FILE ]; then
rm $RESULT_FILE
fi
touch $RESULT_FILE
checksum_file() {
echo $(openssl md5 $1 | awk '{print $2}')
}
FILES=()
while read -r -d ''; do
FILES+=("$REPLY")
done < <(find . -type f \( -name "build.gradle*" -o -name "*.versions.toml" -o -name "gradle-wrapper.properties" \) -print0)
# Loop through files and append MD5 to result file
for FILE in ${FILES[@]}; do
echo $(checksum_file $FILE) >> $RESULT_FILE
done
# Now sort the file so that it is idempotent
sort $RESULT_FILE -o $RESULT_FILE

71
gradle.properties Normal file
View File

@ -0,0 +1,71 @@
#
# Copyright 2023 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.
#
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Ensure important default jvmargs aren't overwritten. See https://github.com/gradle/gradle/issues/19750
org.gradle.jvmargs=-Xmx6g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:+UseParallelGC -XX:MaxMetaspaceSize=2g
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
# To avoid validating resources in the project on every debug build.
android.disableResourceValidation=true
# Enable caching between builds.
org.gradle.caching=true
# Disable if causes issues
org.gradle.unsafe.configuration-cache=false
# Disable auto installation of Java toolchains for reproducible builds
org.gradle.java.installations.auto-download=false
# Disable build features that are enabled by default,
# https://developer.android.com/studio/releases/gradle-plugin#buildFeatures
android.defaults.buildfeatures.resvalues=false
android.defaults.buildfeatures.shaders=false
android.sdk.defaultTargetSdkToCompileSdkIfUnset=false
android.enableAppCompileTimeRClass=false
android.usesSdkInManifest.disallowed=false
android.uniquePackageNames=false
android.dependency.useConstraints=true
android.r8.strictFullModeForKeepRules=false
android.r8.optimizedResourceShrinking=false
android.builtInKotlin=false
android.newDsl=false

226
gradle/libs.versions.toml Normal file
View File

@ -0,0 +1,226 @@
#
# Copyright 2022 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.
#
[versions]
agp = "9.1.1"
fragmentCompose = "1.8.6"
kotlin = "2.2.10"
coreKtx = "1.17.0"
junit = "4.13.2"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
kotlinxSerializationJson = "1.8.0"
lifecycleRuntimeKtx = "2.8.7"
activityCompose = "1.10.0"
composeBom = "2025.02.00"
appcompat = "1.7.0"
material = "1.12.0"
coil = "2.4.0"
navigationCompose = "2.8.7"
coroutines = "1.7.3"
photopickerCompose = "1.0.0-alpha01"
play-services-location = "21.1.0"
accompanist = "0.32.0"
androidx-datastore = "1.0.0"
androidx-navigation = "2.7.7"
androidx-window = "1.5.0"
casa = "0.5.1"
gradleDownloadTask = "4.1.2"
ksp = "1.9.22-1.0.17"
composeCompiler = "1.5.9"
hilt = "2.48.1"
kotlin-serialization = "1.8.0"
ktlint = "0.48.1"
junit4 = "4.13.2"
androidxEspresso = "3.5.1"
androidxTestCore = "1.5.0"
androidxTestExtJunit = "1.1.5"
androidxTestExtTruth = "1.5.0"
androidxTestRules = "1.5.0"
androidxTestRunner = "1.5.2"
androidxUiAutomator = "2.2.0"
camerax = "1.5.0-beta01"
material3Android = "1.3.2"
media3 = "1.5.0"
constraintlayout = "2.1.4"
glide-compose = "1.0.0-beta01"
glance = "1.3.0-alpha01"
tensorflowLite = "2.9.0"
tensorflowLiteGpuDelegatePlugin = "0.4.4"
tensorflowLiteSupport = "0.4.2"
barcodeScanningCommon = "17.0.0"
playServicesMlkitBarcodeScanning = "18.3.1"
protobuf = "0.9.4"
firebaseCrashlyticsBuildtools = "3.0.5"
uwb = "1.0.0-alpha10"
telecom = "1.1.0-alpha04"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-fragment-compose = { module = "androidx.fragment:fragment-compose", version.ref = "fragmentCompose" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
androidx-uwb = { module = "androidx.core.uwb:uwb", version.ref = "uwb" }
androidx-photopicker-compose = { module = "androidx.photopicker:photopicker-compose", version.ref = "photopickerCompose" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-ui-runtime = { group = "androidx.compose.runtime", name = "runtime" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-compose-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
coil = { module = "io.coil-kt:coil", version.ref = "coil" }
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
coil-video = { module = "io.coil-kt:coil-video", version.ref = "coil" }
kotlin-coroutines-play = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-play-services", version.ref = "coroutines" }
play-services-location = { module = "com.google.android.gms:play-services-location", version.ref = "play-services-location" }
androidx-core-telecom = { module = "androidx.core:core-telecom", version.ref = "telecom" }
# Core dependencies
android-gradlePlugin = { module = "com.android.tools.build:gradle", version.ref = "agp" }
androidx-activity = "androidx.activity:activity-ktx:1.13.0-alpha01"
androidx-core = "androidx.core:core-ktx:1.18.0-alpha01"
androidx-exifinterface = "androidx.exifinterface:exifinterface:1.3.7"
# Fragment 1.7.0 alpha and Transition 1.5.0 alpha are required for predictive back to work with Fragments and transitions
androidx-fragment = "androidx.fragment:fragment-ktx:1.7.0-alpha10"
androidx-transition = "androidx.transition:transition-ktx:1.5.0-alpha06"
androidx-navigation-fragment = { module = "androidx.navigation:navigation-fragment", version.ref = "androidx-navigation" }
androidx-navigation-testing = { module = "androidx.navigation:navigation-testing", version.ref = "androidx-navigation" }
androidx-navigation-ui = { module = "androidx.navigation:navigation-ui", version.ref = "androidx-navigation" }
androidx-lifecycle-viewmodel-compose = "androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0"
androidx-viewpager2 = "androidx.viewpager2:viewpager2:1.0.0"
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
casa-base = { module = "com.google.android.catalog.framework:casa-base", version.ref = "casa" }
casa-processor = { module = "com.google.android.catalog.framework:casa-processor", version.ref = "casa" }
casa-ui = { module = "com.google.android.catalog.framework:casa-ui", version.ref = "casa" }
compose-animation-animation = { module = "androidx.compose.animation:animation" }
compose-foundation-foundation = { module = "androidx.compose.foundation:foundation" }
compose-foundation-layout = { module = "androidx.compose.foundation:layout" }
compose-material3 = { module = "androidx.compose.material3:material3" }
compose-material-material = { module = "androidx.compose.material:material" }
compose-material-iconsext = { module = "androidx.compose.material:material-icons-extended" }
compose-runtime-runtime = { module = "androidx.compose.runtime:runtime" }
compose-runtime-livedata = { module = "androidx.compose.runtime:runtime-livedata" }
compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" }
compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" }
compose-ui-ui = { module = "androidx.compose.ui:ui" }
compose-ui-util = { module = "androidx.compose.ui:util" }
coil-base = { module = "io.coil-kt:coil", version.ref = "coil" }
google-ksp-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" }
gradle-download-task = { module = "de.undercouch:gradle-download-task", version.ref = "gradleDownloadTask" }
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
hilt-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" }
hilt-testing = { group = "com.google.dagger", name = "hilt-android-testing", version.ref = "hilt" }
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
kotlin-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
kotlin-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
# Testing
androidx-test-core = { group = "androidx.test", name = "core-ktx", version.ref = "androidxTestCore" }
androidx-test-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidxEspresso" }
androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit-ktx", version.ref = "androidxTestExtJunit" }
androidx-test-ext-truth = { group = "androidx.test.ext", name = "truth", version.ref = "androidxTestExtTruth" }
androidx-test-rules = { group = "androidx.test", name = "rules", version.ref = "androidxTestRules" }
androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "androidxTestRunner" }
androidx-test-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "androidxUiAutomator" }
junit4 = { group = "junit", name = "junit", version.ref = "junit4" }
# Sample specific dependencies
lottie = "com.airbnb.android:lottie:6.0.0"
rxjava2-android = 'io.reactivex.rxjava2:rxandroid:2.1.1'
androidx-work-runtime-ktx = "androidx.work:work-runtime-ktx:2.9.0"
androidx-core-remoteviews = "androidx.core:core-remoteviews:1.0.0"
androidx-glance-appwidget = {group = "androidx.glance", name = "glance-appwidget", version.ref = "glance"}
androidx-glance-preview = {group = "androidx.glance", name = "glance-preview", version.ref = "glance"}
androidx-glance-appwidget-preview = {group = "androidx.glance", name = "glance-appwidget-preview", version.ref = "glance"}
androidx-glance-material3 = {group = "androidx.glance", name = "glance-material3", version.ref = "glance"}
androidx-graphics-core = { group = "androidx.graphics", name = "graphics-core", version = "1.0.0-beta01" }
androidx-startup = 'androidx.startup:startup-runtime:1.1.1'
androidx-window = { module = "androidx.window:window", version.ref = "androidx-window" }
androidx-window-java = { module = "androidx.window:window-java", version.ref = "androidx-window" }
androidx-window-rxjava2 = { module = "androidx.window:window-rxjava2", version.ref = "androidx-window" }
androidx-window-core = { module = "androidx.window:window-core", version.ref = "androidx-window" }
androidx-media = "androidx.media:media:1.7.0"
androidx-constraintlayout = "androidx.constraintlayout:constraintlayout:2.1.4"
androidx-corepip = "androidx.core:core-pip:1.0.0-SNAPSHOT"
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "androidx-datastore" }
androidx-draganddrop = "androidx.draganddrop:draganddrop:1.0.0"
androidx-dynamicanimation = "androidx.dynamicanimation:dynamicanimation-ktx:1.0.0-alpha03"
androidx-material3-android = { group = "androidx.compose.material3", name = "material3-android", version.ref = "material3Android" }
androidx-media3-common = { module = "androidx.media3:media3-common", version.ref = "media3" }
androidx-media3-effect = { module = "androidx.media3:media3-effect", version.ref = "media3" }
androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" }
androidx-media3-transformer = { module = "androidx.media3:media3-transformer", version.ref = "media3" }
androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "media3" }
# CameraX
androidx-camerax-core = { module = "androidx.camera:camera-core", version.ref = "camerax" }
androidx-camerax-compose = { module = "androidx.camera:camera-compose", version.ref = "camerax" }
androidx-camerax-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camerax" }
androidx-camerax-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camerax" }
androidx-camerax-video = { module = "androidx.camera:camera-video", version.ref = "camerax" }
androidx-camerax-view = { module = "androidx.camera:camera-view", version.ref = "camerax" }
androidx-camerax-mlkit-vision = { module = "androidx.camera:camera-mlkit-vision", version.ref = "camerax" }
androidx-camerax-extensions = { module = "androidx.camera:camera-extensions", version.ref = "camerax" }
fresco = "com.facebook.fresco:fresco:3.0.0"
fresco-nativeimagetranscoder = "com.facebook.fresco:nativeimagetranscoder:2.6.0!!"
glide = "com.github.bumptech.glide:glide:4.15.1"
glide-compose = { group = "com.github.bumptech.glide", name = "compose", version.ref = "glide-compose" }
appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
tensorflow-lite = { module = "org.tensorflow:tensorflow-lite", version.ref = "tensorflowLite" }
tensorflow-lite-gpu = { module = "org.tensorflow:tensorflow-lite-gpu", version.ref = "tensorflowLite" }
tensorflow-lite-gpu-delegate-plugin = { module = "org.tensorflow:tensorflow-lite-gpu-delegate-plugin", version.ref = "tensorflowLiteGpuDelegatePlugin" }
tensorflow-lite-select-tf-ops = { module = "org.tensorflow:tensorflow-lite-select-tf-ops", version.ref = "tensorflowLite" }
tensorflow-lite-support = { module = "org.tensorflow:tensorflow-lite-support", version.ref = "tensorflowLiteSupport" }
barcode-scanning-common = { group = "com.google.mlkit", name = "barcode-scanning-common", version.ref = "barcodeScanningCommon" }
play-services-mlkit-barcode-scanning = { group = "com.google.android.gms", name = "play-services-mlkit-barcode-scanning", version.ref = "playServicesMlkitBarcodeScanning" }
firebase-crashlytics-buildtools = { group = "com.google.firebase", name = "firebase-crashlytics-buildtools", version.ref = "firebaseCrashlyticsBuildtools" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
android-library = { id = "com.android.library", version.ref = "agp" }
compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
affectedmoduledetector = { id = "com.dropbox.affectedmoduledetector", version = "0.2.0" }
versionCatalogUpdate = { id = "nl.littlerobots.version-catalog-update", version = "0.7.0" }
benManesVersions = { id = "com.github.ben-manes.versions", version = "0.44.0" }
android-test = { id = "com.android.test", version.ref = "agp" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
protobuf = { id = "com.google.protobuf", version.ref = "protobuf" }

View File

@ -0,0 +1,61 @@
/*
* Copyright 2022 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.
*/
val ktlintVersion = "0.48.1"
initscript {
val spotlessVersion = "6.13.0"
repositories {
mavenCentral()
}
dependencies {
classpath("com.diffplug.spotless:spotless-plugin-gradle:$spotlessVersion")
}
}
rootProject {
// We don't want to run it for the "build-logic" rootProject separately
if (this.name != "Platform Samples") {
return@rootProject
}
apply<com.diffplug.gradle.spotless.SpotlessPlugin>()
extensions.configure<com.diffplug.gradle.spotless.SpotlessExtension> {
predeclareDeps()
}
extensions.configure<com.diffplug.gradle.spotless.SpotlessExtensionPredeclare> {
kotlin {
target("**/*.kt")
targetExclude("**/build/**/*.kt")
ktlint(ktlintVersion).userData(mapOf("android" to "true"))
licenseHeaderFile(rootProject.file("spotless/copyright.kt"))
}
format("kts") {
target("**/*.kts")
targetExclude("**/build/**/*.kts")
// Look for the first line that doesn't have a block comment (assumed to be the license)
licenseHeaderFile(rootProject.file("spotless/copyright.kts"), "(^(?![\\/ ]\\*).*$)")
}
format("xml") {
target("**/*.xml")
targetExclude("**/build/**/*.xml")
// Look for the first XML tag that isn't a comment (<!--) or the xml declaration (<?xml)
licenseHeaderFile(rootProject.file("spotless/copyright.xml"), "(<[^!?])")
}
}
}

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

252
gradlew vendored Normal file
View File

@ -0,0 +1,252 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# 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.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

94
gradlew.bat vendored Normal file
View File

@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

6
renovate.json Normal file
View File

@ -0,0 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"local>android/.github:renovate-config"
]
}

View File

@ -0,0 +1,25 @@
# Accessibility Samples
This is a catalog of accessibility-specific APIs to demonstrate the usage of each API.
More info at
- [Accessibility Overview](https://developer.android.com/guide/topics/ui/accessibility)
- [Accessibility in Compose](https://developer.android.com/jetpack/compose/accessibility)
## License
```
Copyright 2022 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.
```

View File

@ -0,0 +1,60 @@
/*
* Copyright 2023 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.
*/
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.android)
}
android {
namespace = "com.example.platform.accessibility"
compileSdk = 36
defaultConfig {
minSdk = 21
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
buildFeatures {
viewBinding = true
}
lint {
targetSdk = 35
}
testOptions {
targetSdk = 35
}
}
dependencies {
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.tooling)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.androidx.fragment)
}

View File

@ -0,0 +1,71 @@
/*
* Copyright 2023 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.
*/
package com.example.platform.accessibility
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Composable
fun ColorContrast() {
Column(
Modifier.verticalScroll(rememberScrollState()),
) {
TextWithPadding(
"Sufficient color contrast benefits users with various visual impairments, but " +
"also helps all users when interacting with their device in extreme lighting" +
" conditions, such as in direct sunlight."
)
TextWithPadding(
"Accessibility Scanner, available on the Play Store, can be used to detect color" +
" contrast issues. You can use Scanner on this screen to see how it" +
" identifies color contrast issues."
)
TextWithProperColorContrast(text = "This text has sufficient color contrast ratio")
TextWithInsufficientColorContrast(
text = "This text does not have sufficient color contrast ratio and should be " +
"identified by Scanner"
)
}
}
@Composable
fun TextWithProperColorContrast(text: String) {
Text(text, color = Color.Blue, modifier = Modifier.padding(30.dp))
}
@Composable
fun TextWithInsufficientColorContrast(text: String) {
Box(
modifier = Modifier
.background(Color.Gray)
.padding(30.dp)
) {
Text(text, color = Color.DarkGray)
}
}

View File

@ -0,0 +1,59 @@
/*
* Copyright 2023 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.
*/
package com.example.platform.accessibility
import android.annotation.SuppressLint
import android.os.Bundle
import android.os.CountDownTimer
import android.view.View
import androidx.fragment.app.Fragment
import com.example.platform.accessibility.databinding.LiveregionFragmentBinding
import java.util.concurrent.TimeUnit
class LiveRegionView : Fragment(R.layout.liveregion_fragment) {
@SuppressLint("SetTextI18n")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val binding = LiveregionFragmentBinding.bind(view)
binding.introText.text = "Turn on TalkBack to navigate and interact with this screen. " +
"Once TalkBack is turned on, move the accessibility focus to the Start Timer" +
" button and activate it. The number in the middle of the screen will start" +
" decrementing by 1 every second. Notice that even though the accessibility focus" +
" remains on the button, TalkBack still announces updates to the timer view."
val timer = object : CountDownTimer(
TimeUnit.SECONDS.toMillis(10), TimeUnit.SECONDS.toMillis(1)
) {
override fun onTick(millisUntilFinished: Long) {
// In liveregion_fragment.xml, timerView sets its accessibilityLiveRegion attribute
// to "polite". This specifies to accessibility services such as TalkBack that
// updates on this node should be announced even when it doesn't have accessibility
// focus.
binding.timerView.text =
TimeUnit.MILLISECONDS.toSeconds(millisUntilFinished).toString()
}
override fun onFinish() {}
}
binding.startButton.setOnClickListener {
timer.start()
}
}
}

View File

@ -0,0 +1,99 @@
/*
* Copyright 2023 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.
*/
package com.example.platform.accessibility
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@Preview
@Composable
fun SpeakableText() {
Column {
TextWithPadding(
text = "Users of screen readers rely on content labels to understand the meaning of" +
" elements on the screen."
)
TextWithPadding(
text = "Turn on TalkBack to navigate this screen to better understand how proper" +
" content labels affect the user experience. You can also run Accessibility" +
"Scanner on this screen to see how Scanner provides tips for better labels."
)
TextWithPadding(
text = "Content labels should be succinct and clearly describe the meaning or action" +
" associated with the element. It should not include the element's type" +
" (ie: button, checkbox), or instruct the user how to specifically interact" +
" with the element. Below are examples of bad content labels that degrade the" +
" user experience."
)
Row(
Modifier
.padding(20.dp)
.align(Alignment.CenterHorizontally)
) {
// Content descriptions should not include the element's type. In this case, including
// the word "button" is extraneous.
IconButton(onClick = { }) {
Icon(Icons.Filled.Edit, contentDescription = "Call Button")
}
// Content descriptions should not include instructions on how to specifically
// interact with the element. In this case, "Double Tap" is extraneous.
IconButton(onClick = { }) {
Icon(Icons.Filled.Edit, contentDescription = "Call Button, Double Tap")
}
// Content descriptions should describe the action associated with the element instead
// of a description of the element itself.
IconButton(onClick = { }) {
Icon(Icons.Filled.Edit, contentDescription = "Pencil")
}
// Elements with a content description make it difficult for users to navigate the app.
IconButton(onClick = { }) {
Icon(Icons.Filled.Edit, contentDescription = null)
}
}
TextWithPadding(
text = "Below is an example of a button with a succinct and descriptive content label."
)
Row(
Modifier
.padding(20.dp)
.align(Alignment.CenterHorizontally)
) {
IconButton(onClick = { }) {
Icon(Icons.Filled.Edit, contentDescription = "Edit")
}
}
}
}

View File

@ -0,0 +1,28 @@
/*
* Copyright 2023 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.
*/
package com.example.platform.accessibility
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun TextWithPadding(text: String) {
Text(text = text, modifier = Modifier.padding(30.dp))
}

View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2023 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical"
android:padding="30dp">
<TextView
android:id="@+id/introText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
tools:text="This is an intro paragraph" />
<!-- This View sets the accessibilityLiveRegion attribute to 'polite' to specify
that accessibility services such as TalkBack should announce changes to this node
even when it doesn't have accessibility focus. -->
<TextView
android:id="@+id/timerView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:accessibilityLiveRegion="polite"
android:padding="@dimen/large_padding"
android:text="@string/timer_start_value"
android:textSize="@dimen/timer_text_size" />
<Button
android:id="@+id/start_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Start Timer"
android:textSize="20sp" />
</LinearLayout>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2023 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.
-->
<resources>
<dimen name="large_padding">50dp</dimen>
<dimen name="timer_text_size">60sp</dimen>
</resources>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2023 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.
-->
<resources>
<!-- Live Region -->
<string name="timer_start_value">10</string>
</resources>

View File

@ -0,0 +1,32 @@
# Camera2 samples
This is a catalog of camera2-specific APIs to demonstrate the usage and capabilities of the API.
- [Camera Preview]():
Demonstrates displaying processed pixel data directly from the camera sensor to the screen using Camera2.
- [Image Capture]():
Demonstrates how to capture an image using Camera2 and encode it into a JPEG container.
- [UltraHDR Image Capture]():
Demonstrates how to capture a 10-bit compressed still image and store it using the new UltraHDR image format using Camera2.
> 🚧 **Work-in-Progress:** we are working on brining more existing and new samples for camera2, and soon, CameraX as well
Check out our guides at https://developer.android.com/training/camera2
## License
```
Copyright 2022 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.
```

View File

@ -0,0 +1,69 @@
/*
* Copyright 2022 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.
*/
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.android)
}
android {
namespace = "com.example.platform.camera"
compileSdk = 36
defaultConfig {
minSdk = 21
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
buildFeatures {
viewBinding = true
}
lint {
targetSdk = 35
}
testOptions {
targetSdk = 35
}
}
dependencies {
implementation(libs.androidx.fragment)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.coil)
implementation(libs.coil.compose)
// EXIF Interface
implementation(libs.androidx.exifinterface)
// ViewPager2
implementation(libs.androidx.viewpager2)
// Link to UltraHDR Graphics Samples
implementation(project(mapOf("path" to ":samples:graphics:ultrahdr")))
}

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2023 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
</manifest>

View File

@ -0,0 +1,79 @@
/*
* Copyright 2023 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.
*/
package com.example.platform.camera.common
import android.content.Context
import android.util.AttributeSet
import android.util.Log
import android.view.SurfaceView
import kotlin.math.roundToInt
/**
* A [SurfaceView] that can be adjusted to a specified aspect ratio and
* performs center-crop transformation of input frames.
*/
class AutoFitSurfaceView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : SurfaceView(context, attrs, defStyle) {
private var aspectRatio = 0f
/**
* Sets the aspect ratio for this view. The size of the view will be
* measured based on the ratio calculated from the parameters.
*
* @param width Camera resolution horizontal size
* @param height Camera resolution vertical size
*/
fun setAspectRatio(width: Int, height: Int) {
require(width > 0 && height > 0) { "Size cannot be negative" }
aspectRatio = width.toFloat() / height.toFloat()
holder.setFixedSize(width, height)
requestLayout()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val width = MeasureSpec.getSize(widthMeasureSpec)
val height = MeasureSpec.getSize(heightMeasureSpec)
if (aspectRatio == 0f) {
setMeasuredDimension(width, height)
} else {
// Performs center-crop transformation of the camera frames
val newWidth: Int
val newHeight: Int
val actualRatio = if (width > height) aspectRatio else 1f / aspectRatio
if (width < height * actualRatio) {
newHeight = height
newWidth = (height * actualRatio).roundToInt()
} else {
newWidth = width
newHeight = (width / actualRatio).roundToInt()
}
Log.d(TAG, "Measured dimensions set: $newWidth x $newHeight")
setMeasuredDimension(newWidth, newHeight)
}
}
companion object {
private val TAG = AutoFitSurfaceView::class.java.simpleName
}
}

View File

@ -0,0 +1,81 @@
/*
* Copyright 2023 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.
*/
package com.example.platform.camera.common
import android.graphics.Point
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.params.StreamConfigurationMap
import android.util.Size
import android.view.Display
import kotlin.math.max
import kotlin.math.min
/** Helper class used to pre-compute shortest and longest sides of a [Size] */
class SmartSize(width: Int, height: Int) {
var size = Size(width, height)
var long = max(size.width, size.height)
var short = min(size.width, size.height)
override fun toString() = "SmartSize(${long}x${short})"
}
/** Standard FHD size for pictures and video */
val SIZE_1080P: SmartSize = SmartSize(1920, 1080)
/** Standard HD size for pictures and video */
val SIZE_720P: SmartSize = SmartSize(1280, 720)
/** Returns a [SmartSize] object for the given [Display] */
fun getDisplaySmartSize(display: Display): SmartSize {
val outPoint = Point()
display.getRealSize(outPoint)
return SmartSize(outPoint.x, outPoint.y)
}
/**
* Returns the largest available PREVIEW size. For more information, see:
* https://d.android.com/reference/android/hardware/camera2/CameraDevice and
* https://developer.android.com/reference/android/hardware/camera2/params/StreamConfigurationMap
*/
fun <T> getPreviewOutputSize(
display: Display,
characteristics: CameraCharacteristics,
targetClass: Class<T>,
format: Int? = null,
): Size? {
// Find which is smaller: screen or 1080p
val screenSize = getDisplaySmartSize(display)
val fhdScreen = screenSize.long >= SIZE_1080P.long || screenSize.short >= SIZE_1080P.short
val maxSize = if (fhdScreen) SIZE_1080P else screenSize
// If image format is provided, use it to determine supported sizes; else use target class
val config = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
if (format == null) assert(StreamConfigurationMap.isOutputSupportedFor(targetClass))
else config?.isOutputSupportedFor(format)?.let { assert(it) }
val allSizes = if (format == null) config?.getOutputSizes(targetClass)
else config?.getOutputSizes(format)
// Get available sizes and sort them by area from largest to smallest.
val validSizes = allSizes
?.sortedWith(compareBy { it.height * it.width })
?.map { SmartSize(it.width, it.height) }
?.reversed()
// Then, get the largest output size that is smaller or equal than our max size.
return validSizes?.first { it.long <= maxSize.long && it.short <= maxSize.short }?.size
}

View File

@ -0,0 +1,33 @@
/*
* Copyright 2023 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.
*/
package com.example.platform.camera.common
import android.hardware.camera2.CaptureResult
import android.media.Image
import java.io.Closeable
/**
* Helper data class used to hold capture metadata with their associated image
*/
data class CombinedCaptureResult(
val image: Image,
val metadata: CaptureResult,
val orientation: Int,
val format: Int,
) : Closeable {
override fun close() = image.close()
}

View File

@ -0,0 +1,25 @@
/*
* Copyright 2023 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.
*/
package com.example.platform.camera.common
import java.util.concurrent.Executor
internal class DirectExecutor : Executor {
override fun execute(r: Runnable) {
r.run()
}
}

View File

@ -0,0 +1,74 @@
/*
* Copyright 2023 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.
*/
package com.example.platform.camera.common
import android.graphics.Bitmap
import android.graphics.Matrix
import android.util.Log
import androidx.exifinterface.media.ExifInterface
private const val TAG: String = "ExifUtils"
/**
* Transforms rotation and mirroring information into one of the [ExifInterface] constants
*/
fun computeExifOrientation(rotationDegrees: Int, mirrored: Boolean) = when {
rotationDegrees == 0 && !mirrored -> ExifInterface.ORIENTATION_NORMAL
rotationDegrees == 0 && mirrored -> ExifInterface.ORIENTATION_FLIP_HORIZONTAL
rotationDegrees == 90 && !mirrored -> ExifInterface.ORIENTATION_ROTATE_90
rotationDegrees == 90 && mirrored -> ExifInterface.ORIENTATION_TRANSPOSE
rotationDegrees == 180 && !mirrored -> ExifInterface.ORIENTATION_ROTATE_180
rotationDegrees == 180 && mirrored -> ExifInterface.ORIENTATION_FLIP_VERTICAL
rotationDegrees == 270 && mirrored -> ExifInterface.ORIENTATION_ROTATE_270
rotationDegrees == 270 && !mirrored -> ExifInterface.ORIENTATION_TRANSVERSE
else -> ExifInterface.ORIENTATION_UNDEFINED
}
/**
* Helper function used to convert an EXIF orientation enum into a transformation matrix
* that can be applied to a bitmap.
*
* @return matrix - Transformation required to properly display [Bitmap]
*/
fun decodeExifOrientation(exifOrientation: Int): Matrix {
val matrix = Matrix()
// Apply transformation corresponding to declared EXIF orientation
when (exifOrientation) {
ExifInterface.ORIENTATION_NORMAL -> Unit
ExifInterface.ORIENTATION_UNDEFINED -> Unit
ExifInterface.ORIENTATION_ROTATE_90 -> matrix.postRotate(90F)
ExifInterface.ORIENTATION_ROTATE_180 -> matrix.postRotate(180F)
ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(270F)
ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.postScale(-1F, 1F)
ExifInterface.ORIENTATION_FLIP_VERTICAL -> matrix.postScale(1F, -1F)
ExifInterface.ORIENTATION_TRANSPOSE -> {
matrix.postScale(-1F, 1F)
matrix.postRotate(270F)
}
ExifInterface.ORIENTATION_TRANSVERSE -> {
matrix.postScale(-1F, 1F)
matrix.postRotate(90F)
}
// Error out if the EXIF orientation is invalid
else -> Log.e(TAG, "Invalid orientation: $exifOrientation")
}
// Return the resulting matrix
return matrix
}

View File

@ -0,0 +1,100 @@
/*
* Copyright 2023 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.
*/
package com.example.platform.camera.common
import android.content.Context
import android.hardware.camera2.CameraCharacteristics
import android.view.OrientationEventListener
import android.view.Surface
import androidx.lifecycle.LiveData
/**
* Calculates closest 90-degree orientation to compensate for the device rotation relative to sensor
* orientation, i.e., allows user to see camera frames with the expected orientation.
*/
class OrientationLiveData(
context: Context,
characteristics: CameraCharacteristics,
) : LiveData<Int>() {
private val listener = object : OrientationEventListener(context.applicationContext) {
override fun onOrientationChanged(orientation: Int) {
val rotation = when {
orientation <= 45 -> Surface.ROTATION_0
orientation <= 135 -> Surface.ROTATION_90
orientation <= 225 -> Surface.ROTATION_180
orientation <= 315 -> Surface.ROTATION_270
else -> Surface.ROTATION_0
}
val relative = computeRelativeRotation(characteristics, rotation)
if (relative != value) postValue(relative)
}
}
override fun onActive() {
super.onActive()
listener.enable()
}
override fun onInactive() {
super.onInactive()
listener.disable()
}
companion object {
// Default sensor orientation.
private const val DEFAULT_SENSOR_ORIENTATION = 90
/**
* Computes rotation required to transform from the camera sensor orientation to the
* device's current orientation in degrees.
*
* @param characteristics the [CameraCharacteristics] to query for the sensor orientation.
* @param surfaceRotation the current device orientation as a Surface constant
* @return the relative rotation from the camera sensor to the current device orientation.
*/
@JvmStatic
private fun computeRelativeRotation(
characteristics: CameraCharacteristics,
surfaceRotation: Int,
): Int {
val sensorOrientationDegrees =
characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)
?: DEFAULT_SENSOR_ORIENTATION
val deviceOrientationDegrees = when (surfaceRotation) {
Surface.ROTATION_0 -> 0
Surface.ROTATION_90 -> 90
Surface.ROTATION_180 -> 180
Surface.ROTATION_270 -> 270
else -> 0
}
// Reverse device orientation for front-facing cameras
val sign = if (characteristics.get(CameraCharacteristics.LENS_FACING) ==
CameraCharacteristics.LENS_FACING_FRONT
) {
1
} else {
-1
}
// Calculate desired JPEG orientation relative to camera orientation to make
// the image upright relative to the device orientation
return (sensorOrientationDegrees - (deviceOrientationDegrees * sign) + 360) % 360
}
}
}

View File

@ -0,0 +1,54 @@
/*
* Copyright 2023 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.
*/
package com.example.platform.camera.common
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import coil.compose.AsyncImage
import coil.request.ImageRequest
@Composable
fun SimpleImageViewer(
location: String,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxWidth()
.background(Color.Black),
) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(location)
.crossfade(true)
.crossfade(100)
.build(),
contentDescription = null,
contentScale = ContentScale.Inside,
)
}
}

View File

@ -0,0 +1,636 @@
/*
* Copyright 2023 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.
*/
package com.example.platform.camera.imagecapture
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.Color
import android.graphics.ImageFormat
import android.hardware.camera2.*
import android.hardware.camera2.params.OutputConfiguration
import android.hardware.camera2.params.SessionConfiguration
import android.media.Image
import android.media.ImageReader
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.HandlerThread
import android.util.Log
import android.view.*
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toDrawable
import androidx.exifinterface.media.ExifInterface
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.example.platform.camera.common.*
import com.example.platform.camera.databinding.Camera2ImageCaptureBinding
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ArrayBlockingQueue
import java.util.concurrent.TimeoutException
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
class Camera2ImageCapture : Fragment() {
/**
* Android ViewBinding.
*/
private var _binding: Camera2ImageCaptureBinding? = null
private val binding get() = _binding!!
/**
* Detects, characterizes, and connects to a CameraDevice (used for all camera operations).
*/
private val cameraManager: CameraManager by lazy {
val context = requireContext().applicationContext
context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
}
/**
* List of camera id provided by the [CameraManager].
*/
private val cameraIds: List<String> by lazy {
cameraManager.cameraIdList.asList()
}
/**
* [CameraCharacteristics] corresponding to the provided Camera ID.
*/
private val characteristics: CameraCharacteristics by lazy {
// This will use the first camera id from cameraIds.
cameraManager.getCameraCharacteristics(cameraIds.first())
}
/**
* [HandlerThread] where all camera operations run
*/
private val cameraThread = HandlerThread("CameraThread").apply { start() }
/**
* [Handler] corresponding to [cameraThread]
*/
private val cameraHandler = Handler(cameraThread.looper)
/**
* [HandlerThread] where all buffer reading operations run
*/
private val imageReaderThread = HandlerThread("imageReaderThread").apply { start() }
/**
* [Handler] corresponding to [imageReaderThread]
*/
private val imageReaderHandler = Handler(imageReaderThread.looper)
/**
* The [CameraDevice] that will be opened in this fragment.
*/
private lateinit var camera: CameraDevice
/**
* Internal reference to the ongoing [CameraCaptureSession] configured with our parameters.
*/
private lateinit var session: CameraCaptureSession
/**
* [OrientationLiveData] listener for changes in the device orientation relative to the camera.
*/
private lateinit var relativeOrientation: OrientationLiveData
/**
* Readers used as buffers for camera still shots
*/
private lateinit var imageReader: ImageReader
/**
* Performs A flash animation to mimic a common shutter when taking a photo.
*/
private val animationTask: Runnable by lazy {
Runnable {
// Flash white animation
binding.fragmentCamera2ImageCaptureOverlay.background =
Color.argb(150, 255, 255, 255).toDrawable()
// Wait for ANIMATION_FAST_MILLIS
binding.fragmentCamera2ImageCaptureOverlay.postDelayed(
{
// Remove white flash animation
binding.fragmentCamera2ImageCaptureOverlay.background = null
},
ANIMATION_FAST_MILLIS,
)
}
}
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission(),
::handleCameraPermissionResult,
)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
_binding = Camera2ImageCaptureBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setUpCamera()
}
override fun onStart() {
super.onStart()
when {
isPermissionGranted() -> view?.post(::initializeCamera)
shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> {
showActionMessage("This sample requires CAMERA permission to work. Please grant it") {
requestCameraPermission()
}
}
else -> {
showActionMessage("Grant permission") {
requestCameraPermission()
}
}
}
}
/**
* Attempt to request camera permissions.
*/
private fun requestCameraPermission() =
requestPermissionLauncher.launch(Manifest.permission.CAMERA)
/**
* Handle result of camera permissions request.
*/
private fun handleCameraPermissionResult(isGranted: Boolean) {
when (isGranted) {
true -> view?.post(::initializeCamera)
false -> view?.let {
showActionMessage("Permissions not granted: Try again") {
requestCameraPermission()
}
}
}
}
private fun showActionMessage(message: String, action: () -> Unit) {
binding.fragmentCamera2ImageCapturePermissionButton.visibility = View.VISIBLE
binding.fragmentCamera2ImageCapturePermissionButton.text = message
binding.fragmentCamera2ImageCapturePermissionButton.setOnClickListener {
action()
}
}
private fun showImage(imageLocation: String) {
binding.fragmentCamera2ImageCaptureButton.visibility = View.GONE
binding.fragmentCamera2PostCaptureBackButton.visibility = View.VISIBLE
binding.fragmentCamera2ImageCaptureImageView.setContent {
SimpleImageViewer(location = imageLocation)
}
}
private fun returnToImageCapture() {
binding.fragmentCamera2ImageCaptureButton.visibility = View.VISIBLE
binding.fragmentCamera2PostCaptureBackButton.visibility = View.GONE
binding.fragmentCamera2ImageCaptureImageView.setContent { }
}
private fun setUpCamera() {
binding.fragmentCamera2ImageCaptureButton.setOnApplyWindowInsetsListener { v, insets ->
v.translationX = (-insets.systemWindowInsetRight).toFloat()
v.translationY = (-insets.systemWindowInsetBottom).toFloat()
insets.consumeSystemWindowInsets()
}
binding.fragmentCamera2PostCaptureBackButton.setOnClickListener {
returnToImageCapture()
}
binding.fragmentCamera2ImageCaptureViewfinder.holder.addCallback(
object : SurfaceHolder.Callback {
override fun surfaceDestroyed(holder: SurfaceHolder) = Unit
override fun surfaceChanged(
holder: SurfaceHolder,
format: Int,
width: Int,
height: Int,
) = Unit
override fun surfaceCreated(holder: SurfaceHolder) {
// Selects appropriate preview size and configures view finder
val previewSize = getPreviewOutputSize(
binding.fragmentCamera2ImageCaptureViewfinder.display,
characteristics,
SurfaceHolder::class.java,
)
Log.d(
TAG,
"Preview size: ${binding.fragmentCamera2ImageCaptureViewfinder.width} " +
"x " + "${binding.fragmentCamera2ImageCaptureViewfinder.height}",
)
Log.d(TAG, "Selected Preview Size: $previewSize")
binding.fragmentCamera2ImageCaptureViewfinder.setAspectRatio(
previewSize?.width ?: SIZE_720P.size.width,
previewSize?.height ?: SIZE_720P.size.height,
)
}
},
)
// Used to rotate the output media to match device orientation
relativeOrientation = OrientationLiveData(requireContext(), characteristics).apply {
observe(viewLifecycleOwner) { orientation ->
Log.d(TAG, "Orientation changed: $orientation")
}
}
}
private fun setUpImageReader() {
// Initialize an image reader which will be used to capture still photos
val configMap = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
configMap?.let { config ->
config.getOutputSizes(ImageFormat.JPEG).maxByOrNull { it.height * it.width }
?.let { size ->
imageReader = ImageReader.newInstance(
size.width, size.height, ImageFormat.JPEG, IMAGE_BUFFER_SIZE,
)
}
}
// Listen to the capture button
binding.fragmentCamera2ImageCaptureButton.setOnClickListener {
// Disable click listener to prevent multiple requests simultaneously in flight
it.isEnabled = false
// Perform I/O heavy operations in a different scope
lifecycleScope.launch(Dispatchers.IO) {
takePhoto().use { result ->
Log.d(TAG, "Result received: $result")
// Save the result to disk
val output = saveResult(result)
Log.d(TAG, "Image saved: ${output.absolutePath}")
// If the result is a JPEG file, update EXIF metadata with orientation info
if (output.extension == "jpg") {
val exif = ExifInterface(output.absolutePath)
exif.setAttribute(
ExifInterface.TAG_ORIENTATION, result.orientation.toString(),
)
exif.saveAttributes()
Log.d(TAG, "EXIF metadata saved: ${output.absolutePath}")
}
// Display the photo taken to user
lifecycleScope.launch(Dispatchers.Main) {
showImage(output.absolutePath)
}
}
// Re-enable click listener after photo is taken
it.post { it.isEnabled = true }
}
}
}
/**
* Begin all camera operations in a coroutine in the main thread. This function:
* - Opens the camera
* - Configures the camera session
* - Starts the preview by dispatching a repeating capture request
*/
private fun initializeCamera() = lifecycleScope.launch(Dispatchers.Main) {
binding.fragmentCamera2ImageCapturePermissionButton.visibility = View.GONE
try {
// Open the selected camera
camera = openCamera(cameraManager, cameraIds.first(), cameraHandler)
// Set up the image reader
setUpImageReader()
// Creates list of Surfaces where the camera will output frames
val targets = listOf(
binding.fragmentCamera2ImageCaptureViewfinder.holder.surface,
imageReader.surface,
)
// Start a capture session using our open camera and list of Surfaces where frames will
// go.
session = createCaptureSession(camera, targets, cameraHandler)
val captureRequest = camera.createCaptureRequest(
CameraDevice.TEMPLATE_PREVIEW,
).apply { addTarget(binding.fragmentCamera2ImageCaptureViewfinder.holder.surface) }
// This will keep sending the capture request as frequently as possible until the
// session is torn down or session.stopRepeating() is called
session.setRepeatingRequest(captureRequest.build(), null, cameraHandler)
} catch (e: Exception) {
Log.e(TAG, "initializeCamera failed", e)
showActionMessage("Camera init failed: Try again") {
view?.post(::initializeCamera)
}
}
}
/**
* Opens the camera and returns the opened device (as the result of the suspend coroutine)
*/
private suspend fun openCamera(
manager: CameraManager,
cameraId: String,
handler: Handler? = null,
): CameraDevice = suspendCancellableCoroutine { cont ->
try {
manager.openCamera(
cameraId,
object : CameraDevice.StateCallback() {
override fun onOpened(device: CameraDevice) = cont.resume(device)
override fun onDisconnected(device: CameraDevice) {
Log.w(TAG, "Camera $cameraId has been disconnected")
}
override fun onError(device: CameraDevice, error: Int) {
val msg = when (error) {
ERROR_CAMERA_DEVICE -> "Fatal (device)"
ERROR_CAMERA_DISABLED -> "Device policy"
ERROR_CAMERA_IN_USE -> "Camera in use"
ERROR_CAMERA_SERVICE -> "Fatal (service)"
ERROR_MAX_CAMERAS_IN_USE -> "Maximum cameras in use"
else -> "Unknown"
}
val exc = RuntimeException("Camera $cameraId error: ($error) $msg")
Log.e(TAG, exc.message, exc)
if (cont.isActive) cont.resumeWithException(exc)
}
},
handler,
)
} catch (e: SecurityException) {
requestCameraPermission()
}
}
/**
* Starts a [CameraCaptureSession] and returns the configured session (as the result of the
* suspend coroutine
*/
private suspend fun createCaptureSession(
device: CameraDevice,
targets: List<Surface>,
handler: Handler? = null,
): CameraCaptureSession = suspendCoroutine { cont ->
// First, create a CameraCaptureSession.StateCallback object for the onConfigured &
// onConfigureFailed callbacks.
val callback = object : CameraCaptureSession.StateCallback() {
override fun onConfigured(session: CameraCaptureSession) = cont.resume(session)
override fun onConfigureFailed(session: CameraCaptureSession) {
val exc = RuntimeException("Camera ${device.id} session configuration failed")
Log.e(TAG, exc.message, exc)
cont.resumeWithException(exc)
}
}
// Create a capture session using the predefined targets; this also involves defining the
// session state callback to be notified of when the session is ready
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val configs = targets.map {
val config = OutputConfiguration(it)
config
}
val sessionConfiguration = SessionConfiguration(
SessionConfiguration.SESSION_REGULAR,
configs,
DirectExecutor(),
callback,
)
device.createCaptureSession(sessionConfiguration)
} else {
device.createCaptureSession(
targets,
callback,
handler,
)
}
}
/**
* Helper function used to capture a still image using the [CameraDevice.TEMPLATE_STILL_CAPTURE]
* template. It performs synchronization between the [CaptureResult] and the [Image] resulting
* from the single capture, and outputs a [CombinedCaptureResult] object.
*/
private suspend fun takePhoto(): CombinedCaptureResult = suspendCoroutine { cont ->
// Start a new image queue.
val imageQueue = ArrayBlockingQueue<Image>(IMAGE_BUFFER_SIZE)
imageReader.setOnImageAvailableListener(
{
val image = it.acquireLatestImage()
Log.d(TAG, "Image available in queue: ${image.timestamp}")
imageQueue.add(image)
},
imageReaderHandler,
)
val request = session.device.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE)
.apply { addTarget(imageReader.surface) }
session.capture(
request.build(),
object : CameraCaptureSession.CaptureCallback() {
override fun onCaptureStarted(
session: CameraCaptureSession,
request: CaptureRequest,
timestamp: Long,
frameNumber: Long,
) {
super.onCaptureStarted(session, request, timestamp, frameNumber)
binding.fragmentCamera2ImageCaptureViewfinder.post(animationTask)
}
override fun onCaptureCompleted(
session: CameraCaptureSession,
request: CaptureRequest,
result: TotalCaptureResult,
) {
super.onCaptureCompleted(session, request, result)
val resultTimestamp = result.get(CaptureResult.SENSOR_TIMESTAMP)
Log.d(TAG, "Capture result received: $resultTimestamp")
// Set a timeout in case image captured is dropped from the pipeline
val exc = TimeoutException("Image de-queuing took too long")
val timeoutRunnable = Runnable { cont.resumeWithException(exc) }
imageReaderHandler.postDelayed(timeoutRunnable, IMAGE_CAPTURE_TIMEOUT_MILLIS)
// Loop in the coroutine's context until an image with matching timestamp comes
// We need to launch the coroutine context again because the callback is done in
// the handler provided to the `capture` method, not in our coroutine context.
@Suppress("BlockingMethodInNonBlockingContext")
lifecycleScope.launch(cont.context) {
while (true) {
// Dequeue images while timestamps don't match
val image = imageQueue.take()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
image.format != ImageFormat.DEPTH_JPEG &&
image.timestamp != resultTimestamp
) continue
Log.d(TAG, "Matching image dequeued: ${image.timestamp}")
// Unset the image reader listener
imageReaderHandler.removeCallbacks(timeoutRunnable)
imageReader.setOnImageAvailableListener(null, null)
// Clear the queue of images, if there are left.
while (imageQueue.size > 0) {
imageQueue.take().close()
}
// Compute EXIF orientation metadata.
val rotation = relativeOrientation.value ?: 0
val mirrored = characteristics.get(CameraCharacteristics.LENS_FACING) ==
CameraCharacteristics.LENS_FACING_FRONT
val exifOrientation = computeExifOrientation(rotation, mirrored)
// Build the result and resume progress
cont.resume(
CombinedCaptureResult(
image,
result,
exifOrientation,
imageReader.imageFormat,
),
)
// There is no need to break out of the loop because this coroutine will
// suspend.
}
}
}
},
cameraHandler,
)
}
/**
* Helper function used to save a [CombinedCaptureResult] into a [File]
*/
private suspend fun saveResult(result: CombinedCaptureResult): File = suspendCoroutine { cont ->
when (result.format) {
// When the format is JPEG or DEPTH JPEG we can simply save the bytes as-is
ImageFormat.JPEG, ImageFormat.DEPTH_JPEG -> {
val buffer = result.image.planes[0].buffer
val bytes = ByteArray(buffer.remaining()).apply { buffer.get(this) }
try {
val output = createFile(requireContext(), "jpg")
FileOutputStream(output).use { it.write(bytes) }
cont.resume(output)
} catch (exc: IOException) {
Log.e(TAG, "Unable to write JPEG image to file", exc)
cont.resumeWithException(exc)
}
}
// When the format is RAW we use the DngCreator utility library
ImageFormat.RAW_SENSOR -> {
val dngCreator = DngCreator(characteristics, result.metadata)
try {
val output = createFile(requireContext(), "dng")
FileOutputStream(output).use { dngCreator.writeImage(it, result.image) }
cont.resume(output)
} catch (exc: IOException) {
Log.e(TAG, "Unable to write DNG image to file", exc)
cont.resumeWithException(exc)
}
}
// No other formats are supported by this sample
else -> {
val exc = RuntimeException("Unknown image format: ${result.image.format}")
Log.e(TAG, exc.message, exc)
cont.resumeWithException(exc)
}
}
}
override fun onDetach() {
super.onDetach()
try {
camera.close()
} catch (exc: Throwable) {
Log.e(TAG, "Error closing camera", exc)
}
}
override fun onDestroyView() {
super.onDestroyView()
cameraThread.quitSafely()
}
private fun isPermissionGranted() = ContextCompat.checkSelfPermission(
requireContext(), CAMERA_PERMISSION,
) == PackageManager.PERMISSION_GRANTED
companion object {
private val TAG = Camera2ImageCapture::class.java.simpleName
private const val CAMERA_PERMISSION = Manifest.permission.CAMERA
/**
* Milliseconds used for UI animations
*/
const val ANIMATION_FAST_MILLIS = 50L
/**
* Maximum number of images that will be held in the reader's buffer
*/
private const val IMAGE_BUFFER_SIZE: Int = 3
/**
* Maximum time allowed to wait for the result of an image capture
*/
private const val IMAGE_CAPTURE_TIMEOUT_MILLIS: Long = 5000
/**
* Create a [File] named a using formatted timestamp with the current date and time.
*
* @return [File] created.
*/
private fun createFile(context: Context, extension: String): File {
val sdf = SimpleDateFormat("yyyy_MM_dd_HH_mm_ss_SSS", Locale.US)
return File(context.filesDir, "IMG_${sdf.format(Date())}.$extension")
}
}
}

View File

@ -0,0 +1,643 @@
/*
* Copyright 2023 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.
*/
package com.example.platform.camera.imagecapture
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.Color
import android.graphics.ImageFormat
import android.hardware.camera2.CameraCaptureSession
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CameraManager
import android.hardware.camera2.CaptureRequest
import android.hardware.camera2.CaptureResult
import android.hardware.camera2.TotalCaptureResult
import android.hardware.camera2.params.DynamicRangeProfiles
import android.hardware.camera2.params.OutputConfiguration
import android.hardware.camera2.params.SessionConfiguration
import android.media.Image
import android.media.ImageReader
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.HandlerThread
import android.util.Log
import android.view.LayoutInflater
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toDrawable
import androidx.core.os.bundleOf
import androidx.exifinterface.media.ExifInterface
import androidx.fragment.app.Fragment
import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope
import com.example.platform.camera.R
import com.example.platform.camera.common.CombinedCaptureResult
import com.example.platform.camera.common.DirectExecutor
import com.example.platform.camera.common.OrientationLiveData
import com.example.platform.camera.common.SIZE_720P
import com.example.platform.camera.common.computeExifOrientation
import com.example.platform.camera.common.getPreviewOutputSize
import com.example.platform.camera.databinding.Camera2UltrahdrCaptureBinding
import com.example.platform.graphics.ultrahdr.display.VisualizingAnUltraHDRGainmap
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.concurrent.ArrayBlockingQueue
import java.util.concurrent.TimeoutException
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
class Camera2UltraHDRCapture : Fragment() {
/**
* Android ViewBinding.
*/
private var _binding: Camera2UltrahdrCaptureBinding? = null
private val binding get() = _binding!!
/**
* Detects, characterizes, and connects to a CameraDevice (used for all camera operations).
*/
private val cameraManager: CameraManager by lazy {
val context = requireContext().applicationContext
context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
}
/**
* List of camera id provided by the [CameraManager].
*/
private val cameraIds: List<String> by lazy {
cameraManager.cameraIdList.asList()
}
/**
* [CameraCharacteristics] corresponding to the provided Camera ID.
*/
private val characteristics: CameraCharacteristics by lazy {
// This will use the first camera id from cameraIds.
cameraManager.getCameraCharacteristics(cameraIds.first())
}
/**
* [HandlerThread] where all camera operations run
*/
private val cameraThread = HandlerThread("CameraThread").apply { start() }
/**
* [Handler] corresponding to [cameraThread]
*/
private val cameraHandler = Handler(cameraThread.looper)
/**
* [HandlerThread] where all buffer reading operations run
*/
private val imageReaderThread = HandlerThread("imageReaderThread").apply { start() }
/**
* [Handler] corresponding to [imageReaderThread]
*/
private val imageReaderHandler = Handler(imageReaderThread.looper)
/**
* The [CameraDevice] that will be opened in this fragment.
*/
private lateinit var camera: CameraDevice
/**
* Internal reference to the ongoing [CameraCaptureSession] configured with our parameters.
*/
private lateinit var session: CameraCaptureSession
/**
* [OrientationLiveData] listener for changes in the device orientation relative to the camera.
*/
private lateinit var relativeOrientation: OrientationLiveData
/**
* Readers used as buffers for camera still shots
*/
private lateinit var imageReader: ImageReader
/**
* Performs A flash animation to mimic a common shutter when taking a photo.
*/
private val animationTask: Runnable by lazy {
Runnable {
// Flash white animation
binding.imageCaptureOverlay.background =
Color.argb(150, 255, 255, 255).toDrawable()
// Wait for ANIMATION_FAST_MILLIS
binding.imageCaptureOverlay.postDelayed(
{
// Remove white flash animation
binding.imageCaptureOverlay.background = null
},
ANIMATION_FAST_MILLIS,
)
}
}
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission(),
::handleCameraPermissionResult,
)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
_binding = Camera2UltrahdrCaptureBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setUpCamera()
}
override fun onStart() {
super.onStart()
when {
isPermissionGranted() -> view?.post(::initializeCamera)
shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> {
showActionMessage("This sample requires CAMERA permission to work. Please grant it") {
requestCameraPermission()
}
}
else -> {
showActionMessage("Grant permission") {
requestCameraPermission()
}
}
}
}
/**
* Attempt to request camera permissions.
*/
private fun requestCameraPermission() =
requestPermissionLauncher.launch(Manifest.permission.CAMERA)
/**
* Handle result of camera permissions request.
*/
private fun handleCameraPermissionResult(isGranted: Boolean) {
when (isGranted) {
true -> view?.post(::initializeCamera)
false -> view?.let {
showActionMessage("Permissions not granted: Try again") {
requestCameraPermission()
}
}
}
}
private fun showActionMessage(message: String, action: () -> Unit) {
binding.capturePermissionButton.visibility = View.VISIBLE
binding.capturePermissionButton.text = message
binding.capturePermissionButton.setOnClickListener {
action()
}
}
private fun showImage(imageLocation: String) {
binding.captureButton.visibility = View.GONE
binding.backButton.visibility = View.VISIBLE
binding.viewfinder.visibility = View.INVISIBLE
val bundle = bundleOf(VisualizingAnUltraHDRGainmap.ARG_KEY_LOCATION to imageLocation)
val imageViewer = VisualizingAnUltraHDRGainmap().apply { arguments = bundle }
requireActivity().supportFragmentManager.commit {
setReorderingAllowed(true)
add(R.id.image_viewer, imageViewer)
}
binding.backButton.setOnClickListener {
binding.captureButton.visibility = View.VISIBLE
binding.viewfinder.visibility = View.VISIBLE
binding.backButton.visibility = View.GONE
requireActivity().supportFragmentManager.commit {
remove(imageViewer)
}
}
}
private fun setUpCamera() {
// Prevents SurfaceView from being destroyed when there is a visibility change. This must
// be called before setting SurfaceHolder.Callback
binding.viewfinder.setSurfaceLifecycle(SurfaceView.SURFACE_LIFECYCLE_FOLLOWS_ATTACHMENT)
binding.viewfinder.holder.addCallback(
object : SurfaceHolder.Callback {
override fun surfaceDestroyed(holder: SurfaceHolder) = Unit
override fun surfaceChanged(
holder: SurfaceHolder,
format: Int,
width: Int,
height: Int,
) = Unit
override fun surfaceCreated(holder: SurfaceHolder) {
// Selects appropriate preview size and configures view finder
val previewSize = getPreviewOutputSize(
binding.viewfinder.display,
characteristics,
SurfaceHolder::class.java,
)
Log.d(
TAG,
"Preview size: ${binding.viewfinder.width} " +
"x " + "${binding.viewfinder.height}",
)
Log.d(TAG, "Selected Preview Size: $previewSize")
binding.viewfinder.setAspectRatio(
previewSize?.width ?: SIZE_720P.size.width,
previewSize?.height ?: SIZE_720P.size.height,
)
}
},
)
// Used to rotate the output media to match device orientation
relativeOrientation = OrientationLiveData(requireContext(), characteristics).apply {
observe(viewLifecycleOwner) { orientation ->
Log.d(TAG, "Orientation changed: $orientation")
}
}
}
private fun setUpImageReader() {
// Initialize an image reader which will be used to capture still photos
val pixelFormat = ImageFormat.JPEG_R
val configMap = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
configMap?.let { config ->
config.getOutputSizes(pixelFormat).maxByOrNull { it.height * it.width }
?.let { size ->
imageReader = ImageReader.newInstance(
size.width, size.height, pixelFormat, IMAGE_BUFFER_SIZE,
)
}
}
// Listen to the capture button
binding.captureButton.setOnClickListener {
// Disable click listener to prevent multiple requests simultaneously in flight
it.isEnabled = false
// Perform I/O heavy operations in a different scope
lifecycleScope.launch(Dispatchers.IO) {
takePhoto().use { result ->
Log.d(TAG, "Result received: $result")
// Save the result to disk, update EXIF metadata with orientation info
val output = saveResult(result)
Log.d(TAG, "Image saved: ${output.absolutePath}")
val exif = ExifInterface(output.absolutePath)
exif.setAttribute(ExifInterface.TAG_ORIENTATION, result.orientation.toString())
exif.saveAttributes()
Log.d(TAG, "EXIF metadata saved: ${output.absolutePath}")
// Display the photo taken to user
lifecycleScope.launch(Dispatchers.Main) {
showImage(output.absolutePath)
}
}
// Re-enable click listener after photo is taken
it.post { it.isEnabled = true }
}
}
}
/**
* In order to capture and UltraHDR image, the camera device need to be capable of:
*
* - Encoding the JPEG/R format (UltraHDR)
*
* This function will check for both to determine device support.
*/
private fun canCaptureUltraHDR(c: CameraCharacteristics): Boolean {
// Query the available output formats.
val formats = c.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)?.outputFormats
val canEncodeUltraHDR = formats?.contains(ImageFormat.JPEG_R) ?: false
return canEncodeUltraHDR
}
/**
* Begin all camera operations in a coroutine in the main thread. This function:
* - Checks if JPEG/R (UltraHDR) is supported as and output format by the device.
* - Opens the camera
* - Configures the camera session
* - Starts the preview by dispatching a repeating capture request
*/
private fun initializeCamera() = lifecycleScope.launch(Dispatchers.Main) {
if (!canCaptureUltraHDR(characteristics)) {
showActionMessage(resources.getString(R.string.ultrahdr_image_capture_not_supported)) {}
return@launch
}
binding.capturePermissionButton.visibility = View.GONE
try {
// Open the selected camera
camera = openCamera(cameraManager, cameraIds.first(), cameraHandler)
// Set up the image reader
setUpImageReader()
// Create a preview configuration that will use HLG10
val previewConfiguration = OutputConfiguration(binding.viewfinder.holder.surface)
previewConfiguration.dynamicRangeProfile = DynamicRangeProfiles.HLG10
// Creates list of Surfaces where the camera will output frames
val targets = listOf(
previewConfiguration,
OutputConfiguration(imageReader.surface),
)
// Start a capture session using our open camera and list of Surfaces where frames will
// go.
session = createCaptureSession(camera, targets)
val captureRequest = camera.createCaptureRequest(
CameraDevice.TEMPLATE_PREVIEW,
).apply { addTarget(binding.viewfinder.holder.surface) }
// This will keep sending the capture request as frequently as possible until the
// session is torn down or session.stopRepeating() is called
session.setRepeatingRequest(captureRequest.build(), null, cameraHandler)
} catch (e: Exception) {
Log.e(TAG, "initializeCamera failed", e)
showActionMessage("Camera init failed: Try again") {
view?.post(::initializeCamera)
}
}
}
/**
* Opens the camera and returns the opened device (as the result of the suspend coroutine)
*/
private suspend fun openCamera(
manager: CameraManager,
cameraId: String,
handler: Handler? = null,
): CameraDevice = suspendCancellableCoroutine { cont ->
try {
manager.openCamera(
cameraId,
object : CameraDevice.StateCallback() {
override fun onOpened(device: CameraDevice) = cont.resume(device)
override fun onDisconnected(device: CameraDevice) {
Log.w(TAG, "Camera $cameraId has been disconnected")
}
override fun onError(device: CameraDevice, error: Int) {
val msg = when (error) {
ERROR_CAMERA_DEVICE -> "Fatal (device)"
ERROR_CAMERA_DISABLED -> "Device policy"
ERROR_CAMERA_IN_USE -> "Camera in use"
ERROR_CAMERA_SERVICE -> "Fatal (service)"
ERROR_MAX_CAMERAS_IN_USE -> "Maximum cameras in use"
else -> "Unknown"
}
val exc = RuntimeException("Camera $cameraId error: ($error) $msg")
Log.e(TAG, exc.message, exc)
if (cont.isActive) cont.resumeWithException(exc)
}
},
handler,
)
} catch (e: SecurityException) {
requestCameraPermission()
}
}
/**
* Starts a [CameraCaptureSession] and returns the configured session (as the result of the
* suspend coroutine
*/
private suspend fun createCaptureSession(
device: CameraDevice,
targets: List<OutputConfiguration>,
): CameraCaptureSession = suspendCoroutine { cont ->
// First, create a CameraCaptureSession.StateCallback object for the onConfigured &
// onConfigureFailed callbacks.
val callback = object : CameraCaptureSession.StateCallback() {
override fun onConfigured(session: CameraCaptureSession) = cont.resume(session)
override fun onConfigureFailed(session: CameraCaptureSession) {
val exc = RuntimeException("Camera ${device.id} session configuration failed")
Log.e(TAG, exc.message, exc)
cont.resumeWithException(exc)
}
}
// Create a capture session using the predefined targets; this also involves defining the
// session state callback to be notified of when the session is ready
val sessionConfiguration = SessionConfiguration(
SessionConfiguration.SESSION_REGULAR,
targets,
DirectExecutor(),
callback,
)
device.createCaptureSession(sessionConfiguration)
}
/**
* Helper function used to capture a still image using the [CameraDevice.TEMPLATE_STILL_CAPTURE]
* template. It performs synchronization between the [CaptureResult] and the [Image] resulting
* from the single capture, and outputs a [CombinedCaptureResult] object.
*/
private suspend fun takePhoto(): CombinedCaptureResult = suspendCoroutine { cont ->
// Start a new image queue.
val imageQueue = ArrayBlockingQueue<Image>(IMAGE_BUFFER_SIZE)
imageReader.setOnImageAvailableListener(
{
val image = it.acquireLatestImage()
Log.d(TAG, "Image available in queue: ${image.timestamp}")
imageQueue.add(image)
},
imageReaderHandler,
)
val request = session.device.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE)
.apply { addTarget(imageReader.surface) }
session.capture(
request.build(),
object : CameraCaptureSession.CaptureCallback() {
override fun onCaptureStarted(
session: CameraCaptureSession,
request: CaptureRequest,
timestamp: Long,
frameNumber: Long,
) {
super.onCaptureStarted(session, request, timestamp, frameNumber)
binding.viewfinder.post(animationTask)
}
override fun onCaptureCompleted(
session: CameraCaptureSession,
request: CaptureRequest,
result: TotalCaptureResult,
) {
super.onCaptureCompleted(session, request, result)
val resultTimestamp = result.get(CaptureResult.SENSOR_TIMESTAMP)
Log.d(TAG, "Capture result received: $resultTimestamp")
// Set a timeout in case image captured is dropped from the pipeline
val exc = TimeoutException("Image de-queuing took too long")
val timeoutRunnable = Runnable { cont.resumeWithException(exc) }
imageReaderHandler.postDelayed(timeoutRunnable, IMAGE_CAPTURE_TIMEOUT_MILLIS)
// Loop in the coroutine's context until an image with matching timestamp comes
// We need to launch the coroutine context again because the callback is done in
// the handler provided to the `capture` method, not in our coroutine context.
@Suppress("BlockingMethodInNonBlockingContext")
lifecycleScope.launch(cont.context) {
while (true) {
// Dequeue images while timestamps don't match
val image = imageQueue.take()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
image.format != ImageFormat.DEPTH_JPEG &&
image.timestamp != resultTimestamp
) continue
Log.d(TAG, "Matching image dequeued: ${image.timestamp}")
// Unset the image reader listener
imageReaderHandler.removeCallbacks(timeoutRunnable)
imageReader.setOnImageAvailableListener(null, null)
// Clear the queue of images, if there are left.
while (imageQueue.size > 0) {
imageQueue.take().close()
}
// Compute EXIF orientation metadata.
val rotation = relativeOrientation.value ?: 0
val mirrored = characteristics.get(CameraCharacteristics.LENS_FACING) ==
CameraCharacteristics.LENS_FACING_FRONT
val exifOrientation = computeExifOrientation(rotation, mirrored)
// Build the result and resume progress
cont.resume(
CombinedCaptureResult(
image,
result,
exifOrientation,
imageReader.imageFormat,
),
)
// There is no need to break out of the loop because this coroutine will
// suspend.
}
}
}
},
cameraHandler,
)
}
/**
* Helper function used to save a [CombinedCaptureResult] into a [File]
*/
private suspend fun saveResult(result: CombinedCaptureResult): File = suspendCoroutine { cont ->
val buffer = result.image.planes[0].buffer
val bytes = ByteArray(buffer.remaining()).apply { buffer.get(this) }
try {
val output = createFile(requireContext())
FileOutputStream(output).use { it.write(bytes) }
cont.resume(output)
} catch (exc: IOException) {
Log.e(TAG, "Unable to write JPEG image to file", exc)
cont.resumeWithException(exc)
}
}
override fun onDetach() {
super.onDetach()
try {
camera.close()
} catch (exc: Throwable) {
Log.e(TAG, "Error closing camera", exc)
}
}
override fun onDestroyView() {
super.onDestroyView()
cameraThread.quitSafely()
}
private fun isPermissionGranted() = ContextCompat.checkSelfPermission(
requireContext(), CAMERA_PERMISSION,
) == PackageManager.PERMISSION_GRANTED
companion object {
private val TAG = Camera2ImageCapture::class.java.simpleName
private const val CAMERA_PERMISSION = Manifest.permission.CAMERA
/**
* Milliseconds used for UI animations
*/
const val ANIMATION_FAST_MILLIS = 50L
/**
* Maximum number of images that will be held in the reader's buffer
*/
private const val IMAGE_BUFFER_SIZE: Int = 3
/**
* Maximum time allowed to wait for the result of an image capture
*/
private const val IMAGE_CAPTURE_TIMEOUT_MILLIS: Long = 5000
/**
* Create a [File] named a using formatted timestamp with the current date and time.
*
* @return [File] created.
*/
private fun createFile(context: Context): File {
val sdf = SimpleDateFormat("yyyy_MM_dd_HH_mm_ss_SSS", Locale.US)
return File(context.filesDir, "IMG_${sdf.format(Date())}.jpg")
}
}
}

View File

@ -0,0 +1,355 @@
/*
* Copyright 2023 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.
*/
package com.example.platform.camera.preview
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.hardware.camera2.CameraCaptureSession
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CameraManager
import android.hardware.camera2.params.OutputConfiguration
import android.hardware.camera2.params.SessionConfiguration
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.HandlerThread
import android.util.Log
import android.view.LayoutInflater
import android.view.Surface
import android.view.SurfaceHolder
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresPermission
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.example.platform.camera.common.DirectExecutor
import com.example.platform.camera.common.OrientationLiveData
import com.example.platform.camera.common.SIZE_720P
import com.example.platform.camera.common.getPreviewOutputSize
import com.example.platform.camera.databinding.Camera2PreviewBinding
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
class Camera2Preview : Fragment() {
/**
* Android ViewBinding.
*/
private var _binding: Camera2PreviewBinding? = null
private val binding get() = _binding!!
/**
* Detects, characterizes, and connects to a CameraDevice (used for all camera operations).
*/
private val cameraManager: CameraManager by lazy {
val context = requireContext().applicationContext
context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
}
/**
* List of camera id provided by the [CameraManager].
*/
private val cameraIds: List<String> by lazy {
cameraManager.cameraIdList.asList()
}
/**
* [CameraCharacteristics] corresponding to the provided Camera ID.
*/
private val characteristics: CameraCharacteristics by lazy {
// This will use the first camera id from cameraIds.
cameraManager.getCameraCharacteristics(cameraIds.first())
}
/**
* [HandlerThread] where all camera operations run
*/
private val cameraThread = HandlerThread("CameraThread").apply { start() }
/**
* [Handler] corresponding to [cameraThread]
*/
private val cameraHandler = Handler(cameraThread.looper)
/**
* The [CameraDevice] that will be opened in this fragment.
*/
private lateinit var camera: CameraDevice
/**
* Internal reference to the ongoing [CameraCaptureSession] configured with our parameters.
*/
private lateinit var session: CameraCaptureSession
/**
* [OrientationLiveData] listener for changes in the device orientation relative to the camera.
*/
private lateinit var relativeOrientation: OrientationLiveData
private val requestPermissionLauncher =
registerForActivityResult(
ActivityResultContracts.RequestPermission(),
::handleCameraPermissionResult,
)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
_binding = Camera2PreviewBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setUpCamera()
}
override fun onStart() {
super.onStart()
when {
isPermissionGranted() -> view?.post(::initializeCamera)
shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> {
showActionMessage("This sample requires CAMERA permission to work. Please grant it") {
requestCameraPermission()
}
}
else -> {
showActionMessage("Grant permission") {
requestCameraPermission()
}
}
}
}
/**
* Attempt to request camera permissions.
*/
private fun requestCameraPermission() =
requestPermissionLauncher.launch(Manifest.permission.CAMERA)
/**
* Handle result of camera permissions request.
*/
private fun handleCameraPermissionResult(isGranted: Boolean) {
when (isGranted) {
true -> view?.post(::initializeCamera)
false -> view?.let {
showActionMessage("Permissions not granted: Try again") {
requestCameraPermission()
}
}
}
}
private fun showActionMessage(message: String, action: () -> Unit) {
binding.fragmentCamera2PreviewAction.visibility = View.VISIBLE
binding.fragmentCamera2PreviewAction.text = message
binding.fragmentCamera2PreviewAction.setOnClickListener {
action()
}
}
private fun setUpCamera() {
binding.fragmentCamera2PreviewViewfinder.holder.addCallback(
object : SurfaceHolder.Callback {
override fun surfaceDestroyed(holder: SurfaceHolder) = Unit
override fun surfaceChanged(
holder: SurfaceHolder,
format: Int,
width: Int,
height: Int,
) = Unit
override fun surfaceCreated(holder: SurfaceHolder) {
// Selects appropriate preview size and configures view finder
val previewSize = getPreviewOutputSize(
binding.fragmentCamera2PreviewViewfinder.display,
characteristics,
SurfaceHolder::class.java,
)
Log.d(
TAG,
"Preview size: ${binding.fragmentCamera2PreviewViewfinder.width} x " +
"${binding.fragmentCamera2PreviewViewfinder.height}",
)
Log.d(TAG, "Selected Preview Size: $previewSize")
binding.fragmentCamera2PreviewViewfinder.setAspectRatio(
previewSize?.width ?: SIZE_720P.size.width,
previewSize?.height ?: SIZE_720P.size.height,
)
}
},
)
// Used to rotate the output media to match device orientation
relativeOrientation = OrientationLiveData(requireContext(), characteristics).apply {
observe(viewLifecycleOwner) { orientation ->
Log.d(TAG, "Orientation changed: $orientation")
}
}
}
/**
* Begin all camera operations in a coroutine in the main thread. This function:
* - Opens the camera
* - Configures the camera session
* - Starts the preview by dispatching a repeating capture request
*/
@RequiresPermission(Manifest.permission.CAMERA)
private fun initializeCamera() = lifecycleScope.launch(Dispatchers.Main) {
binding.fragmentCamera2PreviewAction.visibility = View.GONE
try {
// Open the selected camera
camera = openCamera(cameraManager, cameraIds.first(), cameraHandler)
// Creates list of Surfaces where the camera will output frames
val targets = listOf(binding.fragmentCamera2PreviewViewfinder.holder.surface)
// Start a capture session using our open camera and list of Surfaces where frames will go
session = createCaptureSession(camera, targets, cameraHandler)
val captureRequest = camera.createCaptureRequest(
CameraDevice.TEMPLATE_PREVIEW,
).apply { addTarget(binding.fragmentCamera2PreviewViewfinder.holder.surface) }
// This will keep sending the capture request as frequently as possible until the
// session is torn down or session.stopRepeating() is called
session.setRepeatingRequest(captureRequest.build(), null, cameraHandler)
} catch (e: Exception) {
Log.e(TAG, "initializeCamera failed", e)
showActionMessage("Camera init failed: Try again") {
view?.post(::initializeCamera)
}
}
}
/**
* Opens the camera and returns the opened device (as the result of the suspend coroutine)
*/
@RequiresPermission(Manifest.permission.CAMERA)
private suspend fun openCamera(
manager: CameraManager,
cameraId: String,
handler: Handler? = null,
): CameraDevice = suspendCancellableCoroutine { cont ->
manager.openCamera(
cameraId,
object : CameraDevice.StateCallback() {
override fun onOpened(device: CameraDevice) = cont.resume(device)
override fun onDisconnected(device: CameraDevice) {
Log.w(TAG, "Camera $cameraId has been disconnected")
}
override fun onError(device: CameraDevice, error: Int) {
val msg = when (error) {
ERROR_CAMERA_DEVICE -> "Fatal (device)"
ERROR_CAMERA_DISABLED -> "Device policy"
ERROR_CAMERA_IN_USE -> "Camera in use"
ERROR_CAMERA_SERVICE -> "Fatal (service)"
ERROR_MAX_CAMERAS_IN_USE -> "Maximum cameras in use"
else -> "Unknown"
}
val exc = RuntimeException("Camera $cameraId error: ($error) $msg")
Log.e(TAG, exc.message, exc)
if (cont.isActive) cont.resumeWithException(exc)
}
},
handler,
)
}
/**
* Starts a [CameraCaptureSession] and returns the configured session (as the result of the
* suspend coroutine
*/
private suspend fun createCaptureSession(
device: CameraDevice,
targets: List<Surface>,
handler: Handler? = null,
): CameraCaptureSession = suspendCoroutine { cont ->
// First, create a CameraCaptureSession.StateCallback object for the onConfigured &
// onConfigureFailed callbacks.
val callback = object : CameraCaptureSession.StateCallback() {
override fun onConfigured(session: CameraCaptureSession) = cont.resume(session)
override fun onConfigureFailed(session: CameraCaptureSession) {
val exc = RuntimeException("Camera ${device.id} session configuration failed")
Log.e(TAG, exc.message, exc)
cont.resumeWithException(exc)
}
}
// Create a capture session using the predefined targets; this also involves defining the
// session state callback to be notified of when the session is ready
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val configs = targets.map {
val config = OutputConfiguration(it)
config
}
val sessionConfiguration = SessionConfiguration(
SessionConfiguration.SESSION_REGULAR,
configs,
DirectExecutor(),
callback,
)
device.createCaptureSession(sessionConfiguration)
} else {
device.createCaptureSession(
targets,
callback,
handler,
)
}
}
override fun onDetach() {
super.onDetach()
try {
camera.close()
} catch (exc: Throwable) {
Log.e(TAG, "Error closing camera", exc)
}
}
override fun onDestroyView() {
super.onDestroyView()
cameraThread.quitSafely()
}
private fun isPermissionGranted() = ContextCompat.checkSelfPermission(
requireContext(), CAMERA_PERMISSION,
) == PackageManager.PERMISSION_GRANTED
companion object {
private val TAG = Camera2Preview::class.java.simpleName
private const val CAMERA_PERMISSION = Manifest.permission.CAMERA
}
}

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2023 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="48dp"
android:height="48dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z"
tools:ignore="VectorRaster" />
</vector>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2023 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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_shutter_pressed" android:state_pressed="true" />
<item android:drawable="@drawable/ic_shutter_focused" android:state_focused="true" />
<item android:drawable="@drawable/ic_shutter_normal" />
</selector>

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2023 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="74"
android:viewportHeight="74">
<path
android:fillColor="#FFFFFF"
android:fillType="evenOdd"
android:pathData="M73.1,37C73.1,17.0637 56.9373,0.9 37,0.9C17.0627,0.9 0.9,17.0637 0.9,37C0.9,56.9373 17.0627,73.1 37,73.1C56.9373,73.1 73.1,56.9373 73.1,37"
android:strokeWidth="1"
android:strokeColor="#00000000" />
<path
android:fillColor="#58A0C4"
android:fillType="evenOdd"
android:pathData="M67.4,37C67.4,53.7895 53.7895,67.4 37,67.4C20.2105,67.4 6.6,53.7895 6.6,37C6.6,20.2105 20.2105,6.6 37,6.6C53.7895,6.6 67.4,20.2105 67.4,37"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2023 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="74"
android:viewportHeight="74">
<path
android:fillColor="#FFFFFF"
android:fillType="evenOdd"
android:pathData="M73.1,37C73.1,17.0637 56.9373,0.9 37,0.9C17.0627,0.9 0.9,17.0637 0.9,37C0.9,56.9373 17.0627,73.1 37,73.1C56.9373,73.1 73.1,56.9373 73.1,37"
android:strokeWidth="1"
android:strokeColor="#00000000" />
<path
android:fillColor="#CFD7DB"
android:fillType="evenOdd"
android:pathData="M67.4,37C67.4,53.7895 53.7895,67.4 37,67.4C20.2105,67.4 6.6,53.7895 6.6,37C6.6,20.2105 20.2105,6.6 37,6.6C53.7895,6.6 67.4,20.2105 67.4,37"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2023 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="74"
android:viewportHeight="74">
<path
android:fillColor="#FFFFFF"
android:fillType="evenOdd"
android:pathData="M73.1,37C73.1,17.0637 56.9373,0.9 37,0.9C17.0627,0.9 0.9,17.0637 0.9,37C0.9,56.9373 17.0627,73.1 37,73.1C56.9373,73.1 73.1,56.9373 73.1,37"
android:strokeWidth="1"
android:strokeColor="#00000000" />
<path
android:fillColor="#58A0C4"
android:fillType="evenOdd"
android:pathData="M67.4,37C67.4,53.7895 53.7895,67.4 37,67.4C20.2105,67.4 6.6,53.7895 6.6,37C6.6,20.2105 20.2105,6.6 37,6.6C53.7895,6.6 67.4,20.2105 67.4,37"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2023 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.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.platform.camera.common.AutoFitSurfaceView
android:id="@+id/fragment_camera2_image_capture_viewfinder"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<Button
android:id="@+id/fragment_camera2_image_capture_permission_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
<View
android:id="@+id/fragment_camera2_image_capture_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foreground="@android:color/transparent" />
<ImageButton
android:id="@+id/fragment_camera2_image_capture_button"
android:layout_width="96dp"
android:layout_height="96dp"
android:layout_gravity="bottom|center"
android:background="@drawable/ic_shutter"
android:contentDescription="@string/capture"
android:scaleType="fitCenter" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/fragment_camera2_image_capture_image_view"
android:layout_width="wrap_content"
android:layout_height="match_parent" />
<ImageButton
android:id="@+id/fragment_camera2_post_capture_back_button"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="top|start"
android:layout_margin="16dp"
android:background="@drawable/ic_back"
android:scaleType="fitCenter"
android:visibility="gone" />
</FrameLayout>

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2023 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.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.platform.camera.common.AutoFitSurfaceView
android:id="@+id/fragment_camera2_preview_viewfinder"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<Button
android:id="@+id/fragment_camera2_preview_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
</FrameLayout>

View File

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2023 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.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.platform.camera.common.AutoFitSurfaceView
android:id="@+id/viewfinder"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<Button
android:id="@+id/capture_permission_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
<View
android:id="@+id/image_capture_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foreground="@android:color/transparent"
tools:ignore="UnusedAttribute" />
<ImageButton
android:id="@+id/capture_button"
android:layout_width="96dp"
android:layout_height="96dp"
android:layout_gravity="bottom|center"
android:layout_margin="16dp"
android:background="@drawable/ic_shutter"
android:contentDescription="@string/capture"
android:scaleType="fitCenter" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/image_viewer"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<Button
android:id="@+id/back_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:layout_margin="16dp"
android:contentDescription="@null"
android:scaleType="fitCenter"
android:text="@string/ultrahdr_image_capture_return_to_camera"
android:visibility="gone" />
</FrameLayout>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2023 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.
-->
<resources>
<string name="capture">Capture</string>
<!-- UltraHDR Image Capture Strings -->
<string name="ultrahdr_image_capture_return_to_camera">Return to Camera</string>
<string name="ultrahdr_image_capture_not_supported">UltraHDR Capture via Camera2 API is not supported on this device.</string>
</resources>

View File

@ -0,0 +1 @@
TBD

View File

@ -0,0 +1,71 @@
/*
* Copyright 2022 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.
*/
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.android)
}
android {
namespace = "com.example.platform.camerax"
compileSdk = 36
defaultConfig {
minSdk = 21
testOptions.targetSdk = 35
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
}
dependencies {
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.material3)
implementation(project(":shared"))
// CameraX
implementation(libs.androidx.camerax.core)
implementation(libs.androidx.camerax.camera2)
implementation(libs.androidx.camerax.compose)
implementation(libs.androidx.camerax.extensions)
implementation(libs.androidx.camerax.lifecycle)
implementation(libs.androidx.camerax.mlkit.vision)
implementation(libs.androidx.camerax.video)
implementation(libs.androidx.camerax.view)
// Image loading
implementation(libs.coil)
implementation(libs.coil.compose)
// Barcode Scanning
implementation(libs.barcode.scanning.common)
implementation(libs.play.services.mlkit.barcode.scanning)
// Permissions
implementation(libs.accompanist.permissions)
}

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2023 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<application>
<!-- Register file provider to create shareable URIs -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" />
</provider>
</application>
</manifest>

View File

@ -0,0 +1,309 @@
/*
* Copyright 2025 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.
*/
package com.example.platform.camerax.basic
import android.Manifest
import android.net.Uri
import androidx.camera.compose.CameraXViewfinder
import androidx.camera.viewfinder.compose.MutableCoordinateTransformer
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.isSpecified
import androidx.compose.ui.geometry.takeOrElse
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.round
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.rememberAsyncImagePainter
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionState
import com.google.accompanist.permissions.PermissionStatus
import com.google.accompanist.permissions.rememberPermissionState
import com.google.accompanist.permissions.shouldShowRationale
import kotlinx.coroutines.delay
import java.util.UUID.randomUUID
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
/**
* This is a basic CameraX sample demonstrating how to use CameraX with Compose. It handles camera
* permissions, displays a camera preview, and captures photos.
*/
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun CameraXBasic(modifier: Modifier = Modifier) {
var showCapturedImage by remember { mutableStateOf<Uri?>(null) }
val cameraPermissionState = rememberPermissionState(Manifest.permission.CAMERA)
val imageCaptureCallbackExecutor: ExecutorService = remember { Executors.newSingleThreadExecutor() }
val viewModel = viewModel { CameraXBasicViewModel() }
DisposableEffect(Unit) {
onDispose {
imageCaptureCallbackExecutor.shutdown()
}
}
Box(modifier = modifier.fillMaxSize()) {
ContentWithPermissionHandling(
cameraPermissionState = cameraPermissionState,
showCapturedImage = showCapturedImage,
onShowCapturedImageChange = { showCapturedImage = it },
viewModel = viewModel,
imageCaptureCallbackExecutor = imageCaptureCallbackExecutor
)
}
}
/**
* Handles camera permission status and displays content accordingly.
*
* If permission is granted, it shows either the camera preview or the captured image.
* If permission is denied, it displays a message and a button to request permission.
*
* @param cameraPermissionState The state of the camera permission.
* @param showCapturedImage The URI of the captured image to display, if any.
* @param onShowCapturedImageChange Callback function to update the [showCapturedImage] state.
* @param viewModel The [CameraXBasicViewModel] for handling camera operations.
* @param imageCaptureCallbackExecutor The executor service for image capture callbacks.
* @param modifier Modifier to be applied to the layout.
*/
@OptIn(ExperimentalPermissionsApi::class)
@Composable
private fun ContentWithPermissionHandling(
cameraPermissionState: PermissionState,
showCapturedImage: Uri?,
onShowCapturedImageChange: (Uri?) -> Unit,
viewModel: CameraXBasicViewModel,
imageCaptureCallbackExecutor: ExecutorService,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
when (cameraPermissionState.status) {
is PermissionStatus.Granted -> {
if (showCapturedImage != null) {
CapturedImageView(uri = showCapturedImage, modifier = modifier) {
onShowCapturedImageChange(null)
}
} else {
CameraPreviewContent(
viewModel = viewModel,
modifier = modifier,
lifecycleOwner = LocalLifecycleOwner.current,
onTakePhotoClick = {
viewModel.takePhoto(
context = context,
callbackExecutor = imageCaptureCallbackExecutor,
onImageCaptured = { uri -> onShowCapturedImageChange(uri) },
onError = { /* Error logging/toast handled within takePhoto */ },
)
},
)
}
}
is PermissionStatus.Denied -> CameraPermissionDeniedView(
status = cameraPermissionState.status,
cameraPermissionState = cameraPermissionState,
modifier = modifier
)
}
}
/**
* Composable function that displays a message when camera permission is denied.
*
* It provides an option to request the permission again.
*
* @param status The current [PermissionStatus] of the camera permission.
* @param cameraPermissionState The [PermissionState] for the camera permission, used to request permission again.
*/
@Composable
@OptIn(ExperimentalPermissionsApi::class)
private fun CameraPermissionDeniedView(
status: PermissionStatus,
cameraPermissionState: PermissionState,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
val textToShow = if (status.shouldShowRationale) {
"The camera is needed to take pictures. Please grant the permission."
} else {
"Camera permission is required to use this feature."
}
Text(text = textToShow)
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = { cameraPermissionState.launchPermissionRequest() }) {
Text("Request Permission")
}
}
}
/**
* Composable function that displays the camera preview.
*
* @param viewModel The [CameraXBasicViewModel] for accessing the camera preview surface request.
* @param modifier Modifier to be applied to the layout.
* @param onTakePhotoClick Callback function to be invoked when the "Take Photo" button is clicked.
* @param lifecycleOwner The lifecycle owner to bind the camera to.
*/
@Composable
private fun CameraPreviewContent(
viewModel: CameraXBasicViewModel,
modifier: Modifier = Modifier,
onTakePhotoClick: () -> Unit,
lifecycleOwner: LifecycleOwner,
) {
val surfaceRequest by viewModel.surfaceRequest.collectAsStateWithLifecycle()
val context = LocalContext.current
LaunchedEffect(lifecycleOwner) {
viewModel.bindToCamera(context.applicationContext, lifecycleOwner)
}
// State for managing the autofocus indicator
var autofocusRequest by remember { mutableStateOf(randomUUID() to Offset.Unspecified) }
// Extracting values from the autofocusRequest state
val autofocusRequestId = autofocusRequest.first
val showAutofocusIndicator = autofocusRequest.second.isSpecified
val autofocusLocation = remember(autofocusRequestId) { autofocusRequest.second }
// Effect to hide the autofocus indicator after a delay
if (showAutofocusIndicator) {
LaunchedEffect(autofocusRequestId) {
delay(1000)
// Clear the offset to finish the request and hide the indicator
autofocusRequest = autofocusRequestId to Offset.Unspecified
}
}
surfaceRequest?.let { request ->
val coordinateTransformer = remember { MutableCoordinateTransformer() }
Box(modifier = modifier.fillMaxSize()) {
CameraXViewfinder(
surfaceRequest = request,
coordinateTransformer = coordinateTransformer,
modifier = Modifier
.fillMaxSize() // Ensure CameraXViewfinder fills the Box
.pointerInput(viewModel, coordinateTransformer) {
detectTapGestures { tapCoords ->
with(coordinateTransformer) {
viewModel.tapToFocus(tapCoords.transform())
}
// Update the state to show the autofocus indicator
autofocusRequest = randomUUID() to tapCoords
}
},
)
AnimatedVisibility(
visible = showAutofocusIndicator,
enter = fadeIn(),
exit = fadeOut(),
modifier = Modifier
.offset { autofocusLocation.takeOrElse { Offset.Zero }.round() }
.offset((-24).dp, (-24).dp), // Adjust offset to center the indicator
) {
Spacer(
Modifier
.border(2.dp, Color.White, CircleShape)
.size(48.dp),
)
}
Button(
onClick = onTakePhotoClick,
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(16.dp),
) { Text("Take Photo") }
}
}
}
/**
* Composable function that displays a captured image.
*
* @param uri The URI of the image to display.
* @param onDismiss Callback function to be invoked when the user dismisses the image view
* (e.g., clicks the back button).
*/
@Composable
fun CapturedImageView(uri: Uri, modifier: Modifier = Modifier, onDismiss: () -> Unit = {}) {
Box(
modifier = modifier.fillMaxSize(),
) {
Image(
painter = rememberAsyncImagePainter(uri),
contentDescription = "Captured Image",
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Fit,
)
IconButton(
onClick = onDismiss,
modifier = Modifier
.align(Alignment.TopStart)
.padding(16.dp),
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back to Camera",
)
}
}
}

View File

@ -0,0 +1,194 @@
/*
* Copyright 2025 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.
*/
package com.example.platform.camerax.basic
import android.content.ContentValues
import android.content.Context
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import android.util.Log
import android.widget.Toast
import androidx.camera.core.CameraControl
import androidx.camera.core.CameraSelector.DEFAULT_BACK_CAMERA
import androidx.camera.core.FocusMeteringAction
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.Preview
import androidx.camera.core.SurfaceOrientedMeteringPointFactory
import androidx.camera.core.SurfaceRequest
import androidx.camera.core.UseCaseGroup
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.lifecycle.awaitInstance
import androidx.compose.ui.geometry.Offset
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.concurrent.ExecutorService
/**
* ViewModel for the CameraX Basic sample.
*
* This ViewModel handles the camera setup, preview display, tap to focus, and photo capture
* functionality using CameraX. It exposes a [StateFlow] for the camera preview [SurfaceRequest]
* to be used in a composable.
*/
class CameraXBasicViewModel : ViewModel() {
private val _surfaceRequest = MutableStateFlow<SurfaceRequest?>(null)
val surfaceRequest: StateFlow<SurfaceRequest?> = _surfaceRequest
private var surfaceMeteringPointFactory: SurfaceOrientedMeteringPointFactory? = null
private var cameraControl: CameraControl? = null
/**
* CameraX Preview use case configuration.
* Sets up a surface provider that updates the [_surfaceRequest] StateFlow.
*/
private val previewUseCase = Preview.Builder().build().apply {
setSurfaceProvider { newSurfaceRequest ->
_surfaceRequest.update { newSurfaceRequest }
surfaceMeteringPointFactory = SurfaceOrientedMeteringPointFactory(
newSurfaceRequest.resolution.width.toFloat(),
newSurfaceRequest.resolution.height.toFloat(),
)
}
}
/**
* CameraX ImageCapture use case configuration.
*/
private val imageCaptureUseCase = ImageCapture.Builder().build()
/**
* Binds the camera to the lifecycle owner and selected use cases.
*
* @param appContext The application context.
* @param lifecycleOwner The lifecycle owner to which the camera will be bound.
*/
suspend fun bindToCamera(appContext: Context, lifecycleOwner: LifecycleOwner) {
val processCameraProvider = ProcessCameraProvider.awaitInstance(appContext)
val useCaseGroup = UseCaseGroup.Builder()
.addUseCase(previewUseCase) // Add Preview UseCase
.addUseCase(imageCaptureUseCase) // Add Image Capture UseCase
.build()
val camera = processCameraProvider.bindToLifecycle(
lifecycleOwner = lifecycleOwner,
cameraSelector = DEFAULT_BACK_CAMERA, // Default to the back camera
useCaseGroup = useCaseGroup,
)
// Assign camera control for tap-to-focus functionality.
cameraControl = camera.cameraControl
// Cancellation signals we're done with the camera
try {
awaitCancellation()
} finally {
processCameraProvider.unbindAll()
cameraControl = null
}
}
/**
* Initiates tap-to-focus at the given coordinates on the preview surface.
*
* @param coordinates The coordinates of the tap relative to the preview surface.
*/
fun tapToFocus(coordinates: Offset) {
val point = surfaceMeteringPointFactory?.createPoint(coordinates.x, coordinates.y)
if (point != null) {
val meteringAction = FocusMeteringAction.Builder(point).build()
cameraControl?.startFocusAndMetering(meteringAction)
}
}
/**
* Takes a photo and saves it to external storage.
*
* @param context The application context.
* @param callbackExecutor The executor to run the image capture callbacks on.
* @param onImageCaptured Callback invoked when the image is successfully captured and saved.
* @param onError Callback invoked if an error occurs during image capture.
*/
fun takePhoto(
context: Context,
callbackExecutor: ExecutorService,
onImageCaptured: (Uri?) -> Unit,
onError: (ImageCaptureException) -> Unit,
) {
val name = SimpleDateFormat(
"yyyy-MM-dd-HH-mm-ss-SSS",
Locale.US,
).format(System.currentTimeMillis())
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, name)
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraXBasic")
}
}
val outputOptions = ImageCapture.OutputFileOptions
.Builder(
context.contentResolver,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues,
)
.build()
imageCaptureUseCase.takePicture(
outputOptions,
callbackExecutor,
ImageSavedCallback(context, onImageCaptured, onError),
)
}
}
/**
* Callback for handling image capture results.
*
* @param context The application context.
* @param onImageCaptured Callback invoked when the image is successfully captured and saved.
* @param onError Callback invoked if an error occurs during image capture.
*/
private class ImageSavedCallback(
private val context: Context,
private val onImageCaptured: (Uri?) -> Unit,
private val onErrorA: (ImageCaptureException) -> Unit,
) : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
val savedUri = output.savedUri
Log.d("CameraXComposeApp", "Photo capture succeeded: $savedUri")
onImageCaptured(savedUri)
}
override fun onError(exc: ImageCaptureException) {
Log.e("CameraXComposeApp", "Photo capture failed: ${exc.message}", exc)
ContextCompat.getMainExecutor(context).execute {
Toast.makeText(context, "Photo capture failed: ${exc.message}", Toast.LENGTH_SHORT)
.show()
}
onErrorA(exc)
}
}

View File

@ -0,0 +1,455 @@
/*
* Copyright 2023 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.
*/
package com.example.platform.camerax.extensions
import android.content.ContentValues
import android.content.Context
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import android.util.Log
import android.widget.Toast
import androidx.camera.core.AspectRatio
import androidx.camera.core.Camera
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.Preview
import androidx.camera.extensions.ExtensionMode
import androidx.camera.extensions.ExtensionsManager
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
// Helper data class for UI state
data class CameraExtensionsState(
val cameraPermissionGranted: Boolean = false,
val isLoading: Boolean = true,
val errorMessage: String? = null,
val lensFacing: Int = CameraSelector.LENS_FACING_BACK,
val availableExtensions: Map<Int, List<Int>> = emptyMap(), // Map<LensFacing, List<ExtensionMode>>
val selectedExtension: Int = ExtensionMode.NONE,
val isTakingPicture: Boolean = false,
val lastCapturedUri: Uri? = null, // Optional: for showing thumbnail
)
class CameraExtensionsViewModel : ViewModel() {
private val _uiState = MutableStateFlow(CameraExtensionsState())
val uiState: StateFlow<CameraExtensionsState> = _uiState.asStateFlow()
internal var cameraProvider: ProcessCameraProvider? = null
internal var extensionsManager: ExtensionsManager? = null
private var imageCapture: ImageCapture? = null
private var preview: Preview? = null
private var camera: Camera? = null
/** Executor for background camera operations */
private lateinit var cameraExecutor: ExecutorService
// Deferred objects to wait for async initialization
private var cameraProviderDeferred = CompletableDeferred<ProcessCameraProvider>()
private var extensionsManagerDeferred = CompletableDeferred<ExtensionsManager>()
// --- Initialization and Setup ---
fun initialize(context: Context) {
if (this::cameraExecutor.isInitialized) return // Avoid re-initialization
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
cameraExecutor = Executors.newSingleThreadExecutor()
viewModelScope.launch(Dispatchers.IO) {
try {
// Initialize CameraProvider and ExtensionsManager concurrently
val providerFuture = ProcessCameraProvider.getInstance(context)
providerFuture.addListener(
{
try {
cameraProvider = providerFuture.get()
cameraProviderDeferred.complete(cameraProvider!!)
// Now initialize ExtensionsManager after getting provider
val extensionsFuture =
ExtensionsManager.getInstanceAsync(context, cameraProvider!!)
extensionsFuture.addListener(
{
try {
extensionsManager = extensionsFuture.get()
extensionsManagerDeferred.complete(extensionsManager!!)
} catch (e: Exception) {
Log.e(TAG, "Failed to initialize ExtensionsManager", e)
extensionsManagerDeferred.completeExceptionally(e)
_uiState.update {
it.copy(
isLoading = false,
errorMessage = "Failed to initialize Camera Extensions: ${e.localizedMessage}",
)
}
}
},
ContextCompat.getMainExecutor(context),
)
} catch (e: Exception) {
Log.e(TAG, "Failed to initialize CameraProvider", e)
cameraProviderDeferred.completeExceptionally(e)
extensionsManagerDeferred.completeExceptionally(e) // Fail extensions too
_uiState.update {
it.copy(
isLoading = false,
errorMessage = "Failed to initialize Camera Provider: ${e.localizedMessage}",
)
}
}
},
ContextCompat.getMainExecutor(context),
)
// Wait for both to complete
cameraProvider = cameraProviderDeferred.await()
extensionsManager = extensionsManagerDeferred.await()
// Check available extensions after initialization
checkAvailableExtensions()
_uiState.update { it.copy(isLoading = false) }
} catch (e: Exception) {
Log.e(TAG, "Initialization failed", e)
_uiState.update {
it.copy(
isLoading = false,
errorMessage = "Camera initialization failed: ${e.localizedMessage}",
)
}
}
}
}
private fun checkAvailableExtensions() {
val provider = cameraProvider ?: return
val manager = extensionsManager ?: return
val allExtensionModes = listOf(
ExtensionMode.BOKEH,
ExtensionMode.HDR,
ExtensionMode.NIGHT,
ExtensionMode.FACE_RETOUCH,
ExtensionMode.AUTO,
)
val available: MutableMap<Int, List<Int>> = mutableMapOf()
// Check for Back Camera
val backCameraSelector = CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build()
if (provider.hasCamera(backCameraSelector)) {
val backExtensions = allExtensionModes.filter { manager.isExtensionAvailable(backCameraSelector, it) }
available[CameraSelector.LENS_FACING_BACK] = listOf(ExtensionMode.NONE) + backExtensions
} else {
available[CameraSelector.LENS_FACING_BACK] = listOf(ExtensionMode.NONE)
}
// Check for Front Camera
val frontCameraSelector =
CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_FRONT).build()
if (provider.hasCamera(frontCameraSelector)) {
val frontExtensions = allExtensionModes
.filter { manager.isExtensionAvailable(frontCameraSelector, it) }
available[CameraSelector.LENS_FACING_FRONT] =
listOf(ExtensionMode.NONE) + frontExtensions
} else {
available[CameraSelector.LENS_FACING_FRONT] = listOf(ExtensionMode.NONE)
}
Log.d(TAG, "Available extensions: $available")
_uiState.update {
val currentLensExtensions = available[it.lensFacing] ?: listOf(ExtensionMode.NONE)
// Reset selected extension if it's not available for the current lens
val newSelectedExtension = if (currentLensExtensions.contains(it.selectedExtension)) {
it.selectedExtension
} else {
ExtensionMode.NONE
}
it.copy(availableExtensions = available, selectedExtension = newSelectedExtension)
}
}
// --- Camera Binding ---
fun bindUseCases(
context: Context,
lifecycleOwner: LifecycleOwner,
surfaceProvider: Preview.SurfaceProvider,
targetRotation: Int,
) {
val provider = cameraProvider ?: run { Log.e(TAG, "CameraProvider not ready"); return }
val manager = extensionsManager ?: run { Log.e(TAG, "ExtensionsManager not ready"); return }
val lensFacing = _uiState.value.lensFacing
val selectedExtension = _uiState.value.selectedExtension
viewModelScope.launch(Dispatchers.Main) { // Ensure binding happens on the main thread
try {
// 1. Create CameraSelector (base or extension-enabled)
val baseCameraSelector =
CameraSelector.Builder().requireLensFacing(lensFacing).build()
val cameraSelector = if (selectedExtension != ExtensionMode.NONE &&
manager.isExtensionAvailable(baseCameraSelector, selectedExtension)
) {
Log.d(
TAG,
"Binding with extension: ${extensionModeToString(selectedExtension)}",
)
manager.getExtensionEnabledCameraSelector(baseCameraSelector, selectedExtension)
} else {
Log.d(
TAG,
"Binding without extension (Mode: ${extensionModeToString(selectedExtension)})",
)
baseCameraSelector
}
// 2. Build Use Cases (Preview and ImageCapture)
// Aspect ratio can be determined more dynamically if needed
val aspectRatio = AspectRatio.RATIO_16_9 // Or AspectRatio.RATIO_4_3
preview = Preview.Builder()
.setTargetRotation(targetRotation)
.setTargetAspectRatio(aspectRatio)
.build()
imageCapture = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
.setTargetRotation(targetRotation)
.setTargetAspectRatio(aspectRatio)
.build()
// 3. Unbind existing use cases before rebinding
provider.unbindAll()
// 4. Bind new use cases
camera = provider.bindToLifecycle(
lifecycleOwner,
cameraSelector,
preview,
imageCapture,
)
// 5. Attach SurfaceProvider
preview?.setSurfaceProvider(surfaceProvider)
Log.d(
TAG,
"Use cases bound successfully for lens $lensFacing, extension ${
extensionModeToString(selectedExtension)
}",
)
} catch (exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
_uiState.update { it.copy(errorMessage = "Failed to bind camera: ${exc.localizedMessage}") }
// Attempt to fallback to NONE mode if extension binding failed?
if (selectedExtension != ExtensionMode.NONE) {
Log.w(TAG, "Falling back to ExtensionMode.NONE")
selectExtension(ExtensionMode.NONE)
// Recursive call might be risky, maybe just signal UI to retry/reset?
}
}
}
}
// --- User Actions ---
fun onPermissionResult(granted: Boolean) {
_uiState.update { it.copy(cameraPermissionGranted = granted) }
if (!granted) {
_uiState.update {
it.copy(
errorMessage = "Camera permission is required.",
isLoading = false,
)
}
}
// Initialization might depend on permission, trigger if needed,
// but `initialize` is usually called once from the Composable's LaunchedEffect.
// If permission is granted later, the Composable's effect should re-trigger binding.
}
fun switchCamera() {
val currentLensFacing = _uiState.value.lensFacing
val newLensFacing = if (currentLensFacing == CameraSelector.LENS_FACING_BACK) {
CameraSelector.LENS_FACING_FRONT
} else {
CameraSelector.LENS_FACING_BACK
}
// Check if the new lens facing has any available extensions (including NONE)
if (_uiState.value.availableExtensions[newLensFacing]?.isNotEmpty() == true) {
Log.d(TAG, "Switching camera to $newLensFacing")
// Reset selected extension if it's not supported by the new lens
val newLensExtensions =
_uiState.value.availableExtensions[newLensFacing] ?: listOf(ExtensionMode.NONE)
val newSelectedExtension =
if (newLensExtensions.contains(_uiState.value.selectedExtension)) {
_uiState.value.selectedExtension
} else {
ExtensionMode.NONE // Default to NONE if current extension not supported
}
_uiState.update {
it.copy(
lensFacing = newLensFacing,
selectedExtension = newSelectedExtension,
)
}
// Rebinding will be triggered by the Composable observing these state changes
} else {
Log.w(
TAG,
"Cannot switch camera: Lens facing $newLensFacing not available or has no modes.",
)
_uiState.update { it.copy(errorMessage = "Cannot switch to other camera.") }
}
}
fun selectExtension(extensionMode: Int) {
val currentLens = _uiState.value.lensFacing
val availableForLens = _uiState.value.availableExtensions[currentLens] ?: listOf()
if (availableForLens.contains(extensionMode)) {
if (_uiState.value.selectedExtension != extensionMode) {
Log.d(TAG, "Selecting extension: ${extensionModeToString(extensionMode)}")
_uiState.update { it.copy(selectedExtension = extensionMode) }
// Rebinding will be triggered by the Composable observing this state change
}
} else {
Log.w(
TAG,
"Extension ${extensionModeToString(extensionMode)} not available for lens $currentLens",
)
}
}
fun takePicture(context: Context) {
val imageCapture = this.imageCapture ?: run {
_uiState.update { it.copy(errorMessage = "Camera not ready for capture.") }
return
}
if (_uiState.value.isTakingPicture) return // Prevent multiple captures
_uiState.update { it.copy(isTakingPicture = true, errorMessage = null) }
val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US).format(System.currentTimeMillis())
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, name)
put(MediaStore.MediaColumns.MIME_TYPE, PHOTO_TYPE)
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
val appName = "CameraXExtensions" // Ensure this string exists
put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/$appName")
}
}
val outputOptions = ImageCapture.OutputFileOptions.Builder(
context.contentResolver,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues,
).build()
imageCapture.takePicture(
outputOptions,
ContextCompat.getMainExecutor(context), // Callback on main thread
object : ImageCapture.OnImageSavedCallback {
override fun onError(exc: ImageCaptureException) {
Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
_uiState.update {
it.copy(
isTakingPicture = false,
errorMessage = "Capture failed: ${exc.message} (Code: ${exc.imageCaptureError})",
)
}
}
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
val savedUri = output.savedUri
Log.d(TAG, "Photo capture succeeded: $savedUri")
_uiState.update { it.copy(isTakingPicture = false, lastCapturedUri = savedUri) }
// Show a Toast with the saved image location
Toast.makeText(
context,
"Photo saved to: $savedUri",
Toast.LENGTH_SHORT
).show()
// Optionally trigger flash animation or sound here via state update
}
},
)
}
fun updateTargetRotation(rotation: Int) {
imageCapture?.targetRotation = rotation
preview?.targetRotation = rotation
}
fun clearErrorMessage() {
_uiState.update { it.copy(errorMessage = null) }
}
// --- Cleanup ---
override fun onCleared() {
super.onCleared()
try {
cameraProvider?.unbindAll()
} catch (e: Exception) {
Log.e(TAG, "Error unbinding camera provider on clear", e)
}
if (this::cameraExecutor.isInitialized) {
cameraExecutor.shutdown()
}
Log.d(TAG, "ViewModel cleared and resources released.")
}
companion object {
private const val TAG = "CameraExtViewModel"
private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
private const val PHOTO_TYPE = "image/jpeg"
// Helper to convert ExtensionMode Int to String for logging/display
fun extensionModeToString(mode: Int): String {
return when (mode) {
ExtensionMode.NONE -> "NONE"
ExtensionMode.BOKEH -> "BOKEH"
ExtensionMode.HDR -> "HDR"
ExtensionMode.NIGHT -> "NIGHT"
ExtensionMode.FACE_RETOUCH -> "FACE_RETOUCH"
ExtensionMode.AUTO -> "AUTO"
else -> "UNKNOWN ($mode)"
}
}
}
}

View File

@ -0,0 +1,442 @@
/*
* Copyright 2023 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.
*/
package com.example.platform.camerax.extensions
import android.Manifest
import android.content.Context
import android.hardware.display.DisplayManager
import android.os.Build
import android.util.Log
import android.view.ViewGroup
import android.view.WindowManager
import androidx.camera.core.CameraSelector
import androidx.camera.view.PreviewView
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.platform.camerax.extensions.CameraExtensionsViewModel.Companion.extensionModeToString
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionStatus
import com.google.accompanist.permissions.rememberPermissionState
import com.google.accompanist.permissions.shouldShowRationale
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun CameraXExtensions(
viewModel: CameraExtensionsViewModel = viewModel(),
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
// Request camera permission using Accompanist
val cameraPermissionState = rememberPermissionState(Manifest.permission.CAMERA)
// --- Initialization ---
// Initialize the ViewModel when permission is granted
LaunchedEffect(cameraPermissionState.status) {
if (cameraPermissionState.status == PermissionStatus.Granted) {
viewModel.initialize(context)
}
}
// --- Display Rotation Listener ---
// Use WindowManager for API levels below 30 to get the display rotation
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
var currentRotation by remember { mutableIntStateOf(windowManager.defaultDisplay.rotation) }
DisposableEffect(context) {
val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
val displayListener = object : DisplayManager.DisplayListener {
override fun onDisplayAdded(displayId: Int) = Unit
override fun onDisplayRemoved(displayId: Int) = Unit
override fun onDisplayChanged(displayId: Int) {
// Use WindowManager for API levels below 30
@Suppress("DEPRECATION")
val newRotation = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
context.display.rotation
} else {
windowManager.defaultDisplay.rotation
}
if (newRotation != currentRotation) {
Log.d("CameraExtScreen", "Rotation changed: $newRotation")
currentRotation = newRotation
viewModel.updateTargetRotation(newRotation) // Inform ViewModel
}
}
}
displayManager.registerDisplayListener(displayListener, null)
onDispose { displayManager.unregisterDisplayListener(displayListener) }
}
// --- UI Structure ---
Box(modifier = Modifier.fillMaxSize()) {
when (cameraPermissionState.status) {
PermissionStatus.Granted -> {
// Permission is granted, show the camera view
CameraView(
viewModel = viewModel,
uiState = uiState,
lifecycleOwner = lifecycleOwner,
targetRotation = currentRotation,
)
}
is PermissionStatus.Denied -> {
// Permission is denied, show a message and a button to request permission
PermissionRequestScreen(
status = cameraPermissionState.status,
onRequestPermission = { cameraPermissionState.launchPermissionRequest() },
)
}
}
}
}
// --- UI Components ---
@Composable
private fun CameraView(
viewModel: CameraExtensionsViewModel,
uiState: CameraExtensionsState,
lifecycleOwner: LifecycleOwner,
targetRotation: Int,
) {
val context = LocalContext.current
val previewView = remember { PreviewView(context) }
// Effect to bind use cases when permission, lens, extension, or rotation changes
LaunchedEffect(
viewModel.cameraProvider, // Add dependency on cameraProvider
viewModel.extensionsManager, // Add dependency on extensionsManager
uiState.lensFacing,
uiState.selectedExtension,
targetRotation, // Rebind if rotation changes significantly for targetRotation setting
) {
// Trigger binding once cameraProvider and extensionsManager are ready
if (viewModel.cameraProvider != null && viewModel.extensionsManager != null && uiState.errorMessage == null) {
Log.d(
"CameraExtScreen",
"Triggering bindUseCases: Lens=${uiState.lensFacing}, Ext=${
extensionModeToString(uiState.selectedExtension)
}, Rot=$targetRotation",
)
viewModel.bindUseCases(
context = context,
lifecycleOwner = lifecycleOwner,
surfaceProvider = previewView.surfaceProvider,
targetRotation = targetRotation,
)
}
}
Box(modifier = Modifier.fillMaxSize()) {
// Camera Preview
AndroidView(
factory = {
previewView.apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
)
scaleType = PreviewView.ScaleType.FILL_CENTER // Adjust as needed
implementationMode = PreviewView.ImplementationMode.PERFORMANCE
}
},
modifier = Modifier.fillMaxSize(),
// No update block needed here as LaunchedEffect handles rebinding
)
// Controls Overlay
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.SpaceBetween,
) {
// Top Row: Extension Selection
ExtensionSelector(
availableExtensions = uiState.availableExtensions[uiState.lensFacing] ?: listOf(),
selectedExtension = uiState.selectedExtension,
onExtensionSelected = { viewModel.selectExtension(it) },
modifier = Modifier.fillMaxWidth(),
)
// Bottom Row: Capture and Switch Camera
Row(
modifier = Modifier
.fillMaxWidth()
.navigationBarsPadding() // Add padding for navigation bar
.padding(bottom = 20.dp), // Extra padding from bottom
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceAround, // Space out buttons
) {
// Placeholder for gallery button if needed
Spacer(modifier = Modifier.size(60.dp))
// Capture Button
CaptureButton(
isTakingPicture = uiState.isTakingPicture,
onClick = { viewModel.takePicture(context) },
)
// Switch Camera Button
SwitchCameraButton(
availableExtensions = uiState.availableExtensions,
currentLensFacing = uiState.lensFacing,
onClick = { viewModel.switchCamera() },
)
}
}
}
// Handle Loading and Error states within the CameraView if permission is granted
when {
uiState.isLoading -> LoadingScreen()
uiState.errorMessage != null -> ErrorScreen(message = uiState.errorMessage)
}
}
@Composable
fun ExtensionSelector(
availableExtensions: List<Int>,
selectedExtension: Int,
onExtensionSelected: (Int) -> Unit,
modifier: Modifier = Modifier,
) {
if (availableExtensions.size <= 1) { // Only show if there's more than NONE
return
}
Row(
modifier = modifier
.statusBarsPadding() // Add padding for status bar
.horizontalScroll(rememberScrollState())
.background(Color.Black.copy(alpha = 0.4f), RoundedCornerShape(16.dp))
.padding(horizontal = 8.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
availableExtensions.forEach { mode ->
val isSelected = mode == selectedExtension
Text(
text = extensionModeToString(mode),
color = if (isSelected) MaterialTheme.colorScheme.primary else Color.White,
fontSize = 12.sp,
modifier = Modifier
.clip(RoundedCornerShape(12.dp))
.clickable { onExtensionSelected(mode) }
.background(
if (isSelected) MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.7f) else Color.Transparent,
RoundedCornerShape(12.dp),
)
.padding(horizontal = 12.dp, vertical = 6.dp),
)
}
}
}
@Composable
fun CaptureButton(isTakingPicture: Boolean, onClick: () -> Unit) {
IconButton(
onClick = onClick,
enabled = !isTakingPicture,
modifier = Modifier
.size(72.dp)
.border(4.dp, Color.White, CircleShape)
.padding(4.dp) // Padding inside the border
.background(Color.White.copy(alpha = 0.3f), CircleShape),
) {
if (isTakingPicture) {
CircularProgressIndicator(
modifier = Modifier.size(64.dp), // Slightly smaller than button
color = MaterialTheme.colorScheme.primary,
strokeWidth = 4.dp,
)
}
}
}
@Composable
fun SwitchCameraButton(
availableExtensions: Map<Int, List<Int>>,
currentLensFacing: Int,
onClick: () -> Unit,
) {
val otherLens = if (currentLensFacing == CameraSelector.LENS_FACING_BACK) {
CameraSelector.LENS_FACING_FRONT
} else {
CameraSelector.LENS_FACING_BACK
}
// Enable switch if the other lens exists in the available extensions map
val isEnabled = availableExtensions.containsKey(otherLens)
IconButton(
onClick = onClick,
enabled = isEnabled,
modifier = Modifier.size(60.dp),
) {
Icon(
Icons.Filled.Refresh,
contentDescription = "Switch camera",
tint = if (isEnabled) Color.White else Color.Gray,
modifier = Modifier.size(36.dp),
)
}
}
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun PermissionRequestScreen(
status: PermissionStatus,
onRequestPermission: () -> Unit,
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Icon(
Icons.Filled.Clear,
contentDescription = null,
tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(64.dp),
)
Spacer(modifier = Modifier.height(16.dp))
val textToShow = if (status.shouldShowRationale) {
"The camera is important for this feature. Please grant the permission."
} else {
"Camera permission is required for this feature to be available. " +
"Please grant the permission"
}
Text(
textToShow,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.headlineSmall,
)
Spacer(modifier = Modifier.height(8.dp))
Text(
"Please grant the permission to continue.",
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyMedium,
)
Spacer(modifier = Modifier.height(24.dp))
Button(onClick = onRequestPermission) {
Text("Grant Permission")
}
}
}
@Composable
fun LoadingScreen() {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.5f)),
contentAlignment = Alignment.Center,
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
CircularProgressIndicator(color = Color.White)
Spacer(modifier = Modifier.height(16.dp))
Text("Initializing Camera...", color = Color.White)
}
}
}
@Composable
fun ErrorScreen(message: String) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Icon(
Icons.Filled.Clear,
contentDescription = null,
tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(64.dp),
)
Spacer(modifier = Modifier.height(16.dp))
Text(
"Camera Error",
textAlign = TextAlign.Center,
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.error,
)
Spacer(modifier = Modifier.height(8.dp))
Text(
message,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyMedium,
)
}
}

View File

@ -0,0 +1,219 @@
/*
* Copyright 2023 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.
*/
package com.example.platform.camerax.mlkit
import android.Manifest
import android.util.Log
import android.util.Size
import androidx.camera.core.CameraSelector
import androidx.camera.core.CameraSelector.LENS_FACING_BACK
import androidx.camera.core.ImageAnalysis.COORDINATE_SYSTEM_VIEW_REFERENCED
import androidx.camera.core.resolutionselector.ResolutionSelector
import androidx.camera.core.resolutionselector.ResolutionStrategy
import androidx.camera.mlkit.vision.MlKitAnalyzer
import androidx.camera.view.LifecycleCameraController
import androidx.camera.view.PreviewView
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import androidx.lifecycle.compose.LocalLifecycleOwner
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionStatus
import com.google.accompanist.permissions.rememberPermissionState
import com.google.mlkit.vision.barcode.BarcodeScanner
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.barcode.common.Barcode
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun CameraXMlKit() {
var qrCodeDetected by remember { mutableStateOf(false) }
var qrCodeContent by remember { mutableStateOf("") }
// Request camera permission
val cameraPermissionState = rememberPermissionState(Manifest.permission.CAMERA)
val barcodeScanner = remember {
BarcodeScanning.getClient(
BarcodeScannerOptions.Builder()
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
.build(),
)
}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
when (cameraPermissionState.status) {
PermissionStatus.Granted -> {
// Permission is granted, show the camera preview
CameraPreview(
barcodeScanner,
{ detected -> qrCodeDetected = detected },
{ content -> qrCodeContent = content },
)
}
is PermissionStatus.Denied -> {
// Permission is denied, show a message and a button to request permission
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
val textToShow =
if ((cameraPermissionState.status as PermissionStatus.Denied).shouldShowRationale) {
"The camera is important for this feature. Please grant the permission."
} else {
"Camera permission is required for this feature to be available. " +
"Please grant the permission"
}
Text(
textToShow,
modifier = Modifier.align(Alignment.CenterHorizontally),
textAlign = TextAlign.Center,
)
Spacer(Modifier.height(8.dp))
Button(onClick = { cameraPermissionState.launchPermissionRequest() }) {
Text("Request permission")
}
}
}
}
QrCodeText(qrCodeDetected, qrCodeContent)
}
}
@Composable
fun QrCodeText(qrCodeDetected: Boolean, qrCodeContent: String) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.BottomCenter,
) {
Text(
text = if (qrCodeDetected) "QR Code Detected: $qrCodeContent" else "No QR Code Detected",
modifier = Modifier.padding(16.dp),
)
}
}
@Composable
fun CameraPreview(
barcodeScanner: BarcodeScanner,
setQrCodeDetected: (Boolean) -> Unit,
setQrCodeContent: (String) -> Unit,
) {
val lifecycleOwner = LocalLifecycleOwner.current
val context = LocalContext.current
var cameraError by remember { mutableStateOf(false) }
val cameraController = remember { LifecycleCameraController(context) }
val previewView = remember { PreviewView(context) }
cameraController.cameraSelector =
CameraSelector.Builder().requireLensFacing(LENS_FACING_BACK).build()
//Throttle the analysis to avoid constant checks.
val resolutionStrategy = ResolutionStrategy(
Size(500, 500),
ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER,
)
val resolutionSelector =
ResolutionSelector.Builder().setResolutionStrategy(resolutionStrategy).build()
cameraController.setImageAnalysisResolutionSelector(resolutionSelector)
cameraController.setImageAnalysisAnalyzer(
ContextCompat.getMainExecutor(context),
MlKitAnalyzer(
listOf(barcodeScanner),
COORDINATE_SYSTEM_VIEW_REFERENCED,
ContextCompat.getMainExecutor(context),
) { result: MlKitAnalyzer.Result? ->
val barcodeResults = result?.getValue(barcodeScanner)
if ((barcodeResults == null) ||
(barcodeResults.isEmpty()) ||
(barcodeResults.first() == null)
) {
setQrCodeDetected(false)
setQrCodeContent("") // Clear the text.
previewView.overlay.clear()
previewView.setOnTouchListener { _, _ -> false }
return@MlKitAnalyzer
}
val qrCode = barcodeResults[0]
val qrCodeViewModel = QrCodeViewModel(qrCode)
val qrCodeDrawable = QrCodeDrawable(qrCodeViewModel)
setQrCodeContent(qrCode.rawValue ?: "") // Display the content.
setQrCodeDetected(true)
previewView.setOnTouchListener(qrCodeViewModel.qrCodeTouchCallback)
previewView.overlay.clear()
previewView.overlay.add(qrCodeDrawable)
},
)
cameraController.bindToLifecycle(lifecycleOwner).also {
//Check if the camera was able to start or if there is a problem.
try {
cameraController.cameraInfo
} catch (e: Exception) {
Log.e("CameraXMlKit", "Camera error: $e")
cameraError = true
}
}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
if (cameraError) {
Text(
text = "Error: could not initialize camera",
modifier = Modifier
.padding(16.dp),
)
} else {
AndroidView(
factory = {
previewView.apply {
this.controller = cameraController
scaleType = PreviewView.ScaleType.FILL_CENTER
}
},
modifier = Modifier.fillMaxSize(),
)
}
}
}

View File

@ -0,0 +1,85 @@
/*
* Copyright 2025 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.
*/
package com.example.platform.camerax.mlkit
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.PixelFormat
import android.graphics.Rect
import android.graphics.drawable.Drawable
/**
* A Drawable that handles displaying a QR Code's data and a bounding box around the QR code.
*/
class QrCodeDrawable(private val qrCodeViewModel: QrCodeViewModel) : Drawable() {
private val boundingRectPaint = Paint().apply {
style = Paint.Style.STROKE
color = Color.YELLOW
strokeWidth = 5F
alpha = 200
}
private val contentRectPaint = Paint().apply {
style = Paint.Style.FILL
color = Color.YELLOW
alpha = 255
}
private val contentTextPaint = Paint().apply {
color = Color.DKGRAY
alpha = 255
textSize = 36F
}
private val contentPadding = 25
private var textWidth = contentTextPaint.measureText(qrCodeViewModel.qrContent).toInt()
override fun draw(canvas: Canvas) {
canvas.drawRect(qrCodeViewModel.boundingRect, boundingRectPaint)
canvas.drawRect(
Rect(
qrCodeViewModel.boundingRect.left,
qrCodeViewModel.boundingRect.bottom + contentPadding/2,
qrCodeViewModel.boundingRect.left + textWidth + contentPadding*2,
qrCodeViewModel.boundingRect.bottom + contentTextPaint.textSize.toInt() + contentPadding),
contentRectPaint
)
canvas.drawText(
qrCodeViewModel.qrContent,
(qrCodeViewModel.boundingRect.left + contentPadding).toFloat(),
(qrCodeViewModel.boundingRect.bottom + contentPadding*2).toFloat(),
contentTextPaint
)
}
override fun setAlpha(alpha: Int) {
boundingRectPaint.alpha = alpha
contentRectPaint.alpha = alpha
contentTextPaint.alpha = alpha
}
override fun setColorFilter(colorFiter: ColorFilter?) {
boundingRectPaint.colorFilter = colorFilter
contentRectPaint.colorFilter = colorFilter
contentTextPaint.colorFilter = colorFilter
}
@Deprecated("Deprecated in Java")
override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
}

View File

@ -0,0 +1,61 @@
/*
* Copyright 2025 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.
*/
package com.example.platform.camerax.mlkit
import android.content.Intent
import android.graphics.Rect
import android.view.MotionEvent
import android.view.View
import androidx.core.net.toUri
import com.google.mlkit.vision.barcode.common.Barcode
/**
* A ViewModel for encapsulating the data for a QR Code, including the encoded data, the bounding
* box, and the touch behavior on the QR Code.
*
* As is, this class only handles displaying the QR Code data if it's a URL. Other data types
* can be handled by adding more cases of Barcode.TYPE_URL in the init block.
*/
class QrCodeViewModel(barcode: Barcode) {
var boundingRect: Rect = barcode.boundingBox!!
var qrContent: String = ""
var qrCodeTouchCallback = { v: View, e: MotionEvent -> false } //no-op
init {
when (barcode.valueType) {
Barcode.TYPE_URL -> {
qrContent = barcode.url?.url ?: "Invalid URL"
qrCodeTouchCallback = { v: View, e: MotionEvent ->
if (e.action == MotionEvent.ACTION_DOWN && boundingRect.contains(
e.x.toInt(), e.y.toInt(),
)
) {
val openBrowserIntent = Intent(Intent.ACTION_VIEW)
openBrowserIntent.data = qrContent.toUri()
v.context.startActivity(openBrowserIntent)
}
true // return true from the callback to signify the event was handled
}
}
// Add other QR Code types here to handle other types of data,
// like Wifi credentials.
else -> {
qrContent = "Unsupported data type: ${barcode.rawValue.toString()}"
}
}
}
}

View File

@ -0,0 +1,505 @@
package com.example.platform.camerax.video
import android.Manifest
import android.content.ContentValues
import android.content.Context
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import android.util.Log
import android.widget.MediaController
import android.widget.Toast
import android.widget.VideoView
import androidx.camera.core.CameraSelector
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.video.*
import androidx.camera.view.PreviewView
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack // Corrected import
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import androidx.core.content.PermissionChecker
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.compose.LocalLifecycleOwner
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.MultiplePermissionsState
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
private enum class CameraScreenMode {
RECORDING,
PLAYBACK
}
/**
* The main screen composable for the camera functionality.
* Manages its own state including permissions (using Accompanist),
* camera executor, preview, recording controls, and screen navigation.
*/
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun CameraXVideo() {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
// --- State Management ---
var recordingState by remember { mutableStateOf<RecordingState>(RecordingState.Idle) }
var videoCapture by remember { mutableStateOf<VideoCapture<Recorder>?>(null) }
var recording by remember { mutableStateOf<Recording?>(null) }
// --- Screen navigation and video URI state ---
var currentScreen by remember { mutableStateOf(CameraScreenMode.RECORDING) }
var lastRecordedVideoUri by remember { mutableStateOf<Uri?>(null) }
val cameraExecutor = remember { Executors.newSingleThreadExecutor() }
DisposableEffect(Unit) {
onDispose {
Log.d(TAG, "Shutting down camera executor.")
cameraExecutor.shutdown()
}
}
val permissionsState = rememberMultiplePermissionsState(
permissions = REQUIRED_PERMISSIONS.toList(),
)
Box(modifier = Modifier.fillMaxSize()) {
when (currentScreen) {
CameraScreenMode.RECORDING -> {
if (permissionsState.allPermissionsGranted) {
Log.d(TAG, "All permissions granted, showing camera preview.")
CameraContent(
lifecycleOwner = lifecycleOwner,
// cameraExecutor removed as it's not directly used by CameraContent
recordingState = recordingState,
onVideoCaptureCreated = { newVideoCapture ->
Log.d(TAG, "VideoCapture instance created.")
videoCapture = newVideoCapture
},
onRecordClick = {
val currentVideoCapture = videoCapture
if (currentVideoCapture != null) {
if (recordingState == RecordingState.Idle) {
Log.d(TAG, "Start Recording button clicked.")
startRecording(
context = context,
videoCapture = currentVideoCapture,
executor = cameraExecutor, // cameraExecutor passed here
onRecordingStarted = { activeRec ->
recording = activeRec
recordingState = RecordingState.Recording
},
onRecordingError = { errorEvent ->
Log.e(TAG, "VideoCapture Error: ${errorEvent.cause}")
recording = null
recordingState = RecordingState.Idle
},
onRecordingComplete = { uri ->
Log.d(TAG, "Recording complete. URI: $uri")
lastRecordedVideoUri = uri
currentScreen =
CameraScreenMode.PLAYBACK
recording = null
recordingState = RecordingState.Idle
},
)
} else {
Log.d(TAG, "Stop Recording button clicked.")
recording?.stop()
}
} else {
Log.e(TAG, "Record button clicked but VideoCapture is null.")
Toast.makeText(context, "Camera not ready.", Toast.LENGTH_SHORT)
.show()
}
},
)
} else {
PermissionRationale(permissionsState)
}
}
CameraScreenMode.PLAYBACK -> {
lastRecordedVideoUri?.let { uri ->
VideoPlaybackScreen(
// Loop enabled here
videoUri = uri,
onBackToRecord = {
currentScreen = CameraScreenMode.RECORDING
lastRecordedVideoUri = null
},
)
} ?: run {
Log.e(TAG, "VideoPlaybackScreen requested but videoUri is null.")
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Text("Error: Video URI not available. Please record again.")
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = {
currentScreen = CameraScreenMode.RECORDING
},
) {
Text("Go Back to Record")
}
}
}
}
}
}
}
@Composable
private fun CameraContent(
lifecycleOwner: LifecycleOwner,
// cameraExecutor: ExecutorService, // Not needed here if startRecording is called from parent
recordingState: RecordingState,
onVideoCaptureCreated: (VideoCapture<Recorder>) -> Unit,
onRecordClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Box(modifier = modifier.fillMaxSize()) {
CameraPreview(
// cameraExecutor is needed by CameraPreview for binding
lifecycleOwner = lifecycleOwner,
// cameraExecutor passed here if CameraPreview handles binding independently
// If binding is managed by CameraXVideo, this might not be needed directly
onVideoCaptureCreated = onVideoCaptureCreated,
)
Column(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
RecordButton(
recordingState = recordingState,
onRecordClick = onRecordClick,
)
}
}
}
@OptIn(ExperimentalPermissionsApi::class)
@Composable
private fun PermissionRationale(
permissionsState: MultiplePermissionsState,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
val textToShow = if (permissionsState.shouldShowRationale) {
"Camera and Audio access are important for this app. Please grant the permissions."
} else {
"Camera and Audio permissions required for this feature to be available. " +
"Please grant the permissions."
}
Text(
textToShow,
textAlign = TextAlign.Center,
modifier = Modifier.padding(bottom = 8.dp),
)
Button(
onClick = {
Log.d(TAG, "Request Permissions button clicked.")
permissionsState.launchMultiplePermissionRequest()
},
) {
Text("Request permissions")
}
}
}
@Composable
fun CameraPreview(
modifier: Modifier = Modifier,
lifecycleOwner: LifecycleOwner,
onVideoCaptureCreated: (VideoCapture<Recorder>) -> Unit,
) {
val context = LocalContext.current
val previewView = remember { PreviewView(context) }
// If CameraX binding needs a specific executor, it should be sourced or passed here.
// Using ContextCompat.getMainExecutor(context) is common for listeners.
// The original 'cameraExecutor' from CameraXVideo could be passed if needed for binding.
val localCameraExecutor =
remember { Executors.newSingleThreadExecutor() } // Or pass from parent
Log.d(TAG, "CameraPreview Composable recomposing/launching.")
LaunchedEffect(lifecycleOwner, context) {
Log.d(TAG, "LaunchedEffect for camera binding starting.")
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener(
{
try {
val cameraProvider = cameraProviderFuture.get()
Log.d(TAG, "CameraProvider obtained.")
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(previewView.surfaceProvider)
Log.d(TAG, "Preview surface provider set.")
}
val recorder = Recorder.Builder()
.setQualitySelector(
QualitySelector.from(
Quality.HIGHEST,
FallbackStrategy.higherQualityOrLowerThan(Quality.SD),
),
)
.build()
val videoCapture: VideoCapture<Recorder> = VideoCapture.withOutput(recorder)
Log.d(TAG, "VideoCapture created, invoking callback.")
onVideoCaptureCreated(videoCapture)
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
Log.d(TAG, "Using default back camera.")
Log.d(TAG, "Unbinding all previous use cases.")
cameraProvider.unbindAll()
Log.d(TAG, "Binding Preview and VideoCapture use cases.")
cameraProvider.bindToLifecycle(
lifecycleOwner, cameraSelector, preview, videoCapture,
)
Log.d(TAG, "CameraX Use cases bound successfully.")
} catch (exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
Toast.makeText(
context,
"Failed to initialize camera: ${exc.message}",
Toast.LENGTH_LONG,
).show()
}
},
ContextCompat.getMainExecutor(context),
)
Log.d(TAG, "CameraProvider listener added.")
}
DisposableEffect(Unit) {
onDispose {
localCameraExecutor.isShutdown.not()
}
}
AndroidView(
{
Log.d(TAG, "AndroidView factory executing.")
previewView
},
modifier = modifier.fillMaxSize(),
)
}
@Composable
fun RecordButton(
recordingState: RecordingState,
onRecordClick: () -> Unit,
) {
Button(
onClick = onRecordClick,
colors = ButtonDefaults.buttonColors(
containerColor = if (recordingState == RecordingState.Recording)
MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
),
) {
Text(if (recordingState == RecordingState.Recording) "Stop Recording" else "Start Recording")
}
}
@Composable
fun VideoPlaybackScreen(
videoUri: Uri,
onBackToRecord: () -> Unit,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
val videoView = remember { VideoView(context) }
DisposableEffect(videoUri) {
val mediaController = MediaController(context)
mediaController.setAnchorView(videoView)
videoView.setVideoURI(videoUri)
videoView.setMediaController(mediaController)
videoView.requestFocus()
videoView.setOnPreparedListener { mp ->
mp.isLooping = true // Enable looping
Log.d(TAG, "VideoView prepared, looping enabled.")
}
videoView.start()
Log.d(TAG, "VideoView playback started with URI: $videoUri")
onDispose {
Log.d(TAG, "Disposing VideoView, stopping playback.")
videoView.stopPlayback()
videoView.setOnCompletionListener(null) // Clean up listener
videoView.setOnPreparedListener(null) // Clean up listener
}
}
Box(modifier = modifier.fillMaxSize()) {
AndroidView(
factory = { videoView },
modifier = Modifier.fillMaxSize(),
)
IconButton(
onClick = onBackToRecord,
modifier = Modifier
.align(Alignment.TopStart)
.padding(16.dp),
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack, // Updated icon
contentDescription = "Record New Video",
tint = MaterialTheme.colorScheme.onSurface,
)
}
}
}
enum class RecordingState {
Idle, Recording
}
private fun startRecording(
context: Context,
videoCapture: VideoCapture<Recorder>,
executor: ExecutorService,
onRecordingStarted: (Recording) -> Unit,
onRecordingError: (VideoRecordEvent.Finalize) -> Unit,
onRecordingComplete: (Uri) -> Unit,
) {
val mediaStoreOutputOptions = createMediaStoreOutputOptions(context)
Log.d(
TAG,
"Preparing recording to: ${mediaStoreOutputOptions.contentValues.getAsString(MediaStore.MediaColumns.DISPLAY_NAME)}",
)
val pendingRecording = videoCapture.output
.prepareRecording(context, mediaStoreOutputOptions)
.apply {
if (PermissionChecker.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) ==
PermissionChecker.PERMISSION_GRANTED
) {
Log.d(TAG, "Audio permission granted, enabling audio.")
withAudioEnabled()
} else {
Log.d(TAG, "Audio permission denied, recording without audio.")
}
}
val activeRecording =
pendingRecording.start(executor) { recordEvent ->
when (recordEvent) {
is VideoRecordEvent.Start -> {
Log.d(TAG, "Recording started successfully.")
}
is VideoRecordEvent.Finalize -> {
if (!recordEvent.hasError()) {
val outputUri = recordEvent.outputResults.outputUri
val msg = "Video capture succeeded: $outputUri"
Log.d(TAG, msg)
ContextCompat.getMainExecutor(context).execute {
Toast.makeText(context.applicationContext, msg, Toast.LENGTH_SHORT)
.show()
}
onRecordingComplete(outputUri)
} else {
val errorCause = recordEvent.cause ?: "Unknown error"
val errorCode = recordEvent.error
Log.e(
TAG,
"Video capture error ($errorCode): $errorCause",
recordEvent.cause,
)
ContextCompat.getMainExecutor(context).execute {
Toast.makeText(
context.applicationContext,
"Recording failed: $errorCause",
Toast.LENGTH_LONG,
).show()
}
onRecordingError(recordEvent)
}
}
is VideoRecordEvent.Status -> {
Log.v(TAG, "Status: ${recordEvent.recordingStats}")
}
is VideoRecordEvent.Pause -> Log.d(TAG, "Recording paused")
is VideoRecordEvent.Resume -> Log.d(TAG, "Recording resumed")
}
}
onRecordingStarted(activeRecording)
Log.d(TAG, "Recording initiated.")
}
private fun createMediaStoreOutputOptions(context: Context): MediaStoreOutputOptions {
val name = "CameraX-recording-" +
SimpleDateFormat(FILENAME_FORMAT, Locale.US)
.format(System.currentTimeMillis()) + ".mp4"
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, name)
put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/CameraX-Video")
}
}
Log.d(TAG, "Creating MediaStoreOutputOptions with name: $name")
return MediaStoreOutputOptions
.Builder(context.contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
.setContentValues(contentValues)
.build()
}
private const val TAG = "CameraXComposeExtended"
private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
val REQUIRED_PERMISSIONS =
mutableListOf(
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO,
).apply {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
Log.d(TAG, "Adding WRITE_EXTERNAL_STORAGE permission for API <= P.")
}
}.toTypedArray()

View File

@ -0,0 +1,249 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidLayouts">
<shared>
<config />
</shared>
</component>
<component name="AutoImportSettings">
<option name="autoReloadType" value="NONE" />
</component>
<component name="ChangeListManager">
<list default="true" id="6c035243-1600-40fc-873a-f2f1807d394a" name="Changes" comment="">
<change beforePath="$PROJECT_DIR$/../../../app/build.gradle.kts" beforeDir="false" afterPath="$PROJECT_DIR$/../../../app/build.gradle.kts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../../../build.gradle.kts" beforeDir="false" afterPath="$PROJECT_DIR$/../../../build.gradle.kts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../../../gradle/libs.versions.toml" beforeDir="false" afterPath="$PROJECT_DIR$/../../../gradle/libs.versions.toml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../../../settings.gradle.kts" beforeDir="false" afterPath="$PROJECT_DIR$/../../../settings.gradle.kts" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="ClangdSettings">
<option name="formatViaClangd" value="false" />
</component>
<component name="ExecutionTargetManager" SELECTED_TARGET="device_and_snapshot_combo_box_target[DeviceId(pluginId=Default, isTemplate=false, identifier=serial=localhost:45443;connection=d32b7868)]" />
<component name="ExternalProjectsData">
<projectState path="$PROJECT_DIR$/../../..">
<ProjectState />
</projectState>
<projectState path="$PROJECT_DIR$">
<ProjectState />
</projectState>
</component>
<component name="ExternalProjectsManager">
<system id="GRADLE">
<state>
<projects_view>
<tree_state>
<expand>
<path>
<item name="" type="6a2764b6:ExternalProjectsStructure$RootNode" />
<item name="com.google.apps.hellouwb" type="f1a62948:ProjectNode" />
</path>
</expand>
<select />
</tree_state>
</projects_view>
</state>
</system>
</component>
<component name="G3PluginsProjectSettings">
<appliedSettings>
<entry key="LinkifyInternalShortlinks" value="true" />
<entry key="RecognizedNullableAnnotations" value="true" />
</appliedSettings>
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$/../../.." />
</component>
<component name="GitSEFilterConfiguration">
<file-type-list>
<filtered-out-file-type name="LOCAL_BRANCH" />
<filtered-out-file-type name="REMOTE_BRANCH" />
<filtered-out-file-type name="TAG" />
<filtered-out-file-type name="COMMIT_BY_MESSAGE" />
</file-type-list>
</component>
<component name="MarkdownSettingsMigration">
<option name="stateVersion" value="1" />
</component>
<component name="ProjectColorInfo">{
&quot;associatedIndex&quot;: 2
}</component>
<component name="ProjectId" id="2O3xesDXeMdvs3oqdLhDpWeKsR9" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"Android App.Platform_Samples.app.executor": "Run",
"Android App.app.executor": "Run",
"Gradle.Upgrade Gradle wrapper.executor": "Run",
"RunOnceActivity.OpenProjectViewOnStart": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.cidr.known.project.marker": "true",
"RunOnceActivity.git.unshallow": "true",
"RunOnceActivity.readMode.enableVisualFormatting": "true",
"cf.first.check.clang-format": "false",
"cidr.known.project.marker": "true",
"git-widget-placeholder": "hello-uwb",
"kotlin-language-version-configured": "true",
"last_opened_file_path": "/usr/local/google/home/ziyiw/androidx-platform-samples/platform-samples/settings.gradle.kts",
"project.structure.last.edited": "Modules",
"project.structure.proportion": "0.17",
"project.structure.side.proportion": "0.2",
"settings.editor.selected.configurable": "project.propVCSSupport.DirectoryMappings"
}
}]]></component>
<component name="PsdUISettings">
<option name="MODULE_TAB" value="Properties" />
<option name="LAST_EDITED_SIGNING_CONFIG" value="debug" />
</component>
<component name="RunManager" selected="Android App.Platform_Samples.app">
<configuration name="Platform_Samples.app" type="AndroidRunConfigurationType" factoryName="Android App" activateToolWindowBeforeRun="false">
<module name="Platform_Samples.app" />
<option name="ANDROID_RUN_CONFIGURATION_SCHEMA_VERSION" value="1" />
<option name="DEPLOY" value="true" />
<option name="DEPLOY_APK_FROM_BUNDLE" value="false" />
<option name="DEPLOY_AS_INSTANT" value="false" />
<option name="ARTIFACT_NAME" value="" />
<option name="PM_INSTALL_OPTIONS" value="" />
<option name="ALL_USERS" value="false" />
<option name="ALWAYS_INSTALL_WITH_PM" value="false" />
<option name="ALLOW_ASSUME_VERIFIED" value="false" />
<option name="CLEAR_APP_STORAGE" value="false" />
<option name="DYNAMIC_FEATURES_DISABLED_LIST" value="" />
<option name="ACTIVITY_EXTRA_FLAGS" value="" />
<option name="MODE" value="default_activity" />
<option name="RESTORE_ENABLED" value="false" />
<option name="RESTORE_FILE" value="" />
<option name="RESTORE_FRESH_INSTALL_ONLY" value="false" />
<option name="CLEAR_LOGCAT" value="false" />
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
<option name="SELECTED_CLOUD_MATRIX_CONFIGURATION_ID" value="-1" />
<option name="SELECTED_CLOUD_MATRIX_PROJECT_ID" value="" />
<option name="DEBUGGER_TYPE" value="Java" />
<Java>
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
<option name="DEBUG_SANDBOX_SDK" value="false" />
</Java>
<Profilers>
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
<option name="STARTUP_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="Java/Kotlin Method Sample (legacy)" />
<option name="STARTUP_NATIVE_MEMORY_PROFILING_ENABLED" value="false" />
<option name="NATIVE_MEMORY_SAMPLE_RATE_BYTES" value="2048" />
</Profilers>
<option name="DEEP_LINK" value="" />
<option name="ACTIVITY" value="" />
<option name="ACTIVITY_CLASS" value="" />
<option name="SEARCH_ACTIVITY_IN_GLOBAL_SCOPE" value="false" />
<option name="SKIP_ACTIVITY_VALIDATION" value="false" />
<method v="2">
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
</method>
</configuration>
<configuration name="app" type="AndroidRunConfigurationType" factoryName="Android App" activateToolWindowBeforeRun="false">
<module name="com.google.apps.hellouwb.app" />
<option name="ANDROID_RUN_CONFIGURATION_SCHEMA_VERSION" value="1" />
<option name="DEPLOY" value="true" />
<option name="DEPLOY_APK_FROM_BUNDLE" value="false" />
<option name="DEPLOY_AS_INSTANT" value="false" />
<option name="ARTIFACT_NAME" value="" />
<option name="PM_INSTALL_OPTIONS" value="" />
<option name="ALL_USERS" value="false" />
<option name="ALWAYS_INSTALL_WITH_PM" value="false" />
<option name="ALLOW_ASSUME_VERIFIED" value="false" />
<option name="CLEAR_APP_STORAGE" value="false" />
<option name="DYNAMIC_FEATURES_DISABLED_LIST" value="" />
<option name="ACTIVITY_EXTRA_FLAGS" value="" />
<option name="MODE" value="default_activity" />
<option name="RESTORE_ENABLED" value="false" />
<option name="RESTORE_FILE" value="" />
<option name="RESTORE_FRESH_INSTALL_ONLY" value="false" />
<option name="CLEAR_LOGCAT" value="false" />
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
<option name="SELECTED_CLOUD_MATRIX_CONFIGURATION_ID" value="-1" />
<option name="SELECTED_CLOUD_MATRIX_PROJECT_ID" value="" />
<option name="DEBUGGER_TYPE" value="Java" />
<Java>
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
<option name="DEBUG_SANDBOX_SDK" value="false" />
</Java>
<Profilers>
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
<option name="STARTUP_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="Java/Kotlin Method Sample (legacy)" />
<option name="STARTUP_NATIVE_MEMORY_PROFILING_ENABLED" value="false" />
<option name="NATIVE_MEMORY_SAMPLE_RATE_BYTES" value="2048" />
</Profilers>
<option name="DEEP_LINK" value="" />
<option name="ACTIVITY" value="" />
<option name="ACTIVITY_CLASS" value="" />
<option name="SEARCH_ACTIVITY_IN_GLOBAL_SCOPE" value="false" />
<option name="SKIP_ACTIVITY_VALIDATION" value="false" />
<method v="2">
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
</method>
</configuration>
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="6c035243-1600-40fc-873a-f2f1807d394a" name="Changes" comment="" />
<created>1680803164569</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1680803164569</updated>
</task>
<servers />
</component>
<component name="play_dynamic_filters_status">
<option name="appIdToCheckInfo">
<map>
<entry key="com.example.platform">
<value>
<CheckInfo lastCheckTimestamp="1754344276157" />
</value>
</entry>
<entry key="com.example.platform.connectivity.callnotification.test">
<value>
<CheckInfo lastCheckTimestamp="1754348346466" />
</value>
</entry>
<entry key="com.example.platform.connectivity.telecom.test">
<value>
<CheckInfo lastCheckTimestamp="1754333221402" />
</value>
</entry>
<entry key="com.example.platform.connectivity.uwb.test">
<value>
<CheckInfo lastCheckTimestamp="1754333858514" />
</value>
</entry>
<entry key="com.example.platform.test">
<value>
<CheckInfo lastCheckTimestamp="1754344276184" />
</value>
</entry>
<entry key="com.google.apps.hellouwb">
<value>
<CheckInfo lastCheckTimestamp="1754092698917" />
</value>
</entry>
<entry key="com.google.apps.hellouwb.test">
<value>
<CheckInfo lastCheckTimestamp="1754092698931" />
</value>
</entry>
</map>
</option>
</component>
</project>

View File

@ -0,0 +1,74 @@
# Android Ultra-wideband sample
## Overview
This project showcases the current features of the
<b>[Android UWB Jetpack library](https://developer.android.com/jetpack/androidx/releases/core-uwb)</b>.
Includes code examples for:
* <b>Device compatibility</b> - How to check if an Android device supports UWB.
* <b>Device Discovery</b> - Ultra-wideband currently does not support a native way to discover devices, so an out of band (OOB) mechanism must be provided. This project uses the
[NearBy Connections API](https://developers.google.com/nearby/connections/overview), but other radio protocols like
Bluetooth, BLE, or Wi-Fi could also be used.
* <b>Simple Ranging</b> - The <b>Ranging</b> screen displays the controllee's distance from the the controller.
* <b>Device Control</b> - The <b>Control</b> screen simulates a use case where a door lock could be
opened when a UWB-capable device is near by.
* <b>Share Media</b> - The <b>Share file</b> screen demonstrates how to transfer a media file using the
selected OOB mechanism when devices are in close proximity.
* <b>Settings</b> - In this screen you can select which Android device will play each role (Controller or Controlee).
## Pre-requisites
* Two UWB-capable Android phones with Android 12 or higher
* Latest version of the [Core Ultra Wideband (UWB) library](https://developer.android.com/jetpack/androidx/releases/core-uwb)
## What is it not?
* An end to end example of Ultra-wideband technology.
The main goal is to demonstrate basic ranging capabilities between two Android devices and
how a selected OOB mechanism could be used to facilitate real use cases. For the latest information on the library status check [this article](https://developer.android.com/guide/topics/connectivity/uwb)
* A reference for a real production app with proper security, network access, app permissions, user authentication, etc. Check out the [Now in Android app](https://github.com/android/nowinandroid) instead.
* A UI/Material Design sample. The interface of the app is deliberately kept simple to focus on the UWB use cases. Check out the [Compose Samples](https://github.com/android/compose-samples) instead.
* A complete Jetpack sample covering all libraries. Check out [Android Sunflower](https://github.com/googlesamples/android-sunflower) or the advanced [GitHub Browser Sample](https://github.com/googlesamples/android-architecture-components/tree/master/GithubBrowserSample) instead.
## Who is it for?
* Intermediate developers looking for a simple way to understand how the UWB Jetpack library can be used.
* Advanced developers looking for a quick reference.
## Opening in Android Studio
To open this app in Android Studio, begin by checking out the entire ```connectivity-samples``` project:
1. Clone the repository, this step checks out the master branch.:
```
git clone git@github.com:android/connectivity-samples.git
```
2. Open the ```UwbRanging``` folder in the IDE.
### License
```
Copyright 2023 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.
```

View File

@ -0,0 +1,112 @@
/*
* Copyright 2023 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.
*/
//@Suppress("DSL_SCOPE_VIOLATION")
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.android)
id("kotlin-parcelize")
alias(libs.plugins.protobuf)
}
android {
namespace = "com.example.platform.connectivity.uwb"
compileSdk = 36
defaultConfig {
minSdk = 31
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
sourceSets {
getByName("main") {
java.srcDirs("src/main/java", "src/main/proto")
}
}
lint {
targetSdk = 35
}
testOptions {
targetSdk = 35
}
}
protobuf {
// Configures the protoc compiler
protoc {
// Automatically download protoc from Maven Central
artifact = "com.google.protobuf:protoc:3.25.3"
}
// Configures the code generation tasks
generateProtoTasks {
all().forEach { task ->
// Generate standard Kotlin data classes from your .proto files
task.plugins {
create("kotlin")
create("java")
}
}
}
}
dependencies {
implementation(libs.androidx.uwb)
implementation(project(mapOf("path" to ":samples:connectivity:audio")))
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.core)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(project(":shared"))
implementation(libs.androidx.datastore.preferences)
implementation(libs.androidx.navigation.compose)
implementation(libs.compose.material.iconsext)
implementation(libs.kotlin.coroutines.play)
implementation("com.google.android.gms:play-services-nearby:19.2.0")
implementation("com.google.protobuf:protobuf-java:3.25.3")
implementation("com.google.protobuf:protobuf-kotlin:3.25.3")
implementation("androidx.datastore:datastore:1.0.0")
implementation(libs.androidx.appcompat)
implementation(libs.kotlinx.serialization.json)
implementation(libs.firebase.crashlytics.buildtools)
androidTestImplementation(libs.androidx.test.core)
androidTestImplementation(libs.androidx.test.espresso.core)
androidTestImplementation(libs.androidx.test.rules)
androidTestImplementation(libs.androidx.test.runner)
debugImplementation(libs.androidx.ui.test.manifest)
androidTestImplementation(libs.hilt.testing)
androidTestImplementation(libs.junit4)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
}

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2022 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" />
<uses-permission android:name="android.permission.UWB_RANGING" />
<application
android:name="com.google.uwb.hellouwb.HelloUwbApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules">
<activity
android:name="com.google.uwb.hellouwb.ui.UwbRangingActivity"
android:exported="true"
android:theme="@style/Theme.Hellouwb">
</activity>
</application>
</manifest>

View File

@ -0,0 +1,29 @@
/*
*
* Copyright (C) 2022 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.google.uwb.hellouwb
import android.content.ContentResolver
import com.google.uwb.hellouwb.data.SettingsStore
import com.google.uwb.hellouwb.data.UwbRangingControlSource
interface AppContainer {
val rangingResultSource: UwbRangingControlSource
val settingsStore: SettingsStore
val contentResolver: ContentResolver
}

View File

@ -0,0 +1,31 @@
/*
*
* Copyright (C) 2022 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.google.uwb.hellouwb
import android.app.Application
import com.google.uwb.hellouwb.data.AppContainerImpl
class HelloUwbApplication : Application() {
lateinit var container: AppContainer
fun initContainer(afterLoading: () -> Unit) {
container = AppContainerImpl(applicationContext, afterLoading)
}
}

Some files were not shown because too many files have changed in this diff Show More