first commit
This commit is contained in:
commit
8750da4c24
6
.editorconfig
Normal file
6
.editorconfig
Normal 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
56
.gitignore
vendored
Normal 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
41
.google/packaging.yaml
Normal 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
3
.idea/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
1
.idea/.name
Normal file
1
.idea/.name
Normal file
@ -0,0 +1 @@
|
|||||||
|
Platform Samples
|
||||||
6
.idea/AndroidProjectSystem.xml
Normal file
6
.idea/AndroidProjectSystem.xml
Normal 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>
|
||||||
45
.idea/assetWizardSettings.xml
Normal file
45
.idea/assetWizardSettings.xml
Normal 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>
|
||||||
124
.idea/codeStyles/Project.xml
Normal file
124
.idea/codeStyles/Project.xml
Normal 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>
|
||||||
5
.idea/codeStyles/codeStyleConfig.xml
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
Normal 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
6
.idea/copyright/AOSP.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<component name="CopyrightManager">
|
||||||
|
<copyright>
|
||||||
|
<option name="notice" value="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." />
|
||||||
|
<option name="myName" value="AOSP" />
|
||||||
|
</copyright>
|
||||||
|
</component>
|
||||||
7
.idea/copyright/profiles_settings.xml
Normal file
7
.idea/copyright/profiles_settings.xml
Normal 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
6
.idea/kotlinc.xml
Normal 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
8
.idea/markdown.xml
Normal 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
10
.idea/migrations.xml
Normal 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>
|
||||||
17
.idea/runConfigurations.xml
Normal file
17
.idea/runConfigurations.xml
Normal 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
1
CODEOWNERS
Normal file
@ -0,0 +1 @@
|
|||||||
|
* @yrezgui @ashnohe @madebymozart @alabiaga
|
||||||
63
CODE_OF_CONDUCT.md
Normal file
63
CODE_OF_CONDUCT.md
Normal 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 hasn’t 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
28
CONTRIBUTING.md
Normal 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
201
LICENSE
Normal 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
68
README.md
Normal file
@ -0,0 +1,68 @@
|
|||||||
|

|
||||||
|
|
||||||
|
# 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
1
app/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/build
|
||||||
24
app/README.md
Normal file
24
app/README.md
Normal 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
113
app/build.gradle.kts
Normal 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
21
app/proguard-rules.pro
vendored
Normal 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
|
||||||
58
app/src/main/AndroidManifest.xml
Normal file
58
app/src/main/AndroidManifest.xml
Normal 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>
|
||||||
238
app/src/main/java/com/example/platform/app/ApiSurface.kt
Normal file
238
app/src/main/java/com/example/platform/app/ApiSurface.kt
Normal 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 }
|
||||||
122
app/src/main/java/com/example/platform/app/CatalogScreen.kt
Normal file
122
app/src/main/java/com/example/platform/app/CatalogScreen.kt
Normal 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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
app/src/main/java/com/example/platform/app/MainActivity.kt
Normal file
36
app/src/main/java/com/example/platform/app/MainActivity.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
app/src/main/java/com/example/platform/app/MainApp.kt
Normal file
19
app/src/main/java/com/example/platform/app/MainApp.kt
Normal 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()
|
||||||
104
app/src/main/java/com/example/platform/app/NavGraph.kt
Normal file
104
app/src/main/java/com/example/platform/app/NavGraph.kt
Normal 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"
|
||||||
1204
app/src/main/java/com/example/platform/app/SampleDemo.kt
Normal file
1204
app/src/main/java/com/example/platform/app/SampleDemo.kt
Normal file
File diff suppressed because it is too large
Load Diff
39
app/src/main/res/drawable-v24/ic_launcher_background.xml
Normal file
39
app/src/main/res/drawable-v24/ic_launcher_background.xml
Normal 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>
|
||||||
237
app/src/main/res/drawable-v24/ic_launcher_foreground.xml
Normal file
237
app/src/main/res/drawable-v24/ic_launcher_foreground.xml
Normal 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>
|
||||||
26
app/src/main/res/drawable-v24/ic_launcher_monochrome.xml
Normal file
26
app/src/main/res/drawable-v24/ic_launcher_monochrome.xml
Normal 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>
|
||||||
21
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
21
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal 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>
|
||||||
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.1 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.4 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.2 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
22
app/src/main/res/values/strings.xml
Normal file
22
app/src/main/res/values/strings.xml
Normal 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&title=%1$s</string>
|
||||||
|
</resources>
|
||||||
20
app/src/main/res/values/themes.xml
Normal file
20
app/src/main/res/values/themes.xml
Normal 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
22
build.gradle.kts
Normal 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
40
checksum.sh
Normal 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
71
gradle.properties
Normal 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
226
gradle/libs.versions.toml
Normal 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" }
|
||||||
61
gradle/spotless-init.gradle.kts
Normal file
61
gradle/spotless-init.gradle.kts
Normal 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
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
252
gradlew
vendored
Normal 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
94
gradlew.bat
vendored
Normal 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
6
renovate.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": [
|
||||||
|
"local>android/.github:renovate-config"
|
||||||
|
]
|
||||||
|
}
|
||||||
25
samples/accessibility/README.md
Normal file
25
samples/accessibility/README.md
Normal 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.
|
||||||
|
```
|
||||||
60
samples/accessibility/build.gradle.kts
Normal file
60
samples/accessibility/build.gradle.kts
Normal 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)
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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))
|
||||||
|
}
|
||||||
@ -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>
|
||||||
21
samples/accessibility/src/main/res/values/dimens.xml
Normal file
21
samples/accessibility/src/main/res/values/dimens.xml
Normal 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>
|
||||||
20
samples/accessibility/src/main/res/values/strings.xml
Normal file
20
samples/accessibility/src/main/res/values/strings.xml
Normal 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>
|
||||||
32
samples/camera/camera2/README.md
Normal file
32
samples/camera/camera2/README.md
Normal 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.
|
||||||
|
```
|
||||||
69
samples/camera/camera2/build.gradle.kts
Normal file
69
samples/camera/camera2/build.gradle.kts
Normal 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")))
|
||||||
|
}
|
||||||
25
samples/camera/camera2/src/main/AndroidManifest.xml
Normal file
25
samples/camera/camera2/src/main/AndroidManifest.xml
Normal 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>
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
@ -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()
|
||||||
|
}
|
||||||
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
26
samples/camera/camera2/src/main/res/drawable/ic_back.xml
Normal file
26
samples/camera/camera2/src/main/res/drawable/ic_back.xml
Normal 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>
|
||||||
20
samples/camera/camera2/src/main/res/drawable/ic_shutter.xml
Normal file
20
samples/camera/camera2/src/main/res/drawable/ic_shutter.xml
Normal 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>
|
||||||
@ -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>
|
||||||
@ -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>
|
||||||
@ -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>
|
||||||
@ -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>
|
||||||
@ -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>
|
||||||
@ -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>
|
||||||
24
samples/camera/camera2/src/main/res/values/strings.xml
Normal file
24
samples/camera/camera2/src/main/res/values/strings.xml
Normal 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>
|
||||||
|
|
||||||
1
samples/camera/camerax/README.md
Normal file
1
samples/camera/camerax/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
TBD
|
||||||
71
samples/camera/camerax/build.gradle.kts
Normal file
71
samples/camera/camerax/build.gradle.kts
Normal 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)
|
||||||
|
}
|
||||||
43
samples/camera/camerax/src/main/AndroidManifest.xml
Normal file
43
samples/camera/camerax/src/main/AndroidManifest.xml
Normal 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>
|
||||||
@ -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",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
@ -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()}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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()
|
||||||
249
samples/connectivity/UwbRanging/.idea/workspace.xml
Normal file
249
samples/connectivity/UwbRanging/.idea/workspace.xml
Normal 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">{
|
||||||
|
"associatedIndex": 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>
|
||||||
74
samples/connectivity/UwbRanging/README.md
Normal file
74
samples/connectivity/UwbRanging/README.md
Normal 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.
|
||||||
|
```
|
||||||
112
samples/connectivity/UwbRanging/build.gradle.kts
Normal file
112
samples/connectivity/UwbRanging/build.gradle.kts
Normal 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)
|
||||||
|
}
|
||||||
39
samples/connectivity/UwbRanging/src/main/AndroidManifest.xml
Normal file
39
samples/connectivity/UwbRanging/src/main/AndroidManifest.xml
Normal 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>
|
||||||
@ -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
|
||||||
|
}
|
||||||
@ -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
Loading…
Reference in New Issue
Block a user