Repository: xplodwild/android_packages_apps_Focal
Branch: master
Commit: d9cc873f3aa6
Files: 153
Total size: 938.0 KB
Directory structure:
gitextract_s1a8vicn/
├── .gitignore
├── Android.mk
├── AndroidManifest.xml
├── MODULE_LICENSE_GPL
├── NOTICE
├── assets/
│ └── picsphere/
│ ├── align_image_stack
│ ├── autooptimiser
│ ├── cpfind
│ ├── enfuse
│ ├── multiblend
│ ├── nona
│ ├── pano_modify
│ ├── ptclean
│ ├── pto_gen
│ ├── pto_var
│ └── tiffinfo
├── libs/
│ ├── LICENSE-metadata-extractor.txt
│ ├── metadata-extractor-2.6.4.jar
│ └── xmpcore.jar
├── res/
│ ├── drawable/
│ │ ├── btn_pin_widget_inactive.xml
│ │ ├── btn_shutter_photo.xml
│ │ ├── btn_shutter_stop.xml
│ │ ├── btn_shutter_video.xml
│ │ ├── cling_button_bg.xml
│ │ └── review_drawer_button.xml
│ ├── layout/
│ │ ├── activity_camera.xml
│ │ ├── handy.xml
│ │ ├── keyguard_widget.xml
│ │ ├── showcase_button.xml
│ │ ├── widget_container.xml
│ │ └── widget_layout.xml
│ ├── values/
│ │ ├── arrays.xml
│ │ ├── attrs.xml
│ │ ├── colors.xml
│ │ ├── config.xml
│ │ ├── ids.xml
│ │ ├── integers.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ ├── values-cs/
│ │ ├── arrays.xml
│ │ └── strings.xml
│ ├── values-da/
│ │ ├── arrays.xml
│ │ └── strings.xml
│ ├── values-de/
│ │ ├── arrays.xml
│ │ └── strings.xml
│ ├── values-el/
│ │ ├── arrays.xml
│ │ └── strings.xml
│ ├── values-en-rGB/
│ │ └── strings.xml
│ ├── values-es/
│ │ ├── arrays.xml
│ │ └── strings.xml
│ ├── values-fi/
│ │ ├── arrays.xml
│ │ └── strings.xml
│ ├── values-fr/
│ │ ├── arrays.xml
│ │ └── strings.xml
│ ├── values-hu/
│ │ ├── arrays.xml
│ │ └── strings.xml
│ ├── values-it/
│ │ ├── arrays.xml
│ │ └── strings.xml
│ ├── values-nl/
│ │ ├── arrays.xml
│ │ └── strings.xml
│ ├── values-pl/
│ │ ├── arrays.xml
│ │ └── strings.xml
│ ├── values-pt-rBR/
│ │ ├── arrays.xml
│ │ └── strings.xml
│ ├── values-pt-rPT/
│ │ ├── arrays.xml
│ │ └── strings.xml
│ ├── values-ru/
│ │ ├── arrays.xml
│ │ └── strings.xml
│ ├── values-sk/
│ │ ├── arrays.xml
│ │ └── strings.xml
│ ├── values-sv/
│ │ └── strings.xml
│ └── xml/
│ └── widget_info.xml
└── src/
└── org/
└── cyanogenmod/
└── focal/
├── BitmapFilter.java
├── CameraActivity.java
├── CameraApplication.java
├── CameraButtonIntentReceiver.java
├── CameraCapabilities.java
├── CameraManager.java
├── DisableCameraReceiver.java
├── Exif.java
├── FocusManager.java
├── PopenHelper.java
├── Profiler.java
├── SettingsStorage.java
├── SnapshotManager.java
├── SoundManager.java
├── Storage.java
├── Util.java
├── WidgetProvider.java
├── XMPHelper.java
├── feats/
│ ├── AutoPictureEnhancer.java
│ ├── BurstCapture.java
│ ├── CaptureTransformer.java
│ ├── GLToolbox.java
│ ├── PixelBuffer.java
│ ├── SoftwareHdrCapture.java
│ ├── SoftwareHdrProcessor.java
│ ├── SoftwareHdrRenderingService.java
│ ├── TextureRenderer.java
│ └── TimerCapture.java
├── pano/
│ ├── Mosaic.java
│ ├── MosaicFrameProcessor.java
│ ├── MosaicPreviewRenderer.java
│ ├── MosaicProxy.java
│ ├── MosaicRenderer.java
│ └── PanoUtil.java
├── picsphere/
│ ├── Capture3DRenderer.java
│ ├── PicSphere.java
│ ├── PicSphereCaptureTransformer.java
│ ├── PicSphereManager.java
│ ├── PicSphereRenderingService.java
│ ├── Quaternion.java
│ ├── SensorFusion.java
│ └── Vector3.java
├── ui/
│ ├── CenteredSeekBar.java
│ ├── CircleTimerView.java
│ ├── ExposureHudRing.java
│ ├── FocusHudRing.java
│ ├── HudRing.java
│ ├── Notifier.java
│ ├── PanoProgressBar.java
│ ├── PreviewFrameLayout.java
│ ├── ReviewDrawer.java
│ ├── RuleOfThirds.java
│ ├── SavePinger.java
│ ├── ShutterButton.java
│ ├── SideBar.java
│ ├── SwitchRingPad.java
│ ├── ThumbnailFlinger.java
│ ├── WidgetRenderer.java
│ └── showcase/
│ ├── AnimationUtils.java
│ ├── ShowcaseView.java
│ ├── ShowcaseViewBuilder.java
│ └── ShowcaseViews.java
└── widgets/
├── AutoExposureWidget.java
├── BurstModeWidget.java
├── EffectWidget.java
├── EnhancementsWidget.java
├── ExposureCompensationWidget.java
├── FlashWidget.java
├── HdrWidget.java
├── IsoWidget.java
├── SceneModeWidget.java
├── SettingsWidget.java
├── ShutterSpeedWidget.java
├── SimpleToggleWidget.java
├── SkinToneWidget.java
├── SoftwareHdrWidget.java
├── TimerModeWidget.java
├── VideoFrWidget.java
├── VideoHdrWidget.java
├── WhiteBalanceWidget.java
└── WidgetBase.java
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
project.properties
ic_launcher-web.png
gen/
bin/
.classpath
.project
.DS_Store
/.settings/org.eclipse.jdt.core.prefs
/.settings
/.idea
/out
/Nemesis.iml
!/libs/Android.mk
!/libs/LICENSE-metadata-extractor.txt
!/libs/metadata-extractor-2.6.4.jar
!/libs/xmpcore.jar
/libs/*/*
/obj
/res\drawable-xxhdpi/Thumbs.db
/jni
/libs/android-support-v4.jar
================================================
FILE: Android.mk
================================================
# Copyright (C) 2013 The CyanogenMod Project
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301, USA.
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_PACKAGE_NAME := Focal
# Change this to LOCAL_JNI_SHARED_LIBRARIES to include
# the binaries in the apk
LOCAL_REQUIRED_MODULES := \
libjni_mosaic2 \
libxmphelper_jni \
libpopen_helper_jni \
libexiv2 \
libglib-2.0 \
libgmodule-2.0 \
libgobject-2.0 \
libgthread-2.0 \
libjpeg \
libpano13 \
libtiffdecoder \
libvigraimpex \
libhugin \
libxml2 \
libiconv \
libpng_static \
autooptimiser \
autopano \
celeste \
nona \
ptclean \
enblend \
enfuse \
libxmptoolkit
LOCAL_STATIC_JAVA_LIBRARIES := \
metadata-extractor \
xmpcore \
android-support-v4
include $(BUILD_PACKAGE)
include $(CLEAR_VARS)
LOCAL_PREBUILT_STATIC_JAVA_LIBRARIES := metadata-extractor:libs/metadata-extractor-2.6.4.jar xmpcore:libs/xmpcore.jar
include $(BUILD_MULTI_PREBUILT)
include $(call all-makefiles-under, $(ANDROID_BUILD_TOP)/external/Focal)
================================================
FILE: AndroidManifest.xml
================================================
================================================
FILE: MODULE_LICENSE_GPL
================================================
================================================
FILE: NOTICE
================================================
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
Copyright (C)
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
================================================
FILE: libs/LICENSE-metadata-extractor.txt
================================================
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.
================================================
FILE: res/drawable/btn_pin_widget_inactive.xml
================================================
================================================
FILE: res/drawable/btn_shutter_photo.xml
================================================
================================================
FILE: res/drawable/btn_shutter_stop.xml
================================================
================================================
FILE: res/drawable/btn_shutter_video.xml
================================================
================================================
FILE: res/drawable/cling_button_bg.xml
================================================
================================================
FILE: res/drawable/review_drawer_button.xml
================================================
================================================
FILE: res/layout/activity_camera.xml
================================================
================================================
FILE: res/layout/handy.xml
================================================
================================================
FILE: res/layout/keyguard_widget.xml
================================================
================================================
FILE: res/layout/showcase_button.xml
================================================
================================================
FILE: res/layout/widget_container.xml
================================================
================================================
FILE: res/layout/widget_layout.xml
================================================
================================================
FILE: res/values/arrays.xml
================================================
ae-bracket-hdrcidcivwhiskywhiskeycheeseok cid, take a pictureautooffontorchred-eye@drawable/ic_widget_flash_auto@drawable/ic_widget_flash_off@drawable/ic_widget_flash_on@drawable/ic_widget_flash_torch@drawable/ic_widget_flash_redeyeAutoDisabledEnabledTorchRed-eye reductionnonemononegativesolarizesepiaposterizeblackboardwhiteboardaquaembosssketchneon@drawable/ic_widget_effect_none@drawable/ic_widget_effect_mono@drawable/ic_widget_effect_negative@drawable/ic_widget_effect_solarize@drawable/ic_widget_effect_sepia@drawable/ic_widget_effect_posterize@drawable/ic_widget_effect_blackboard@drawable/ic_widget_effect_whiteboard@drawable/ic_widget_effect_aqua@drawable/ic_widget_effect_emboss@drawable/ic_widget_effect_sketch@drawable/ic_widget_effect_neonDisabledBlack & WhiteNegativeSolarizeSepiaPosterizeBlackboardWhiteboardAquaEmbossSketchNeonautoISO_HJRISO100ISO200ISO400ISO800ISO1600@drawable/ic_widget_iso_auto@drawable/ic_widget_iso_hjr@drawable/ic_widget_iso_100@drawable/ic_widget_iso_200@drawable/ic_widget_iso_400@drawable/ic_widget_iso_800@drawable/ic_widget_iso_1600@string/iso_hint_auto@string/iso_hint_hjr@string/iso_hint_100@string/iso_hint_200@string/iso_hint_400@string/iso_hint_800@string/iso_hint_1600autoanti-motion-blurARasdbabybacklightbacklight-portraitbarcodebeachcandlelightdarkdishdocumentfireworksflowershandheld-twilighthdrhigh-sensitivitylandscapenightnightportraitnight-portraitpartypetportraitsnowsoft-skinsportsspot-lightsteadyphotosunsetsweep-stitchtheatre@drawable/ic_widget_scenemode_auto@drawable/ic_widget_scenemode_antimotionblur@drawable/ic_widget_scenemode_ar@drawable/ic_widget_scenemode_asd@drawable/ic_widget_scenemode_baby@drawable/ic_widget_scenemode_backlight@drawable/ic_widget_scenemode_backlightportrait@drawable/ic_widget_scenemode_barcode@drawable/ic_widget_scenemode_beach@drawable/ic_widget_scenemode_candlelight@drawable/ic_widget_scenemode_dark@drawable/ic_widget_scenemode_dish@drawable/ic_widget_scenemode_document@drawable/ic_widget_scenemode_fireworks@drawable/ic_widget_scenemode_flowers@drawable/ic_widget_scenemode_handheld@drawable/ic_widget_scenemode_hdr@drawable/ic_widget_scenemode_highsensitivity@drawable/ic_widget_scenemode_landscape@drawable/ic_widget_scenemode_night@drawable/ic_widget_scenemode_nightportrait@drawable/ic_widget_scenemode_nightportrait@drawable/ic_widget_scenemode_party@drawable/ic_widget_scenemode_pet@drawable/ic_widget_scenemode_portrait@drawable/ic_widget_scenemode_snow@drawable/ic_widget_scenemode_softskin@drawable/ic_widget_scenemode_sports@drawable/ic_widget_scenemode_spotlight@drawable/ic_widget_scenemode_steadyphoto@drawable/ic_widget_scenemode_sunset@drawable/ic_widget_scenemode_sweepstitch@drawable/ic_widget_scenemode_theatreAutomaticMotion Blur ReductionAugmented RealityBest ShotBabyBacklightPortrait BacklightBarcodeBeachCandlelightDarkDishDocumentFireworksFlowersHandheld TwilightHDRHigh sensitivityLandscapeNightNight portraitNight portraitPartyPetPortraitSnowSoft skinSportsSpotlightSteady photoSunsetSweep stitchTheatreautocloudyincandescentfluorescentdaylightcloudy-daylight@drawable/ic_widget_wb_auto@drawable/ic_widget_wb_cloudy@drawable/ic_widget_wb_incandescent@drawable/ic_widget_wb_fluorescent@drawable/ic_widget_wb_daylight@drawable/ic_widget_wb_cloudy_daylightAutomaticCloudyIncandescentFluorescentDaylightCloudy Daylightframe-averagecenter-weightedspot-metering@drawable/ic_widget_autoexposure_frameaverage@drawable/ic_widget_autoexposure_centerweighted@drawable/ic_widget_autoexposure_spotmeteringFrame averageCenter weightedSpot meteringAuto1\"1/21/41/81/161/321/1001/1251/2501/5001/10001/20001/3333
================================================
FILE: res/values/attrs.xml
================================================
================================================
FILE: res/values/colors.xml
================================================
#AA000000#BB33B5E5#9033B5E5#FF33B5E5#FF4444#FFFFFF#FF2E2E2E#FF33525E#FF0099CC#FFFF2222#FF5E5233#FF99CC00#88333333#8833B5E5
================================================
FILE: res/values/config.xml
================================================
falsetruefalse40961280720falsefalse
================================================
FILE: res/values/ids.xml
================================================
================================================
FILE: res/values/integers.xml
================================================
364dp4dp8dp12108dp1dp32dp140dp96dp100dip125dip6dip4dip16dip
================================================
FILE: res/values/strings.xml
================================================
FocalLaunch FocalShutter buttonUnable to connect to cameraRecordingDouble-tap to take a pictureFull HD (1080p)HD (720p)SD (480p)MMS (288p)OKYour device doesn\'t support GLES2Your device doesn\'t have a gyroscopeLong-press the shutter button to\nfinish your sphere.DisabledEnabledCouldn\'t play the video, no player foundTap anywhere to shootPlease wait\u2026RETOUCHGALLERYPhotoVideoPicSpherePanoramaFacingBurst %d shotsDisable burst modeInfinite burstAuto bracketingChoose widgets in the sidebarChoose widgetsImage sizeShow exposure ringAuto-enhancementRule of ThirdsLight measureBurst modeEffectsColor adjust.Exposure comp.FlashHigh Dynamic RangeISOScene modeSettingsShutter speedSkin Tone EnhancementHigh Dynamic RangeTimer modeVideo framerateVideo High Dynamic RangeWhite balance"'PANO'_yyyyMMdd_HHmmss"Failed to render panorama.\nTry to take a shorter one.Rendering panorama\u2026UNDOTake a picture to start a spherePlease wait for the current PicSphere to renderRendering\u2026 (%d %%)Rendering started\u2026Rendering PicSphere\u2026PicSphere rendering failedMake sure pictures only overlap on their edges.You need at least two pictures.Preparing\u2026Generating metadata\u2026Writing camera orientation\u2026Finding control points\u2026Aligning images\u2026Cleaning points\u2026Cropping empty areas\u2026Stitching pictures\u2026Blending pictures\u2026Computing HDR picture\u2026HDR rendering failedThe source pictures have been kept.Welcome!The options are in the sidebar,\njust slide it.Shutter buttonTap the shutter button to take a picture.\nSlide it to access other capture modes.Panorama modeTap the shutter to start your panorama,\nthen pan what you want to capture.\nOnce done, tap the shutter button again.PicSphere modeAlign your first picture where you like,\nthen tap the shutter to take each picture\nof the sphere. The blue dots are points\nof reference you can follow.\nLong-press the shutter once you\'re done.Automatic ISOHands-Jitter Reduction ISO100 ISO200 ISO400 ISO800 ISO1600 ISO
================================================
FILE: res/values/styles.xml
================================================
================================================
FILE: res/values-cs/arrays.xml
================================================
cidcivwhiskywhiskeysýrcide, udělej fotkuAutomatickyZakázánPovolenSvítilnaRedukce červených očíVypnutýČernobílýNegativníSolarizaceSépiePosterizaceČerná tabuleBílá tabuleVodaReliéfSkicaŽádnýAutomatickýRedukce rozmazáníRozšířená realitaNejlepší snímekDítěProtisvětloPortrét s protisvětlemČárový kódPlážOsvíceno svíčkamiTemnéJídloDokumentOhňostrojKvětinyStmívání bez stativuHDRVysoká citlivostKrajinaNocNoční portrétNoční portrétPártyMazlíčekPortrétSníhMěkké světloSportyReflektorZe stativuZápad sluncePlynulé panoramaDivadloAutomatickyZataženoŽárovkaZářivkaDenní světloZataženo ve dneCeloplošné průměrovéSe zdůrazněným středemBodové
================================================
FILE: res/values-cs/strings.xml
================================================
FocalSpustit FocalSpoušťNelze přistupovat k fotoaparátu zařízeníNahráváníDvojitým dotykem pořídíte fotografiiZařízení nepodporuje standard GLES2Zařízení nedisponuje gyroskopemDlouhým stiskem spouště\ndokončíte sférickou fotografii.ZakázanoPovolenoVideo nelze přehrát, není dostupný přehrávačDotykem kdekoliv pořídíte fotografiiPočkejte prosím\u2026RETUŠOVATGALERIEFotografieSférická fotografiePřední kameraPořízení %d snímků v řaděZakázat pořizování snímků v řaděNekonečné pořizování snímků v řaděAutomatické posouvání expoziceZvolte widgety v postraní lištěZvolte widgetyVelikost obrázkuZobrazit expoziční prstenecAutomatické vylepšeníPravidlo třetinRežím měření expoziceVícenásobný režimBarevné efektyBarevné vylepšeníKompenzace expoziceRežim bleskuDynamický rozsah expozice (HDR)Citlivost ISOScénický režimNastaveníRychlost uzávěrkyVylepšení tonality kůžeDynamický rozsah expozice (HDR)Režim časovačeSnímková rychlost videaDynamický rozsah expozice pro videoVyvážení bíléVykreslení panorama selhalo.\nZkuste pořídit kratší panorama.Vykreslování panorama\u2026ZPĚTPro zahájení snímání sférické fotografie pořiďte fotografiiProsím vyčkejte dokud není sférická fotofrafie kompletně vykreslenaVykreslování\u2026 (%d %%)Vykreslování zahájeno\u2026Vykreslování sférické fotografie\u2026Vykreslování sférické fotografie selhaloUjistěte se, zda se pořízení fotografie na hranách překrývají.Je potřeba pořídit alespoň dvě fotografie.Připravování\u2026Vytváření metadat\u2026Ukládání orientace fotoaparátu\u2026Vyhledávání řídících bodů\u2026Zarovnávání obrázků\u2026Čištění bodů\u2026Odstříhávání prázdých oblastí\u2026Sestavování obrázků\u2026Prolínání obrázků\u2026Vytváření obrázku HDR\u2026Vykreslování obrázku HDR selhaloZdorjové obrázky zůstali uloženy.Vítáme Vás!Možnosti jsou dostupné na postraní liště,\nstačí ji jen vysunout.Tlačítko spouštěDotykem na tlačítko spouště pořídíte obrázek.\nPřesunem získáte možnost změnit režim.Režim panoramaDotykem na spoušti zahájíte snímání panorama,\npoté natáčejte zařízení do směru, který chcete zachytit.\nDlaším dotykem na spoušti dojde k ukončení snímání panorama.Režim sférické fotografieSrovnejte umístění první fotografie,\npoté se dotkněte spouště pro zahájení snímání\nsférické fotografie. Modré body Vás budou navádět pro získání fotografie\nv daném směru.\nDlouhým stiskem spouště bude pořizování ukončeno.automatické ISOISO pro redukci třesoucích se rukou
================================================
FILE: res/values-da/arrays.xml
================================================
cidcivwhiskywhiskeycheeseok cid, take a pictureAutoDeaktiveretAktiveretLommelygteRødøjereduktionDeaktiveretSort & HvidNegativSolariseringSepiaPosteriseringSort tavleHvid tavleVandReliefSkitseNeonAutomatiskAntisløringAugmenteret virkelighedBedste fotoBabyBagbelysningBagbelyst portrætStregkodeStrandStearinlysMørkFadDokumentFyrværkeriBlomsterHåndholdt SkumringHDRHøj følsomhedLandskabNatNatportrætNatportrætFestKæledyrPortrætSneBlød hudSportSpotlightStabilt fotoSolnedgangSammenhæftningTeaterAutomatiskOverskyetHvidglødendeFluorescerendeDagslysOverskyet dagslysRammegennemsnitCentervægtetSpotmåling
================================================
FILE: res/values-da/strings.xml
================================================
FocalStart FocalUdløserknapKan ikke forbinde til kameraOptagerDobbelttryk for at tage et billedeFull HD (1080p)HD (720p)SD (480p)MMS (288p)OKDin enhed understøtter ikke GLES2Din enhed har ikke et gyroskopTryk på udløserknappen længe for at\nafslutte din boble.DeaktiveretAktiveretKunne ikke afspille video, ingen afspiller fundetTryk overalt for at tage et billedeVent venligst\u2026RETOUCHÉRGALLERIFotoVideoBilledBoblePanoramaSekundært kameraBurst %d fotosDeaktivér burst-tilstandUendelig burstAuto-bracketingVælg widgets på sidelinjenVælg widgetsBilledstørrelseVis eksponeringsringAuto-forbedringDet gyldne snitEksponeringsmålingstilstandBurst-tilstandFarveeffekterFarveforbedringerEksponeringskompensationBlitz-tilstandHDRISO-følsomhedScenetilstandIndstillingerUdløserhastighedForbedring af hudfarveHDRTimer-tilstandRammefrekvens til videoHDR til videoHvidbalanceKunne ikke rendere panorama.\nPrøv at lave et kortere ét.Renderer panorama\u2026FORTRYDTag et billede for at starte en bobleVent venligst, mens den aktuelle BilledBoble renderesRenderer... (%d %%)Rendering startet\u2026Renderer BilledBoble\u2026Kunne ikke rendere BilledBobleSørg for, at billederne kun overlapper på deres kanter.Du skal bruge mindst to billeder.Forbereder\u2026Optimerer matchede billeder\u2026Rydder matchende punkter\u2026Beskærer tomme områder\u2026Sammenhæfter billeder\u2026Blender billeder\u2026Beregner HDR-billede\u2026Kunne ikke rendere HDRDe oprindelige billeder er bibeholdt.Velkommen!Valgmulighederne er på sidelinjen,\ndu skal bare stryge den.UdløserknapTryk på udløserknappen for at tage et billede.\nStryg den for at tilgå andre optagertilstande.Tilstanden PanoramaTryk på udløserknappen for at starte din panorama,\nog panorér derefter til det, du vil indfange.\nTryk på udløserknappen igen, når du er færdig.Tilstanden BilledBobleJustér dit første billede, hvor du vil,\nog tryk derefter på udløserknappen for at tage hvert billde\ni boblen. De blå prikker er reference-\npunkter, som du kan følge.\nHold udløserknappen nede længe, når du er færdig.Automatisk ISOHands-Jitter Reduction ISO100 ISO200 ISO400 ISO800 ISO1600 ISO
================================================
FILE: res/values-de/arrays.xml
================================================
CidCivWhiskeyCheeseAutomatischAusAnLeuchteRote Augen-ReduzierungAusSchwarz & WeißNegativSolarisiertSepiaPosterisiertKreidetafelWhiteboardAquaPrägungSkizzeNeonAutomatischBewegungsunschärfe-ReduktionErweiterte RealitätBeste AufnahmeBabyHintergrundlichtGegenlicht-PorträtBarcodeStrandKerzenscheinDunkelheitSpeisenDokumenteFeuerwerkBlumenDämmerungHDRHohe EmpfindlichkeitLandschaftNachtNacht-PorträtNacht-PorträtPartyTierPorträtSchneeWeiche HautSportScheinwerferStationäres FotoSonnenuntergangSchwenkpanoramaTheaterAutomatischBewölktGlühlampeLeuchtstofflampeTageslichtTageslicht bewölktMehrfeldMittenbetontSpot-Messung
================================================
FILE: res/values-de/strings.xml
================================================
FocalFocal wird gestartetAuslöserKeine Verbindung zur Kamera möglichAufnehmenDoppelklicken, um ein Foto zu machenFull HD (1080p)HD (720p)SD (480p)MMS (288p)OKDiese Funktion benötigt ein Gerät mit GLES2-UnterstützungDiese Funktion benötigt ein Gerät mit GyroskopAuslöser lange drücken, um die PicSphere zu beendenAusAnVideo kann nicht abgespielt werden, weil kein geeignetes Wiedergabeprogramm gefunden wurdeBildschirm berühren, um zu fotografierenBitte warten\u2026RETUSCHEGALERIEFotoVideoPicSpherePanoramaKamera wechseln%d Aufnahmen in SerieSerienaufnahme ausUnendlichBelichtungsreiheWidgets der Seitenleiste wählenWidgets auswählenBildgrößeBelichtungsring anzeigenAutom. VerbesserungGitternetzBelichtungsmessungSerienaufnahmeFarbeffekteFarbverbesserungenBelichtungskorrekturBlitzmodusHDR-AufnahmeISOSzenenmodusEinstellungenAuslösegeschwindigkeitVerbesserung der HauttöneHDR-Aufnahme (SW)SelbstauslöserVideo-BildrateHDR-VideoWeißabgleichDas Panoramabild konnte nicht erstellt werden./nBitte versuchen Sie eine kürzere AufnahmePanoramabild wird erstellt\u2026RückgängigZum Starten ein Bild machenBitte warten, bis das laufende Zusammenfügen abgeschlossen istPicSphere wird zusammengefügt\u2026 (%d %%)Zusammenfügen gestartet\u2026PicSphere wird zusammengefügt\u2026Zusammenfügen fehlgeschlagenBitte darauf achten, dass sich Fotos nur am Rand überlappen.Es sind mindestens zwei Fotos erforderlich.Vorbereiten\u2026Metadaten generieren\u2026Kameraausrichtung speichern\u2026Kontrollpunkte suchen\u2026Bilder ausrichten\u2026Kontrollpunkte löschen\u2026Leere Bereiche entfernen\u2026Bilder zusammenfügen\u2026Bilder überblenden\u2026HDR-Bild wird verarbeitet\u2026HDR-Berechnung fehlgeschlagenDie Quellbilder wurden erhalten.Willkommen!Die Optionen sind in der Seitenleiste,\nziehen Sie sie einfach heraus.AuslöserDrücken Sie den Auslöser, um ein Foto zu machen.\nZiehen Sie ihn, um weitere Modi anzuzeigen.Panorama-ModusDrücken Sie den Auslöser, um ein Panorama zu beginnen und schwenken Sie Ihr Gerät für die Aufnahme.\nWenn Sie fertig sind, drücken Sie den Auslöser erneut.PicSphere-ModusRichten Sie das erste Bild aus. Danach drücken Sie für jedes Bild der PicSphere einmal den Auslöser. Die blauen Punkte dienen als Referenz.\nDrücken Sie den Auslöser lange, um die Aufnahme abzuschließen.AutomatischBildstabilisierungISO 100ISO 200ISO 400ISO 800ISO 1600
================================================
FILE: res/values-el/arrays.xml
================================================
cidcivwhiskywhiskeycheeseok cid, take a pictureΑυτόματοΑπενεργοποιημένοΕνεργοποιημένοΦακόςΜείωση κόκκινων ματιώνΑπενεργοποιημένοΑσπρόμαυροΑρνητικόΥπερφωτισμόςΣέπιαΠοστεροποίησηΜαύρος πίνακαςΆσπρος πίνακαςΝερόΞεθώριασμαΣκίτσοNeonΑυτόματηΜείωση motion blurΕπαυξημένη πραγματικότηταΚαλύτερη λήψηΜωρόΟπίσθιος φωτισμόςΠορτραίτο με οπίσθιο φωτισμόBarcodeΠαραλίαΦως κεριούΣκοτάδιΠιάτοΈγγραφοΠυροτεχνήματαΛουλούδιαΑμυδρό φωςHDRΥψηλή ευαισθησίαΤοπίοΝύχταΝυχτερινό πορτραίτοΝυχτερινό πορτραίτοΠάρτιΚατοικίδιοΠορτραίτοΧιόνιΑπαλό δέρμαΑθλήματαΠροβολέαςΣταθερή φωτογραφίαΗλιοβασίλεμαΛειτουργία sweepΘέατροΑυτόματηΣυννεφιάΛαμπτήρας πυρακτώσεωςΛαμπτήρας φθορισμούΦως ημέραςΦώς συννεφιασμένης ημέραςΜέσος όρος κάδρουΜέτρηση κέντρου βάρουςΣημειακή μέτρηση
================================================
FILE: res/values-el/strings.xml
================================================
FocalΕκκίνηση FocalΠλήκτρο κλείστρουΔεν είναι δυνατή η σύνδεση με τη φωτογραφική μηχανήΕγγραφήΔιπλό πάτημα για λήψη φωτογραφίαςFull HD (1080p)HD (720p)SD (480p)MMS (288p)ΟΚΗ συσκευή σας δεν υποστηρίζει GLES2Η συσκευή σας δεν έχει γυροσκόπιοΚρατήστε πατημένο το πλήκτρο\nκλείστρου για να ολοκληρώσετε το sphere σας.ΑνενεργόΕνεργόΔεν είναι δυνατή η αναπαραγωγή του βίντεο, δεν βρέθηκε πρόγραμμα αναπαραγωγήςΠιέστε οπουδήποτε για λήψηΠαρακαλώ περιμένετε\u2026ΡΕΤΟΥΣΣΥΛΛΟΓΗΦωτογραφίαΒίντεοPicSphereΠανόραμαΕμπρόσθιαΡίψη %d λήψεωνΑπενεργοποίηση λειτουργίας ριπήςΑπεριόριστη ριπήΑυτόματο bracketingΕπιλέξτε widget στην πλαϊνή μπάραΕπιλέξτε widgetΜέγεθος εικόναςΕμφάνιση δαχτυλιδιού έκθεσηςΑυτόματη βελτίωσηΝόμος των τρίτωνΛειτουργία μέτρησης έκθεσηςΛειτουργία ριπήςΕφέ χρώματοςΒελτιώσεις χρώματοςΑντιστάθμιση έκθεσηςΛειτουργία φλαςΥψηλό δυναμικό εύρος (HDR)Ευαισθησία ISOΛειτουργία σκηνήςΡυθμίσειςΤαχύτητα κλείστρουΒελτίωση τόνου δέρματοςΥψηλό δυναμικό εύρος (HDR)Λειτουργία χρονοδιακόπτηΡυθμός καρέ βίντεοΥψηλό δυναμικό εύρος βίντεο (HDR)Ισορροπία λευκούΑπέτυχε η επεξεργασία του πανοράματος.\nΠροσπαθήστε να τραβήξετε ένα μικρότερο.Επεξεργασία πανοράματος\u2026ΑΝΑΙΡΕΣΗΤραβήξτε μια φωτογραφία για να ξεκινήσετε ένα sphereΠαρακαλώ περιμένετε την επεξεργασία του τρέχοντος PicSphereΕπεξεργασία\u2026 (%d %%)Η επεξεργασία ξεκίνησε\u2026Επεξεργασία PicSphere\u2026Απέτυχε η επεξεργασία του PicSphereΒεβαιωθείτε ότι οι εικόνες επικαλύπτονται μόνο στις άκρες τους.Χρειάζεστε τουλάχιστον δύο φωτογραφίες.Προετοιμασία\u2026Δημιουργία μεταδεδομένων\u2026Αποθήκευση προσανατολισμού μηχανής\u2026Εύρεση σημείων ελέγχου\u2026Βελτίωση ταιριάγματος\u2026Καθαρισμός σημείων\u2026Περικόπή άδειων περιοχών\u2026Συρραφή εικόνων\u2026Συνδυασμός εικόνων\u2026Υπολογισμός φωτογραφίας HDR\u2026Απέτυχε η επεξεργασία HDRΟι αρχικές φωτογραφίες έχουν αποθηκευτεί.Καλώς ήλθατε!Οι επιλογές είναι στην πλαϊνή μπάρα,\nαπλώς σύρτε την.Πλήκτρο κλείστρουΠιέστε το πλήκτρο κλείστρου για να τραβήξετε\nμια φωτογραφία. Συρετέ το για πρόσβαση\nστις άλλες λειτουργίες λήψης.Λειτουργία πανοράματοςΠιέστε το πλήκτρο κλείστρου για να ξεκινήσετε το\nπανόραμα και επιλέξτε τι θέλετε να καταγράψετε.\nΜόλις τελειώσετε, πιέστε ξανά το πλήκτρο κλείστρου.Λειτουργία PicSphereΕυθυγραμμίστε την πρώτη εικόνα όπου σας αρέσει\nκαι μετά πιέστε το πλήκτρο κλείστρου για να τραβήξετε\nκάθε φωτογραφία του sphere. Οι μπλε τελείες είναι\nσημεία αναφοράς που μπορείτε να ακολουθήσετε.\nΜόλις τελειώσετε, πιέστε παρατεταμένα το πλήκτρο κλείστρου.Αυτόματο ISOISO μείωσης κουνήματος χεριούISO 100ISO 200ISO 400ISO 800ISO 1600
================================================
FILE: res/values-en-rGB/strings.xml
================================================
Colour adjust.
================================================
FILE: res/values-es/arrays.xml
================================================
cidcivwhiskywhiskeypatatabien cid, toma una fotoAutomáticoApagadoEncendidoLinternaOjos rojosNingunoBlanco y negroNegativoSolarizarSepiaPosterizarFondo oscuroFondo claroAguaRealzarDibujoNeónAutomáticoReducción desenfoque movimientoRealidad aumentadaMejor tomaBebéContraluzRetrato a contraluzCódigo de barrasPlayaLuz de velaOscuroReflectorDocumentoFuegos artificialesFloresCrepúsculoHDRAlta sensibilidadPaisajeNocheRetrato nocturnoInterior nocturnoFiestaMascotaRetratoNieveSuavizadoDeporteDestacadoEstableAtarcederSuperposiciónTeatroAutomáticoNubladoIncandescenteFluorescenteLuz naturalParc. nubladoMatricialPonderada al centroPuntual
================================================
FILE: res/values-es/strings.xml
================================================
FocalAbrir FocalDisparadorImposible conectar con la cámaraGrabandoDoble toque para tomar una fotoFull HD (1080p)HD (720p)SD (480p)MMS (288p)ACEPTAREl dispositivo no soporta GLES2El dispositivo no dispone de giróscopoPara finalizar la captura haz una\npulsación larga sobre el disparador.DeshabilitadoHabilitadoImposible reproducir el vídeo. No se ha encontrado un reproductorToca en cualquier sitio para dispararPor favor, espera\u2026RETOCARGALERÍAFotoVídeoPicSpherePanorámicaFrontalRáfaga de %d disparosModo ráfaga deshabilitadoRáfaga continuaExposición HDR automáticaElegir los widgets para la barra lateralElegir widgetsTamaño de imagenControl de exposiciónMejoras automáticasRegla de los terciosMedición de exposiciónModo de ráfagaEfectos de colorMejoras de colorCompensación de exposiciónModo de flashAlto rango dinámico (HDR)Sensibilidad ISOModo de escenaAjustesVelocidad obturadorMejora tono de pielAlto rango dinámico (HDR)TemporizadorFotogramas por segundoVídeo de alto rango dinámicoBalance de blancosError al procesar la foto panorámica.\nIntenta tomar una captura más corta.Procesando la foto panorámica\u2026DESHACERToma una foto para iniciar PicSpherePor favor, espera a que termine el procesado de la foto PicSphere actualProcesando\u2026 (%d %%)Iniciando procesamiento\u2026Procesando la foto PicSphere\u2026Error al procesar la foto PicSphereAsegúrate de que las imágenes solo se solapan en sus bordes.Necesitas al menos dos imágenes.Preparando\u2026Generando metadatos\u2026Guardando orientación\u2026Buscando puntos de control\u2026Optimizando coincidencias\u2026Limpiando zonas coincidentes\u2026Recortando áreas vacías\u2026Procesando imágenes\u2026Uniendo imágenes\u2026Procesando imagen HDR\u2026Error al procesar la imagen HDRLas imágenes originales se han conservado.¡Bienvenido!Las opciones se encuentran en la barra lateral.\nDesliza sobre la pantalla para mostrarlas.DisparadorToca sobre el disparador para tomar una foto.\nDesliza sobre él para acceder a otros modos de captura.PanorámicaToca sobre el disparador para iniciar la foto panorámica\ny desplaza lentamente la cámara sobre lo que deseas capturar.\nPara terminar, toca sobre el disparador nuevamente.PicSphereToca sobre el disparador para tomar la primera imagen.\nAhora, toca sucesivamente el disparador en cada uno de los\npuntos azules que aparecerán como referencia, para ir\ncomponiendo la foto PicSphere.\nUn toque largo sobre el disparador finalizará el proceso.ISO automáticoISO reductor temblorISO 100ISO 200ISO 400ISO 800ISO 1600
================================================
FILE: res/values-fi/arrays.xml
================================================
cidcivwhiskywhiskeycheeseok cid, take a pictureAutomaattinenPois käytöstäKäytössäLamppuPunasilmäisyyden poistoPois käytöstäMustavalkoinenNegatiiviValotusSeepiaJulisteLiitutauluValkotauluAquaKohokuvaPiirrosNeonAutomaattinenLiikkeen sumennuksen vähentäminenLisätty todellisuusParas otosVauvaTaustavaloTaustavalo/MuotokuvaViivakoodiRantaKynttilänvaloPimeäTarjoiluDokumenttiIlotulitusKukatHämärä/KäsikuvausHDRErittäin herkkäVaakatasoYöYö/MuotokuvaYö/MuotokuvaJuhlaLemmikkiMuotokuvaLumiPehmeä ihoUrheiluValokeilaVakaa kuvaAuringonlaskuPuhdistettu yhdistelmäTeatteriAutomaattinenPilvinenHehkuvaFluoresoivaPäivänvaloPilvinen/PäivänvaloKeskimääräinen kehysKeskelle keskittynytPisteeseen kohdistettu
================================================
FILE: res/values-fi/strings.xml
================================================
FocalKäynnistä FocalKuvausnäppäinEi yhteyttä kameraanNauhoitetaanTupla-kosketa ottaaksesi kuvanFull HD (1080p)HD (720p)SD (480p)MMS (288p)OKLaitteesi ei tue GLES2:taLaitteessasi ei ole gyroskooppiaPaina kuvausnäppäintä pitkään lopettaaksesi spheren.Pois käytöstäKäytössäVideon toistaminen epäonnistui, soitinta ei löydyPaina mihin tahansa ottaaksesi kuvanOdota\u2026UUDELLEENKOSKETAGALLERIAKuvaVideoPicSpherePanoraamaKameran vaihto%d kuvan sarjaPoista sarjakuvaus käytöstäLoputon sarjakuvausAutomaattinen haarukointiValitse sivuvalikon widgetitValitse widgetitKuvan kokoNäytä valotuskehäAutomaattinen parantaminen3x3-ruudukkoAutomaattinen valotusarvoSarjakuvausVäriefektitVärimuutoksetValotuksen korjausSalamaHigh Dynamic RangeISO-herkkyysScene ModeAsetuksetSulkimen nopeusIhonvärin vaihtoHDRAjastinVideon kuvausnopeusVideo HDRValkotasapainoPanoraamakuvan mallinnus epäonnistui.\Yritä ottaa lyhyempi kuva.Mallinnetaan panoraamaa\u2026PERUOta kuva aloittaaksesi spherenOdota kun PicSphere-kuvaa mallinnetaanMallinnetan\u2026 (%d %%)Mallinnus aloitettu\u2026Mallinnetaan PicSphere-kuvaa\u2026PicSpheren mallinnus epäonnistuiVarmista että kuvat ovat päällekkäin vain reunoilla.Tarvitset vähintään kaksi kuvaa.Valmistellaan\u2026Luodaan metadataa\u2026Tarkistetaan kameran asentoa\u2026Etsitään ohjauspisteet\u2026Käännetään kuvia\u2026Puhdistetaan pisteitä\u2026Rajataan tyhjiä alueita\u2026Yhdistetään kuvia\u2026Sulautetaan kuvia\u2026Luodaan HDR-kuvaa\u2026HDR-kuvan mallinnus epäonnistuiAlkuperäiset kuvat säilytettiin.Tervetuloa!Asetukset ovat sivuvalikossa, liu\'uta näyttääksesi.KuvausnäppäinPaina kuvausnäppäintä ottaksesi kuvan.\nLiu\'ta sitä päästäksesi muihin tiloihin.PanoraamatilaPaina kuvausnäppäintä aloittaaksesi panoraamakuvan,\nja kuvaa haluamaltasi alueelta.\nKun kuva on valmis, paina suljinnäppäintä uudestaan.PicSphere-tilaOsoita ensimmäinen kuva mihin haluat,\nsitten paina kuvausnäppäintä ottaaksesi jokaisen kuvan spherestä.\nSiniset pisteet ovat apuna.\nPaina kuvausnäppäintä pitkään kun olet valmis.Automaattinen ISOKäsien tärinän vähentäminen ISO100 ISO200 ISO400 ISO800 ISO1600 ISO
================================================
FILE: res/values-fr/arrays.xml
================================================
ouistiticivwhiskywhiskeycheeseceriseok cid, prend une photoAutomatiqueDésactivéActivéFlashYeux rougesDésactivéNoir \u0026 blancNegatifSolariserSépiaPostériséTableau noirTableau blancEauEmbossageCroquisNéonAutomatiqueRéduction du flou de mouvementRéalité augmentéeMeilleure priseBébéRétroéclairéPortrait rétroéclairéCode barrePlageBougieFoncéDélavéDocumentFeux d\'artificeFleursCrépusculeHDRHaute sensibilitéPaysageNuitPortrait de nuitPortrait de nuitPartieAnimalPortraitNeigePeau douceSportsProjecteurPhoto stableCoucher de soleilPoint de balayageThéâtreAutomatiqueNuageuxIncandescentFluorescentLumière du jourLumière du jour nuageuxCadre moyenPondéré au centreMesure du spot
================================================
FILE: res/values-fr/strings.xml
================================================
FocalLancer FocalDéclencheurImpossible de lancer l\'appareil photoEnregistrementTapez deux fois pour prendre une photoFull HD (1080p)HD (720p)SD (480p)MMS (288p)OKVotre appareil n\'est pas\ncompatible OpenGLES 2.0Votre appareil n\'a pas de gyroscopeMaintenez appuyé le déclencheur\npour terminer la sphère.DésactivéActivéImpossible de lire la vidéoAppuyez n\'importe où pour capturerVeuillez patienter\u2026RETOUCHERGALERIEPhotoVidéoPicSpherePanoramaDirectionRafale de %d imagesSans mode rafaleRafale infinieBracketing automatiqueWidgets à afficherWidgets à afficherTaille de l\'imageBague d\'expositionAuto-améliorationRègle de troisMode de mesure d\'expositionMode rafaleEffets de couleurRéglage des couleursExpositionMode de flashHigh Dynamic RangeSensibilité ISOMode scèneParamètresVitesse d\'obturationAmélioration de la peauHigh Dynamic RangeRetardateurVitesse vidéoVidéo High Dynamic RangeBalance des blancsLe rendu a échoué.\nEssayez de prendre un panorama plus étroit.Rendu du panorama\u2026ANNULERPrenez une photo pour commencerVeuillez attendre la fin du rendu\nde la sphère précédenteRendu en cours\u2026 (%d %%)Rendu de la PicSphere\u2026Rendu de la PicSphere\u2026Rendu de la PicSphere échouéLes images ne doivent se superposer que sur les bords.Deux images minimum sont requises.Préparation\u2026Génération des métadonnées\u2026Écriture de l\'orientation de l\'appareil photo\u2026Recherche des points de contrôle\u2026Alignement des images\u2026Nettoyage des points\u2026Rognage des coins\u2026Déformation des images\u2026Finalisation de l\'image\u2026Rendu HDR en cours\u2026Échec du rendu HDRLes images originales ont été conservées.Bienvenue\u00A0!Les paramètres sont situés dans la\nbarre latérale.DéclencheurAppuyez sur le déclencheur pour\nprendre une photo.\nFaites-le glisser pour changer de mode.Mode PanoramaAppuyez sur le déclencheur pour démarrer,\npuis orientez simplement le téléphone.\nUne fois fini, appuyez de nouveau sur le déclencheur.Mode PicSpherePrenez la première photo où vous le souhaitez,\npuis chaque photo de votre sphère.\nVous pouvez utiliser les points bleus\ncomme point de repère idéal.\nMaintenez appuyé le déclencheur pour finir.ISO automatiqueISO réduction du tremblementISO 100ISO 200ISO 400ISO 800ISO 1600
================================================
FILE: res/values-hu/arrays.xml
================================================
cidcivwhiskywhiskeycheeseok cid, take a pictureAutomatikusLetiltvaEngedélyezveVakuVörösszem csökkentésLetiltvaFekete & FehérNegatívSzolarizáltSzépiaPoszterizáltFeketetáblaFehértáblaVízDomborműVázlatNeonAutomatikusBemozdulás csökkentésKiterjesztett valóságLegjobb képGyerekHáttérvilágításHáttérvilágítású portréVonalkódStrandGyertyafénySötétÉtelDokumentumTűzijátékVirágokKézben tartott szürkületHDRNagy érzékenységTájképÉjszakaiÉjszakai portréÉjszakai beltéri portréBuliHáziállatPortréHóLágy bőrSportReflektorfényÁllóképNapnyugtaVarrottSzínházAutomatikusFelhősIzzólámpaFénycsőNapfényFelhős napfényKiértékelőKözépre súlyozottSpotmérés
================================================
FILE: res/values-hu/strings.xml
================================================
FocalFocal indításaExponáló gombNem lehet csatlakozni a kameráhozFelvételKép készítéséhez duplán érintse megFull HD (1080p)HD (720p)SD (480p)MMS (288p)OKA készülék nem támogatja a GLES2-tA készülék nem rendelkezik giroszkóppalA gömbpanoráma befejezéséhez\nhosszan érintse meg az exponáló gombot.LetiltvaEngedélyezveNem lehet lejátszani a videót, nem található lejátszóBárhol érintse meg a kép készítéséhezKérem, várjon\u2026RETUSÁLÁSGALÉRIAFotóVideóGömbpanorámaPanorámaKamera%d képSorozatfelvétel letiltvaVégtelen számú képAutomatikus expozíció sorozatVálasszon modulokat az oldalsávonVálasszon modulokatKép méretExpozíció gyűrű megjelenítéseAuto-feljavításHarmadolási szabályExponálás mérés módjaSorozatfelvételSzínhatásokSzínfokozásExpo korrekcióZseblámpaHDRISO érzékenységKép jellegeBeállításokZársebességBőrtónus kiemeléseHDRIdőzítőVideó képkockaVideó HDRFehéregyensúlyNem sikerült összeállítani a panorámaképet.\nPróbáljon meg egy képkockával kisebb képet készíteni.Panorámakép összeállítása\u2026VISSZAVONÁSA gömbpanoráma készítésének elindításához készítsen egy képetVárjon, amíg a gömbpanoráma elkészülÖsszeállítás... (%d %%)Összeállítás elindítva\u2026Gömbpanoráma összeállítása\u2026Nem sikerült összeállítani a gömbpanorámátGyőződjön meg róla, hogy a képek átfedik egymást.Legalább két képre van szükség.Előkészítés\u2026Metaadatok generálása\u2026Kamera orientáció rögzítése\u2026Vezérlőpontok keresése\u2026Képek elrendezése\u2026Azonos részek eltávolítása\u2026Üres területek kivágása\u2026Képek összeillesztésének rögzítése\u2026Képek elegyítése\u2026HDR kép számítása\u2026HDR összeállítás sikertelenAz eredeti kép megtartva.Üdvözöljük!A beállításokat az oldalsávon találja,\ncsak húzza elő.Exponáló gombÉrintse meg az exponáló gombot a kép elkészítéséhez.\nTovábbi felvételi módokhoz csúsztatással fér hozzá.Panoráma módÉrintse meg az exponáló gombot a panoráma kép készítés elindításához,\nazután pásztázzon arra, amit rögzíteni kíván.\nMiután végzett, érintse meg ismét az exponáló gombot.Gömbpanoráma módIgazítsa oda az első képet ahová szeretné,\nmajd minden egyes képhez érintse meg az exponáló gombot.\nA kék referenciapontok segítenek a lekövetésben.\nNyomja meg hosszan az exponáló gombot a befejezéshez.Automatikus ISOKézremegést csökkentő ISO100 ISO200 ISO400 ISO800 ISO1600 ISO
================================================
FILE: res/values-it/arrays.xml
================================================
sorridetesorridiscattafotociisscatta una fotoAutoDisattivatoAttivatoTorciaRiduzione occhi rossiDisattivatoBianco & neroNegativoSolarizzaSeppiaPosterizzaLavagnaLavagna biancaAcquaRilievoDisegnoNeonAutomaticoRiduzione sfocatura movimentoRealtà aumentataScatto miglioreBabyLuce da dietroRitratto luce da dietroCodice a barreSpiaggiaLuce di candelaScuroPiattoDocumentoFuochi d\'artificioFioriCrepuscoloHDRAlta sensibilitàPaesaggioNotteRitratto di notteRitratto di nottePartyCuccioloRitrattoNevePelle morbidaSportLampadinaFoto stabilizzataTramontoPanorama multifotoTeatroAutomaticoNuvolosoIncandescenteFluorescenteDiurnoDiurno nuvolosoValutativaMedia pesata al centroSpot
================================================
FILE: res/values-it/strings.xml
================================================
FocalAvvia FocalPulsante scattoImpossibile collegarsi alla fotocameraRegistrazioneScatta con doppio toccoFull HD (1080p)HD (720p)SD (480p)MMS (288p)OKIl dispositivo non supporta GLES2Il dispositivo non ha il giroscopioTenere premuto il pulsante scatto per finire la sfera.DisattivatoAttivatoImpossibile riprodurre il video, nessun lettore trovatoToccare un punto qualsiasi per scattareAttendere…RITOCCOGALLERIAFotoVideoPicSferaPanoramaFrontale%d scatti in sequenzaDisattiva scatti multipliScatti infinitiAuto bracketingWidget nella barra degli strumentiScegliere i widgetDimensione immagineEsposizioneMiglioramenti automaticiRegola dei terziAutoesposizioneScatti continuiEffetti coloreEsaltazione coloriCompensazione esposizioneFlashHDR - High Dynamic RangeSensibilità ISOScenaImpostazioniVelocità otturatoreEsaltazione toni pelleHDR - High Dynamic RangeTimerVideo FramerateVideo High Dynamic RangeBilanciamento biancoImpossibile elaborare il panorama.\nProvare con uno più breve.Elaborazione panorama…ANNULLAScattare una foto per cominciareAttendere l\'elaborazione della PicSferaElaborazione... (%d %%)Elaborazione iniziata…Elaborazione PicSsfera…Errore nell\'elaborazione della PicSferaAssicurarsi che gli scatti si sovrappongano solo sui bordi.Occorrono almeno due scatti.Preparazione…Generazione dei metadati\u2026Registrazione orientamento fotocamera\u2026Ricerca dei punti di controllo\u2026Ottimizzazione…Azzeramento punti di corrispondenza…Eliminazione zone vuote…Deformazione…Fusione degli scatti…Elaborazione HDR…Elaborazione HDR fallitaGli scatti originali sono stati mantenuti.Benvenuto!Le opzioni sono nella barra degli strumenti,\nattivabile scorrendo da sinistra a destra.Pulsante scattoToccare il pulsante scatto per fotografare.\nScorrere dal pulsante per scegliere le modalità di scatto.PanoramaToccare il pulsante per iniziare,\npoi fare una lenta panoramica.\nUna volta fatto, premere nuovamente il pulsante.PicSferaScattare a piacimento la prima immagine,\npoi premere il pulsante per ogni\nimmagine successiva della sfera,\nutilizzando i puntini blu di riferimento.\nPremere a lungo il pulsante una volta terminato.ISO automaticoISO riduzione tremolio100 ISO200 ISO400 ISO800 ISO1600 ISO
================================================
FILE: res/values-nl/arrays.xml
================================================
cidcivwhiskeycheeseok cid, take a pictureAutomatischUitgeschakeldIngeschakeldZaklampRode ogen verminderenUitgeschakeldZwart-witNegatiefSolariserenSepiaPosteriserenSchoolbordWhiteboardAquaReliëfSchetsNeonAutomatischBewegingsonscherpte verwijderenToegevoegde realiteitBeste opnameBabyAchtergrondlichtPortret (achtergrondlicht)StreepjescodeStrandKaarslichtDonkerGerechtDocumentVuurwerkBloemenSchemeringHDRHoge gevoeligheidLandschapNachtNachtportretNachtportretFeestHuisdierPortretSneeuwZachte huidSportSchijnwerperStabiele fotoZonsondergangDraaipanoramaTheaterAutomatischBewolktGloeilampTL-lampDaglichtDaglicht (bewolkt)MeerveldsCentrumgerichtSpot
================================================
FILE: res/values-nl/strings.xml
================================================
FocalFocal startenSluiterknopKan geen verbinding maken met de cameraOpnemenDubbeltikken om een foto te makenFull HD (1080p)HD (720p)SD (480p)MMS (288p)OKDit apparaat ondersteunt geen GLES2Dit apparaat heeft geen gyroscoopSluiterknop lang indrukken/n om de PicSphere te voltooienUitgeschakeldIngeschakeldKan video niet afspelen, geen mediaspeler gevondenTik ergens om een foto te makenEen ogenblik geduld\u2026BEWERKENGALERIJFotoVideoPicSpherePanoramaCamera wisselen%d opnamen achter elkaarSerieopname uitschakelenOneindigBrackets automatischZijbalkwidgets kiezenWidgets kiezenGrootte van fotoBelichtingsring tonenAutomatisch verbeterenRasterBelichting metenSerieopnameKleureffectenKleurverbeteringenBelichtingscompensatieFlitsHDRISOScènekeuzeInstellingenSluitertijdHuidtint verbeterenHDR (SW)TimerVideoframerateHDR-videoWitbalansPanoramaopname mislukt./n Probeer een kortere.Panorama renderen\u2026ONGEDAAN MAKENNeem een foto om de PicSphere te startenEen ogenlbik geduld a.u.b. De huidige PicSphere wordt voltooidRenderen\u2026 (%d %%)Renderen gestart\u2026PicSphere renderen\u2026PicSphere renderen misluktZorg ervoor dat de afbeeldingen alleen met de randen overlappenEr zijn minimaal twee foto\'s nodig.Voorbereiden\u2026Metadata genereren\u2026Camerastand opslaan\u2026Controlepunten zoeken\u2026Afbeeldingen uitlijnen\u2026Punten opruimen\u2026Lege stukken wegknippen\u2026Afbeeldingen aan elkaar plakken\u2026Afbeelding in elkaar laten overvloeien\u2026HDR-afbeelding berekenen\u2026HDR renderen misluktDe bronafbeeldingen zijn bewaard.Welkom!De opties staan in de zijbalk,\nsleep eroverheen om te kiezen.SluiterknopTik op de sluiterknop om een foto te maken.\nSleep erover voor andere opnamestanden.PanoramaopnameTik op de sluiterknop om de panoramaopname te starten\nen richt op wat u wilt opnemen.\nTik nogmaals op de sluiterknop wanneer u klaar bent.PicSphereLijn de eerste afbeelding uit,\ntik op de sluiterknop om alle foto\'s te maken\nvan de sphere. De blauwe stippen zijn referentiepunten die u kunt volgen.\nHoud de sluiterknop ingedrukt als u klaar bent.AutomatischBeeldstabilisatie (HJR)ISO 100ISO 200ISO 400ISO 800ISO 1600
================================================
FILE: res/values-pl/arrays.xml
================================================
CidCivWhiskeyCheeseAutomatycznyWyłączonyWłączonyLatarkaRedukcja czerwonych oczuWyłączonyCzarny & BiałyNegatywSolaryzacjaSepiaPosteryzacjaCzarna tablicaBiała tablicaWodaWyrycieSzkicNeonAutomatycznyRedukcja rozmycia w ruchuRozszerzona rzeczywistośćNajlepsze zdjęcieDzieckoPod światłoPorter pod światłoKod kreskowyPlażaŚwiatło świecyMrokJedzenieDokumentFajerwerkiKwiatyZmierzchHDRWysoka czułośćKrajobrazNocNocny portretNocny portretImprezaZwierzęPortretŚniegMiękka skóraSportReflektorStacjonarne zdjęcieZachód słońcaZszyta panoramaTeatrAutomatycznyPochmurnoRozżarzonyFluorescencyjnyŚwiatło dzienneZachmurzony dzieńŚrednia polaCentralnie ważonyPunktowy
================================================
FILE: res/values-pl/strings.xml
================================================
FocalUruchom FocalSpust migawkiNie można połączyć z aparatemNagrywanieNaciśnij dwukrotnie by zrobić zdjęcieFull HD (1080p)HD (720p)SD (480p)MMS (288p)OKTwoje urządzenie nie obsługuje GLES2Twoje urządzenie nie posiada żyroskopuNaciśnij długo spust migawki aby zakończyć PicSphereWyłączonyWłączonyNie można odtworzyć wideo, nie znaleziono odtwarzacza.Dotknij ekranu aby zrobić zdjęcieProszę czekać\u2026RETUSZGALERIAZdjęcieWideoPicSpherePanoramaZmiana aparatuSeria %d zdjęćWyłącz serię zdjęćNieskończona seriaSeria o różnej ekspozycjiWybierz widżety paska bocznegoWybierz widżetyRozmiar zdjęciaPokaż pierścień ekspozycjiAuto-wzmocnienieSiatkaTryb pomiaru ekspozycjiZdjęcia seryjneEfekt kolorystycznyWzmocnienie kolorówKompensacja ekspozycjiTryb lampy błyskowejHDRCzułość ISOTryb scenyUstawieniaSzybkość migawkiPoprawa koloru skóryHDR (SW)SamowyzwalaczWideo-klatek/sekHDR-WideoBalans bieliObraz panoramy nie może zostać utworzony./nSpróbuj zrobić krótszą panoramę.Renderowanie panoramy\u2026WytnijZrób zdjęcię by zacząćProszę czekać na zakończenie renderowania PicSpherePicSphere renderuje\u2026 (%d %%)Renderowanie rozpoczęte\u2026Renderowanie PicSphere\u2026Błąd renderowania PicSphereUpewnij się, że zdjęcia zachodzą tylko na swoje krawędzie.PicSphere potrzebuje przynajmniej dwóch zdjęć.Przygotowywanie\u2026Generowanie metadanych\u2026Zapisywanie orientacji aparatu\u2026Znajdowanie punktu kontrolnego\u2026Dopasowywanie obrazów\u2026Usuwanie punktów\u2026Kadrowanie pustych obszarów\u2026Zszywanie zdjęć\u2026Mieszanie zdjęć\u2026Obraz HDR jest przetwarzany\u2026Błąd renderowania HDRZdjęcia źródłowe zostały zachowane.Witam!Opcje znajdują się na pasku bocznym,\npo prostu go przesuń.Spust migawkiDotknij przycisku migawki żeby zrobić zdjęcie.\nPrzesuń go żeby uzyskać dostęp do innych trybów.Tryb PanoramyDotknij spustu migawki żeby zacząć panoramę,\nnastępnie przesuń i uchwyć obraz.\nKiedy skończysz, dotknij spustu migawki jeszcze raz.Tryb PicSphereZrób pierwsze zdjęcie gdzie tylko chcesz,\nnastępnie dotknij przycisku migawki żeby zrobić każde zdjęcie sfery.\nNiebieskie kropki są punktami za którymi powinieś podążać.\nNaciśnij długo przycisk migawki kiedy skończysz.AutomatycznyStabilizacja obrazuISO 100ISO 200ISO 400ISO 800ISO 1600
================================================
FILE: res/values-pt-rBR/arrays.xml
================================================
cidcivwhiskywhiskeycheeseok cid, tire uma fotoAutoDesativadoAtivadoTochaRedução de olhos vermelhosDesativadoPreto & BrancoNegativoSolarizaçãoSépiaPosterizaçãoQuadro negroQuadro brancoAquaRealçarRascunhoNéonAutomáticoRedução de DesfocagemRealidade aumentadaMelhor fotoBebêLuz de fundoLuz de fundo retratoCódigo de barrasPraiaLuz de velasEscuroPratoDocumentoFogos de artifícioFloresCrepúsculoHDRAlta sensitividadePaisagemNoiteNoite retratoNoite retratoFestaBicho de estimaçãoRetratoNevePeleEsportesHolofoteMoto estávelPôr do solVarre pontoTeatroAutomáticoNubladoIncandescenteFluorescenteLuz do diaLuz do dia nubladoQuadro médioPonderado ao centroMedição pontual
================================================
FILE: res/values-pt-rBR/strings.xml
================================================
FocalAbrir FocalBotão de disparoNão foi possível conectar à câmeraGravandoToque duplo para tirar um fotoFull HD (1080p)HD (720p)SD (480p)MMS (288p)OKSeu dispositivo não suporta GLES2Seu dispositivo não possui um giroscópioPressione o botão de disparar para\nterminar sua esfera.DesativadoAtivadoNão foi possível reproduzir o vídeo, nenhum tocador foi encontradoToque em qualquer lugar para tirar fotoPor favor, aguarde\u2026RETOQUEGALERIAFotoVídeoFotoEsferaPanoramaDireção%d fotos em modo contínuoDesativar disparo em modo contínuoContínuo infinitoEnquadramento automáticoEscolher widget na barra lateralEscolher widgetsTamanho da imagemMostrar exposição de anelReaçar automáticoRegra dos TerçosModo de medida de exposiçãoModo contínuoEfeitos de coresRealce de coresCompensação de exposiçãoModo de flashHDRSensitividade ISOModo de cenaConfiguraçõesVelocidade do disparadorRealce de tone de peleHDRModo temporizadorTaxa de frames do vídeoHDR de vídeoBalanço do brancoFalha ou renderizar o panorama.\nTente tirar um mais curto.Renderizando o panorama\u2026DESFAZERTire uma foto para iniciar a esferaPor favor aguarde a FotoEsfera atual renderizarRenderizando\u2026 (%d %%)Renderização iniciada\u2026Renderizando FotoEsfera\u2026Falha ao renderizar FotoEsferaCertifique que as fotos somente sobrepõe nas bordas.Você precisa de pelo menos duas fotos.Preparando\u2026Gerando metadados\u2026Gravando orientação da câmera\u2026Buscando pontos de controle\u2026Alinhando imagens\u2026Limpando pontos\u2026Cortando áreas vazias\u2026Juntando as fotos…Misturando as fotos\u2026Computando imagem HDR\u2026Falha ao renderizar HDRAs fotos de origem foram mantidas.Bem vindo!As opções estão na barra lateral.\nsó deslize-a.Botão de disparoToque no botão de disparo para tirar uma foto.\nDeslize para acessar outros modos de captura.Modo panoramaToque o botão de disparo para iniciar seu panorama.\nentão desloque para onde você deseja capturar.\nAo terminar, toque no disparador de novo.Modo FotoEsperaAlinhe sua primeira foto onde você desejar,\n então toque no disparador para tirar cada foto\nda esfera. Os pontos azuis são pontos\nde referência que você pode seguir.\nPressione o disparador quando você terminar.ISO AutomáticoISO Redução de Vibração100 ISO200 ISO400 ISO800 ISO1600 ISO
================================================
FILE: res/values-pt-rPT/arrays.xml
================================================
cidcivwhiskywhiskeycheeseok cid, tira uma fotoAutoInativoAtivoTochaRedução de olhos vermelhosInativoPreto e BrancoNegativoSolarizaçãoSépiaPosterizaçãoQuadro negroQuadro brancoAquaRealçarEsboçoNeonAutomáticoRedução de desfoqueRealidade AumentadaMelhor fotoBebéLuz de fundoLuz de fundo retratoCódigo de barrasPraiaLuz de velasEscuroPratoDocumentoFogo de artifícioFloresCrepúsculoHDRAlta sensibilidadePaisagemNoiteNoite retratoNoite retratoFestaAnimal de estimaçãoRetratoNevePeleDesportoHolofoteFoto estávelPôr do solCosturaTeatroAutomáticoNubladoIncandescenteFluorescenteLuz do solLuz do sol nubladaMédia de framePonderado ao centroMedição pontual
================================================
FILE: res/values-pt-rPT/strings.xml
================================================
FocalAbrir FocalBotão obturadorNão é possível ligar à câmaraA gravarToque duas vezes para tirar uma fotografiaFull HD (1080p)HD (720p)SD (480p)MMS (288p)OKO seu dispositivo não suporta GLES2O seu dispositivo não tem um giroscópioPressione continuamente o botão\nobturador para terminar a sua sphere.InactivoActivoNão foi possível reproduzir o vídeo, nenhum reprodutor encontradoToque em qualquer sítio para capturarPor favor aguarde\u2026RETOCARGALERIAFotoVídeoPicSpherePanoramaDireção%d fotos em modo contínuoDestavar disparo em modo contínuoContínuo infinitoEnquadramento automáticoEscolha os widgets na barra lateralEscolher widgetsTamanho da imagemMostrar anel de exposiçãoMelhoria automáticaRegra dos TerçosModo de medida da exposiçãoModo de disparo contínuoEfeitos de corRealce de corCompensação de exposiçãoModo de flashHDRSensibilidade ISOModo de cenaDefiniçõesVelocidade de obturaçãoRealce de tom de peleHDRModo de temporizadorTaxa de frames de vídeoHDR de vídeoEquilíbrio de brancosFalha ao compor o panorama.\nTente tirar um mais curto.A compor panorama\u2026DESFAZERTire uma foto para começar uma spherePor favor aguarde que a PicSphere atual se componhaA compor\u2026 (%d %%)Composição iniciada\u2026A compor a PicSphere\u2026Composição da PicSphere falhadaAssegure-se que as imagens só se sobrepõem nas suas margens.Precisa de pelo menos duas imagens.A preparar\u2026A gerar metadados\u2026A gravar a orientação da câmara\u2026À procura dos pontos de controlo\u2026A alinhar imagens\u2026A limpar pontos\u2026A cortar áreas vazias\u2026A juntar as fotos\u2026A misturar as fotos\u2026A computar imagem HDR\u2026Falha ao compor HDRAs imagens de origem foram mantidas.Bem-vindo!As opções estão na barra lateral,\né só deslizá-la.Botão obturadorToque no botão obturador para tirar uma fotografia.\nDeslize-o para aceder a outros modos de captura.Modo panoramaToque no obturador para começar o panorama,\ndepois desloque para onde quer capturar.\nPara terminar, toque no obturador de novo.Modo PicSphereAlinhe a sua primeira imagem onde quer,\ndepois toque no obturador para tirar cada foto\nda esfera. Os pontos azuis são pontos\nde referência que pode seguir.\nPressione continuamente o obturador para terminar.ISO AutomáticoISO Redução de Vibração100 ISO200 ISO400 ISO800 ISO1600 ISO
================================================
FILE: res/values-ru/arrays.xml
================================================
АвтоВыкл.Вкл.ФонарикУдаление эфф. красных глазВыкл.Ч/БНегативСоляризацияСепияПостеризацияТёмная доскаДоскаПод водойРельефРисунокНеонАвтоматическиУстранение размытого движенияДополненная реальностьЛучший снимокРебёнокКонтровой светПортрет с контровым светомШтрих-кодПляжИскусственный светТемнотаЕдаДокументФейерверкЦветыСумеркиHDRВысокая чувствительностьПейзажНочьПортрет ночьюПортрет ночьюВечеринкаДомашнее животноеПортретСнегМягкая кожаСпортОсвещениеСтабильный снимокЗакатSweep stitchТеатрАвтоПасмурный деньЛампа накаливанияЛампа дн. светаСолнечный светОблачноУсреднённыйПо центру кадраТочечный замер
================================================
FILE: res/values-ru/strings.xml
================================================
FocalЗапуск FocalКнопка затвораНе удаётся подключиться к камереЗаписьНажмите дважды, чтобы сделать снимокOKВаше устройство не поддерживает GLES2Ваше устройство не оснащено гироскопомНажмите и удерживайте кнопку затвора для окончания фотосферыВыключеноВключеноНе удалось воспроизвести видео. Видеоплеер не найден.Нажмите в любом месте, чтобы сделать снимокПожалуйста, подождите\u2026РЕТУШЬГАЛЕРЕЯФотоВидеоФотосфераПанорамаФронт. камераСерия из %d снимковВыключить серийную съёмкуНепрерывная съёмкаАвтобрекетингВыберите виджеты в боковой панелиВыберите виджетыРазмер изображенияПоказывать кольцо экспозицииАвтоулучшениеСеткаЭкспозамерСерийная съёмкаЦветовые эффектыУлучшение цветаКомпенсация экспозицииРежим вспышкиHDRЧувствительность ISOРежим съёмкиНастройкиСкорость съёмкиУлучшение цвета кожиHDRТаймерЧастота кадров видеоHDR-видеоБаланс белогоОшибка создания панорамы.\nПопробуйте сделать ещё снимок.Создание панорамы\u2026ОТМЕНАСделайте снимок для создания фотосферыПожалуйста, подождите завершения создания текущей фотосферыСоздание\u2026 (%d %%)Создание началось\u2026Создание фотосферы\u2026Создание фотосферы завершилось с ошибкойУбедитесь, что снимки накладываются друг на друга по краямСделайте минимум два снимкаПодготовка\u2026Создание метаданных\u2026Запись ориентации камеры\u2026Поиск контрольных точек\u2026Выравнивание изображений\u2026Очистка точек\u2026Обрезка пустых областей\u2026Сборка изображений\u2026Смешивание изображений\u2026Вычисление HDR-изображения\u2026Создание HDR-изображения завершилось с ошибкойИсходные изображения были сохраненыДобро пожаловать!Настройки находятся в боковой панели, \nпросто сдвиньте её.Кнопка затвораНажмите на кнопку затвора, чтобы сделать снимок.\nСдвиньте её для доступа к другим режимам съёмки.Режим панорамыНажмите на кнопку затвора для начала создания панорамы,\nа затем перемещайте камеру.\nДля завершения панорамы нажмите на кнопку затвора.Режим фотосферыНаведите объектив на понравившееся место,\n затем делайте снимки фотосферы.\nСледуйте по направлению синих точек.\nДолгое нажатие кнопки затвора завершает создание фотосферы.Авто ISOHJR ISO100 ISO200 ISO400 ISO800 ISO1600 ISO
================================================
FILE: res/values-sk/arrays.xml
================================================
cidcivwhiskywhiskeysýýrodfoť maAutoZakázanýPovolenýSvietidloOdstránenie červených očíZakázanýČiernobielyNegatívSolarizovaťSépiaPlagátČierna tabuľaBiela tabuľaVodaReliéfNáčrtokNeónAutomatickáOdstránenie rozmazania pohybomRozšírená realitaNajlepší záberDieťaPodsvieteniePodsvietenie portrétuČiarový kódPlដeroTmaDishDokumentOhňostrojKvetySúmrak voľnou rukouHDRVysoká citlivosťKrajinaNocNočný portrétNočný portrétVečierokZvieratáPortrétSnehJemný odtieň pletiŠportReflektorStála fotografiaZápad slnkaSpájanie pohybomDivadloAutomatickyZamračenéŽiarovkaŽiarivkaDenné svetloNepriame denné svetloPriemer snímkuVyváženie na stredBodové meranie
================================================
FILE: res/values-sk/strings.xml
================================================
FocalSpustiť FocalTlačidlo spúšteNepodarilo sa pripojiť k fotoaparátuZaznamenáva saDvojitým ťuknutím zachyťte snímkuFull HD (1080p)HD (720p)SD (480p)MMS (288p)OKVaše zariadenie nepodporuje GLES2Vaše zariadenie neobsahuje gyroskopDlhým stlačením spúšte\ndokončíte sféru.ZakázanéPovolenéNedá sa prehrať video, nenašiel sa žiadny prehrávačŤuknite kamkoľvek na zachytenie snímkyProsím, čakajte\u2026ŤUKNITE ZNOVUGALÉRIAFotografiaVideoPicSpherePanorámaPredný fotoaparátDávka %d snímkovZakázať dávkový režimNekonečná dávkaAuto bracketingZvoľte miniaplikácie v bočnej lišteZvolenie miniaplikáciíVeľkosť obrázkaZobrazenie kruhu expozícieAutomatické vylepšenieRozdelenie na tretinyRežim merania expozícieDávkový režimFarebné efektyVylepšenie fariebVyrovnanie expozícieRežim bleskuVeľmi dynamický rozsahCitlivosť ISORežim scényNastaveniaRýchlosť spúšteVylepšenie odtieňu pletiVeľmi dynamický rozsahRežim časovačaSnímková frekvencia videaVeľmi dynamický rozsah videaVyváženie bielejZlyhalo vykreslenie panorámy.\nSkúste zachytiť kratšiu.Vykresľuje sa panoráma\u2026VRÁTIŤ SPÄŤZachytením snímky spustíte sféruProsím, počkajte na vykreslenie aktuálnej PicSphereVykresľuje sa... (%d %%)Vykresľovanie bolo spustené\u2026Vykresľovanie PicSphere\u2026Vykresľovanie PicSphere zlyhaloUistite sa, že sa obrázky prelínajú iba v rohoch.Potrebujete aspoň dva snímky.Pripravuje sa\u2026Generujú sa metaúdaje\u2026Zapisuje sa otočenie fotoaparátu\u2026Hľadajú sa kontrolné body\u2026Optimalizujú sa zhody\u2026Čistia sa body zhôd\u2026Orezávajú sa prázdne plochy\u2026Spájajú sa snímky\u2026Tvarujú sa snímky\u2026Vypočítavanie HDR snímky\u2026Vykresľovanie HDR zlyhaloPôvodná snímka bola uchovaná.Vitajte!Voľby sú v bočnej lište,\nstačí ju potiahnúť.Tlačidlo spúšteŤuknutím na tlačidlo spúšte zachytíte snímku.\nJeho potiahnútím sprístupníte ostatné režimy snímania.Režim panorámyŤuknutím na tlačidlo spúšte spustíte vašu panorámu,\npotom posúvajte na to, čo chcete zachytiť.\nNa dokončenie ťuknite znovu na tlačidlo spúšte.Režim PicSphereZarovnajte vašu prvú snímku podľa potreby,\npotom ťuknite na spúšť na zachytenie každej snímky\nsféry. Modré bodky sú referenčné\nbody, ktoré môžete nasledovať.\nDlhším stlačením spúšte dokončíte sféru.Automatické ISOISO odstraňujúce chvenie rúk100 ISO200 ISO400 ISO800 ISO1600 ISO
================================================
FILE: res/values-sv/strings.xml
================================================
FocalStarta FocalAvtryckareKan inte kontakta kameranInspelningDubbelklicka för att ta en bildFull HD (1080p)HD (720p)SD (480p)MMS (288p)OKDin enhet stöder inte GLES2Din enhet har inget gyroskopLångtryck på avtryckaren för att slutföra din sfär.AvaktiveradAktiveradKan inte spela videon, ingen spelare funnenTryck var som helst för att ta en bildVar god vänta\u2026RETUSCHERAGALLERIFotoVideoFotosfärPanoramaFrontkameraSekvensfota %d bilderAvaktivera sekvenslägeOändlig sekvensAutogafflingVälj widgets i sidmenynVälj widgetsBildstorlekVisa exponeringsringAuto-förbättringTredjedelsregelnExponeringsmätarlägeSekvenslägeFärgeffekterFärgförbättringarExponeringskompensationBlixtlägeHigh Dynamic RangeISO-graderScenlägeInställningarSlutartidHudtonsförbättringHigh Dynamic RangeSjälvutlösareFramerateVideo High Dynamic RangeVitbalansLyckades inte rendera panorama.\nFörsök att ta en kortare.Renderar panorama\u2026ÅNGRATa en bild för att påbörja en sfärVar god vänta medan den nuvarande bildsfären renderasRenderar\u2026 (%d %%)Rendering påbörjad\u2026Renderar bildsfär\u2026Lyckades inte rendera bildsfärKontrollera att bilderna endast överlappar vid kanterna.Du behöver åtminstone två bilder.Förbereder\u2026Optimerar matchningar\u2026Rensar matchningspunkter\u2026Beskär tomma områden\u2026Sömmar bilder\u2026Förenar bilder\u2026Beräknar HDR-bild\u2026HDR-renderingen misslyckadesOriginalbilderna har sparats.Välkommen!Inställningarna finns i sidmenyn,\nsvep bara fram den.AvtryckareTryck på avtryckaren för att ta en bild.\nSvep från den för att komma åt andra kameralägen.PanoramalägeTryck på avtryckaren för att börja din panorama,\npanorera sedan det du vill fota.\nNär du är klar, tryck på avtryckaren igen.BildsfärlägeTa din första bild var du vill,\ntryck sedan på avtryckaren för att övriga bilder\nav sfären. De blå prickarna är\nreferenspunkter som du kan följa.\nLångtryck på avtryckaren när du är klar.
================================================
FILE: res/xml/widget_info.xml
================================================
================================================
FILE: src/org/cyanogenmod/focal/BitmapFilter.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BlurMaskFilter;
import android.graphics.BlurMaskFilter.Blur;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.LightingColorFilter;
import android.graphics.Paint;
import android.renderscript.Allocation;
import android.renderscript.Element;
import android.renderscript.RenderScript;
import android.renderscript.ScriptIntrinsicBlur;
import java.util.HashMap;
import java.util.Map;
/**
* This class renders a bitmap with a certain effect
* and cache it for future use.
*/
public class BitmapFilter {
private static BitmapFilter mSingleton;
private Map mGlowCache;
private RenderScript mRS;
private Allocation mBlurInputAllocation;
private Allocation mBlurOutputAllocation;
private ScriptIntrinsicBlur mBlurScript;
public static BitmapFilter getSingleton() {
if (mSingleton == null) {
mSingleton = new BitmapFilter();
}
return mSingleton;
}
private BitmapFilter() {
mGlowCache = new HashMap();
}
/**
* Blurs a bitmap. There's no caching on this one.
*
* @param src The bitmap to blur
* @return A blurred bitmap
*/
public Bitmap getBlur(Context context, Bitmap src, float radius) {
if (android.os.Build.VERSION.SDK_INT >= 17) {
if (mRS == null) {
mRS = RenderScript.create(context);
}
if (mBlurScript == null) {
mBlurScript = ScriptIntrinsicBlur.create(mRS, Element.U8_4(mRS));
}
if (mBlurInputAllocation == null) {
mBlurInputAllocation = Allocation.createFromBitmap(mRS, src,
Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_GRAPHICS_TEXTURE);
mBlurScript.setInput(mBlurInputAllocation);
} else {
mBlurInputAllocation.copyFrom(src);
}
if (mBlurOutputAllocation == null) {
mBlurOutputAllocation = Allocation.createTyped(mRS, mBlurInputAllocation.getType());
}
mBlurScript.setRadius(radius);
mBlurScript.forEach(mBlurOutputAllocation);
mBlurOutputAllocation.copyTo(src);
}
return src;
}
/**
* Returns a glowed image of the provided icon. If the
* provided name is already in the cache, the cached image
* will be returned. Otherwise, the bitmap will be glowed and
* cached under the provided name
*
* @param name The name of the bitmap
* @param src The bitmap of the icon itself
* @return Glowed bitmap
*/
public Bitmap getGlow(String name, int glowColor, Bitmap src) {
if (mGlowCache.containsKey(name)) {
return mGlowCache.get(name);
} else {
// An added margin to the initial image
int margin = 0;
int halfMargin = margin / 2;
// The glow radius
int glowRadius = 4;
// Extract the alpha from the source image
Bitmap alpha = src.extractAlpha();
// The output bitmap (with the icon + glow)
Bitmap bmp = Bitmap.createBitmap(src.getWidth() + margin,
src.getHeight() + margin, Bitmap.Config.ARGB_8888);
// The canvas to paint on the image
Canvas canvas = new Canvas(bmp);
Paint paint = new Paint();
paint.setColor(glowColor);
// Outer glow
ColorFilter emphasize = new LightingColorFilter(glowColor, 1);
paint.setColorFilter(emphasize);
canvas.drawBitmap(src, halfMargin, halfMargin, paint);
paint.setColorFilter(null);
paint.setMaskFilter(new BlurMaskFilter(glowRadius, Blur.OUTER));
canvas.drawBitmap(alpha, halfMargin, halfMargin, paint);
// Cache icon
mGlowCache.put(name, bmp);
return bmp;
}
}
}
================================================
FILE: src/org/cyanogenmod/focal/CameraActivity.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal;
import android.app.Activity;
import android.app.ActivityManager;
import android.content.Context;
import android.content.pm.ConfigurationInfo;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.hardware.Camera;
import android.opengl.GLSurfaceView;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.GestureDetector;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.OrientationEventListener;
import android.view.ScaleGestureDetector;
import android.view.Surface;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import org.cyanogenmod.focal.feats.CaptureTransformer;
import org.cyanogenmod.focal.feats.SoftwareHdrCapture;
import org.cyanogenmod.focal.pano.MosaicProxy;
import org.cyanogenmod.focal.picsphere.PicSphereCaptureTransformer;
import org.cyanogenmod.focal.picsphere.PicSphereManager;
import org.cyanogenmod.focal.ui.CircleTimerView;
import org.cyanogenmod.focal.ui.ExposureHudRing;
import org.cyanogenmod.focal.ui.FocusHudRing;
import org.cyanogenmod.focal.ui.Notifier;
import org.cyanogenmod.focal.ui.PanoProgressBar;
import org.cyanogenmod.focal.ui.ReviewDrawer;
import org.cyanogenmod.focal.ui.SavePinger;
import org.cyanogenmod.focal.ui.ShutterButton;
import org.cyanogenmod.focal.ui.SideBar;
import org.cyanogenmod.focal.ui.SwitchRingPad;
import org.cyanogenmod.focal.ui.ThumbnailFlinger;
import org.cyanogenmod.focal.ui.WidgetRenderer;
import org.cyanogenmod.focal.ui.showcase.ShowcaseView;
import fr.xplod.focal.R;
public class CameraActivity extends Activity implements CameraManager.CameraReadyListener,
ShowcaseView.OnShowcaseEventListener {
public final static String TAG = "CameraActivity";
public final static int CAMERA_MODE_PHOTO = 1;
public final static int CAMERA_MODE_VIDEO = 2;
public final static int CAMERA_MODE_PANO = 3;
public final static int CAMERA_MODE_PICSPHERE = 4;
// whether or not to enable profiling
private final static boolean DEBUG_PROFILE = true;
private static int mCameraMode = CAMERA_MODE_PHOTO;
private CameraManager mCamManager;
private SnapshotManager mSnapshotManager;
private MainSnapshotListener mSnapshotListener;
private FocusManager mFocusManager;
private PicSphereManager mPicSphereManager;
private MosaicProxy mMosaicProxy;
private CameraOrientationEventListener mOrientationListener;
private GestureDetector mGestureDetector;
private CaptureTransformer mCaptureTransformer;
private Handler mHandler;
private boolean mPaused;
private int mOrientation = OrientationEventListener.ORIENTATION_UNKNOWN;
private int mOrientationCompensation = 0;
private SideBar mSideBar;
private WidgetRenderer mWidgetRenderer;
private FocusHudRing mFocusHudRing;
private ExposureHudRing mExposureHudRing;
private SwitchRingPad mSwitchRingPad;
private ShutterButton mShutterButton;
private SavePinger mSavePinger;
private PanoProgressBar mPanoProgressBar;
private Button mPicSphereUndo;
private CircleTimerView mTimerView;
private ViewGroup mRecTimerContainer;
private static Notifier mNotifier;
private ReviewDrawer mReviewDrawer;
private ScaleGestureDetector mZoomGestureDetector;
private TextView mHelperText;
private ShowcaseView mShowcaseView;
private boolean mHasPinchZoomed;
private boolean mCancelSideBarClose;
private boolean mIsFocusButtonDown;
private boolean mIsShutterButtonDown;
private boolean mUserWantsExposureRing;
private boolean mIsFullscreenShutter;
private int mShowcaseIndex;
private boolean mIsCamSwitching;
private boolean mIsShutterLongClicked = false;
private CameraPreviewListener mCamPreviewListener;
private GLSurfaceView mGLSurfaceView;
private boolean mIsFocusing = false;
private final static int SHOWCASE_INDEX_WELCOME_1 = 0;
private final static int SHOWCASE_INDEX_WELCOME_2 = 1;
private final static int SHOWCASE_INDEX_PANORAMA = 0;
private final static int SHOWCASE_INDEX_PICSPHERE = 0;
private final static String KEY_SHOWCASE_WELCOME = "SHOWCASE_WELCOME";
private final static String KEY_SHOWCASE_PANORAMA = "SHOWCASE_PANORAMA";
private final static String KEY_SHOWCASE_PICSPHERE = "SHOWCASE_PICSPHERE";
/**
* Gesture listeners to apply on camera previews views
*/
private View.OnTouchListener mPreviewTouchListener = new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_UP) {
mSideBar.clampSliding();
mReviewDrawer.clampSliding();
}
// Process HUD gestures only if we aren't pinching
mHasPinchZoomed = false;
mZoomGestureDetector.onTouchEvent(ev);
if (!mHasPinchZoomed) {
mGestureDetector.onTouchEvent(ev);
}
return true;
}
};
/**
* Event: Activity created
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_camera);
mPaused = false;
mIsCamSwitching = false;
getWindow().getDecorView()
.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);
mUserWantsExposureRing = true;
mIsFullscreenShutter = false;
mSideBar = (SideBar) findViewById(R.id.sidebar_scroller);
mWidgetRenderer = (WidgetRenderer) findViewById(R.id.widgets_container);
mSavePinger = (SavePinger) findViewById(R.id.save_pinger);
mTimerView = (CircleTimerView) findViewById(R.id.timer_view);
mHelperText = (TextView) findViewById(R.id.txt_helper);
mPicSphereUndo = (Button) findViewById(R.id.btn_picsphere_undo);
mSwitchRingPad = (SwitchRingPad) findViewById(R.id.switch_ring_pad);
mSwitchRingPad.setListener(new MainRingPadListener());
mPanoProgressBar = (PanoProgressBar) findViewById(R.id.panorama_progress_bar);
mRecTimerContainer = (ViewGroup) findViewById(R.id.recording_timer_container);
mNotifier = (Notifier) findViewById(R.id.notifier_container);
mReviewDrawer = (ReviewDrawer) findViewById(R.id.review_drawer);
// Create orientation listener. This should be done first because it
// takes some time to get first orientation.
mOrientationListener = new CameraOrientationEventListener(this);
mOrientationListener.enable();
mHandler = new Handler();
// Setup the camera hardware and preview
setupCamera();
SoundManager.getSingleton().preload(this);
// Setup HUDs
mFocusHudRing = (FocusHudRing) findViewById(R.id.hud_ring_focus);
mExposureHudRing = (ExposureHudRing) findViewById(R.id.hud_ring_exposure);
mExposureHudRing.setManagers(mCamManager);
// Setup shutter button
mShutterButton = (ShutterButton) findViewById(R.id.btn_shutter);
MainShutterClickListener shutterClickListener = new MainShutterClickListener();
mShutterButton.setOnClickListener(shutterClickListener);
mShutterButton.setOnLongClickListener(shutterClickListener);
mShutterButton.setOnTouchListener(shutterClickListener);
mShutterButton.setSlideListener(new MainShutterSlideListener());
// Setup gesture detection
mGestureDetector = new GestureDetector(this, new GestureListener());
mZoomGestureDetector = new ScaleGestureDetector(this, new ZoomGestureListener());
findViewById(R.id.gl_renderer_container).setOnTouchListener(mPreviewTouchListener);
// Use SavePinger to animate a bit while we open the camera device
mSavePinger.setPingMode(SavePinger.PING_MODE_SIMPLE);
mSavePinger.startSaving();
// Hack because review drawer size might not be measured yet
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
mReviewDrawer.open();
mReviewDrawer.close();
}
}, 300);
startShowcaseWelcome();
}
public int getOrientation() {
return mOrientationCompensation;
}
public void startShowcaseWelcome() {
if (SettingsStorage.getAppSetting(this, KEY_SHOWCASE_WELCOME, "0").equals("0")) {
SettingsStorage.storeAppSetting(this, KEY_SHOWCASE_WELCOME, "1");
ShowcaseView.ConfigOptions co = new ShowcaseView.ConfigOptions();
co.hideOnClickOutside = true;
mShowcaseView = ShowcaseView.insertShowcaseView(mSideBar,
this, getString(R.string.showcase_welcome_1_title),
getString(R.string.showcase_welcome_1_body), co);
// Animate gesture
Point size = new Point();
getWindowManager().getDefaultDisplay().getSize(size);
mShowcaseView.animateGesture(size.x/2, size.y*2.0f/3.0f, size.x/2, size.y/2.0f);
mShowcaseView.setOnShowcaseEventListener(this);
mShowcaseIndex = SHOWCASE_INDEX_WELCOME_1;
}
}
public void startShowcasePanorama() {
if (SettingsStorage.getAppSetting(this, KEY_SHOWCASE_PANORAMA, "0").equals("0")) {
SettingsStorage.storeAppSetting(this, KEY_SHOWCASE_PANORAMA, "1");
ShowcaseView.ConfigOptions co = new ShowcaseView.ConfigOptions();
co.hideOnClickOutside = true;
Point size = new Point();
getWindowManager().getDefaultDisplay().getSize(size);
mShowcaseView = ShowcaseView.insertShowcaseView(size.x/2, size.y - Util.dpToPx(this, 16),
this, getString(R.string.showcase_panorama_title),
getString(R.string.showcase_panorama_body), co);
mShowcaseIndex = SHOWCASE_INDEX_PANORAMA;
}
}
public void startShowcasePicSphere() {
if (SettingsStorage.getAppSetting(this, KEY_SHOWCASE_PICSPHERE, "0").equals("0")) {
SettingsStorage.storeAppSetting(this, KEY_SHOWCASE_PICSPHERE, "1");
ShowcaseView.ConfigOptions co = new ShowcaseView.ConfigOptions();
co.hideOnClickOutside = true;
Point size = new Point();
getWindowManager().getDefaultDisplay().getSize(size);
mShowcaseView = ShowcaseView.insertShowcaseView(size.x / 2,
size.y - Util.dpToPx(this, 16), this, getString(R.string.showcase_picsphere_title),
getString(R.string.showcase_picsphere_body), co);
mShowcaseIndex = SHOWCASE_INDEX_PICSPHERE;
mShowcaseView.notifyOrientationChanged(mOrientationCompensation);
}
}
@Override
protected void onPause() {
// Pause the camera preview
mPaused = true;
if (mCamManager != null) {
mCamManager.pause();
}
if (mSnapshotManager != null) {
mSnapshotManager.onPause();
}
if (mOrientationListener != null) {
mOrientationListener.disable();
}
if (mPicSphereManager != null) {
mPicSphereManager.onPause();
}
if (SoftwareHdrCapture.isServiceBound()) {
try {
unbindService(SoftwareHdrCapture.getServiceConnection());
} catch (IllegalArgumentException e) {
// Do nothing
}
}
// Reset capture transformers on pause, if we are in
// PicSphere mode
if (mCameraMode == CAMERA_MODE_PICSPHERE) {
mCaptureTransformer = null;
}
super.onPause();
}
@Override
protected void onResume() {
// Restore the camera preview
mPaused = false;
if (mCamManager != null) {
mCamManager.resume();
}
super.onResume();
if (mSnapshotManager != null) {
mSnapshotManager.onResume();
}
if (mPicSphereManager != null) {
mPicSphereManager.onResume();
}
mOrientationListener.enable();
mReviewDrawer.close();
}
@Override
public void onBackPressed() {
if (mReviewDrawer.isOpen()) {
mReviewDrawer.close();
} else {
super.onBackPressed();
}
}
/**
* Returns the mode of the activity
* See CameraActivity.CAMERA_MODE_*
*
* @return int
*/
public static int getCameraMode() {
return mCameraMode;
}
/**
* Notify, like a toast, but orientation aware
* @param text The text to show
* @param lengthMs The duration
*/
public static void notify(String text, int lengthMs) {
mNotifier.notify(text, lengthMs);
}
/**
* Notify, like a toast, but orientation aware at the specified position
* @param text The text to show
* @param lengthMs The duration
*
*/
public static void notify(String text, int lengthMs, float x, float y) {
mNotifier.notify(text, lengthMs, x, y);
}
/**
* @return The Panorama Progress Bar view
*/
public PanoProgressBar getPanoProgressBar() {
return mPanoProgressBar;
}
public void displayOverlayBitmap(Bitmap bmp) {
final ImageView iv = (ImageView) findViewById(R.id.camera_preview_overlay);
iv.setImageBitmap(bmp);
iv.setAlpha(1.0f);
iv.setScaleType(ImageView.ScaleType.FIT_CENTER);
Util.fadeIn(iv);
iv.setVisibility(View.VISIBLE);
}
public void hideOverlayBitmap() {
final ImageView iv = (ImageView) findViewById(R.id.camera_preview_overlay);
Util.fadeOut(iv);
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
iv.setVisibility(View.GONE);
}
}, 300);
}
/**
* Sets the mode of the activity
* See CameraActivity.CAMERA_MODE_*
*
* @param newMode
*/
public void setCameraMode(final int newMode) {
if (mCameraMode == newMode) {
return;
}
if (mCamManager.getParameters() == null) {
mHandler.post(new Runnable() {
public void run() {
setCameraMode(newMode);
}
});
}
if (mCamPreviewListener != null) {
mCamPreviewListener.onPreviewPause();
}
setHelperText("");
// Reset PicSphere 3D renderer if we were in PS mode
if (mCameraMode == CAMERA_MODE_PICSPHERE) {
resetPicSphere();
} else if (mCameraMode == CAMERA_MODE_PANO) {
resetPanorama();
}
else if (mCameraMode == CAMERA_MODE_VIDEO){
// must release the camera
// to reset internals - at least on find5
mCamManager.pause();
mCamManager.resume();
}
mCameraMode = newMode;
// Reset any capture transformer
mCaptureTransformer = null;
if (newMode == CAMERA_MODE_PHOTO) {
mShutterButton.setImageDrawable(getResources().getDrawable(R.drawable.btn_shutter_photo));
mCamManager.setStabilization(false);
} else if (newMode == CAMERA_MODE_VIDEO) {
mShutterButton.setImageDrawable(getResources().getDrawable(R.drawable.btn_shutter_video));
mCamManager.setStabilization(true);
mNotifier.notify(getString(R.string.double_tap_to_snapshot), 2500);
} else if (newMode == CAMERA_MODE_PICSPHERE) {
initializePicSphere();
mShutterButton.setImageDrawable(getResources().getDrawable(R.drawable.btn_shutter_photo));
startShowcasePicSphere();
} else if (newMode == CAMERA_MODE_PANO) {
mShutterButton.setImageDrawable(getResources().getDrawable(R.drawable.btn_shutter_photo));
}
mCamManager.setCameraMode(mCameraMode);
if (newMode == CAMERA_MODE_PANO) {
initializePanorama();
startShowcasePanorama();
}
// Reload pictures in the ReviewDrawer
mReviewDrawer.updateFromGallery(newMode != CAMERA_MODE_VIDEO, 0);
mHandler.post(new Runnable() {
public void run() {
updateCapabilities();
}
});
}
/**
* Sets the active capture transformer. See {@link CaptureTransformer} for
* more details on what's a capture transformer.
*
* @param transformer The new transformer to apply
*/
public void setCaptureTransformer(CaptureTransformer transformer) {
if (mCaptureTransformer != null) {
mSnapshotManager.removeListener(mCaptureTransformer);
}
mCaptureTransformer = transformer;
if (mCaptureTransformer != null && mSnapshotManager != null) {
mSnapshotManager.addListener(transformer);
}
}
/**
* Updates the orientation of the whole UI (in place)
* based on the calculations given by the orientation listener
*/
public void updateInterfaceOrientation() {
setViewRotation(mShutterButton, mOrientationCompensation);
setViewRotation(mRecTimerContainer, mOrientationCompensation);
setViewRotation(mPanoProgressBar, mOrientationCompensation);
setViewRotation(mPicSphereUndo, mOrientationCompensation);
setViewRotation(mHelperText, mOrientationCompensation);
mNotifier.notifyOrientationChanged(mOrientationCompensation);
mSideBar.notifyOrientationChanged(mOrientationCompensation);
mWidgetRenderer.notifyOrientationChanged(mOrientationCompensation);
mSwitchRingPad.notifyOrientationChanged(mOrientationCompensation);
mSavePinger.notifyOrientationChanged(mOrientationCompensation);
mReviewDrawer.notifyOrientationChanged(mOrientationCompensation);
}
public void updateCapabilities() {
// Populate the sidebar buttons a little later (so we have camera parameters)
mHandler.post(new Runnable() {
public void run() {
Camera.Parameters params = mCamManager.getParameters();
// We don't have the camera parameters yet, retry later
if (params == null) {
if (!mPaused) {
mHandler.postDelayed(this, 100);
}
} else {
mCamManager.startParametersBatch();
// Close all widgets
mWidgetRenderer.closeAllWidgets();
// Update focus/exposure ring support
updateRingsVisibility();
// Update sidebar
mSideBar.checkCapabilities(CameraActivity.this,
(ViewGroup) findViewById(R.id.widgets_container));
// Set orientation
updateInterfaceOrientation();
mCamManager.stopParametersBatch();
}
}
});
}
public void updateRingsVisibility() {
// Rings logic:
// * PicSphere and panorama don't need it (infinity focus when possible)
// * Show focus all the time otherwise in photo and video
// * Show exposure ring in photo and video, if it's not toggled off
// * Fullscreen shutter hides all the rings
if ((mCameraMode == CAMERA_MODE_PHOTO && !mIsFullscreenShutter)
|| mCameraMode == CAMERA_MODE_VIDEO) {
mFocusHudRing.setVisibility(mCamManager.isFocusAreaSupported() ?
View.VISIBLE : View.GONE);
mExposureHudRing.setVisibility(mCamManager.isExposureAreaSupported()
&& mUserWantsExposureRing ? View.VISIBLE : View.GONE);
} else {
mFocusHudRing.setVisibility(View.GONE);
mExposureHudRing.setVisibility(View.GONE);
}
}
public boolean isExposureRingVisible() {
return (mExposureHudRing.getVisibility() == View.VISIBLE);
}
public void setExposureRingVisible(boolean visible) {
mUserWantsExposureRing = visible;
updateRingsVisibility();
// Internally reset the position of the exposure ring, while still
// leaving it at its position so that if the user toggles it back
// on, it will appear at its previous location
mCamManager.setExposurePoint(0, 0);
}
public void startTimerCountdown(int timeMs) {
mTimerView.animate().alpha(1.0f).setDuration(300).start();
mTimerView.setIntervalTime(timeMs);
mTimerView.startIntervalAnimation();
}
public void hideTimerCountdown() {
mTimerView.animate().alpha(0.0f).setDuration(300).start();
}
protected void setupCamera() {
// Setup the Camera hardware and preview
mCamManager = new CameraManager(this);
((CameraApplication)getApplication()).setCameraManager(mCamManager);
setGLRenderer(mCamManager.getRenderer());
mCamPreviewListener = new CameraPreviewListener();
mCamManager.setPreviewPauseListener(mCamPreviewListener);
mCamManager.setCameraReadyListener(this);
mCamManager.open(Camera.CameraInfo.CAMERA_FACING_BACK);
}
@Override
public void onCameraReady() {
runOnUiThread(new Runnable() {
@Override
public void run() {
Profiler.getDefault().start("OnCameraReady");
Camera.Parameters params = mCamManager.getParameters();
if (params == null) {
// Are we too fast? Let's try again.
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
onCameraReady();
}
}, 20);
return;
}
mCamManager.updateDisplayOrientation();
Camera.Size picSize = params.getPictureSize();
Camera.Size sz = Util.getOptimalPreviewSize(CameraActivity.this, params.getSupportedPreviewSizes(),
((float) picSize.width / (float) picSize.height));
if (sz == null) {
Log.e(TAG, "No preview size!! Something terribly wrong with camera!");
return;
}
//mCamManager.setPreviewSize(sz.width, sz.height);
if (mIsCamSwitching) {
mCamManager.restartPreviewIfNeeded();
mIsCamSwitching = false;
}
if (mFocusManager == null) {
mFocusManager = new FocusManager(mCamManager);
mFocusManager.setListener(new MainFocusListener());
}
mFocusHudRing.setManagers(mCamManager, mFocusManager);
if (mSnapshotManager == null) {
mSnapshotManager = new SnapshotManager(mCamManager, mFocusManager, CameraActivity.this);
mSnapshotListener = new MainSnapshotListener();
mSnapshotManager.addListener(mSnapshotListener);
}
// Hide sidebar after start
mCancelSideBarClose = false;
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
if (!mCancelSideBarClose) {
mSideBar.slideClose();
mWidgetRenderer.notifySidebarSlideClose();
}
}
}, 1500);
Profiler.getDefault().start("OnCameraReady-updateCapa");
updateCapabilities();
Profiler.getDefault().logProfile("OnCameraReady-updateCapa");
mSavePinger.stopSaving();
Profiler.getDefault().logProfile("OnCameraReady");
}
});
}
public void onCameraFailed() {
Log.e(TAG, "Could not open camera HAL");
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(CameraActivity.this,
getResources().getString(R.string.cannot_connect_hal),
Toast.LENGTH_LONG).show();
}
});
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_FOCUS:
case KeyEvent.KEYCODE_VOLUME_DOWN:
// Use the volume down button as focus button
if (!mIsFocusButtonDown) {
mCamManager.doAutofocus(mFocusManager);
mCamManager.setLockSetup(true);
mIsFocusButtonDown = true;
}
return true;
case KeyEvent.KEYCODE_CAMERA:
case KeyEvent.KEYCODE_VOLUME_UP:
// Use the volume up button as shutter button (or snapshot button in video mode)
if (!mIsShutterButtonDown) {
if (mCameraMode == CAMERA_MODE_VIDEO) {
mSnapshotManager.queueSnapshot(true, 0);
} else {
mShutterButton.performClick();
}
mIsShutterButtonDown = true;
}
return true;
}
return super.onKeyDown(keyCode, event);
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_FOCUS:
case KeyEvent.KEYCODE_VOLUME_DOWN:
mIsFocusButtonDown = false;
mCamManager.setLockSetup(false);
break;
case KeyEvent.KEYCODE_CAMERA:
case KeyEvent.KEYCODE_VOLUME_UP:
mIsShutterButtonDown = false;
break;
}
return super.onKeyUp(keyCode, event);
}
public CameraManager getCamManager() {
return mCamManager;
}
public SnapshotManager getSnapManager() {
return mSnapshotManager;
}
public PicSphereManager getPicSphereManager() {
return mPicSphereManager;
}
public ReviewDrawer getReviewDrawer() {
return mReviewDrawer;
}
public void initializePicSphere() {
// Check if device has a gyroscope and GLES2 support
// XXX: Should we make a fallback for super super old devices?
final ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
final ConfigurationInfo configurationInfo = activityManager.getDeviceConfigurationInfo();
final boolean supportsEs2 = configurationInfo.reqGlEsVersion >= 0x20000;
if (!supportsEs2) {
mNotifier.notify(getString(R.string.no_gles20_support), 4000);
return;
}
// Close widgets and slide sidebar to make room and focus on the sphere
mSideBar.slideClose();
mWidgetRenderer.closeAllWidgets();
// Setup the 3D rendering
if (mPicSphereManager == null) {
mPicSphereManager = new PicSphereManager(this, mSnapshotManager);
}
setGLRenderer(mPicSphereManager.getRenderer());
// Setup the capture transformer
final PicSphereCaptureTransformer transformer =
new PicSphereCaptureTransformer(this);
setCaptureTransformer(transformer);
mPicSphereUndo.setVisibility(View.VISIBLE);
mPicSphereUndo.setAlpha(0.0f);
mPicSphereUndo.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
transformer.removeLastPicture();
}
});
// Notify how to start a sphere
setHelperText(getString(R.string.picsphere_start_hint));
}
/**
* Tear down the PicSphere mode and set the default renderer back on the preview
* GL surface.
*/
public void resetPicSphere() {
// Reset the normal renderer
setGLRenderer(mCamManager.getRenderer());
// Tear down PicSphere capture system
if (mPicSphereManager != null) {
mPicSphereManager.tearDown();
}
setCaptureTransformer(null);
if (mPicSphereUndo != null) {
mPicSphereUndo.setVisibility(View.GONE);
}
}
/**
* Initializes the panorama (mosaic) subsystem
*/
public void initializePanorama() {
mMosaicProxy = new MosaicProxy(this);
setCaptureTransformer(mMosaicProxy);
mCamManager.setRenderToTexture(null);
updateRingsVisibility();
}
/**
* Turns off the panorama (mosaic) subsystem
*/
public void resetPanorama() {
if (mMosaicProxy != null) {
mMosaicProxy.tearDown();
}
setGLRenderer(mCamManager.getRenderer());
}
public void setGLRenderer(GLSurfaceView.Renderer renderer) {
final ViewGroup container = ((ViewGroup) findViewById(R.id.gl_renderer_container));
// Delete the previous GL Surface View (if any)
if (mGLSurfaceView != null) {
container.removeView(mGLSurfaceView);
mGLSurfaceView = null;
}
// Make a new GL view using the provided renderer
mGLSurfaceView = new GLSurfaceView(this);
mGLSurfaceView.setEGLContextClientVersion(2);
mGLSurfaceView.setRenderer(renderer);
container.addView(mGLSurfaceView);
}
/**
* Toggles the fullscreen shutter that lets user take pictures by tapping on the screen
*/
public void toggleFullscreenShutter() {
if (mIsFullscreenShutter) {
mIsFullscreenShutter = false;
mShutterButton.animate().translationY(0).setDuration(400).start();
} else {
mIsFullscreenShutter = true;
mShutterButton.animate().translationY(mShutterButton.getHeight()).setDuration(400).start();
notify(getString(R.string.fullscreen_shutter_info), 2000);
}
updateRingsVisibility();
}
/**
* Show a persistent helper text that indicates the user a required action
* @param text The text to show, or empty/null to hide
*/
public void setHelperText(final CharSequence text) {
setHelperText(text, false);
}
/**
* Show a persistent helper text that indicates the user a required action
* @param text The text to show, or empty/null to hide
* @param beware Show the text in red
*/
public void setHelperText(final CharSequence text, final boolean beware) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (text == null || text.equals("")) {
// Hide it
Util.fadeOut(mHelperText);
} else {
mHelperText.setText(text);
if (beware) {
mHelperText.setTextColor(getResources().getColor(R.color.clock_red));
} else {
mHelperText.setTextColor(0xFFFFFFFF);
}
Util.fadeIn(mHelperText);
}
}
});
}
public void setPicSphereUndoVisible(final boolean visible) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (visible) {
mPicSphereUndo.setVisibility(View.VISIBLE);
mPicSphereUndo.setAlpha(1.0f);
} else {
mPicSphereUndo.animate().alpha(0.0f).setDuration(200).start();
}
}
});
}
/**
* Recursively rotates the Views of ViewGroups
*
* @param vg the root ViewGroup
* @param rotation the angle to which rotate the views
*/
public static void setViewGroupRotation(ViewGroup vg, float rotation) {
final int childCount = vg.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = vg.getChildAt(i);
if (child instanceof ViewGroup) {
setViewGroupRotation((ViewGroup) child, rotation);
} else {
setViewRotation(child, rotation);
}
}
}
public static void setViewRotation(View v, float rotation) {
v.animate().rotation(rotation).setDuration(200)
.setInterpolator(new DecelerateInterpolator()).start();
}
@Override
public void onShowcaseViewHide(ShowcaseView showcaseView) {
switch (mShowcaseIndex) {
case SHOWCASE_INDEX_WELCOME_1:
mShowcaseIndex = SHOWCASE_INDEX_WELCOME_2;
Point size = new Point();
getWindowManager().getDefaultDisplay().getSize(size);
ShowcaseView.ConfigOptions co = new ShowcaseView.ConfigOptions();
co.hideOnClickOutside = true;
mShowcaseView = ShowcaseView.insertShowcaseView(size.x / 2,
size.y - Util.dpToPx(this, 16), this,
getString(R.string.showcase_welcome_2_title),
getString(R.string.showcase_welcome_2_body), co);
// animate gesture
mShowcaseView.animateGesture(size.x / 2,
size.y - Util.dpToPx(this, 16), size.x / 2, size.y / 2);
mShowcaseView.setOnShowcaseEventListener(this);
// ping the button
mSwitchRingPad.animateHint();
break;
}
}
@Override
public void onShowcaseViewShow(ShowcaseView showcaseView) {
// Do nothing here
}
/**
* Listener that is called when the preview pauses or resumes
*/
private class CameraPreviewListener implements CameraManager.PreviewPauseListener {
@Override
public void onPreviewPause() {
// XXX: Do a little animation
}
@Override
public void onPreviewResume() {
// XXX: Do a little animation
}
}
/**
* Listener that is called when a ring pad button is activated (finger release above)
*/
private class MainRingPadListener implements SwitchRingPad.RingPadListener {
@Override
public void onButtonActivated(int eventId) {
switch (eventId) {
case SwitchRingPad.BUTTON_CAMERA:
setCameraMode(CAMERA_MODE_PHOTO);
break;
case SwitchRingPad.BUTTON_PANO:
setCameraMode(CAMERA_MODE_PANO);
break;
case SwitchRingPad.BUTTON_VIDEO:
setCameraMode(CAMERA_MODE_VIDEO);
break;
case SwitchRingPad.BUTTON_PICSPHERE:
setCameraMode(CAMERA_MODE_PICSPHERE);
break;
case SwitchRingPad.BUTTON_SWITCHCAM:
mIsCamSwitching = true;
if (mCamManager.getCurrentFacing() == Camera.CameraInfo.CAMERA_FACING_FRONT) {
mCamManager.open(Camera.CameraInfo.CAMERA_FACING_BACK);
} else {
mCamManager.open(Camera.CameraInfo.CAMERA_FACING_FRONT);
}
break;
}
}
}
/**
* Listener that is called when shutter button is slided, to open ring pad view
*/
private class MainShutterSlideListener implements ShutterButton.ShutterSlideListener {
@Override
public void onSlideOpen() {
mSwitchRingPad.animateOpen();
// Tapping the shutter button locked exposure/WB, so we unlock it if we slide our finger
mCamManager.setLockSetup(false);
// Cancel long-press action
mIsShutterLongClicked = false;
}
@Override
public void onSlideClose() {
mSwitchRingPad.animateClose();
}
@Override
public boolean onMotionEvent(MotionEvent ev) {
return mSwitchRingPad.onTouchEvent(ev);
}
@Override
public void onShutterButtonPressed() {
// Animate the ring pad
mSwitchRingPad.animateHint();
// Make the review drawer super translucent if it is open
mReviewDrawer.setTemporaryHide(true);
// Lock automatic settings
mCamManager.setLockSetup(true);
// Turn on stabilization
mCamManager.setStabilization(true);
}
}
/**
* When the shutter button is pressed
*/
public class MainShutterClickListener implements OnClickListener,
View.OnLongClickListener, View.OnTouchListener {
@Override
public void onClick(View v) {
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
mReviewDrawer.setTemporaryHide(false);
}
}, 500);
if (mSnapshotManager == null) return;
// If we have a capture transformer, apply it, otherwise use the default
// behavior.
if (mCaptureTransformer != null) {
mCaptureTransformer.onShutterButtonClicked(mShutterButton);
} else if (CameraActivity.getCameraMode() == CameraActivity.CAMERA_MODE_PHOTO) {
mSnapshotManager.queueSnapshot(true, 0);
} else if (CameraActivity.getCameraMode() == CameraActivity.CAMERA_MODE_VIDEO) {
if (!mSnapshotManager.isRecording()) {
mSnapshotManager.startVideo();
mShutterButton.setImageDrawable(getResources()
.getDrawable(R.drawable.btn_shutter_stop));
} else {
mSnapshotManager.stopVideo();
mShutterButton.setImageDrawable(getResources()
.getDrawable(R.drawable.btn_shutter_video));
}
} else {
Log.e(TAG, "Unknown Camera Mode: " + mCameraMode + " ; No capture transformer");
}
}
@Override
public boolean onLongClick(View view) {
if (mCaptureTransformer != null) {
mCaptureTransformer.onShutterButtonLongPressed(mShutterButton);
} else {
mIsShutterLongClicked = true;
if (mFocusManager != null) {
mFocusManager.checkFocus();
}
}
return true;
}
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
// If we long-press the shutter button and no capture transformer handles it, we
// will just have nothing happening. We register the long click event in here, and
// trigger a snapshot once it's released.
if (motionEvent.getActionMasked() == MotionEvent.ACTION_UP && mIsShutterLongClicked) {
mIsShutterLongClicked = false;
onClick(view);
}
return view.onTouchEvent(motionEvent);
}
}
/**
* Focus listener to animate the focus HUD ring from FocusManager events
*/
private class MainFocusListener implements FocusManager.FocusListener {
@Override
public void onFocusStart(final boolean smallAdjust) {
mIsFocusing = true;
runOnUiThread(new Runnable() {
@Override
public void run() {
mFocusHudRing.animateWorking(smallAdjust ? 200 : 1500);
}
});
}
@Override
public void onFocusReturns(final boolean smallAdjust, final boolean success) {
mIsFocusing = false;
runOnUiThread(new Runnable() {
@Override
public void run() {
mFocusHudRing.animatePressUp();
if (!smallAdjust) {
mFocusHudRing.setFocusImage(success);
} else {
mFocusHudRing.setFocusImage(true);
}
}
});
}
}
/**
* Snapshot listener for when snapshots are taken, in SnapshotManager
*/
private class MainSnapshotListener implements SnapshotManager.SnapshotListener {
private long mRecordingStartTimestamp;
private TextView mTimerTv;
private boolean mIsRecording;
private Runnable mUpdateTimer = new Runnable() {
@Override
public void run() {
long recordingDurationMs = System.currentTimeMillis() - mRecordingStartTimestamp;
int minutes = (int) Math.floor(recordingDurationMs / 60000.0);
int seconds = (int) recordingDurationMs / 1000 - minutes * 60;
mTimerTv.setText(String.format("%02d:%02d", minutes, seconds));
// Loop infinitely until recording stops
if (mIsRecording) {
mHandler.postDelayed(this, 500);
}
}
};
@Override
public void onSnapshotShutter(final SnapshotManager.SnapshotInfo info) {
final FrameLayout layout = (FrameLayout) findViewById(R.id.thumb_flinger_container);
// Fling the preview
final ThumbnailFlinger flinger = new ThumbnailFlinger(CameraActivity.this);
mHandler.post(new Runnable() {
@Override
public void run() {
layout.addView(flinger);
flinger.setRotation(90);
flinger.setImageBitmap(info.mThumbnail);
flinger.doAnimation();
}
});
// Unlock camera auto settings
mCamManager.setLockSetup(false);
mCamManager.setStabilization(false);
}
@Override
public void onSnapshotPreview(SnapshotManager.SnapshotInfo info) {
// Do nothing here
}
@Override
public void onSnapshotProcessing(SnapshotManager.SnapshotInfo info) {
runOnUiThread(new Runnable() {
public void run() {
if (mSavePinger != null) {
mSavePinger.setPingMode(SavePinger.PING_MODE_ENHANCER);
mSavePinger.startSaving();
}
}
});
}
@Override
public void onSnapshotSaved(SnapshotManager.SnapshotInfo info) {
String uriStr = info.mUri.toString();
// Add the new image to the gallery and the review drawer
int originalImageId = Integer.parseInt(uriStr.substring(uriStr
.lastIndexOf("/") + 1, uriStr.length()));
Log.v(TAG, "Adding snapshot to gallery: " + originalImageId);
mReviewDrawer.addImageToList(originalImageId);
mReviewDrawer.scrollToLatestImage();
}
@Override
public void onMediaSavingStart() {
runOnUiThread(new Runnable() {
@Override
public void run() {
mSavePinger.setPingMode(SavePinger.PING_MODE_SAVE);
mSavePinger.startSaving();
}
});
}
@Override
public void onMediaSavingDone() {
runOnUiThread(new Runnable() {
@Override
public void run() {
mSavePinger.stopSaving();
}
});
}
@Override
public void onVideoRecordingStart() {
mTimerTv = (TextView) findViewById(R.id.recording_timer_text);
mRecordingStartTimestamp = System.currentTimeMillis();
mIsRecording = true;
runOnUiThread(new Runnable() {
@Override
public void run() {
mHandler.post(mUpdateTimer);
mRecTimerContainer.setVisibility(View.VISIBLE);
}
});
}
@Override
public void onVideoRecordingStop() {
mIsRecording = false;
runOnUiThread(new Runnable() {
@Override
public void run() {
mRecTimerContainer.setVisibility(View.GONE);
}
});
}
}
/**
* Handles the orientation changes without turning the actual activity
*/
private class CameraOrientationEventListener extends OrientationEventListener {
public CameraOrientationEventListener(Context context) {
super(context);
}
@Override
public void onOrientationChanged(int orientation) {
// We keep the last known orientation. So if the user first orient
// the camera then point the camera to floor or sky, we still have
// the correct orientation.
if (orientation == ORIENTATION_UNKNOWN) {
return;
}
mOrientation = Util.roundOrientation(orientation, mOrientation);
// Notify camera of the raw orientation
mCamManager.setOrientation(mOrientation);
// Adjust orientationCompensation for the native orientation of the device.
Configuration config = getResources().getConfiguration();
int rotation = getWindowManager().getDefaultDisplay().getRotation();
Util.getDisplayRotation(CameraActivity.this);
boolean nativeLandscape = false;
if (((rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180)
&& config.orientation == Configuration.ORIENTATION_LANDSCAPE)
|| ((rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270)
&& config.orientation == Configuration.ORIENTATION_PORTRAIT)) {
nativeLandscape = true;
}
int orientationCompensation = mOrientation; // + (nativeLandscape ? 0 : 90);
if (orientationCompensation == 90) {
orientationCompensation += 180;
} else if (orientationCompensation == 270) {
orientationCompensation -= 180;
}
// Avoid turning all around
float angleDelta = orientationCompensation - mOrientationCompensation;
if (angleDelta >= 270) {
orientationCompensation -= 360;
}
if (mOrientationCompensation != orientationCompensation) {
mOrientationCompensation = orientationCompensation;
updateInterfaceOrientation();
}
}
}
/**
* Handles the swipe and tap gestures on the lower layer of the screen
* (ie. the preview surface)
*
* @note Remember that the default orientation of the screen is landscape, thus
* the side bar is at the BOTTOM of the screen, and is swiped UP/DOWN.
*/
public class GestureListener extends GestureDetector.SimpleOnGestureListener {
private static final int SWIPE_MIN_DISTANCE = 10;
private final float DRAG_MIN_DISTANCE = Util.dpToPx(CameraActivity.this, 5.0f);
private static final int SWIPE_MAX_OFF_PATH = 80;
private static final int SWIPE_THRESHOLD_VELOCITY = 800;
// Allow to drag the side bar up to half of the screen
private static final int SIDEBAR_THRESHOLD_FACTOR = 2;
private boolean mCancelSwipe = false;
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
if (mPaused) return false;
// A single tap equals to touch-to-focus in photo/video
if ((mCameraMode == CAMERA_MODE_PHOTO && !mIsFullscreenShutter)
|| mCameraMode == CAMERA_MODE_VIDEO) {
if (mFocusManager != null) {
mFocusHudRing.setPosition(e.getRawX(), e.getRawY());
mFocusManager.refocus();
}
} else if (mCameraMode == CAMERA_MODE_PHOTO && mIsFullscreenShutter) {
// We are in fullscreen shutter mode, so just take a picture
mSnapshotManager.queueSnapshot(true, 0);
}
return super.onSingleTapConfirmed(e);
}
@Override
public boolean onDoubleTap(MotionEvent e) {
// In VIDEO mode, a double tap snapshots (or volume up)
if (mCameraMode == CAMERA_MODE_VIDEO) {
mSnapshotManager.queueSnapshot(true, 0);
} else if (mCameraMode == CAMERA_MODE_PHOTO) {
// Toggle fullscreen shutter
toggleFullscreenShutter();
}
return super.onDoubleTap(e);
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
if (e1 == null || e2 == null) {
return false;
}
// Detect drag of the side bar or review drawer
if (Math.abs(e1.getY() - e2.getY()) < SWIPE_MAX_OFF_PATH) {
if (e1.getRawX() < Util.getScreenSize(CameraActivity.this)
.x / SIDEBAR_THRESHOLD_FACTOR) {
if (e1.getX() - e2.getX() > SWIPE_MIN_DISTANCE ||
e2.getX() - e1.getX() > SWIPE_MIN_DISTANCE) {
mSideBar.slide(-distanceX);
mWidgetRenderer.notifySidebarSlideStatus(-distanceX);
mCancelSwipe = true;
mCancelSideBarClose = true;
}
return true;
}
} else if (Math.abs(e1.getY() - e2.getY()) > DRAG_MIN_DISTANCE) {
mReviewDrawer.slide(-distanceY);
}
return true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
try {
if (Math.abs(e1.getY() - e2.getY()) < SWIPE_MAX_OFF_PATH) {
// swipes to open/close the sidebar and/or hide/restore the widgets
if (e2.getX() - e1.getX() > SWIPE_MIN_DISTANCE
&& Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) {
if (mWidgetRenderer.isHidden() && mWidgetRenderer.getWidgetsCount() > 0) {
mWidgetRenderer.restoreWidgets();
} else {
mSideBar.slideOpen();
mWidgetRenderer.notifySidebarSlideOpen();
mCancelSideBarClose = true;
}
} else if (e1.getX() - e2.getX() > SWIPE_MIN_DISTANCE
&& Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) {
if (mSideBar.isOpen()) {
mSideBar.slideClose();
mWidgetRenderer.notifySidebarSlideClose();
mCancelSideBarClose = true;
} else if (!mWidgetRenderer.isHidden()
&& mWidgetRenderer.getWidgetsCount() > 0
&& !mCancelSwipe) {
mWidgetRenderer.hideWidgets();
}
}
}
if (Math.abs(e1.getX() - e2.getX()) < SWIPE_MAX_OFF_PATH) {
// swipes up/down to open/close the review drawer
if (e1.getY() - e2.getY() > SWIPE_MIN_DISTANCE
&& Math.abs(velocityY) > SWIPE_THRESHOLD_VELOCITY) {
mReviewDrawer.close();
} else if (e2.getY() - e1.getY() > SWIPE_MIN_DISTANCE
&& Math.abs(velocityY) > SWIPE_THRESHOLD_VELOCITY) {
mReviewDrawer.open();
}
}
} catch (Exception e) {
// Do nothing here
}
mCancelSwipe = false;
return true;
}
}
/**
* Handles the pinch-to-zoom gesture
*/
private class ZoomGestureListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
@Override
public boolean onScale(ScaleGestureDetector detector) {
Camera.Parameters params = mCamManager.getParameters();
if (params == null) return false;
if (!mIsFocusing) {
if (detector.getScaleFactor() > 1.0f) {
params.setZoom(Math.min(params.getZoom() + 1, params.getMaxZoom()));
} else if (detector.getScaleFactor() < 1.0f) {
params.setZoom(Math.max(params.getZoom() - 1, 0));
} else {
return false;
}
mHasPinchZoomed = true;
mCamManager.setParameters(params);
}
return true;
}
}
}
================================================
FILE: src/org/cyanogenmod/focal/CameraApplication.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal;
import android.app.Application;
import android.util.Log;
/**
* Manages the application itself (on top of the activity), mainly to force Camera getting
* closed in case of crash.
*/
public class CameraApplication extends Application {
private final static String TAG = "FocalApp";
private Thread.UncaughtExceptionHandler mDefaultExHandler;
private CameraManager mCamManager;
private Thread.UncaughtExceptionHandler mExHandler = new Thread.UncaughtExceptionHandler() {
public void uncaughtException(Thread thread, Throwable ex) {
if (mCamManager != null) {
Log.e(TAG, "Uncaught exception! Closing down camera safely firsthand");
mCamManager.forceCloseCamera();
}
mDefaultExHandler.uncaughtException(thread, ex);
}
};
@Override
public void onCreate() {
super.onCreate();
mDefaultExHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(mExHandler);
}
public void setCameraManager(CameraManager camMan) {
mCamManager = camMan;
}
}
================================================
FILE: src/org/cyanogenmod/focal/CameraButtonIntentReceiver.java
================================================
/*
* Copyright (C) 2007 The Android Open Source Project
* Copyright (C) 2013 Guillaume Lesniak
*
* Licensed under the Apache License, Version 2.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 org.cyanogenmod.focal;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
/**
* {@code CameraButtonIntentReceiver} is invoked when the camera button is
* long-pressed.
*
* It is declared in {@code AndroidManifest.xml} to receive the
* {@code android.intent.action.CAMERA_BUTTON} intent.
*
*/
public class CameraButtonIntentReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Intent i = new Intent(Intent.ACTION_MAIN);
i.setClass(context, CameraActivity.class);
i.addCategory(Intent.CATEGORY_LAUNCHER);
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TOP);
context.startActivity(i);
}
}
================================================
FILE: src/org/cyanogenmod/focal/CameraCapabilities.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal;
import android.hardware.Camera;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import org.cyanogenmod.focal.widgets.*;
import java.util.ArrayList;
import java.util.List;
/**
* This class holds all the possible widgets of the
* sidebar. It checks for support prior to adding them
* effectively in the sidebar.
*/
public class CameraCapabilities {
private List mWidgets;
/**
* Default constructor, initializes all the widgets. They will
* then be sorted by populateSidebar.
*
* @param context The CameraActivity context
*/
public CameraCapabilities(CameraActivity context) {
mWidgets = new ArrayList();
CameraManager cam = context.getCamManager();
// Populate the list of widgets.
// Basically, if we add a new widget, we just put it here.
// They will populate the sidebar in the same order as here.
mWidgets.add(new FlashWidget(cam, context));
mWidgets.add(new WhiteBalanceWidget(cam, context));
mWidgets.add(new SceneModeWidget(cam, context));
mWidgets.add(new HdrWidget(cam, context));
mWidgets.add(new SoftwareHdrWidget(context));
mWidgets.add(new VideoHdrWidget(cam, context));
mWidgets.add(new EffectWidget(cam, context));
mWidgets.add(new ExposureCompensationWidget(cam, context));
mWidgets.add(new EnhancementsWidget(cam, context));
mWidgets.add(new AutoExposureWidget(cam, context));
mWidgets.add(new IsoWidget(cam, context));
mWidgets.add(new ShutterSpeedWidget(cam, context));
mWidgets.add(new BurstModeWidget(context));
mWidgets.add(new TimerModeWidget(context));
mWidgets.add(new VideoFrWidget(cam, context));
mWidgets.add(new SettingsWidget(context, this));
}
/**
* @return The list of currently enabled/capable widgets
*/
public List getWidgets() {
return mWidgets;
}
/**
* Populates the sidebar (through sideBarContainer) with the widgets actually
* compatible with the device.
*
* @param params The Camera parameters returned from the HAL for compatibility check
* @param sideBarContainer The side bar layout that will contain all the toggle buttons
* @param homeContainer The viewgroup containing home shortcuts
* @param widgetsContainer The container of the final rendered widgets
*/
public void populateSidebar(Camera.Parameters params, ViewGroup sideBarContainer,
ViewGroup homeContainer, ViewGroup widgetsContainer) {
List unsupported = new ArrayList();
for (int i = 0; i < mWidgets.size(); i++) {
final WidgetBase widget = mWidgets.get(i);
// Add the widget to the sidebar if it is supported by the device.
// The compatibility is determined by widgets themselves.
if (widget.isSupported(params)) {
widgetsContainer.addView(widget.getWidget());
homeContainer.addView(widget.getShortcutButton());
sideBarContainer.addView(widget.getToggleButton());
// If the widget is pinned, show it on the main screen, otherwise in the bar
if (SettingsStorage.getShortcutSetting(widget.getWidget().getContext(),
widget.getClass().getCanonicalName())) {
widget.getShortcutButton().setVisibility(View.VISIBLE);
widget.getToggleButton().setVisibility(View.GONE);
} else {
widget.getShortcutButton().setVisibility(View.GONE);
widget.getToggleButton().setVisibility(View.VISIBLE);
}
} else {
unsupported.add(widget);
}
}
for (int i = 0; i < unsupported.size(); i++) {
mWidgets.remove(unsupported.get(i));
}
}
}
================================================
FILE: src/org/cyanogenmod/focal/CameraManager.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal;
import android.app.Activity;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.SurfaceTexture;
import android.hardware.Camera;
import android.hardware.Camera.AutoFocusCallback;
import android.hardware.Camera.AutoFocusMoveCallback;
import android.media.CamcorderProfile;
import android.media.MediaRecorder;
import android.opengl.GLES11Ext;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import android.os.Build;
import android.os.Handler;
import android.util.Log;
import org.apache.http.NameValuePair;
import org.apache.http.message.BasicNameValuePair;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import fr.xplod.focal.R;
/**
* This class is responsible for interacting with the Camera HAL.
* It provides easy open/close, helper methods to set parameters or
* toggle features, etc. in an asynchronous fashion.
*/
public class CameraManager {
private final static String TAG = "CameraManager";
private final static int FOCUS_WIDTH = 80;
private final static int FOCUS_HEIGHT = 80;
private final static boolean DEBUG_LOG_PARAMS = false;
private final static boolean DEBUG_PROFILER = false;
private CameraPreview mPreview;
private Camera mCamera;
private boolean mCameraReady;
private int mCurrentFacing;
private Point mTargetSize;
private AutoFocusMoveCallback mAutoFocusMoveCallback;
private Camera.Parameters mParameters;
private int mOrientation;
private MediaRecorder mMediaRecorder;
private PreviewPauseListener mPreviewPauseListener;
private CameraReadyListener mCameraReadyListener;
private Handler mHandler;
private Activity mContext;
private boolean mIsModeSwitching;
private List mPendingParameters;
private boolean mIsResuming;
private CameraRenderer mRenderer;
private boolean mIsRecordingHint;
private boolean mIsPreviewStarted;
private boolean mParametersBatch;
public interface PreviewPauseListener {
/**
* This method is called when the preview is about to pause.
* This allows the CameraActivity to display an animation when the preview
* has to stop.
*/
public void onPreviewPause();
/**
* This method is called when the preview resumes
*/
public void onPreviewResume();
}
public interface CameraReadyListener {
/**
* Called when a camera has been successfully opened. This allows the
* main activity to continue setup operations while the camera
* sets up in a different thread.
*/
public void onCameraReady();
/**
* Called when the camera failed to initialize
*/
public void onCameraFailed();
}
private class ParametersThread extends Thread {
public void run() {
while (true) {
synchronized (this) {
try {
wait();
} catch (InterruptedException e) {
// Do nothing here
return;
}
Log.v(TAG, "Batch parameter setting starting.");
String existingParameters = getParameters().flatten();
// If the camera died, just forget about this.
if (existingParameters == null) continue;
List copy = new ArrayList(mPendingParameters);
mPendingParameters.clear();
Camera.Parameters params = getParameters();
for (NameValuePair pair : copy) {
String key = pair.getName();
String val = pair.getValue();
Log.v(TAG, "Setting parameter " + key+ " to " + val);
params.set(key, val);
}
try {
mCamera.setParameters(params);
} catch (RuntimeException e) {
Log.e(TAG, "Could not set parameters batch", e);
}
// Read them from sensor
mParameters = null;// getParameters();
}
}
}
}
private ParametersThread mParametersThread = null;
final Object mParametersSync = new Object();
public CameraManager(Activity context) {
mPreview = new CameraPreview();
mMediaRecorder = new MediaRecorder();
mCameraReady = true;
mHandler = new Handler();
mIsModeSwitching = false;
mContext = context;
mPendingParameters = new ArrayList();
mParametersThread = new ParametersThread();
mParametersThread.start();
mIsResuming = false;
mIsRecordingHint = false;
mRenderer = new CameraRenderer();
mIsPreviewStarted = false;
}
/**
* Opens the camera and show its preview in the preview
*
* @param cameraId The facing of the camera
* @return true if the operation succeeded, false otherwise
*/
public boolean open(final int cameraId) {
if (mCamera != null) {
if (mPreviewPauseListener != null) {
mPreviewPauseListener.onPreviewPause();
}
// Close the previous camera
releaseCamera();
}
mCameraReady = false;
// Try to open the camera
new Thread() {
public void run() {
try {
if (DEBUG_PROFILER) Profiler.getDefault().start("CameraOpen");
if (mCamera != null) {
Log.e(TAG, "Previous camera not closed! Not opening");
return;
}
mCamera = Camera.open(cameraId);
Log.v(TAG, "Camera is open");
if (Build.VERSION.SDK_INT >= 17) {
mCamera.enableShutterSound(false);
}
mCamera.setPreviewCallback(mPreview);
mCurrentFacing = cameraId;
mParameters = mCamera.getParameters();
if (DEBUG_LOG_PARAMS) {
String params = mCamera.getParameters().flatten();
final int step = params.length() > 256 ? 256 : params.length();
for (int i = 0; i < params.length(); i += step) {
Log.d(TAG, params);
params = params.substring(step);
}
}
// Mako hack to raise FPS
if (Build.DEVICE.equals("mako")) {
Camera.Size maxSize = mParameters.getSupportedPictureSizes().get(0);
mParameters.setPictureSize(maxSize.width, maxSize.height);
}
if (mAutoFocusMoveCallback != null) {
setAutoFocusMoveCallback(mAutoFocusMoveCallback);
}
} catch (Exception e) {
Log.e(TAG, "Error while opening cameras: " + e.getMessage(), e);
if (mCameraReadyListener != null) {
mCameraReadyListener.onCameraFailed();
}
return;
}
// Update the preview surface holder with the new opened camera
mPreview.notifyCameraChanged(false);
if (mCameraReadyListener != null) {
mCameraReadyListener.onCameraReady();
}
if (mPreviewPauseListener != null) {
mPreviewPauseListener.onPreviewResume();
}
mPreview.setPauseCopyFrame(false);
mCameraReady = true;
if (DEBUG_PROFILER) Profiler.getDefault().logProfile("CameraOpen");
}
}.start();
return true;
}
public void setPreviewPauseListener(PreviewPauseListener listener) {
mPreviewPauseListener = listener;
}
public void setCameraReadyListener(CameraReadyListener listener) {
mCameraReadyListener = listener;
}
/**
* Returns the preview surface used to display the Camera's preview
*
* @return CameraPreview
*/
public CameraPreview getPreviewSurface() {
return mPreview;
}
/**
* @return The GLES20-compatible renderer for the camera preview
*/
public CameraRenderer getRenderer() {
return mRenderer;
}
/**
* @return The facing of the current open camera
*/
public int getCurrentFacing() {
return mCurrentFacing;
}
/**
* Returns the parameters structure of the current running camera
*
* @return Camera.Parameters
*/
public Camera.Parameters getParameters() {
synchronized (mParametersSync) {
if (mCamera == null) {
Log.w(TAG, "getParameters when camera is null");
return null;
}
int tries = 0;
while (mParameters == null) {
try {
mParameters = mCamera.getParameters();
break;
} catch (RuntimeException e) {
Log.e(TAG, "Error while getting parameters: ", e);
if (tries < 3) {
tries++;
try {
Thread.sleep(100);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
} else {
Log.e(TAG, "Failed to get parameters after 3 tries");
break;
}
}
}
}
return mParameters;
}
public void pause() {
mPreview.setPauseCopyFrame(true);
releaseCamera();
mParametersThread.interrupt();
mParametersThread = null;
}
public void resume() {
mIsResuming = true;
reconnectToCamera();
mParametersThread = new ParametersThread();
mParametersThread.start();
}
/**
* Used by CameraApplication safeguard to release the camera when the app crashes.
*/
public void forceCloseCamera() {
if (mCamera != null) {
try {
mCamera.release();
mCamera = null;
mParameters = null;
} catch (Exception e) {
// Do nothing
}
}
}
private void releaseCamera() {
if (mCamera != null && mCameraReady) {
Log.v(TAG, "Releasing camera facing " + mCurrentFacing);
mCamera.release();
mCamera = null;
mParameters = null;
mPreview.notifyCameraChanged(false);
mCameraReady = true;
}
}
public void reconnectToCamera() {
if (mCameraReady) {
open(mCurrentFacing);
} else {
Log.e(TAG, "reconnectToCamera but camera not ready!");
}
}
public void setVideoSize(int width, int height){
Log.v(TAG, "setVideoSize " + width + "x" + height);
Camera.Parameters params = getParameters();
params.set("video-size", "" + width +"x" + height);
// TODO: maybe need to set picture-size here too for
// video snapshots
List sizes = params.getSupportedPreviewSizes();
// TODO: support of preferred preview size
// this is currently breaking camera if preview
// size != video-size
Camera.Size preferred = params.getPreferredPreviewSizeForVideo();
if (preferred == null) {
preferred = sizes.get(0);
}
Camera.Size optimalPreview = null;
int product = preferred.width * preferred.height;
Iterator it = sizes.iterator();
// Remove the preview sizes that are not preferred.
while (it.hasNext()) {
Camera.Size size = it.next();
if (size.width * size.height > product) {
it.remove();
continue;
}
// TODO: workaround for now to choose same size then video
if (size.width == width && size.height == height){
optimalPreview = size;
break;
}
}
if (optimalPreview == null){
// TODO: support of preview size different to video size
// right now this is crashing e.g. oppo has an preferred
// video preview of 1920x1080
optimalPreview = Util.getOptimalPreviewSize((Activity) mContext, sizes,
(double) width / height);
}
setPreviewSize(optimalPreview.width, optimalPreview.height);
}
public void setPreviewSize(int width, int height) {
mTargetSize = new Point(width, height);
if (mCamera != null) {
Camera.Parameters params = getParameters();
params.setPreviewSize(width, height);
Log.v(TAG, "Preview size is " + width + "x" + height);
if (!mIsModeSwitching) {
synchronized (mParametersSync) {
try {
safeStopPreview();
mParameters = params;
mCamera.setParameters(mParameters);
// TODO: preview aspect ratio is wrong in video mode
mPreview.notifyPreviewSize(width, height);
// TODO: why dont restart preview here?
// setPreviewSize is called on video mode switching too
//if (mIsResuming) {
updateDisplayOrientation();
safeStartPreview();
//mIsResuming = false;
//}
mPreview.setPauseCopyFrame(false);
} catch (RuntimeException ex) {
Log.e(TAG, "Unable to set Preview Size", ex);
}
}
Log.d(TAG, "setPreviewSize - stop");
}
}
}
private void safeStartPreview() {
if (!mIsPreviewStarted && mCamera != null) {
Log.d(TAG, "safeStartPreview");
mCamera.startPreview();
mIsPreviewStarted = true;
}
}
private void safeStopPreview() {
if (mIsPreviewStarted && mCamera != null) {
Log.d(TAG, "safeStopPreview");
mCamera.stopPreview();
mIsPreviewStarted = false;
}
}
public void startParametersBatch() {
mParametersBatch = true;
}
public void stopParametersBatch() {
mParametersBatch = false;
if (mParametersThread == null) return;
synchronized (mParametersThread) {
mParametersThread.notifyAll();
}
}
public void setParameterAsync(String key, String value) {
if (mParametersThread == null) return;
synchronized (mParametersThread) {
mPendingParameters.add(new BasicNameValuePair(key, value));
if (!mParametersBatch) {
mParametersThread.notifyAll();
}
}
}
/**
* Sets a parameters class in a synchronous way. Use with caution, prefer setParameterAsync.
* @param params Parameters
*/
public void setParameters(Camera.Parameters params) {
synchronized (mParametersSync) {
mCamera.setParameters(params);
}
}
/**
* Locks the automatic settings of the camera device, like White balance and
* exposure.
*
* @param lock true to lock, false to unlock
*/
public void setLockSetup(boolean lock) {
final Camera.Parameters params = getParameters();
if (params == null) {
// Params might be null if we pressed or swipe the shutter button
// while the camera is not ready
return;
}
if (params.isAutoExposureLockSupported()) {
params.setAutoExposureLock(lock);
}
if (params.isAutoWhiteBalanceLockSupported()) {
params.setAutoWhiteBalanceLock(lock);
}
new Thread() {
public void run() {
synchronized (mParametersSync) {
try {
mCamera.setParameters(params);
} catch (RuntimeException e) {
// Do nothing here
}
}
}
}.start();
}
/**
* Returns the last frame of the preview surface
*
* @return Bitmap
*/
public Bitmap getLastPreviewFrame() {
// Decode the last frame bytes
byte[] data = mPreview.getLastFrameBytes();
Camera.Parameters params = getParameters();
if (params == null) {
return null;
}
Camera.Size previewSize = params.getPreviewSize();
if (previewSize == null) {
return null;
}
int previewWidth = previewSize.width;
int previewHeight = previewSize.height;
// Convert YUV420SP preview data to RGB
try {
if (data != null && data.length > 8) {
Bitmap bitmap = Util.decodeYUV420SP(mContext, data, previewWidth, previewHeight);
if (mCurrentFacing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
// Frontcam has the image flipped, flip it back to not look weird in portrait
Matrix m = new Matrix();
m.preScale(-1, 1);
Bitmap dst = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
bitmap.getHeight(), m, false);
bitmap.recycle();
bitmap = dst;
}
return bitmap;
} else {
return null;
}
} catch (ArrayIndexOutOfBoundsException e) {
// TODO: FIXME: On some devices, the resolution of the preview might abruptly change,
// thus the YUV420SP data is not the size we expect, causing OOB exception
return null;
}
}
public Context getContext() {
return mContext;
}
/**
* Defines a new size for the recorded picture
* @param sz The size string in format widthxheight
*/
public void setPictureSize(String sz) {
String[] splat = sz.split("x");
int width = Integer.parseInt(splat[0]);
int height = Integer.parseInt(splat[1]);
Log.v(TAG, "setPictureSize " + width + "x" + height);
Camera.Parameters params = getParameters();
params.setPictureSize(width, height);
// set optimal preview - needs preview restart
Camera.Size optimalPreview = Util.getOptimalPreviewSize(mContext, params.getSupportedPreviewSizes(),
((float) width / (float) height));
setPreviewSize(optimalPreview.width, optimalPreview.height);
}
/**
* Takes a snapshot
*/
public void takeSnapshot(final Camera.ShutterCallback shutterCallback,
final Camera.PictureCallback raw, final Camera.PictureCallback jpeg) {
Log.v(TAG, "takePicture");
if (Util.deviceNeedsStopPreviewToShoot()) {
safeStopPreview();
}
SoundManager.getSingleton().play(SoundManager.SOUND_SHUTTER);
if (mCamera != null) {
new Thread() {
public void run() {
try {
mCamera.takePicture(shutterCallback, raw, jpeg);
} catch (RuntimeException e) {
Log.e(TAG, "Unable to take picture", e);
CameraActivity.notify("Unable to take picture", 1000);
}
}
}.start();
}
}
/**
* Prepares the MediaRecorder to record a video. This must be called before
* startVideoRecording to setup the recording environment.
*
* @param fileName Target file path
* @param profile Target profile (quality)
*/
public void prepareVideoRecording(String fileName, CamcorderProfile profile) {
// Unlock the camera for use with MediaRecorder
mCamera.unlock();
mMediaRecorder.setCamera(mCamera);
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
mMediaRecorder.setProfile(profile);
mMediaRecorder.setOutputFile(fileName);
// Set maximum file size.
long maxFileSize = Storage.getStorage().getAvailableSpace()
- Storage.LOW_STORAGE_THRESHOLD;
mMediaRecorder.setMaxFileSize(maxFileSize);
mMediaRecorder.setMaxDuration(0); // infinite
try {
mMediaRecorder.prepare();
} catch (IllegalStateException e) {
Log.e(TAG, "Cannot prepare MediaRecorder", e);
} catch (IOException e) {
Log.e(TAG, "Cannot prepare MediaRecorder", e);
}
mPreview.postCallbackBuffer();
}
public void startVideoRecording() {
Log.v(TAG, "startVideoRecording");
try {
mMediaRecorder.start();
} catch (Exception e) {
Log.e(TAG, "Unable to start recording", e);
CameraActivity.notify("Error while starting recording", 1000);
}
mPreview.postCallbackBuffer();
}
public void stopVideoRecording() {
Log.v(TAG, "stopVideoRecording");
try {
mMediaRecorder.stop();
} catch (Exception e) {
Log.e(TAG, "Cannot stop MediaRecorder", e);
}
mCamera.lock();
mMediaRecorder.reset();
mPreview.postCallbackBuffer();
}
/**
* @return The orientation of the device
*/
public int getOrientation() {
return mOrientation;
}
/**
* Sets the current orientation of the device
* @param orientation The orientation, in degrees
*/
public void setOrientation(int orientation) {
orientation += 90;
if (mOrientation == orientation) return;
mOrientation = orientation;
// Rotate the pictures accordingly (display is kept at 90 degrees)
Camera.CameraInfo info =
new android.hardware.Camera.CameraInfo();
Camera.getCameraInfo(mCurrentFacing, info);
//orientation = (360 - orientation + 45) / 90 * 90;
int rotation = 0;
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
rotation = (info.orientation - orientation + 360) % 360;
} else { // back-facing camera
rotation = (info.orientation + orientation) % 360;
}
//setParameterAsync("rotation", Integer.toString(rotation));
}
public void restartPreviewIfNeeded() {
new Thread() {
public void run() {
synchronized (mParametersSync) {
try {
// Normally, we should use safeStartPreview everywhere. However, some
// cameras implicitly stops preview, and we don't know. So we just force
// it here.
mCamera.startPreview();
mPreview.setPauseCopyFrame(false);
} catch (Exception e) {
// ignore
}
mIsPreviewStarted = true;
}
}
}.start();
}
public void setCameraMode(final int mode) {
if (mPreviewPauseListener != null) {
mPreviewPauseListener.onPreviewPause();
}
// Unlock any exposure/stab lock that was caused by
// swiping the ring
setLockSetup(false);
new Thread() {
public void run() {
synchronized (mParametersSync) {
Log.d(TAG, "setCameraMode -- start " + mode);
mIsModeSwitching = true;
Camera.Parameters params = getParameters();
if (params == null) {
// We're likely in the middle of a transient state.
// Just do that again shortly when the camera will
// be available.
return;
}
boolean shouldStartPreview = false;
if (mode == CameraActivity.CAMERA_MODE_VIDEO) {
if (!mIsRecordingHint) {
params.setRecordingHint(true);
mIsRecordingHint = true;
safeStopPreview();
shouldStartPreview = true;
}
} else {
if (mIsRecordingHint) {
params.setRecordingHint(false);
mIsRecordingHint = false;
safeStopPreview();
shouldStartPreview = true;
}
}
if (mode == CameraActivity.CAMERA_MODE_PANO) {
// Apply special settings for panorama mode
initializePanoramaMode();
} else {
// Make sure the Infinity mode from panorama is gone
params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
}
if (mode == CameraActivity.CAMERA_MODE_PICSPHERE) {
// If we are in PicSphere mode, we look for a correct 4:3 resolution. We
// default the preview size to 640x480 however, as we don't need anything
// bigger than that. We prefer to have a smaller resolution in case our
// recommended resolution isn't available, as it will be faster to render.
Point size = Util.findBestPicSpherePictureSize(params.getSupportedPictureSizes(), true);
params.setPictureSize(size.x, size.y);
params.setPreviewSize(640, 480);
// Set focus mode to infinity
setInfinityFocus(params);
} else {
setPreviewSize(mTargetSize.x, mTargetSize.y);
}
try {
mCamera.setParameters(params);
} catch (Exception e) {
Log.e(TAG, "Unable to set parameters", e);
}
mParameters = mCamera.getParameters();
if (shouldStartPreview) {
updateDisplayOrientation();
safeStartPreview();
}
}
mPreview.setPauseCopyFrame(false);
mIsModeSwitching = false;
if (mPreviewPauseListener != null) {
mPreviewPauseListener.onPreviewResume();
}
}
}.start();
}
/**
* Updates the orientation of the display
*/
public void updateDisplayOrientation() {
android.hardware.Camera.CameraInfo info =
new android.hardware.Camera.CameraInfo();
android.hardware.Camera.getCameraInfo(mCurrentFacing, info);
int degrees = 0; //Util.getDisplayRotation(null);
int result;
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
result = (info.orientation + degrees) % 360;
result = (360 - result) % 360; // compensate the mirror
} else { // back-facing
result = (info.orientation - degrees + 360) % 360;
}
mCamera.setDisplayOrientation(result);
}
/**
* Initializes the Panorama (mosaic) mode
*/
private void initializePanoramaMode() {
Camera.Parameters parameters = getParameters();
int pixels = mContext.getResources().getInteger(R.integer.config_panoramaDefaultWidth)
* mContext.getResources().getInteger(R.integer.config_panoramaDefaultHeight);
List supportedSizes = parameters.getSupportedPreviewSizes();
Point previewSize = Util.findBestPanoPreviewSize(supportedSizes, false, false, pixels);
Log.v(TAG, "preview h = " + previewSize.y + " , w = " + previewSize.x);
parameters.setPreviewSize(previewSize.x, previewSize.y);
mTargetSize = previewSize;
List frameRates = parameters.getSupportedPreviewFpsRange();
if (frameRates != null) {
int last = frameRates.size() - 1;
int minFps = (frameRates.get(last))[Camera.Parameters.PREVIEW_FPS_MIN_INDEX];
int maxFps = (frameRates.get(last))[Camera.Parameters.PREVIEW_FPS_MAX_INDEX];
parameters.setPreviewFpsRange(minFps, maxFps);
Log.v(TAG, "preview fps: " + minFps + ", " + maxFps);
}
setInfinityFocus(parameters);
parameters.setRecordingHint(false);
mParameters = parameters;
}
private void setInfinityFocus(Camera.Parameters parameters) {
List supportedFocusModes = parameters.getSupportedFocusModes();
if (supportedFocusModes != null
&& supportedFocusModes.indexOf(Camera.Parameters.FOCUS_MODE_INFINITY) >= 0) {
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_INFINITY);
} else {
// Use the default focus mode and log a message
Log.w(TAG, "Cannot set the focus mode to "
+ Camera.Parameters.FOCUS_MODE_INFINITY
+ " because the mode is not supported.");
}
}
/**
* Trigger the autofocus function of the device
*
* @param cb The AF callback
* @return true if we could start the AF, false otherwise
*/
public boolean doAutofocus(final AutoFocusCallback cb) {
if (mCamera != null) {
try {
// Make sure our auto settings aren't locked
setLockSetup(false);
// Trigger af
mCamera.cancelAutoFocus();
mHandler.post(new Runnable() {
public void run() {
try {
mCamera.autoFocus(cb);
} catch (Exception e) {
// Do nothing here
}
}
});
} catch (Exception e) {
return false;
}
return true;
} else {
return false;
}
}
/**
* Returns whether or not the current open camera device supports
* focus area (focus ring)
*
* @return true if supported
*/
public boolean isFocusAreaSupported() {
if (mCamera != null) {
try {
return (getParameters().getMaxNumFocusAreas() > 0);
} catch (Exception e) {
return false;
}
} else {
return false;
}
}
/**
* Returns whether or not the current open camera device supports
* exposure metering area (exposure ring)
*
* @return true if supported
*/
public boolean isExposureAreaSupported() {
if (mCamera != null) {
try {
return (getParameters().getMaxNumMeteringAreas() > 0);
} catch (Exception e) {
return false;
}
} else {
return false;
}
}
/**
* Defines the main focus point of the camera to the provided x and y values.
* Those values must between -1000 and 1000, where -1000 is the top/left, and 1000 the bottom/right
*
* @param x The X position of the focus point
* @param y The Y position of the focus point
*/
public void setFocusPoint(int x, int y) {
if (x < -1000 || x > 1000 || y < -1000 || y > 1000) {
Log.e(TAG, "setFocusPoint: values are not ideal " + "x= " + x + " y= " + y);
return;
}
Camera.Parameters params = getParameters();
if (params != null && params.getMaxNumFocusAreas() > 0) {
List focusArea = new ArrayList();
focusArea.add(new Camera.Area(new Rect(x, y, x + FOCUS_WIDTH, y + FOCUS_HEIGHT), 1000));
params.setFocusAreas(focusArea);
try {
mCamera.setParameters(params);
} catch (Exception e) {
// Ignore, we might be setting it too
// fast since previous attempt
}
}
}
/**
* Defines the exposure metering point of the camera to the provided x and y values.
* Those values must between -1000 and 1000, where -1000 is the top/left, and 1000 the bottom/right
*
* @param x The X position of the exposure metering point
* @param y The Y position of the exposure metering point
*/
public void setExposurePoint(int x, int y) {
Camera.Parameters params = getParameters();
if (params != null && params.getMaxNumMeteringAreas() > 0) {
List exposureArea = new ArrayList();
exposureArea.add(new Camera.Area(new Rect(x, y, x + FOCUS_WIDTH, y + FOCUS_HEIGHT), 1000));
params.setMeteringAreas(exposureArea);
try {
mCamera.setParameters(params);
} catch (Exception e) {
// Ignore, we might be setting it too
// fast since previous attempt
}
}
}
public void setAutoFocusMoveCallback(AutoFocusMoveCallback cb) {
mAutoFocusMoveCallback = cb;
List focusModes = mParameters.getSupportedFocusModes();
if (mCamera != null && focusModes != null && focusModes.contains(
Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
try {
mCamera.setAutoFocusMoveCallback(cb);
} catch (RuntimeException e) {
Log.e(TAG, "Unable to set AutoFocusMoveCallback", e);
}
}
}
/**
* Enable the device image stabilization system.
* @param enabled True to stabilize
*/
public void setStabilization(boolean enabled) {
Camera.Parameters params = getParameters();
if (params == null) {
return;
}
if (CameraActivity.getCameraMode() == CameraActivity.CAMERA_MODE_PHOTO) {
// In wrappers: sony has sony-is, HTC has ois_mode, etc.
if (params.get("image-stabilization") != null) {
params.set("image-stabilization", enabled ? "on" : "off");
}
} else if (CameraActivity.getCameraMode() == CameraActivity.CAMERA_MODE_VIDEO) {
if (params.isVideoStabilizationSupported()) {
params.setVideoStabilization(enabled);
}
}
try {
mCamera.setParameters(params);
} catch (Exception e) {
// Do nothing here
}
}
/**
* Sets the camera device to render to a texture rather than a SurfaceHolder
* @param texture The texture to which render
*/
public void setRenderToTexture(SurfaceTexture texture) {
mPreview.setRenderToTexture(texture);
Log.i(TAG, "Needs to render to texture, rebooting preview");
mPreview.notifyCameraChanged(true);
}
/**
* The CameraPreview class handles the Camera preview feed
* and setting the surface holder.
*/
public class CameraPreview implements Camera.PreviewCallback {
private final static String TAG = "CameraManager.CameraPreview";
private SurfaceTexture mTexture;
private byte[] mLastFrameBytes;
private boolean mPauseCopyFrame;
public CameraPreview() {
}
/**
* Sets that the camera preview should rather render to a texture than the default
* SurfaceHolder.
* Note that you have to restart camera preview manually after setting this.
* Set to null to reset render to the SurfaceHolder.
*
* @param texture The target texture
*/
public void setRenderToTexture(SurfaceTexture texture) {
mTexture = texture;
}
public void setPauseCopyFrame(boolean pause) {
mPauseCopyFrame = pause;
if (!pause && mCamera != null) {
postCallbackBuffer();
}
}
public void notifyPreviewSize(int width, int height) {
mLastFrameBytes = new byte[2048000];
// Update preview aspect ratio
mRenderer.updateRatio(width, height);
}
public byte[] getLastFrameBytes() {
return mLastFrameBytes;
}
public void notifyCameraChanged(boolean startPreview) {
synchronized (mParametersSync) {
if (mCamera != null) {
if (startPreview) {
safeStopPreview();
}
setupCamera();
try {
mCamera.setPreviewTexture(mTexture);
if (startPreview) {
updateDisplayOrientation();
safeStartPreview();
postCallbackBuffer();
}
} catch (RuntimeException e) {
Log.e(TAG, "Cannot set preview texture", e);
} catch (IOException e) {
Log.e(TAG, "Error setting camera preview", e);
}
}
}
}
public void restartPreview() {
synchronized (mParametersSync) {
if (mCamera != null) {
try {
safeStopPreview();
mCamera.setParameters(mParameters);
updateDisplayOrientation();
safeStartPreview();
setPauseCopyFrame(false);
} catch (RuntimeException e) {
Log.e(TAG, "Cannot set preview texture", e);
}
}
}
}
public void postCallbackBuffer() {
if (mCamera != null && !mPauseCopyFrame) {
mCamera.addCallbackBuffer(mLastFrameBytes);
mCamera.setPreviewCallbackWithBuffer(CameraPreview.this);
}
}
private void setupCamera() {
// Set device-specifics here
try {
Resources res = mContext.getResources();
if (res != null) {
if (res.getBoolean(R.bool.config_qualcommZslCameraMode) &&
!Util.deviceNeedsDisableZSL()) {
if (res.getBoolean(R.bool.config_useSamsungZSL)) {
//mCamera.sendRawCommand(1508, 0, 0);
}
mParameters.set("camera-mode", 1);
}
}
mCamera.setDisplayOrientation(90);
mCamera.setParameters(mParameters);
postCallbackBuffer();
} catch (Exception e) {
Log.e(TAG, "Could not set device specifics");
}
}
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
if (mCamera != null && !mPauseCopyFrame) {
mCamera.addCallbackBuffer(mLastFrameBytes);
}
}
}
public class CameraRenderer implements GLSurfaceView.Renderer {
private final float[] mTransformMatrix;
int mTexture;
private SurfaceTexture mSurface;
private final static String VERTEX_SHADER =
"attribute vec4 vPosition;\n" +
"attribute vec2 a_texCoord;\n" +
"varying vec2 v_texCoord;\n" +
"uniform mat4 u_xform;\n" +
"void main() {\n" +
" gl_Position = vPosition;\n" +
" v_texCoord = vec2(u_xform * vec4(a_texCoord, 1.0, 1.0));\n" +
"}\n";
private final static String FRAGMENT_SHADER =
"#extension GL_OES_EGL_image_external : require\n" +
"precision mediump float;\n" +
"uniform samplerExternalOES s_texture;\n" +
"varying vec2 v_texCoord;\n" +
"void main() {\n" +
" gl_FragColor = texture2D(s_texture, v_texCoord);\n" +
"}\n";
private FloatBuffer mVertexBuffer, mTextureVerticesBuffer;
private int mProgram;
private int mPositionHandle;
private int mTextureCoordHandle;
private int mTransformHandle;
private int mWidth;
private int mNaturalWidth;
private int mHeight;
private int mNaturalHeight;
private float mNaturalRatio;
private float mRatio = 4.0f/3.0f;
private float mUpdateRatioTo = -1;
private Object fSync = new Object();
// Number of coordinates per vertex in this array
static final int COORDS_PER_VERTEX = 2;
private float mSquareVertices[];
private final float mTextureVertices[] =
{ 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f };
public CameraRenderer() {
mTransformMatrix = new float[16];
}
public void updateRatio(int width, int height) {
synchronized(fSync){
mUpdateRatioTo = 1;
float ratio = (float) width/(float) height;
if (ratio != mNaturalRatio){
float widthRatio = (float) mNaturalWidth/(float) width;
float heightRatio = (float) mNaturalHeight/(float) height;
mRatio = widthRatio / heightRatio;
} else {
mRatio = 1;
}
Log.d(TAG, "updateRatio " + width+"x"+height + " mRatio="+mRatio);
}
}
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
mTexture = createTexture();
mSurface = new SurfaceTexture(mTexture);
GLES20.glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
setRenderToTexture(mSurface);
ByteBuffer bb2 = ByteBuffer.allocateDirect(mTextureVertices.length * 4);
bb2.order(ByteOrder.nativeOrder());
mTextureVerticesBuffer = bb2.asFloatBuffer();
mTextureVerticesBuffer.put(mTextureVertices);
mTextureVerticesBuffer.position(0);
// Load shaders
int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, VERTEX_SHADER);
int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER);
mProgram = GLES20.glCreateProgram(); // create empty OpenGL ES Program
GLES20.glAttachShader(mProgram, vertexShader); // add the vertex shader to program
GLES20.glAttachShader(mProgram, fragmentShader); // add the fragment shader to program
GLES20.glLinkProgram(mProgram);
// Since we only use one program/texture/vertex array, we bind them once here
// and then we only draw what we need in onDrawFrame
GLES20.glUseProgram(mProgram);
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTexture);
// Setup vertex buffer. Use a default 4:3 ratio, this will get updated once we have
// a preview aspect ratio.
// Regenerate vertexes
mSquareVertices = new float[]{ 1.0f * 1.0f,
1.0f, -1.0f,
1.0f, -1.0f,
-1.0f,
1.0f * 1.0f, -1.0f };
ByteBuffer bb = ByteBuffer.allocateDirect(mSquareVertices.length * 4);
bb.order(ByteOrder.nativeOrder());
mVertexBuffer = bb.asFloatBuffer();
mVertexBuffer.put(mSquareVertices);
mVertexBuffer.position(0);
mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT,
false, 0, mVertexBuffer);
GLES20.glEnableVertexAttribArray(mPositionHandle);
mTextureCoordHandle = GLES20.glGetAttribLocation(mProgram, "a_texCoord");
GLES20.glVertexAttribPointer(mTextureCoordHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT,
false, 0, mTextureVerticesBuffer);
GLES20.glEnableVertexAttribArray(mTextureCoordHandle);
mTransformHandle = GLES20.glGetUniformLocation(mProgram, "u_xform");
}
public void onDrawFrame(GL10 unused) {
synchronized(fSync){
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
if (mUpdateRatioTo > 0) {
Log.d(TAG, "onDrawFrame " + " mRatio="+mRatio);
int deltaWidth = (int) Math.abs(mWidth - mWidth * mRatio);
GLES20.glViewport(-deltaWidth / 2, 0,
(int) (mWidth * mRatio + deltaWidth / 2.0f), mHeight);
mUpdateRatioTo = -1;
}
if (mSurface != null) {
mSurface.updateTexImage();
mSurface.getTransformMatrix(mTransformMatrix);
GLES20.glUniformMatrix4fv(mTransformHandle, 1, false, mTransformMatrix, 0);
}
if (mUpdateRatioTo > 0) {
// TODO mUpdateRatioTo would be the new ratio but still mRatio is used here
GLES20.glViewport(0, 0, (int) (mWidth*mRatio), mHeight);
mUpdateRatioTo = -1;
}
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, 4);
}
}
public void onSurfaceChanged(GL10 unused, int width, int height) {
mWidth = width;
mHeight = height;
if (mWidth > mHeight){
mNaturalWidth = mWidth;
mNaturalHeight = mHeight;
} else {
mNaturalWidth = mHeight;
mNaturalHeight = mWidth;
}
mNaturalRatio = (float) mNaturalWidth/(float) mNaturalHeight;
mRatio = mNaturalRatio;
GLES20.glViewport(0, 0, width, height);
Log.d(TAG, "onSurfaceChanged " + width+"x"+height + " mNaturalRatio="+mNaturalRatio);
}
public int loadShader(int type, String shaderCode) {
int shader = GLES20.glCreateShader(type);
GLES20.glShaderSource(shader, shaderCode);
GLES20.glCompileShader(shader);
return shader;
}
private int createTexture() {
int[] texture = new int[1];
GLES20.glGenTextures(1,texture, 0);
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texture[0]);
return texture[0];
}
}
// TODO: just added for debugging
public static void dumpParameter(Camera.Parameters parameters) {
String flattened = parameters.flatten();
StringTokenizer tokenizer = new StringTokenizer(flattened, ";");
Log.d(TAG, "Dump all camera parameters:");
while (tokenizer.hasMoreElements()) {
Log.d(TAG, tokenizer.nextToken());
}
}
}
================================================
FILE: src/org/cyanogenmod/focal/DisableCameraReceiver.java
================================================
/*
* Copyright (C) 2012 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 org.cyanogenmod.focal;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.hardware.Camera.CameraInfo;
import android.util.Log;
// We want to disable camera-related activities if there is no camera. This
// receiver runs when BOOT_COMPLETED intent is received. After running once
// this receiver will be disabled, so it will not run again.
public class DisableCameraReceiver extends BroadcastReceiver {
private static final String TAG = "FocalDisableCameraReceiver";
private static final String ACTIVITIES[] = {
"org.cyanogenmod.focal.CameraActivity",
};
@Override
public void onReceive(Context context, Intent intent) {
// Disable camera-related activities if there is no camera.
boolean needCameraActivity = (android.hardware.Camera.getNumberOfCameras() > 0);
if (!needCameraActivity) {
Log.i(TAG, "no camera detected: disable all focal activities");
for (int i = 0; i < ACTIVITIES.length; i++) {
disableComponent(context, ACTIVITIES[i]);
}
}
// Disable this receiver so it won't run again.
disableComponent(context, "org.cyanogenmod.focal.DisableCameraReceiver");
}
private void disableComponent(Context context, String klass) {
ComponentName name = new ComponentName(context, klass);
PackageManager pm = context.getPackageManager();
// We need the DONT_KILL_APP flag, otherwise we will be killed
// immediately because we are in the same app.
pm.setComponentEnabledSetting(name,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
}
}
================================================
FILE: src/org/cyanogenmod/focal/Exif.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
* Copyright (C) 2010 The Android Open Source Project
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal;
import android.util.Log;
public class Exif {
private static final String TAG = "CameraExif";
// Returns the degrees in clockwise. Values are 0, 90, 180, or 270.
public static int getOrientation(byte[] jpeg) {
if (jpeg == null) {
return 0;
}
int offset = 0;
int length = 0;
// ISO/IEC 10918-1:1993(E)
while (offset + 3 < jpeg.length && (jpeg[offset++] & 0xFF) == 0xFF) {
int marker = jpeg[offset] & 0xFF;
// Check if the marker is a padding.
if (marker == 0xFF) {
continue;
}
offset++;
// Check if the marker is SOI or TEM.
if (marker == 0xD8 || marker == 0x01) {
continue;
}
// Check if the marker is EOI or SOS.
if (marker == 0xD9 || marker == 0xDA) {
break;
}
// Get the length and check if it is reasonable.
length = pack(jpeg, offset, 2, false);
if (length < 2 || offset + length > jpeg.length) {
Log.e(TAG, "Invalid length");
return 0;
}
// Break if the marker is EXIF in APP1.
if (marker == 0xE1 && length >= 8 &&
pack(jpeg, offset + 2, 4, false) == 0x45786966 &&
pack(jpeg, offset + 6, 2, false) == 0) {
offset += 8;
length -= 8;
break;
}
// Skip other markers.
offset += length;
length = 0;
}
// JEITA CP-3451 Exif Version 2.2
if (length > 8) {
// Identify the byte order.
int tag = pack(jpeg, offset, 4, false);
if (tag != 0x49492A00 && tag != 0x4D4D002A) {
Log.e(TAG, "Invalid byte order");
return 0;
}
boolean littleEndian = (tag == 0x49492A00);
// Get the offset and check if it is reasonable.
int count = pack(jpeg, offset + 4, 4, littleEndian) + 2;
if (count < 10 || count > length) {
Log.e(TAG, "Invalid offset");
return 0;
}
offset += count;
length -= count;
// Get the count and go through all the elements.
count = pack(jpeg, offset - 2, 2, littleEndian);
while (count-- > 0 && length >= 12) {
// Get the tag and check if it is orientation.
tag = pack(jpeg, offset, 2, littleEndian);
if (tag == 0x0112) {
// We do not really care about type and count, do we?
int orientation = pack(jpeg, offset + 8, 2, littleEndian);
switch (orientation) {
case 1:
return 0;
case 3:
return 180;
case 6:
return 90;
case 8:
return 270;
}
Log.i(TAG, "Unsupported orientation");
return 0;
}
offset += 12;
length -= 12;
}
}
Log.i(TAG, "Orientation not found");
return 0;
}
private static int pack(byte[] bytes, int offset, int length,
boolean littleEndian) {
int step = 1;
if (littleEndian) {
offset += length - 1;
step = -1;
}
int value = 0;
while (length-- > 0) {
value = (value << 8) | (bytes[offset] & 0xFF);
offset += step;
}
return value;
}
}
================================================
FILE: src/org/cyanogenmod/focal/FocusManager.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal;
import android.hardware.Camera;
import android.hardware.Camera.AutoFocusCallback;
import android.hardware.Camera.AutoFocusMoveCallback;
import android.os.Handler;
public class FocusManager implements AutoFocusCallback, AutoFocusMoveCallback {
public final static String TAG = "FocusManager";
public interface FocusListener {
/**
* This method is called when the focus operation starts (whether through touch to focus,
* or via continuous-picture focus mode).
*
* @param smallAdjust If this parameter is set to true, the focus is being done because of
* continuous focus mode and thus only make a small adjustment
*/
public void onFocusStart(boolean smallAdjust);
/**
* This method is called when the focus operation ends
*
* @param smallAdjust If this parameter is set to true, the focus is being done because of
* continuous focus mode and made only a small adjustment
* @param success If the focus operation was successful. Note that if smallAdjust is true,
* this parameter will always be false.
*/
public void onFocusReturns(boolean smallAdjust, boolean success);
}
// Miliseconds during which we assume the focus is good
private int mFocusKeepTimeMs = 3000;
private long mLastFocusTimestamp = 0;
private Handler mHandler;
private CameraManager mCamManager;
private FocusListener mListener;
private boolean mIsFocusing;
public FocusManager(CameraManager cam) {
mHandler = new Handler();
mCamManager = cam;
mIsFocusing = false;
mCamManager.setAutoFocusMoveCallback(this);
Camera.Parameters params = mCamManager.getParameters();
if (params.getSupportedFocusModes().contains("auto")) {
params.setFocusMode("auto");
}
// Do a first focus after 1 second
mHandler.postDelayed(new Runnable() {
public void run() {
checkFocus();
}
}, 1000);
}
public void setListener(FocusListener listener) {
mListener = listener;
}
public void checkFocus() {
long time = System.currentTimeMillis();
if (time - mLastFocusTimestamp > mFocusKeepTimeMs && !mIsFocusing) {
refocus();
} else if (time - mLastFocusTimestamp > mFocusKeepTimeMs * 2) {
// Force a refocus after 2 times focus failed
refocus();
}
}
/**
* Sets the time during which the focus is considered valid
* before refocusing
*
* @param timeMs Time in miliseconds
*/
public void setFocusKeepTime(int timeMs) {
mFocusKeepTimeMs = timeMs;
}
public void refocus() {
if (mCamManager.doAutofocus(this)) {
mIsFocusing = true;
if (mListener != null) {
mListener.onFocusStart(false);
}
}
}
@Override
public void onAutoFocus(boolean success, Camera cam) {
mLastFocusTimestamp = System.currentTimeMillis();
mIsFocusing = false;
if (mListener != null) {
mListener.onFocusReturns(false, success);
}
}
@Override
public void onAutoFocusMoving(boolean start, Camera cam) {
if (mIsFocusing && !start) {
// We were focusing, but we stopped, notify time of last focus
mLastFocusTimestamp = System.currentTimeMillis();
}
if (start) {
if (mListener != null) {
mListener.onFocusStart(true);
}
} else {
if (mListener != null) {
mListener.onFocusReturns(true, false);
}
}
mIsFocusing = start;
}
}
================================================
FILE: src/org/cyanogenmod/focal/PopenHelper.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal;
/**
* Helper class calling a JNI native popen method to run system
* commands
*/
public class PopenHelper {
static {
System.loadLibrary("xmptoolkit");
System.loadLibrary("popen_helper_jni");
}
/**
* Runs the specified command-line program in the command parameter.
* Note that this command is ran with the shell interpreter (/system/bin/sh), thus
* a shell script or a bunch of commands can be called in one method call.
* The output of the commands (stdout/stderr) is sent out to the logcat with the
* INFORMATION level.
*
* @params command The command to run
* @return The call return code
*/
public static native int run(String command);
}
================================================
FILE: src/org/cyanogenmod/focal/Profiler.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal;
import android.os.SystemClock;
import android.util.Log;
import java.util.HashMap;
/**
* Simple profiler to help debug app speed
*/
public class Profiler {
private static final String TAG = "Profiler";
private static Profiler sDefault = null;
private HashMap mTimestamps;
public static Profiler getDefault() {
if (sDefault == null) sDefault = new Profiler();
return sDefault;
}
private Profiler() {
mTimestamps = new HashMap();
}
public void start(String name) {
mTimestamps.put(name, SystemClock.uptimeMillis());
}
public void logProfile(String name) {
long time = mTimestamps.get(name);
long delta = SystemClock.uptimeMillis() - time;
Log.d(TAG, "Time for '" + name + "': " + delta + "ms");
}
}
================================================
FILE: src/org/cyanogenmod/focal/SettingsStorage.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal;
import android.content.Context;
import android.content.SharedPreferences;
/**
* Manages the storage of all the settings
*/
public class SettingsStorage {
private final static String PREFS_CAMERA = "nemesis-camera";
private final static String PREFS_APP = "nemesis-app";
private final static String PREFS_VISIBILITY = "nemesis-visibility";
private final static String PREFS_SHORTCUTS = "nemesis-shortcuts";
public final static String TAG = "SettingsStorage";
private static void store(Context context, String prefsName, String key, String value) {
SharedPreferences prefs = context.getSharedPreferences(prefsName, 0);
SharedPreferences.Editor editor = prefs.edit();
editor.putString(key, value);
editor.commit();
}
private static String retrieve(Context context, String prefsName, String key, String def) {
SharedPreferences prefs = context.getSharedPreferences(prefsName, 0);
return prefs.getString(key, def);
}
/**
* Stores a setting of the camera parameters array
* @param context
* @param key
* @param value
*/
public static void storeCameraSetting(Context context, int facing, String key, String value) {
store(context, PREFS_CAMERA, Integer.toString(facing) + ":" + key, value);
}
/**
* Returns a setting of the camera
* @param context
* @param key
* @param def Default if key doesn't exist
* @return
*/
public static String getCameraSetting(Context context, int facing, String key, String def) {
return retrieve(context, PREFS_CAMERA, Integer.toString(facing) + ":" + key, def);
}
/**
* Stores a setting of the camera parameters array
* @param context
* @param key
* @param value
*/
public static void storeAppSetting(Context context, String key, String value) {
store(context, PREFS_APP, key, value);
}
/**
* Returns a setting of the app
* @param context
* @param key
* @param def Default if key doesn't exist
* @return
*/
public static String getAppSetting(Context context, String key, String def) {
return retrieve(context, PREFS_APP, key, def);
}
/**
* Stores a setting of the widgets visibility
* @param context
* @param key
* @param visible
*/
public static void storeVisibilitySetting(Context context, String key, boolean visible) {
store(context, PREFS_VISIBILITY, key, visible ? "true" : "false");
}
/**
* Returns a setting of the app (always defaults to visible)
* @param context
* @param key
* @return
*/
public static boolean getVisibilitySetting(Context context, String key) {
return retrieve(context, PREFS_VISIBILITY, key, "true").equals("true");
}
/**
* Stores a setting of the widgets shortcut
* @param context
* @param key
* @param pinned
*/
public static void storeShortcutSetting(Context context, String key, boolean pinned) {
store(context, PREFS_SHORTCUTS, key, pinned ? "true" : "false");
}
/**
* Returns a setting of the widgets shortcut
* @param context
* @param key
* @return
*/
public static boolean getShortcutSetting(Context context, String key) {
return retrieve(context, PREFS_SHORTCUTS, key, "false").equals("true");
}
}
================================================
FILE: src/org/cyanogenmod/focal/SnapshotManager.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.CursorIndexOutOfBoundsException;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.hardware.Camera;
import android.location.Location;
import android.media.CamcorderProfile;
import android.media.ExifInterface;
import android.media.MediaRecorder;
import android.net.Uri;
import android.os.Handler;
import android.os.ParcelFileDescriptor;
import android.os.SystemClock;
import android.provider.MediaStore;
import android.util.Log;
import com.drew.imaging.jpeg.JpegMetadataReader;
import com.drew.imaging.jpeg.JpegProcessingException;
import com.drew.metadata.Directory;
import com.drew.metadata.Metadata;
import com.drew.metadata.Tag;
import org.cyanogenmod.focal.feats.AutoPictureEnhancer;
import org.cyanogenmod.focal.feats.PixelBuffer;
import org.cyanogenmod.focal.widgets.SimpleToggleWidget;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import fr.xplod.focal.R;
/**
* This class manages taking snapshots and videos from Camera
*/
public class SnapshotManager {
public final static String TAG = "SnapshotManager";
private boolean mPaused;
public interface SnapshotListener {
/**
* This callback is called when a snapshot is taken (shutter)
*
* @param info A structure containing information about the snapshot taken
*/
public void onSnapshotShutter(SnapshotInfo info);
/**
* This callback is called when we have a preview for the snapshot
*
* @param info A structure containing information about the snapshot
*/
public void onSnapshotPreview(SnapshotInfo info);
/**
* This callback is called when a snapshot is being processed (auto color enhancement, etc)
*
* @param info A structure containing information about the snapshot
*/
public void onSnapshotProcessing(SnapshotInfo info);
/**
* This callback is called when a snapshot has been saved to the internal memory
*
* @param info A structure containing information about the snapshot
*/
public void onSnapshotSaved(SnapshotInfo info);
/**
* This callback is called when ImageSaver starts a job of saving an image, or
* MediaRecorder is storing a video
* The primary purpose of this method is to show the SavePinger
*/
public void onMediaSavingStart();
/**
* This callback is called when ImageSaver has done its job of saving an image,
* or MediaRecorder is done storing a video
* The primary purpose of this method is to hide the SavePinger
*/
public void onMediaSavingDone();
/**
* This callback is called when a video starts recording
*/
public void onVideoRecordingStart();
/**
* This callback is called when a video stops recording
*/
public void onVideoRecordingStop();
}
public class SnapshotInfo {
// Whether or not the snapshot has to be saved to internal memory
public boolean mSave;
// The URI of the saved shot, if it has been saved, and if the save is done (mSave = true).
// This field will likely be only different than null when onSnapshotSaved is called.
public Uri mUri;
// The exposure compensation value set for the shot, IF a specific
// value was needed
public int mExposureCompensation;
// A bitmap containing a thumbnail of the image
public Bitmap mThumbnail;
// Whether or not to bypass image processing (even if user enabled it)
public boolean mBypassProcessing;
}
private Context mContext;
private CameraManager mCameraManager;
private FocusManager mFocusManager;
private boolean mBypassProcessing;
// Photo-related variables
private boolean mWaitExposureSettle;
private int mResetExposure;
private List mSnapshotsQueue;
private int mCurrentShutterQueueIndex;
private List mListeners;
private Handler mHandler;
private ContentResolver mContentResolver;
private ImageSaver mImageSaver;
private ImageNamer mImageNamer;
private PixelBuffer mOffscreenGL;
private AutoPictureEnhancer mAutoPicEnhancer;
private boolean mImageIsProcessing;
private boolean mDoAutoEnhance;
// Video-related variables
private long mRecordingStartTime;
private boolean mIsRecording;
private VideoNamer mVideoNamer;
private CamcorderProfile mProfile;
// The video file that the hardware camera is about to record into
// (or is recording into.)
private String mVideoFilename;
private ParcelFileDescriptor mVideoFileDescriptor;
// The video file that has already been recorded, and that is being
// examined by the user.
private String mCurrentVideoFilename;
private Uri mCurrentVideoUri;
private ContentValues mCurrentVideoValues;
private Camera.ShutterCallback mShutterCallback = new Camera.ShutterCallback() {
@Override
public void onShutter() {
Log.v(TAG, "onShutter");
Camera.Size s = mCameraManager.getParameters().getPictureSize();
mImageNamer.prepareUri(mContentResolver, System.currentTimeMillis(), s.width, s.height, 0);
// On shutter confirmed, play a small flashing animation
final SnapshotInfo snap = mSnapshotsQueue.get(mCurrentShutterQueueIndex);
for (SnapshotListener listener : mListeners) {
listener.onSnapshotShutter(snap);
}
// If we used Samsung HDR, reset exposure
if (mContext.getResources().getBoolean(R.bool.config_useSamsungHDR) &&
SimpleToggleWidget.isWidgetEnabled(mContext, mCameraManager, "scene-mode", "hdr")) {
mCameraManager.setParameterAsync("exposure-compensation",
Integer.toString(mResetExposure));
}
}
};
private Camera.PictureCallback mJpegPictureCallback = new Camera.PictureCallback() {
@Override
public void onPictureTaken(byte[] jpegData, Camera camera) {
Log.v(TAG, "onPicture: Got JPEG data");
mCameraManager.restartPreviewIfNeeded();
// Calculate the width and the height of the jpeg.
final Camera.Size s = mCameraManager.getParameters().getPictureSize();
int orientation = CameraActivity.getCameraMode() == CameraActivity.CAMERA_MODE_PANO ? 0 :
mCameraManager.getOrientation();
final int width = s.width,
height = s.height;
if (mSnapshotsQueue.size() == 0) {
Log.e(TAG, "DERP! Why is snapshotqueue empty? Two JPEG callbacks!?");
return;
}
final SnapshotInfo snap = mSnapshotsQueue.get(0);
// If we have a Samsung HDR, convert from YUV422 to JPEG first
if (mContext.getResources().getBoolean(R.bool.config_useSamsungHDR) &&
SimpleToggleWidget.isWidgetEnabled(mContext, mCameraManager, "scene-mode", "hdr")) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Bitmap bm = Util.decodeYUV422P(jpegData, s.width, s.height);
// TODO: Replace 90 with real JPEG compression level when we'll have that setting
bm.compress(Bitmap.CompressFormat.JPEG, 90, baos);
jpegData = baos.toByteArray();
}
// Store the jpeg on internal memory if needed
if (snap.mSave) {
final Uri uri = mImageNamer.getUri();
final String title = mImageNamer.getTitle();
snap.mUri = uri;
// If the orientation is somehow negative, avoid the Gallery crashing dumbly
// (see com/android/gallery3d/ui/PhotoView.java line 758 (setTileViewPosition))
while (orientation < 0) {
orientation += 360;
}
orientation = orientation % 360;
final int correctedOrientation = orientation;
final byte[] finalData = jpegData;
if (!snap.mBypassProcessing && mDoAutoEnhance) {
new Thread() {
public void run() {
mImageIsProcessing = true;
for (SnapshotListener listener : mListeners) {
listener.onSnapshotProcessing(snap);
}
// Read EXIF
List tagsList = new ArrayList();
BufferedInputStream is = new BufferedInputStream(
new ByteArrayInputStream(finalData));
try {
Metadata metadata = JpegMetadataReader.readMetadata(is, false);
for (Directory directory : metadata.getDirectories()) {
for (Tag tag : directory.getTags()) {
tagsList.add(tag);
}
}
} catch (JpegProcessingException e) {
Log.e(TAG, "Error processing input JPEG", e);
}
// XXX: PixelBuffer has to be created every time because the GL context
// can only be used from its original thread. It's not very intense, but
// ideally we would be re-using the same thread every time.
try {
mOffscreenGL = new PixelBuffer(mContext, s.width, s.height);
mAutoPicEnhancer = new AutoPictureEnhancer(mContext);
mOffscreenGL.setRenderer(mAutoPicEnhancer);
mAutoPicEnhancer.setTexture(BitmapFactory.decodeByteArray(finalData,
0, finalData.length));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
mOffscreenGL.getBitmap().compress(Bitmap.CompressFormat.JPEG, 90, baos);
if (mImageSaver != null) {
mImageSaver.addImage(baos.toByteArray(), uri, title, null,
width, height, correctedOrientation, tagsList, snap);
} else {
Log.e(TAG, "ImageSaver was null: couldn't save image!");
}
if (mPaused && mImageSaver != null) {
// We were paused, stop the saver now
mImageSaver.finish();
mImageSaver = null;
}
mImageIsProcessing = false;
}
catch (Exception e) {
// The rendering failed, the device might not be compatible for
// whatever reason. We just save the original file.
if (mImageSaver != null) {
mImageSaver.addImage(finalData, uri, title, null,
width, height, correctedOrientation, snap);
}
CameraActivity.notify("Auto-enhance failed: Original shot saved", 2000);
}
catch (OutOfMemoryError e) {
// The rendering failed, the device might not be compatible for
// whatever reason. We just save the original file.
if (mImageSaver != null) {
mImageSaver.addImage(finalData, uri, title, null,
width, height, correctedOrientation, snap);
}
CameraActivity.notify("Error: Out of memory. Original shot saved", 2000);
}
}
}.start();
} else {
// Just save it as is
mImageSaver.addImage(finalData, uri, title, null,
width, height, correctedOrientation, snap);
}
}
// Camera is ready to take another shot, doit
if (mSnapshotsQueue.size() > mCurrentShutterQueueIndex + 1) {
mCurrentShutterQueueIndex++;
mHandler.post(mCaptureRunnable);
}
// We're done with our shot here!
mCurrentShutterQueueIndex--;
mSnapshotsQueue.remove(0);
}
};
private Runnable mCaptureRunnable = new Runnable() {
@Override
public void run() {
if (mWaitExposureSettle) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
mCameraManager.takeSnapshot(mShutterCallback, null, mJpegPictureCallback);
}
};
private Runnable mPreviewCaptureRunnable = new Runnable() {
@Override
public void run() {
Camera.Size s = mCameraManager.getParameters().getPreviewSize();
mImageNamer.prepareUri(mContentResolver, System.currentTimeMillis(),
s.width, s.height, 0);
SnapshotInfo info = new SnapshotInfo();
info.mSave = true;
info.mExposureCompensation = 0;
info.mThumbnail = mCameraManager.getLastPreviewFrame();
for (SnapshotListener listener : mListeners) {
listener.onSnapshotShutter(info);
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
info.mThumbnail.compress(Bitmap.CompressFormat.JPEG, 80, baos);
byte[] jpegData = baos.toByteArray();
// Calculate the width and the height of the jpeg.
int orientation = Exif.getOrientation(jpegData) - mCameraManager.getOrientation();
int width, height;
width = s.width;
height = s.height;
Uri uri = mImageNamer.getUri();
String title = mImageNamer.getTitle();
info.mUri = uri;
mImageSaver.addImage(jpegData, uri, title, null,
width, height, orientation);
for (SnapshotListener listener : mListeners) {
listener.onSnapshotSaved(info);
}
}
};
public SnapshotManager(CameraManager man, FocusManager focusMan, Context ctx) {
mContext = ctx;
mCameraManager = man;
mFocusManager = focusMan;
mSnapshotsQueue = new ArrayList();
mListeners = new ArrayList();
mHandler = new Handler();
mImageSaver = new ImageSaver();
mImageNamer = new ImageNamer();
mVideoNamer = new VideoNamer();
mContentResolver = ctx.getContentResolver();
mProfile = CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH);
mPaused = false;
mImageIsProcessing = false;
}
public void addListener(SnapshotListener listener) {
if (!mListeners.contains(listener)) {
mListeners.add(listener);
}
}
public void removeListener(SnapshotListener listener) {
mListeners.remove(listener);
}
/**
* Sets whether or not to bypass image processing (for burst shot or hdr for instance)
* This value is reset after each snapshot queued!
* @param bypass
*/
public void setBypassProcessing(boolean bypass) {
mBypassProcessing = bypass;
}
public void setAutoEnhance(boolean enhance) {
mDoAutoEnhance = enhance;
}
public boolean getAutoEnhance() {
return mDoAutoEnhance;
}
public void prepareNamerUri(int width, int height) {
if (mImageNamer == null) {
// ImageNamer can be dead if the user exitted the app.
// We restart it temporarily.
mImageNamer = new ImageNamer();
}
mImageNamer.prepareUri(mContentResolver,
System.currentTimeMillis(), width, height, 0);
}
public Uri getNamerUri() {
if (mImageNamer == null) {
// ImageNamer can be dead if the user exitted the app.
// We restart it temporarily.
mImageNamer = new ImageNamer();
}
return mImageNamer.getUri();
}
public String getNamerTitle() {
if (mImageNamer == null) {
// ImageNamer can be dead if the user exitted the app.
// We restart it temporarily.
mImageNamer = new ImageNamer();
}
return mImageNamer.getTitle();
}
public void saveImage(Uri uri, String title, int width, int height,
int orientation, byte[] jpegData) {
if (mImageSaver == null) {
// ImageSaver can be dead if the user exitted the app.
// We restart it temporarily.
mImageSaver = new ImageSaver();
}
mImageSaver.addImage(jpegData, uri, title, null,
width, height, orientation);
mImageSaver.waitDone();
mImageSaver.finish();
}
/**
* Queues a snapshot that will be taken as soon as possible
*
* @param save Whether or not to save the snapshot in gallery
* (for example, software HDR doesn't need all the shots to
* be saved)
* @param exposureCompensation If the shot has to be taken at a different
* exposure value, otherwise set it to 0
*/
public void queueSnapshot(boolean save, int exposureCompensation) {
if (CameraActivity.getCameraMode() == CameraActivity.CAMERA_MODE_VIDEO
&& mContext.getResources().getBoolean(R.bool.config_usePreviewForVideoSnapshot)) {
// We use the preview data for the video snapshot, instead of queueing a normal snapshot
new Thread(mPreviewCaptureRunnable).start();
return;
}
if (mSnapshotsQueue.size() == 2) return; // No more than 2 shots at a time
// If we use Samsung HDR, we must set exposure level, as it corresponds to the HDR bracket
if (mContext.getResources().getBoolean(R.bool.config_useSamsungHDR) &&
SimpleToggleWidget.isWidgetEnabled(mContext, mCameraManager, "scene-mode", "hdr")) {
exposureCompensation = mCameraManager.getParameters().getMaxExposureCompensation();
mResetExposure = mCameraManager.getParameters().getExposureCompensation();
}
SnapshotInfo info = new SnapshotInfo();
info.mSave = save;
info.mExposureCompensation = exposureCompensation;
info.mThumbnail = mCameraManager.getLastPreviewFrame();
info.mBypassProcessing = mBypassProcessing;
Camera.Parameters params = mCameraManager.getParameters();
if (params != null && params.getExposureCompensation() != exposureCompensation) {
mCameraManager.setParameterAsync("exposure-compensation",
Integer.toString(exposureCompensation));
mWaitExposureSettle = true;
}
mSnapshotsQueue.add(info);
// Reset bypass in any case
mBypassProcessing = false;
if (mSnapshotsQueue.size() == 1) {
// We had no other snapshot queued so far, so start things up
Log.v(TAG, "No snapshot queued, starting runnable");
mCurrentShutterQueueIndex = 0;
new Thread(mCaptureRunnable).start();
}
}
public ImageNamer getImageNamer() {
return mImageNamer;
}
public ImageSaver getImageSaver() {
return mImageSaver;
}
public CamcorderProfile getVideoProfile(){
return mProfile;
}
public void setVideoProfile(final CamcorderProfile profile) {
mProfile = profile;
if (CameraActivity.getCameraMode() == CameraActivity.CAMERA_MODE_VIDEO) {
// TODO: setVideoSize is handling preview changing
mCameraManager.setVideoSize(profile.videoFrameWidth, profile.videoFrameHeight);
}
}
/**
* Starts recording a video with the current settings
*/
public void startVideo() {
Log.v(TAG, "startVideo");
// Setup output file
generateVideoFilename(mProfile.fileFormat);
mCameraManager.prepareVideoRecording(mVideoFilename, mProfile);
mCameraManager.startVideoRecording();
mIsRecording = true;
mRecordingStartTime = SystemClock.uptimeMillis();
for (SnapshotListener listener : mListeners) {
listener.onVideoRecordingStart();
}
}
/**
* Stops the current recording video, if any
*/
public void stopVideo() {
Log.v(TAG, "stopVideo");
if (mIsRecording) {
// Stop the video in a thread to not stall the UI thread
new Thread() {
public void run() {
mCameraManager.stopVideoRecording();
mHandler.post(new Runnable() {
@Override
public void run() {
for (SnapshotListener listener : mListeners) {
listener.onVideoRecordingStop();
listener.onMediaSavingDone();
}
}
});
}
}.start();
mCurrentVideoFilename = mVideoFilename;
mIsRecording = false;
for (SnapshotListener listener : mListeners) {
listener.onVideoRecordingStop();
listener.onMediaSavingStart();
}
addVideoToMediaStore();
}
}
/**
* Returns whether or not a video is recording
*/
public boolean isRecording() {
return mIsRecording;
}
/**
* Add the last recorded video to the MediaStore
*
* @return True if operation succeeded
*/
private boolean addVideoToMediaStore() {
boolean fail = false;
if (mVideoFileDescriptor == null) {
mCurrentVideoValues.put(MediaStore.Video.Media.SIZE,
new File(mCurrentVideoFilename).length());
long duration = SystemClock.uptimeMillis() - mRecordingStartTime;
if (duration > 0) {
mCurrentVideoValues.put(MediaStore.Video.Media.DURATION, duration);
} else {
Log.w(TAG, "Video duration <= 0 : " + duration);
}
try {
mCurrentVideoUri = mVideoNamer.getUri();
// Rename the video file to the final name. This avoids other
// apps reading incomplete data. We need to do it after the
// above mVideoNamer.getUri() call, so we are certain that the
// previous insert to MediaProvider is completed.
String finalName = mCurrentVideoValues.getAsString(
MediaStore.Video.Media.DATA);
if (new File(mCurrentVideoFilename).renameTo(new File(finalName))) {
mCurrentVideoFilename = finalName;
}
mContentResolver.update(mCurrentVideoUri, mCurrentVideoValues
, null, null);
mContext.sendBroadcast(new Intent(Util.ACTION_NEW_VIDEO,
mCurrentVideoUri));
} catch (Exception e) {
// We failed to insert into the database. This can happen if
// the SD card is unmounted.
Log.e(TAG, "failed to add video to media store", e);
mCurrentVideoUri = null;
mCurrentVideoFilename = null;
fail = true;
} finally {
Log.v(TAG, "Current video URI: " + mCurrentVideoUri);
}
}
mCurrentVideoValues = null;
return fail;
}
/**
* Generates a filename for the next video to record
*
* @param outputFileFormat The file format of the video
*/
private void generateVideoFilename(int outputFileFormat) {
long dateTaken = System.currentTimeMillis();
String title = Util.createVideoName(dateTaken);
// Used when emailing.
String filename = title + convertOutputFormatToFileExt(outputFileFormat);
String mime = convertOutputFormatToMimeType(outputFileFormat);
String path = Storage.getStorage().generateDirectory() + '/' + filename;
String tmpPath = path + ".tmp";
mCurrentVideoValues = new ContentValues(7);
mCurrentVideoValues.put(MediaStore.Video.Media.TITLE, title);
mCurrentVideoValues.put(MediaStore.Video.Media.DISPLAY_NAME, filename);
mCurrentVideoValues.put(MediaStore.Video.Media.DATE_TAKEN, dateTaken);
mCurrentVideoValues.put(MediaStore.Video.Media.MIME_TYPE, mime);
mCurrentVideoValues.put(MediaStore.Video.Media.DATA, path);
mCurrentVideoValues.put(MediaStore.Video.Media.RESOLUTION,
Integer.toString(mProfile.videoFrameWidth) + "x" +
Integer.toString(mProfile.videoFrameHeight));
Location loc = null; // TODO: mLocationManager.getCurrentLocation();
if (loc != null) {
mCurrentVideoValues.put(MediaStore.Video.Media.LATITUDE, loc.getLatitude());
mCurrentVideoValues.put(MediaStore.Video.Media.LONGITUDE, loc.getLongitude());
}
mVideoNamer.prepareUri(mContentResolver, mCurrentVideoValues);
mVideoFilename = tmpPath;
Log.v(TAG, "New video filename: " + mVideoFilename);
}
private String convertOutputFormatToMimeType(int outputFileFormat) {
if (outputFileFormat == MediaRecorder.OutputFormat.MPEG_4) {
return "video/mp4";
}
return "video/3gpp";
}
private String convertOutputFormatToFileExt(int outputFileFormat) {
if (outputFileFormat == MediaRecorder.OutputFormat.MPEG_4) {
return ".mp4";
}
return ".3gp";
}
public void onPause() {
mPaused = true;
if (!mImageIsProcessing && mImageSaver != null) {
// We wait until the last processing image was saved
mImageSaver.finish();
}
mImageNamer.finish();
mVideoNamer.finish();
mImageSaver = null;
mImageNamer = null;
mVideoNamer = null;
}
public void onResume() {
mPaused = false;
// Restore threads if needed
if (mImageSaver == null) {
mImageSaver = new ImageSaver();
}
if (mImageNamer == null) {
mImageNamer = new ImageNamer();
}
if (mVideoNamer == null) {
mVideoNamer = new VideoNamer();
}
}
// Each SaveRequest remembers the data needed to save an image.
private static class SaveRequest {
byte[] data;
Uri uri;
String title;
Location loc;
int width, height;
int orientation;
List exifTags;
SnapshotInfo snap;
}
// We use a queue to store the SaveRequests that have not been completed
// yet. The main thread puts the request into the queue. The saver thread
// gets it from the queue, does the work, and removes it from the queue.
//
// The main thread needs to wait for the saver thread to finish all the work
// in the queue, when the activity's onPause() is called, we need to finish
// all the work, so other programs (like Gallery) can see all the images.
//
// If the queue becomes too long, adding a new request will block the main
// thread until the queue length drops below the threshold (QUEUE_LIMIT).
// If we don't do this, we may face several problems: (1) We may OOM
// because we are holding all the jpeg data in memory. (2) We may ANR
// when we need to wait for saver thread finishing all the work (in
// onPause() or gotoGallery()) because the time to finishing a long queue
// of work may be too long.
private class ImageSaver extends Thread {
private static final int QUEUE_LIMIT = 3;
private ArrayList mQueue;
private boolean mStop;
// Runs in main thread
public ImageSaver() {
mQueue = new ArrayList();
start();
}
// Runs in main thread
public void addImage(final byte[] data, Uri uri, String title,
Location loc, int width, int height, int orientation, SnapshotInfo snap) {
addImage(data, uri, title, loc, width, height, orientation, null, snap);
}
// Runs in main thread
public void addImage(final byte[] data, Uri uri, String title,
Location loc, int width, int height, int orientation) {
addImage(data, uri, title, loc, width, height, orientation, null, null);
}
// Runs in main thread
public void addImage(final byte[] data, Uri uri, String title,
Location loc, int width, int height, int orientation,
List exifTags, SnapshotInfo snap) {
SaveRequest r = new SaveRequest();
r.data = data;
r.uri = uri;
r.title = title;
r.loc = (loc == null) ? null : new Location(loc); // make a copy
r.width = width;
r.height = height;
r.orientation = orientation;
r.exifTags = exifTags;
r.snap = snap;
synchronized (this) {
while (mQueue.size() >= QUEUE_LIMIT) {
try {
wait();
} catch (InterruptedException ex) {
// ignore.
}
}
mQueue.add(r);
notifyAll(); // Tell saver thread there is new work to do.
}
}
// Runs in saver thread
@Override
public void run() {
while (true) {
SaveRequest r;
synchronized (this) {
if (mQueue.isEmpty()) {
notifyAll(); // notify main thread in waitDone
// Note that we can only stop after we saved all images
// in the queue.
if (mStop) break;
try {
wait();
} catch (InterruptedException ex) {
// ignore.
}
continue;
}
r = mQueue.get(0);
for (SnapshotListener listener : mListeners) {
listener.onMediaSavingStart();
}
}
storeImage(r.data, r.uri, r.title, r.loc, r.width, r.height,
r.orientation, r.exifTags, r.snap);
synchronized (this) {
mQueue.remove(0);
for (SnapshotListener listener : mListeners) {
listener.onMediaSavingDone();
}
notifyAll(); // the main thread may wait in addImage
}
}
}
// Runs in main thread
public void waitDone() {
synchronized (this) {
while (!mQueue.isEmpty()) {
try {
wait();
} catch (InterruptedException ex) {
// ignore.
}
}
}
}
// Runs in main thread
public void finish() {
waitDone();
synchronized (this) {
mStop = true;
notifyAll();
}
try {
join();
} catch (InterruptedException ex) {
// ignore.
}
}
// Runs in saver thread
private void storeImage(final byte[] data, Uri uri, String title,
Location loc, int width, int height, int orientation,
List exifTags, SnapshotInfo snap) {
boolean ok = Storage.getStorage().updateImage(mContentResolver, uri, title, loc,
orientation, data, width, height);
if (ok) {
if (exifTags != null && exifTags.size() > 0) {
// Write exif tags to final picture
try {
ExifInterface exifIf = new ExifInterface(Util.getRealPathFromURI(mContext, uri));
for (Tag tag : exifTags) {
// move along
String[] hack = tag.toString().split("\\]");
hack = hack[1].split("-");
exifIf.setAttribute(tag.getTagName(), hack[1].trim());
}
exifIf.saveAttributes();
} catch (IOException e) {
Log.e(TAG, "Couldn't write exif", e);
} catch (CursorIndexOutOfBoundsException e) {
Log.e(TAG, "Couldn't find original picture", e);
}
}
Util.broadcastNewPicture(mContext, uri);
}
if (snap != null) {
for (SnapshotListener listener : mListeners) {
listener.onSnapshotSaved(snap);
}
}
}
}
private static class ImageNamer extends Thread {
private boolean mRequestPending;
private ContentResolver mResolver;
private long mDateTaken;
private int mWidth, mHeight;
private boolean mStop;
private Uri mUri;
private String mTitle;
// Runs in main thread
public ImageNamer() {
start();
}
// Runs in main thread
public synchronized void prepareUri(ContentResolver resolver,
long dateTaken, int width, int height, int rotation) {
if (rotation % 180 != 0) {
int tmp = width;
width = height;
height = tmp;
}
mRequestPending = true;
mResolver = resolver;
mDateTaken = dateTaken;
mWidth = width;
mHeight = height;
notifyAll();
}
// Runs in main thread
public synchronized Uri getUri() {
// wait until the request is done.
while (mRequestPending) {
try {
wait();
} catch (InterruptedException ex) {
// Do nothing here
}
}
// Return the uri generated
Uri uri = mUri;
mUri = null;
return uri;
}
// Runs in main thread, should be called after getUri().
public synchronized String getTitle() {
return mTitle;
}
// Runs in namer thread
@Override
public synchronized void run() {
while (true) {
if (mStop) break;
if (!mRequestPending) {
try {
wait();
} catch (InterruptedException ex) {
// ignore.
}
continue;
}
cleanOldUri();
generateUri();
mRequestPending = false;
notifyAll();
}
cleanOldUri();
}
// Runs in main thread
public synchronized void finish() {
mStop = true;
notifyAll();
}
// Runs in namer thread
private void generateUri() {
mTitle = Util.createJpegName(mDateTaken);
mUri = Storage.getStorage().newImage(mResolver, mTitle, mDateTaken, mWidth, mHeight);
}
// Runs in namer thread
private void cleanOldUri() {
if (mUri == null) return;
Storage.getStorage().deleteImage(mResolver, mUri);
mUri = null;
}
}
private static class VideoNamer extends Thread {
private boolean mRequestPending;
private ContentResolver mResolver;
private ContentValues mValues;
private boolean mStop;
private Uri mUri;
// Runs in main thread
public VideoNamer() {
start();
}
// Runs in main thread
public synchronized void prepareUri(
ContentResolver resolver, ContentValues values) {
mRequestPending = true;
mResolver = resolver;
mValues = new ContentValues(values);
notifyAll();
}
// Runs in main thread
public synchronized Uri getUri() {
// wait until the request is done.
while (mRequestPending) {
try {
wait();
} catch (InterruptedException ex) {
// ignore.
}
}
Uri uri = mUri;
mUri = null;
return uri;
}
// Runs in namer thread
@Override
public synchronized void run() {
while (true) {
if (mStop) break;
if (!mRequestPending) {
try {
wait();
} catch (InterruptedException ex) {
// Do nothing here
}
continue;
}
cleanOldUri();
generateUri();
mRequestPending = false;
notifyAll();
}
cleanOldUri();
}
// Runs in main thread
public synchronized void finish() {
mStop = true;
notifyAll();
}
// Runs in namer thread
private void generateUri() {
Uri videoTable = Uri.parse("content://media/external/video/media");
mUri = mResolver.insert(videoTable, mValues);
}
// Runs in namer thread
private void cleanOldUri() {
if (mUri == null) return;
mResolver.delete(mUri, null, null);
mUri = null;
}
}
}
================================================
FILE: src/org/cyanogenmod/focal/SoundManager.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal;
import android.content.Context;
import android.media.AudioManager;
import android.media.SoundPool;
import fr.xplod.focal.R;
/**
* Manages sounds played by the app. Since we have a limited
* set of sounds that can be played, we pre-load them and we use
* hardcoded values to play them quickly.
*/
public class SoundManager {
public final static int SOUND_SHUTTER = 0;
private final static int SOUND_MAX = 1;
private static SoundManager mSingleton;
public static SoundManager getSingleton() {
if (mSingleton == null) {
mSingleton = new SoundManager();
}
return mSingleton;
}
private SoundPool mSoundPool;
private int[] mSoundsFD = new int[SOUND_MAX];
/**
* Default constructor
* Creates the sound pool to play the audio files. Make sure to
* call preload() before doing anything so the sounds are loaded!
*/
private SoundManager() {
mSoundPool = new SoundPool(3, AudioManager.STREAM_NOTIFICATION, 0);
}
public void preload(Context ctx) {
mSoundsFD[SOUND_SHUTTER] = mSoundPool.load(ctx, R.raw.snd_capture, 1);
}
/**
* Immediately play the specified sound
*
* @param sound The sound to play, see SoundManager.SOUND_*
* @note Make sure preload() was called before doing play!
*/
public void play(int sound) {
mSoundPool.play(mSoundsFD[sound], 1.0f, 1.0f, 0, 0, 1.0f);
}
}
================================================
FILE: src/org/cyanogenmod/focal/Storage.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal;
import android.annotation.TargetApi;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.location.Location;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.StatFs;
import android.provider.MediaStore.Images;
import android.provider.MediaStore.Images.ImageColumns;
import android.provider.MediaStore.MediaColumns;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
public class Storage {
private static final String TAG = "CameraStorage";
public static final long UNAVAILABLE = -1L;
public static final long PREPARING = -2L;
public static final long UNKNOWN_SIZE = -3L;
public static final long LOW_STORAGE_THRESHOLD = 50000000;
private String mRoot = Environment.getExternalStorageDirectory().toString();
private static Storage sStorage;
// Singleton
private Storage() {
// Do nothing here
}
public static Storage getStorage() {
if (sStorage == null) {
sStorage = new Storage();
}
return sStorage;
}
public void setRoot(String root) {
mRoot = root;
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private static void setImageSize(ContentValues values, int width, int height) {
values.put(MediaColumns.WIDTH, width);
values.put(MediaColumns.HEIGHT, height);
}
public String writeFile(String title, byte[] data) {
String path = generateFilepath(title);
FileOutputStream out = null;
try {
out = new FileOutputStream(path);
out.write(data);
} catch (Exception e) {
Log.e(TAG, "Failed to write data", e);
} finally {
try {
out.close();
} catch (Exception e) {
}
}
return path;
}
// Save the image and add it to media store.
public Uri addImage(ContentResolver resolver, String title,
long date, Location location, int orientation, byte[] jpeg,
int width, int height) {
// Save the image.
String path = writeFile(title, jpeg);
return addImage(resolver, title, date, location, orientation,
jpeg.length, path, width, height);
}
// Add the image to media store.
public Uri addImage(ContentResolver resolver, String title,
long date, Location location, int orientation, int jpegLength,
String path, int width, int height) {
// Insert into MediaStore.
ContentValues values = new ContentValues(9);
values.put(ImageColumns.TITLE, title);
values.put(ImageColumns.DISPLAY_NAME, title + ".jpg");
values.put(ImageColumns.DATE_TAKEN, date);
values.put(ImageColumns.MIME_TYPE, "image/jpeg");
// Clockwise rotation in degrees. 0, 90, 180, or 270.
values.put(ImageColumns.ORIENTATION, orientation);
values.put(ImageColumns.DATA, path);
values.put(ImageColumns.SIZE, jpegLength);
setImageSize(values, width, height);
if (location != null) {
values.put(ImageColumns.LATITUDE, location.getLatitude());
values.put(ImageColumns.LONGITUDE, location.getLongitude());
}
Uri uri = null;
try {
uri = resolver.insert(Images.Media.EXTERNAL_CONTENT_URI, values);
} catch (Throwable th) {
// This can happen when the external volume is already mounted, but
// MediaScanner has not notify MediaProvider to add that volume.
// The picture is still safe and MediaScanner will find it and
// insert it into MediaProvider. The only problem is that the user
// cannot click the thumbnail to review the picture.
Log.e(TAG, "Failed to write MediaStore" + th);
}
return uri;
}
// nullewImage() and updateImage() together do the same work as
// addImage. newImage() is the first step, and it inserts the DATE_TAKEN and
// DATA fields into the database.
//
// We also insert hint values for the WIDTH and HEIGHT fields to give
// correct aspect ratio before the real values are updated in updateImage().
public Uri newImage(ContentResolver resolver, String title,
long date, int width, int height) {
String path = generateFilepath(title);
// Insert into MediaStore.
ContentValues values = new ContentValues(4);
values.put(ImageColumns.DATE_TAKEN, date);
values.put(ImageColumns.DATA, path);
setImageSize(values, width, height);
Uri uri = null;
try {
uri = resolver.insert(Images.Media.EXTERNAL_CONTENT_URI, values);
} catch (Throwable th) {
// This can happen when the external volume is already mounted, but
// MediaScanner has not notify MediaProvider to add that volume.
// The picture is still safe and MediaScanner will find it and
// insert it into MediaProvider. The only problem is that the user
// cannot click the thumbnail to review the picture.
Log.e(TAG, "Failed to new image" + th);
}
return uri;
}
// This is the second step. It completes the partial data added by
// newImage. All columns other than DATE_TAKEN and DATA are inserted
// here. This method also save the image data into the file.
//
// Returns true if the update is successful.
public boolean updateImage(ContentResolver resolver, Uri uri,
String title, Location location, int orientation, byte[] jpeg,
int width, int height) {
// Save the image.
String path = generateFilepath(title);
String tmpPath = path + ".tmp";
FileOutputStream out = null;
try {
// Write to a temporary file and rename it to the final name. This
// avoids other apps reading incomplete data.
out = new FileOutputStream(tmpPath);
out.write(jpeg);
out.close();
new File(tmpPath).renameTo(new File(path));
} catch (Exception e) {
Log.e(TAG, "Failed to write image", e);
return false;
} finally {
try {
out.close();
} catch (Exception e) {
// Do nothing here
}
}
// Insert into MediaStore.
ContentValues values = new ContentValues(9);
values.put(ImageColumns.TITLE, title);
values.put(ImageColumns.DISPLAY_NAME, title + ".jpg");
values.put(ImageColumns.MIME_TYPE, "image/jpeg");
// Clockwise rotation in degrees. 0, 90, 180, or 270.
values.put(ImageColumns.ORIENTATION, orientation);
values.put(ImageColumns.SIZE, jpeg.length);
setImageSize(values, width, height);
if (location != null) {
values.put(ImageColumns.LATITUDE, location.getLatitude());
values.put(ImageColumns.LONGITUDE, location.getLongitude());
}
try {
resolver.update(uri, values, null, null);
} catch (Throwable th) {
Log.e(TAG, "Failed to update image (" + th + ") ; uri=" + uri + " values=" + values);
return false;
}
return true;
}
public void deleteImage(ContentResolver resolver, Uri uri) {
try {
resolver.delete(uri, null, null);
} catch (Throwable th) {
Log.e(TAG, "Failed to delete image: " + uri);
}
}
private String generateDCIM() {
return new File(mRoot, Environment.DIRECTORY_DCIM).toString();
}
public String generateDirectory() {
return generateDCIM() + "/Camera";
}
private String generateFilepath(String title) {
return generateDirectory() + '/' + title + ".jpg";
}
public String generateBucketId() {
return String.valueOf(generateDirectory().toLowerCase().hashCode());
}
public int generateBucketIdInt() {
return generateDirectory().toLowerCase().hashCode();
}
public long getAvailableSpace() {
String state = Environment.getExternalStorageState();
Log.d(TAG, "External storage state=" + state);
if (Environment.MEDIA_CHECKING.equals(state)) {
return PREPARING;
}
if (!Environment.MEDIA_MOUNTED.equals(state)) {
return UNAVAILABLE;
}
File dir = new File(generateDirectory());
dir.mkdirs();
if (!dir.isDirectory() || !dir.canWrite()) {
return UNAVAILABLE;
}
try {
StatFs stat = new StatFs(generateDirectory());
return stat.getAvailableBlocks() * (long) stat.getBlockSize();
} catch (Exception e) {
Log.i(TAG, "Fail to access external storage", e);
}
return UNKNOWN_SIZE;
}
/**
* OSX requires plugged-in USB storage to have path /DCIM/NNNAAAAA to be
* imported. This is a temporary fix for bug#1655552.
*/
public void ensureOSXCompatible() {
File nnnAAAAA = new File(generateDCIM(), "100ANDRO");
if (!(nnnAAAAA.exists() || nnnAAAAA.mkdirs())) {
Log.e(TAG, "Failed to create " + nnnAAAAA.getPath());
}
}
}
================================================
FILE: src/org/cyanogenmod/focal/Util.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.hardware.Camera.Size;
import android.net.Uri;
import android.os.Build;
import android.provider.BaseColumns;
import android.provider.MediaStore;
import android.renderscript.Allocation;
import android.renderscript.Element;
import android.renderscript.RSIllegalArgumentException;
import android.renderscript.RenderScript;
import android.renderscript.ScriptIntrinsicYuvToRGB;
import android.renderscript.Type;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.OrientationEventListener;
import android.view.Surface;
import android.view.View;
import android.view.WindowManager;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
public class Util {
public final static String TAG = "Nemesis.Util";
// Orientation hysteresis amount used in rounding, in degrees
public static final int ORIENTATION_HYSTERESIS = 5;
public static final String REVIEW_ACTION = "com.android.camera.action.REVIEW";
// See android.hardware.Camera.ACTION_NEW_PICTURE.
public static final String ACTION_NEW_PICTURE = "android.hardware.action.NEW_PICTURE";
// See android.hardware.Camera.ACTION_NEW_VIDEO.
public static final String ACTION_NEW_VIDEO = "android.hardware.action.NEW_VIDEO";
// Screen size holder
private static Point mScreenSize = new Point();
private static int mRotation = 90;
private static DateFormat mJpegDateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS");
/**
* Returns the orientation of the display
* In our case, since we're locked in Landscape, it should always
* be 90
*
* @param activity
* @return Orientation angle of the display
*/
public static int getDisplayRotation(Activity activity) {
if (activity != null) {
int rotation = activity.getWindowManager().getDefaultDisplay()
.getRotation();
switch (rotation) {
case Surface.ROTATION_0:
mRotation = 0;
break;
case Surface.ROTATION_90:
mRotation = 90;
break;
case Surface.ROTATION_180:
mRotation = 180;
break;
case Surface.ROTATION_270:
mRotation = 270;
break;
}
}
return mRotation;
}
/**
* Rounds the orientation so that the UI doesn't rotate if the user
* holds the device towards the floor or the sky
*
* @param orientation New orientation
* @param orientationHistory Previous orientation
* @return Rounded orientation
*/
public static int roundOrientation(int orientation, int orientationHistory) {
boolean changeOrientation = false;
if (orientationHistory == OrientationEventListener.ORIENTATION_UNKNOWN) {
changeOrientation = true;
} else {
int dist = Math.abs(orientation - orientationHistory);
dist = Math.min(dist, 360 - dist);
changeOrientation = (dist >= 45 + ORIENTATION_HYSTERESIS);
}
if (changeOrientation) {
return ((orientation + 45) / 90 * 90) % 360;
}
return orientationHistory;
}
/**
* Returns the size of the screen
*
* @param activity
* @return Point where x=width and y=height
*/
public static Point getScreenSize(Activity activity) {
if (activity != null) {
WindowManager service =
(WindowManager) activity.getSystemService(Context.WINDOW_SERVICE);
service.getDefaultDisplay().getSize(mScreenSize);
}
return mScreenSize;
}
/**
* Returns the optimal preview size for photo shots
*
* @param currentActivity
* @param sizes
* @param targetRatio
* @return
*/
public static Size getOptimalPreviewSize(Activity currentActivity,
List sizes, double targetRatio) {
// Use a very small tolerance because we want an exact match.
final double ASPECT_TOLERANCE = 0.01;
if (sizes == null) {
return null;
}
Size optimalSize = null;
double minDiff = Double.MAX_VALUE;
// Because of bugs of overlay and layout, we sometimes will try to
// layout the viewfinder in the portrait orientation and thus get the
// wrong size of preview surface. When we change the preview size, the
// new overlay will be created before the old one closed, which causes
// an exception. For now, just get the screen size.
Point point = getScreenSize(currentActivity);
int targetHeight = Math.min(point.x, point.y);
// Try to find an size match aspect ratio and size
for (Size size : sizes) {
double ratio = (double) size.width / size.height;
if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) {
continue;
}
if (Math.abs(size.height - targetHeight) < minDiff) {
optimalSize = size;
minDiff = Math.abs(size.height - targetHeight);
}
}
// Cannot find the one match the aspect ratio. This should not happen.
// Ignore the requirement.
if (optimalSize == null) {
Log.w(TAG, "No preview size match the aspect ratio");
minDiff = Double.MAX_VALUE;
for (Size size : sizes) {
if (Math.abs(size.height - targetHeight) < minDiff) {
optimalSize = size;
minDiff = Math.abs(size.height - targetHeight);
}
}
}
return optimalSize;
}
/**
* Fade in a view from startAlpha to endAlpha during duration milliseconds
* @param view
* @param startAlpha
* @param endAlpha
* @param duration
*/
public static void fadeIn(View view, float startAlpha, float endAlpha, long duration) {
if (view.getVisibility() == View.VISIBLE) {
return;
}
view.setVisibility(View.VISIBLE);
Animation animation = new AlphaAnimation(startAlpha, endAlpha);
animation.setDuration(duration);
view.startAnimation(animation);
}
/**
* Fade in a view with the default time
* @param view
*/
public static void fadeIn(View view) {
fadeIn(view, 0F, 1F, 400);
// We disabled the button in fadeOut(), so enable it here.
view.setEnabled(true);
}
public static void fadeOut(View view) {
if (view.getVisibility() != View.VISIBLE) return;
// Since the button is still clickable before fade-out animation
// ends, we disable the button first to block click.
view.setEnabled(false);
Animation animation = new AlphaAnimation(1F, 0F);
animation.setDuration(400);
view.startAnimation(animation);
view.setVisibility(View.GONE);
}
/**
* Converts the provided byte array from YUV420SP into an RGBA bitmap.
* @param context
* @param yuv420sp The YUV420SP data
* @param width Width of the data's picture
* @param height Height of the data's picture
* @return A decoded bitmap
* @throws NullPointerException
* @throws IllegalArgumentException
*/
public static Bitmap decodeYUV420SP(Context context, byte[] yuv420sp, int width, int height)
throws NullPointerException, IllegalArgumentException {
Bitmap bmp = null;
if (Build.VERSION.SDK_INT >= 17) {
final RenderScript rs = RenderScript.create(context);
final ScriptIntrinsicYuvToRGB script = ScriptIntrinsicYuvToRGB
.create(rs, Element.RGBA_8888(rs));
Type.Builder tb = new Type.Builder(rs, Element.RGBA_8888(rs));
tb.setX(width);
tb.setY(height);
Allocation allocationOut = Allocation.createTyped(rs, tb.create());
Allocation allocationIn = Allocation.createSized(rs, Element.U8(rs),
(height * width) + ((height) * (width) * 2));
script.setInput(allocationIn);
bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
try {
allocationIn.copyFrom(yuv420sp);
script.forEach(allocationOut);
allocationOut.copyTo(bmp);
} catch (RSIllegalArgumentException ex) {
Log.e(TAG, "Cannot copy YUV420SP data", ex);
}
} else {
final int frameSize = width * height;
int[] rgb = new int[frameSize];
for (int j = 0, yp = 0; j < height; j++) {
int uvp = frameSize + (j >> 1) * width, u = 0, v = 0;
for (int i = 0; i < width; i++, yp++) {
int y = (0xff & ((int) yuv420sp[yp])) - 16;
if (y < 0) {
y = 0;
}
if ((i & 1) == 0) {
v = (0xff & yuv420sp[uvp++]) - 128;
u = (0xff & yuv420sp[uvp++]) - 128;
}
int y1192 = 1192 * y;
int r = (y1192 + 1634 * v);
int g = (y1192 - 833 * v - 400 * u);
int b = (y1192 + 2066 * u);
if (r < 0) {
r = 0;
} else if (r > 262143) {
r = 262143;
}
if (g < 0) {
g = 0;
} else if (g > 262143) {
g = 262143;
}
if (b < 0) {
b = 0;
} else if (b > 262143) {
b = 262143;
}
rgb[yp] = 0xff000000 | ((r << 6) & 0xff0000) | ((g >> 2) & 0xff00)
| ((b >> 10) & 0xff);
}
}
bmp = Bitmap.createBitmap(rgb, width, height, Bitmap.Config.ARGB_8888);
}
return bmp;
}
public static Bitmap decodeYUV422P(byte[] yuv422p, int width, int height)
throws NullPointerException, IllegalArgumentException {
final int frameSize = width * height;
int[] rgb = new int[frameSize];
for (int j = 0, yp = 0; j < height; j++) {
int up = frameSize + (j * (width/2)), u = 0, v = 0;
int vp = ((int)(frameSize*1.5) + (j*(width/2)));
for (int i = 0; i < width; i++, yp++) {
int y = (0xff & ((int) yuv422p[yp])) - 16;
if (y < 0) {
y = 0;
}
if ((i & 1) == 0) {
u = (0xff & yuv422p[up++]) - 128;
v = (0xff & yuv422p[vp++]) - 128;
}
int y1192 = 1192 * y;
int r = (y1192 + 1634 * v);
int g = (y1192 - 833 * v - 400 * u);
int b = (y1192 + 2066 * u);
if (r < 0) {
r = 0;
} else if (r > 262143) {
r = 262143;
}
if (g < 0) {
g = 0;
} else if (g > 262143) {
g = 262143;
}
if (b < 0) {
b = 0;
} else if (b > 262143) {
b = 262143;
}
rgb[yp] = 0xff000000 | ((r << 6) & 0xff0000) | ((g >> 2) & 0xff00)
| ((b >> 10) & 0xff);
}
}
return Bitmap.createBitmap(rgb, width, height, Bitmap.Config.ARGB_8888);
}
public static String createJpegName(long dateTaken) {
return "IMG_" + mJpegDateFormat.format(new Date(dateTaken));
}
public static String createVideoName(long dateTaken) {
return "VID_" + mJpegDateFormat.format(new Date(dateTaken));
}
/**
* Broadcast an intent to notify of a new picture in the Gallery
* @param context
* @param uri
*/
public static void broadcastNewPicture(Context context, Uri uri) {
context.sendBroadcast(new Intent(ACTION_NEW_PICTURE, uri));
// Keep compatibility
context.sendBroadcast(new Intent("com.android.camera.NEW_PICTURE", uri));
}
/**
* Removes an image from the gallery
* @param cr
* @param id
*/
public static void removeFromGallery(ContentResolver cr, long id) {
cr.delete(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
BaseColumns._ID + "=" + Long.toString(id), null);
}
/**
* Converts the specified DP to PIXELS according to current screen density
* @param context
* @param dp
* @return
*/
public static float dpToPx(Context context, float dp) {
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
return (int) ((dp * displayMetrics.density) + 0.5);
}
/**
* Returns the physical path (on emmc/sd) of the provided URI from MediaGallery
* @param context
* @param contentURI
* @return
*/
public static String getRealPathFromURI(Context context, Uri contentURI) {
Cursor cursor = context.getContentResolver().query(contentURI, null, null, null, null);
if (cursor == null) { // Source is Dropbox or other similar local file path
return contentURI.getPath();
} else {
cursor.moveToFirst();
int idx = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA);
return cursor.getString(idx);
}
}
/**
* Returns the best Panorama preview size
* @param supportedSizes
* @param need4To3
* @param needSmaller
* @return
*/
public static Point findBestPanoPreviewSize(List supportedSizes, boolean need4To3,
boolean needSmaller, int defaultPixels) {
Point output = null;
int pixelsDiff = defaultPixels;
for (Size size : supportedSizes) {
int h = size.height;
int w = size.width;
// we only want 4:3 format.
int d = defaultPixels - h * w;
if (needSmaller && d < 0) { // no bigger preview than 960x720.
continue;
}
if (need4To3 && (h * 4 != w * 3)) {
continue;
}
d = Math.abs(d);
if (d < pixelsDiff) {
output = new Point(w, h);
pixelsDiff = d;
}
}
return output;
}
/**
* Returns the best PicSphere picture size. The reference size is 2048x1536 from Nexus 4.
* @param supportedSizes Supported picture size
* @param needSmaller If a larger image size is accepted
* @return A Point where X and Y corresponds to Width and Height
*/
public static Point findBestPicSpherePictureSize(List supportedSizes, boolean needSmaller) {
Point output = null;
final int defaultPixels = 2048*1536;
int pixelsDiff = defaultPixels;
for (Size size : supportedSizes) {
int h = size.height;
int w = size.width;
int d = defaultPixels - h * w;
if (needSmaller && d < 0) { // no bigger preview than 960x720.
continue;
}
// we only want 4:3 format.
if ((h * 4 != w * 3)) {
continue;
}
d = Math.abs(d);
if (d < pixelsDiff) {
output = new Point(w, h);
pixelsDiff = d;
}
}
if (output == null) {
// Fail-safe, default to 640x480 is nothing suitable is found
output = new Point(640, 480);
}
return output;
}
/**
* Older devices need to stop preview before taking a shot
* (example: galaxy S, galaxy S2, etc)
* @return true if the device is an old one
*/
public static boolean deviceNeedsStopPreviewToShoot() {
String[] oldDevices = {"smdk4210", "aries"};
boolean needs = Arrays.asList(oldDevices).contains(Build.BOARD);
Log.e(TAG, "Device " + Build.BOARD + (needs ? " needs ": " doesn't need ") + "to stop preview");
return needs;
}
/**
* Disable ZSL mode on certain Qualcomm models
* @return true if the device needs ZSL disabled
*/
public static boolean deviceNeedsDisableZSL() {
String[] noZSL = {"apexqtmo", "expressatt", "aegis2vzw"};
boolean needs = Arrays.asList(noZSL).contains(Build.PRODUCT);
Log.e(TAG, "Device " + Build.PRODUCT + (needs ? " needs ": " doesn't need ") + "to disable ZSL");
return needs;
}
}
================================================
FILE: src/org/cyanogenmod/focal/WidgetProvider.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal;
import android.app.KeyguardManager;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.content.Intent;
import android.widget.RemoteViews;
import fr.xplod.focal.R;
public class WidgetProvider extends AppWidgetProvider {
public void onUpdate(final Context context, AppWidgetManager appWidgetManager,
int[] appWidgetIds) {
final int N = appWidgetIds.length;
// Perform this loop procedure for each App Widget
// that belongs to this provider
for (int i=0; i mMaxTextureSize || bitmap.getHeight() > mMaxTextureSize) {
mImageWidth = mMaxTextureSize;
mImageHeight = mMaxTextureSize;
} else {
mImageWidth = bitmap.getWidth();
mImageHeight = bitmap.getHeight();
}
mTexRenderer.updateTextureSize(mImageWidth, mImageHeight);
// Upload to texture
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextures[0]);
if (mImageWidth != bitmap.getWidth() || mImageHeight != bitmap.getHeight()) {
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, scale(bitmap,
mImageWidth, mImageHeight), 0);
} else {
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
}
bitmap.recycle();
System.gc();
Runtime.getRuntime().gc();
// Set texture parameters
GLToolbox.initTexParams();
}
private void initEffects() {
EffectFactory effectFactory = mEffectContext.getFactory();
if (mAutoFixEffect != null) {
mAutoFixEffect.release();
}
mAutoFixEffect = effectFactory.createEffect( EffectFactory.EFFECT_AUTOFIX);
mAutoFixEffect.setParameter("scale", 0.4f);
mMinMaxEffect = effectFactory.createEffect( EffectFactory.EFFECT_BLACKWHITE);
mMinMaxEffect.setParameter("black", .1f);
mMinMaxEffect.setParameter("white", .8f);
}
private void applyEffects() {
mMinMaxEffect.apply(mTextures[0], mImageWidth, mImageHeight, mTextures[0]);
mAutoFixEffect.apply(mTextures[0], mImageWidth, mImageHeight, mTextures[0]);
}
private void renderResult() {
mTexRenderer.renderTexture(mTextures[0]);
}
@Override
public void onDrawFrame(GL10 gl) {
if (!mInitialized) {
// Only need to do this once
mEffectContext = EffectContext.createWithCurrentGlContext();
mTexRenderer.init();
mInitialized = true;
}
if (mBitmapToLoad != null) {
loadTextureImpl(mBitmapToLoad);
mBitmapToLoad = null;
} else {
Log.e(TAG, "Bitmap to load is null");
}
// Render the effect
initEffects();
applyEffects();
renderResult();
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
final int mMaxTextureSize =
mContext.getResources().getInteger(R.integer.config_maxTextureSize);
if (mTexRenderer != null) {
if (width > mMaxTextureSize || height > mMaxTextureSize) {
mTexRenderer.updateViewSize(mMaxTextureSize, mMaxTextureSize);
} else {
mTexRenderer.updateViewSize(width, height);
}
}
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
}
}
================================================
FILE: src/org/cyanogenmod/focal/feats/BurstCapture.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.feats;
import android.os.Handler;
import android.util.Log;
import org.cyanogenmod.focal.CameraActivity;
import org.cyanogenmod.focal.SnapshotManager;
import org.cyanogenmod.focal.ui.ShutterButton;
/**
* Burst capture mode
*/
public class BurstCapture extends CaptureTransformer {
public final static String TAG = "BurstCapture";
private int mBurstCount = -1;
private int mShotsDone;
private boolean mBurstInProgress = false;
private Handler mHandler;
private CameraActivity mActivity;
public BurstCapture(CameraActivity activity) {
super(activity.getCamManager(), activity.getSnapManager());
mHandler = new Handler();
mActivity = activity;
}
/**
* Set the number of shots to take, or 0 for an infinite shooting
* (that will need to be stopped using terminateBurstShot)
*
* @param count The number of shots
*/
public void setBurstCount(int count) {
mBurstCount = count;
}
/**
* Starts the burst shooting
*/
public void startBurstShot() {
mShotsDone = 0;
mBurstInProgress = true;
mSnapManager.queueSnapshot(true, 0);
}
public void terminateBurstShot() {
mBurstInProgress = false;
}
private void tryTakeShot() {
mHandler.post(new Runnable() {
@Override
public void run() {
mSnapManager.setBypassProcessing(true);
mSnapManager.queueSnapshot(true, 0);
}
});
}
@Override
public void onShutterButtonClicked(ShutterButton button) {
if (mBurstInProgress) {
terminateBurstShot();
} else {
startBurstShot();
}
}
@Override
public void onSnapshotShutter(final SnapshotManager.SnapshotInfo info) {
}
@Override
public void onSnapshotPreview(SnapshotManager.SnapshotInfo info) {
}
@Override
public void onSnapshotProcessing(SnapshotManager.SnapshotInfo info) {
}
@Override
public void onSnapshotSaved(SnapshotManager.SnapshotInfo info) {
// XXX: Show it in the quick review drawer
if (!mBurstInProgress) {
return;
}
mShotsDone++;
Log.v(TAG, "Done " + mShotsDone + " shots");
if (mShotsDone < mBurstCount || mBurstCount == 0) {
tryTakeShot();
} else {
mBurstInProgress = false;
}
}
@Override
public void onMediaSavingStart() {
}
@Override
public void onMediaSavingDone() {
}
@Override
public void onVideoRecordingStart() {
}
@Override
public void onVideoRecordingStop() {
}
}
================================================
FILE: src/org/cyanogenmod/focal/feats/CaptureTransformer.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.feats;
import org.cyanogenmod.focal.CameraManager;
import org.cyanogenmod.focal.SnapshotManager;
import org.cyanogenmod.focal.ui.ShutterButton;
/**
* This class is a base class for all the effects/features affecting
* the camera at capture, such as burst mode, or software HDR. It lets
* you easily take more shots, process them, or throw them away, all of this
* in one unique tap on the shutter button by the user.
*/
public abstract class CaptureTransformer implements SnapshotManager.SnapshotListener {
protected CameraManager mCamManager;
protected SnapshotManager mSnapManager;
public CaptureTransformer(CameraManager camMan, SnapshotManager snapshotMan) {
mCamManager = camMan;
mSnapManager = snapshotMan;
}
/**
* Triggers the logic of the CaptureTransformer, when the user
* pressed the shutter button.
*/
public abstract void onShutterButtonClicked(ShutterButton button);
/**
* Triggers a secondary action when the shutter button is long-pressed
* (optional)
*/
public void onShutterButtonLongPressed(ShutterButton button) { }
}
================================================
FILE: src/org/cyanogenmod/focal/feats/GLToolbox.java
================================================
/*
* Copyright (C) 2012 The Android Open Source Project
* Copyright (C) 2013 Guillaume Lesniak
*
* Licensed under the Apache License, Version 2.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 org.cyanogenmod.focal.feats;
import android.opengl.GLES20;
public class GLToolbox {
public static int loadShader(int shaderType, String source) {
int shader = GLES20.glCreateShader(shaderType);
if (shader != 0) {
GLES20.glShaderSource(shader, source);
GLES20.glCompileShader(shader);
int[] compiled = new int[1];
GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);
if (compiled[0] == 0) {
String info = GLES20.glGetShaderInfoLog(shader);
GLES20.glDeleteShader(shader);
shader = 0;
throw new RuntimeException("Could not compile shader " +
shaderType + ":" + info);
}
}
return shader;
}
public static int createProgram(String vertexSource,
String fragmentSource) {
int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);
if (vertexShader == 0) {
return 0;
}
int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
if (pixelShader == 0) {
return 0;
}
int program = GLES20.glCreateProgram();
if (program != 0) {
GLES20.glAttachShader(program, vertexShader);
checkGlError("glAttachShader");
GLES20.glAttachShader(program, pixelShader);
checkGlError("glAttachShader");
GLES20.glLinkProgram(program);
int[] linkStatus = new int[1];
GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);
if (linkStatus[0] != GLES20.GL_TRUE) {
String info = GLES20.glGetProgramInfoLog(program);
GLES20.glDeleteProgram(program);
program = 0;
throw new RuntimeException("Could not link program: " + info);
}
}
return program;
}
public static void checkGlError(String op) {
int error;
while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
throw new RuntimeException(op + ": glError " + error);
}
}
public static void initTexParams() {
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S,
GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T,
GLES20.GL_CLAMP_TO_EDGE);
}
}
================================================
FILE: src/org/cyanogenmod/focal/feats/PixelBuffer.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.feats;
import android.app.ActivityManager;
import android.content.Context;
import android.graphics.Bitmap;
import android.opengl.GLSurfaceView;
import android.util.Log;
import fr.xplod.focal.R;
import org.cyanogenmod.focal.Util;
import java.nio.IntBuffer;
import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.egl.EGLContext;
import javax.microedition.khronos.egl.EGLDisplay;
import javax.microedition.khronos.egl.EGLSurface;
import javax.microedition.khronos.opengles.GL10;
import static javax.microedition.khronos.egl.EGL10.EGL_ALPHA_SIZE;
import static javax.microedition.khronos.egl.EGL10.EGL_BLUE_SIZE;
import static javax.microedition.khronos.egl.EGL10.EGL_DEFAULT_DISPLAY;
import static javax.microedition.khronos.egl.EGL10.EGL_DEPTH_SIZE;
import static javax.microedition.khronos.egl.EGL10.EGL_GREEN_SIZE;
import static javax.microedition.khronos.egl.EGL10.EGL_HEIGHT;
import static javax.microedition.khronos.egl.EGL10.EGL_LARGEST_PBUFFER;
import static javax.microedition.khronos.egl.EGL10.EGL_NONE;
import static javax.microedition.khronos.egl.EGL10.EGL_NO_CONTEXT;
import static javax.microedition.khronos.egl.EGL10.EGL_RED_SIZE;
import static javax.microedition.khronos.egl.EGL10.EGL_STENCIL_SIZE;
import static javax.microedition.khronos.egl.EGL10.EGL_SUCCESS;
import static javax.microedition.khronos.egl.EGL10.EGL_WIDTH;
import static javax.microedition.khronos.opengles.GL10.GL_RGBA;
import static javax.microedition.khronos.opengles.GL10.GL_UNSIGNED_BYTE;
/**
* Offscreen OpenGL renderer
*/
public class PixelBuffer {
final static String TAG = "PixelBuffer";
final static boolean LIST_CONFIGS = false;
final static private int EGL_OPENGL_ES2_BIT = 4;
final static private int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
final static private int EGL_BIND_TO_TEXTURE_RGB = 0x3039;
final static private int EGL_BIND_TO_TEXTURE_RGBA = 0x303A;
final static private int EGL_TEXTURE_FORMAT = 0x3080;
final static private int EGL_TEXTURE_TARGET = 0x3081;
final static private int EGL_TEXTURE_RGB = 0x305D;
final static private int EGL_TEXTURE_RGBA = 0x305E;
final static private int EGL_TEXTURE_2D = 0x305F;
GLSurfaceView.Renderer mRenderer; // Borrow this interface
int mWidth, mHeight;
Bitmap mBitmap;
EGL10 mEGL;
EGLDisplay mEGLDisplay;
EGLConfig[] mEGLConfigs;
EGLConfig mEGLConfig;
EGLContext mEGLContext;
EGLSurface mEGLSurface;
GL10 mGL;
Context mContext;
String mThreadOwner;
public PixelBuffer(Context context, int width, int height) {
mWidth = width;
mHeight = height;
mContext = context;
int[] version = new int[2];
int[] attribList = null;
final int mMaxTextureSize =
mContext.getResources().getInteger(R.integer.config_maxTextureSize);
if (mWidth < mMaxTextureSize && mHeight < mMaxTextureSize) {
// The texture is smaller than the maximum supported size, use it directly.
attribList = new int[] {
EGL_WIDTH, mWidth,
EGL_HEIGHT, mHeight,
EGL_TEXTURE_FORMAT, EGL_TEXTURE_RGBA,
EGL_TEXTURE_TARGET, EGL_TEXTURE_2D,
EGL_NONE
};
} else {
// Use the maximum supported texture size.
attribList = new int[] {
EGL_WIDTH, mMaxTextureSize,
EGL_HEIGHT, mMaxTextureSize,
EGL_LARGEST_PBUFFER, 1,
EGL_TEXTURE_FORMAT, EGL_TEXTURE_RGBA,
EGL_TEXTURE_TARGET, EGL_TEXTURE_2D,
EGL_NONE
};
}
// No error checking performed, minimum required code to elucidate logic
mEGL = (EGL10) EGLContext.getEGL();
mEGLDisplay = mEGL.eglGetDisplay(EGL_DEFAULT_DISPLAY);
mEGL.eglInitialize(mEGLDisplay, version);
mEGLConfig = chooseConfig(); // Choosing a config is a little more complicated
// Make sure you run in OpenGL ES 2.0, as everything in Nemesis uses a shader pipeline
mEGLContext = mEGL.eglCreateContext(mEGLDisplay, mEGLConfig, EGL_NO_CONTEXT, new int[] {
EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE
});
mEGLSurface = mEGL.eglCreatePbufferSurface(mEGLDisplay, mEGLConfig, attribList);
mEGL.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext);
mGL = (GL10) mEGLContext.getGL();
// Record thread owner of OpenGL context
mThreadOwner = Thread.currentThread().getName();
}
public void setRenderer(GLSurfaceView.Renderer renderer) {
mRenderer = renderer;
// Does this thread own the OpenGL context?
if (!Thread.currentThread().getName().equals(mThreadOwner)) {
Log.e(TAG, "setRenderer: This thread does not own the OpenGL context.");
return;
}
// Call the renderer initialization routines
mRenderer.onSurfaceCreated(mGL, mEGLConfig);
mRenderer.onSurfaceChanged(mGL, mWidth, mHeight);
}
public Bitmap getBitmap() {
// Do we have a renderer?
if (mRenderer == null) {
Log.e(TAG, "getBitmap: Renderer was not set.");
return null;
}
// Does this thread own the OpenGL context?
if (!Thread.currentThread().getName().equals(mThreadOwner)) {
Log.e(TAG, "getBitmap: This thread does not own the OpenGL context.");
return null;
}
// Call the renderer draw routine
mRenderer.onDrawFrame(mGL);
convertToBitmap();
return mBitmap;
}
private EGLConfig chooseConfig() {
int[] attribList = new int[] {
EGL_DEPTH_SIZE, 0,
EGL_STENCIL_SIZE, 0,
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_ALPHA_SIZE, 8,
EGL_BIND_TO_TEXTURE_RGBA, 1,
EGL10.EGL_SURFACE_TYPE, EGL10.EGL_PBUFFER_BIT,
EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
EGL_NONE
};
// No error checking performed, minimum required code to elucidate logic
// Expand on this logic to be more selective in choosing a configuration
int[] numConfig = new int[1];
mEGL.eglChooseConfig(mEGLDisplay, attribList, null, 0, numConfig);
int configSize = numConfig[0];
mEGLConfigs = new EGLConfig[configSize];
mEGL.eglChooseConfig(mEGLDisplay, attribList, mEGLConfigs, configSize, numConfig);
int error = mEGL.eglGetError();
if (error != EGL_SUCCESS) {
Log.e(TAG, "eglChooseConfig: " + error);
}
if (LIST_CONFIGS) {
listConfig();
}
return mEGLConfigs[0]; // Best match is probably the first configuration
}
private void listConfig() {
Log.i(TAG, "Config List {");
for (EGLConfig config : mEGLConfigs) {
int d, s, r, g, b, a;
// Expand on this logic to dump other attributes
d = getConfigAttrib(config, EGL_DEPTH_SIZE);
s = getConfigAttrib(config, EGL_STENCIL_SIZE);
r = getConfigAttrib(config, EGL_RED_SIZE);
g = getConfigAttrib(config, EGL_GREEN_SIZE);
b = getConfigAttrib(config, EGL_BLUE_SIZE);
a = getConfigAttrib(config, EGL_ALPHA_SIZE);
Log.i(TAG, " = <" + d + "," + s + "," +
r + "," + g + "," + b + "," + a + ">");
}
Log.i(TAG, "}");
}
private int getConfigAttrib(EGLConfig config, int attribute) {
int[] value = new int[1];
return mEGL.eglGetConfigAttrib(mEGLDisplay, config,
attribute, value)? value[0] : 0;
}
private void convertToBitmap() {
System.gc();
Runtime.getRuntime().gc();
final int mMaxTextureSize =
mContext.getResources().getInteger(R.integer.config_maxTextureSize);
boolean isScaled = (mWidth > mMaxTextureSize || mHeight > mMaxTextureSize);
int scaledWidth = isScaled ? mMaxTextureSize : mWidth;
int scaledHeight = isScaled ? mMaxTextureSize : mHeight;
IntBuffer ib = IntBuffer.allocate(scaledWidth*scaledHeight);
IntBuffer ibt = IntBuffer.allocate(scaledWidth*scaledHeight);
mGL.glReadPixels(0, 0, scaledWidth, scaledHeight, GL_RGBA, GL_UNSIGNED_BYTE, ib);
// Convert upside down mirror-reversed image to right-side up normal image.
for (int i = 0; i < scaledHeight; i++) {
for (int j = 0; j < scaledWidth; j++) {
ibt.put((scaledHeight-i-1)*scaledWidth + j, ib.get(i*scaledWidth + j));
}
}
mBitmap = Bitmap.createBitmap(scaledWidth, scaledHeight, Bitmap.Config.ARGB_8888);
mBitmap.copyPixelsFromBuffer(ibt);
// Release IntBuffers memory
ibt = null;
ib = null;
if (isScaled) {
ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo();
ActivityManager activityManager =
(ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
activityManager.getMemoryInfo(mi);
long availableMegs = mi.availMem / 1048576L;
Log.d(TAG, "Available memory: " + availableMegs + "MB");
while (availableMegs < 200) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.w(TAG, "Waiting for more memory! (Free: " + availableMegs + "MB)");
// We're going to need some memory
System.gc();
Runtime.getRuntime().gc();
activityManager.getMemoryInfo(mi);
availableMegs = mi.availMem / 1048576L;
}
// Image was converted to a power of two texture, scale it back
Log.v(TAG, "Image was scaled, scaling back to " + mWidth + "x" + mHeight);
Bitmap scaled = Bitmap.createScaledBitmap(mBitmap, mWidth, mHeight, true);
mBitmap.recycle();
mBitmap = scaled;
}
}
}
================================================
FILE: src/org/cyanogenmod/focal/feats/SoftwareHdrCapture.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.feats;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.net.Uri;
import android.os.Handler;
import android.os.IBinder;
import android.util.Log;
import org.cyanogenmod.focal.CameraActivity;
import org.cyanogenmod.focal.SnapshotManager;
import org.cyanogenmod.focal.Util;
import org.cyanogenmod.focal.ui.ShutterButton;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
/**
* Software HDR capture mode
*/
public class SoftwareHdrCapture extends CaptureTransformer {
public final static String TAG = "SoftwareHdrCapture";
private final static int SHOTS_COUNT = 3;
private int mShotsDone;
private boolean mBurstInProgress = false;
private Handler mHandler;
private CameraActivity mActivity;
private List mPictures;
private List mPicturesUri;
private static SoftwareHdrRenderingService mBoundService;
private static boolean mIsBound;
private static ServiceConnection mServiceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
// This is called when the connection with the service has been
// established, giving us the service object we can use to
// interact with the service. Because we have bound to a explicit
// service that we know is running in our own process, we can
// cast its IBinder to a concrete class and directly access it.
mBoundService = ((SoftwareHdrRenderingService.LocalBinder)service).getService();
}
public void onServiceDisconnected(ComponentName className) {
// This is called when the connection with the service has been
// unexpectedly disconnected -- that is, its process crashed.
// Because it is running in our same process, we should never
// see this happen.
mBoundService = null;
}
};
public SoftwareHdrCapture(CameraActivity activity) {
super(activity.getCamManager(), activity.getSnapManager());
mHandler = new Handler();
mPictures = new ArrayList();
mPicturesUri = new ArrayList();
mActivity = activity;
doBindService();
}
public static ServiceConnection getServiceConnection() {
return mServiceConnection;
}
public static boolean isServiceBound() {
return mIsBound;
}
void doBindService() {
// Establish a connection with the service. We use an explicit
// class name because we want a specific service implementation that
// we know will be running in our own process (and thus won't be
// supporting component replacement by other applications).
Log.v(TAG, "Binding Software HDR rendering service");
mActivity.bindService(new Intent(mActivity, SoftwareHdrRenderingService.class),
mServiceConnection, Context.BIND_AUTO_CREATE);
mIsBound = true;
}
public int getShotExposure(int shotId) {
if (shotId == 0) {
return mCamManager.getParameters().getMinExposureCompensation();
} else if (shotId == 1) {
return 0;
} else if (shotId == 2) {
return mCamManager.getParameters().getMaxExposureCompensation();
}
Log.e(TAG, "Unknown shot exposure ID " + shotId);
return 0;
}
/**
* Starts the HDR shooting
*/
public void startBurstShot() {
mShotsDone = 0;
mBurstInProgress = true;
mSnapManager.queueSnapshot(true, getShotExposure(mShotsDone));
// Open the quick review drawer
mActivity.getReviewDrawer().openQuickReview();
}
private void tryTakeShot() {
mHandler.post(new Runnable() {
@Override
public void run() {
mSnapManager.setBypassProcessing(true);
mSnapManager.queueSnapshot(true, getShotExposure(mShotsDone));
}
});
}
@Override
public void onShutterButtonClicked(ShutterButton button) {
startBurstShot();
}
@Override
public void onSnapshotShutter(final SnapshotManager.SnapshotInfo info) {
}
@Override
public void onSnapshotPreview(SnapshotManager.SnapshotInfo info) {
}
@Override
public void onSnapshotProcessing(SnapshotManager.SnapshotInfo info) {
}
@Override
public void onSnapshotSaved(SnapshotManager.SnapshotInfo info) {
if (!mBurstInProgress) {
return;
}
mPictures.add(Uri.fromFile(new File(Util.getRealPathFromURI(mActivity, info.mUri))));
mPicturesUri.add(info.mUri);
mShotsDone++;
Log.v(TAG, "Done " + mShotsDone + " shots");
if (mShotsDone < SHOTS_COUNT) {
tryTakeShot();
} else {
// Reset exposure
mCamManager.getParameters().setExposureCompensation(0);
// Render
int orientation = (360 - mActivity.getOrientation()) % 360;
mBoundService.render(mPictures, mPicturesUri, mActivity.getSnapManager(), orientation);
mShotsDone = 0;
}
}
@Override
public void onMediaSavingStart() {
}
@Override
public void onMediaSavingDone() {
}
@Override
public void onVideoRecordingStart() {
}
@Override
public void onVideoRecordingStop() {
}
public void tearDown() {
if (mIsBound) {
// Detach our existing connection.
mActivity.unbindService(mServiceConnection);
mIsBound = false;
}
}
}
================================================
FILE: src/org/cyanogenmod/focal/feats/SoftwareHdrProcessor.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.feats;
import android.content.Context;
import android.net.Uri;
import android.util.Log;
import org.cyanogenmod.focal.SnapshotManager;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.RandomAccessFile;
import java.util.List;
/**
* Manages the processing of multiple shots into one HDR shot
*/
public class SoftwareHdrProcessor {
public final static String TAG = "SoftwareHdr";
private String mPathPrefix;
private File mTempPath;
private List mPictures;
private SnapshotManager mSnapManager;
private Uri mOutputUri;
private String mOutputTitle;
private Context mContext;
private BufferedReader mProcStdOut;
private BufferedReader mProcStdErr;
private Thread mOutputLogger = new Thread() {
public void run() {
while (true) {
try {
Thread.sleep(10);
consumeProcLogs();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
public SoftwareHdrProcessor(Context context, SnapshotManager snapMan) {
mSnapManager = snapMan;
mContext = context;
}
public void setPictures(List pictures) {
mPictures = pictures;
}
public File getTempPath() {
return mTempPath;
}
private void run(String command) throws IOException {
Runtime rt = Runtime.getRuntime();
Process proc = rt.exec(command, new String[]{"PATH="+mPathPrefix+":/system/bin",
"LD_LIBRARY_PATH="+mPathPrefix+":/system/lib"});
mProcStdOut = new BufferedReader(new
InputStreamReader(proc.getInputStream()));
mProcStdErr = new BufferedReader(new
InputStreamReader(proc.getErrorStream()));
try {
proc.waitFor();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public boolean render(final int orientation) {
mOutputLogger.start();
// Prepare a temporary directory
Log.d(TAG, "Preparing temp dir for Software HDR rendering...");
File appFilesDir = mContext.getFilesDir();
mPathPrefix = appFilesDir.getAbsolutePath() + "/";
String tempPathStr = appFilesDir.getAbsolutePath() + "/" + System.currentTimeMillis();
mTempPath = new File(tempPathStr);
mTempPath.mkdir();
// Process our images
try {
if (!doAlignImageStack()) {
return false;
}
if (!doEnfuse()) {
return false;
}
// Save it to gallery
// XXX: This needs opening the output byte array...
// Isn't there any way to update gallery data without having to reload/save
// the JPEG file? Because we have it already, we could just move it.
byte[] jpegData;
RandomAccessFile f = new RandomAccessFile(mTempPath+"/final.jpg", "r");
try {
// Get and check length
long longlength = f.length();
int length = (int) longlength;
if (length != longlength) {
throw new IOException("File size >= 2 GB");
}
// Read file and return data
jpegData = new byte[length];
f.readFully(jpegData);
} finally {
f.close();
}
mSnapManager.prepareNamerUri(100,100);
mOutputUri = mSnapManager.getNamerUri();
mOutputTitle = mSnapManager.getNamerTitle();
mSnapManager.saveImage(mOutputUri, mOutputTitle, 100, 100, orientation, jpegData);
} catch (IOException ex) {
Log.e(TAG, "Unable to process: ", ex);
return false;
}
return true;
}
private void consumeProcLogs() {
String line;
try {
if (mProcStdOut != null && mProcStdOut.ready()) {
while ((line = mProcStdOut.readLine()) != null) {
Log.i(TAG, line);
}
}
if (mProcStdErr != null && mProcStdErr.ready()) {
while ((line = mProcStdErr.readLine()) != null) {
Log.e(TAG, line);
}
}
} catch (IOException e) {
Log.e(TAG, "Error while consuming proc logs", e);
}
}
private boolean doAlignImageStack() throws IOException {
Log.d(TAG, "Align Image Stack...");
String filesStr = "";
for (Uri picture : mPictures) {
if (new File(picture.getPath()).exists()) {
filesStr += " " + picture.getPath();
}
}
run("align_image_stack -v -v -v -C -g 4 -a "+mTempPath+"/project " + filesStr);
consumeProcLogs();
Log.d(TAG, "Align Image Stack... done");
return true;
}
private boolean doEnfuse() throws IOException {
Log.d(TAG, "Enfuse...");
// Build the list of output files. The convention set up by
// AlignImageStack is projectXXXX.tif, so we basically build that
// list out of the number of shots we fed to align_image_stack
String files = "";
for (int i = 0; i < mPictures.size(); i++) {
// Check if file exists, otherwise enfuse will fail
String filePath = mTempPath + "/" + String.format("project%04d.tif", i);
if (new File(filePath).exists()) {
files += " " + filePath;
}
}
run("enfuse -o "+mTempPath+"/final.jpg --compression=jpeg " + files);
consumeProcLogs();
Log.d(TAG, "Enfuse... done");
return true;
}
}
================================================
FILE: src/org/cyanogenmod/focal/feats/SoftwareHdrRenderingService.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.feats;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Intent;
import android.net.Uri;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;
import fr.xplod.focal.R;
import org.cyanogenmod.focal.SnapshotManager;
import org.cyanogenmod.focal.Util;
import org.cyanogenmod.focal.picsphere.PicSphere;
import java.io.File;
import java.util.List;
/**
* Service that handles HDR rendering outside
* the app context (as the rendering thread would get killed)
*/
public class SoftwareHdrRenderingService extends Service {
private NotificationManager mNM;
// Unique Identification Number for the Notification.
// We use it on Notification start, and to cancel it.
private int NOTIFICATION = 4242;
private boolean mHasFailed = false;
/**
* Class for clients to access. Because we know this service always
* runs in the same process as its clients, we don't need to deal with
* IPC.
*/
public class LocalBinder extends Binder {
SoftwareHdrRenderingService getService() {
return SoftwareHdrRenderingService.this;
}
}
@Override
public void onCreate() {
mNM = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i("LocalService", "Received start id " + startId + ": " + intent);
// We want this service to continue running until it is explicitly
// stopped, so return sticky.
return START_STICKY;
}
@Override
public void onDestroy() {
// Cancel the persistent notification.
mNM.cancel(NOTIFICATION);
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
// This is the object that receives interactions from clients. See
// RemoteService for a more complete example.
private final IBinder mBinder = new LocalBinder();
public void render(final List pictures, final List picturesUri,
final SnapshotManager snapMan, final int orientation) {
// Display a notification
mNM.notify(NOTIFICATION, buildProgressNotification());
mHasFailed = false;
new Thread() {
public void run() {
SoftwareHdrProcessor processor =
new SoftwareHdrProcessor(SoftwareHdrRenderingService.this, snapMan);
processor.setPictures(pictures);
if (processor.render(orientation)) {
mNM.cancel(NOTIFICATION);
removeTempFiles(picturesUri, processor.getTempPath());
} else {
mHasFailed = true;
mNM.notify(NOTIFICATION, buildFailureNotification(getString(
R.string.software_hdr_failed), getString(
R.string.software_hdr_failed_details)));
}
SoftwareHdrRenderingService.this.stopSelf();
}
}.start();
}
private void removeTempFiles(List pictures, File tempPath) {
// Remove source pictures and temporary path
for (Uri uri : pictures) {
List segments = uri.getPathSegments();
Util.removeFromGallery(getContentResolver(),
Integer.parseInt(segments.get(segments.size() - 1)));
}
tempPath.delete();
}
private Notification buildProgressNotification() {
Notification.Builder mBuilder =
new Notification.Builder(this)
.setSmallIcon(R.drawable.ic_launcher)
.setContentTitle(getString(R.string.software_hdr_notif_title))
.setContentText(getString(R.string.please_wait))
.setOngoing(true);
return mBuilder.build();
}
private Notification buildFailureNotification(String title, String text) {
Notification.Builder mBuilder =
new Notification.Builder(this)
.setSmallIcon(R.drawable.ic_launcher)
.setContentTitle(title)
.setContentText(text)
.setOngoing(false);
return mBuilder.build();
}
}
================================================
FILE: src/org/cyanogenmod/focal/feats/TextureRenderer.java
================================================
/*
* Copyright (C) 2012 The Android Open Source Project
* Copyright (C) 2013 Guillaume Lesniak
*
* Licensed under the Apache License, Version 2.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 org.cyanogenmod.focal.feats;
import android.opengl.GLES20;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
public class TextureRenderer {
private int mProgram;
private int mTexSamplerHandle;
private int mTexCoordHandle;
private int mPosCoordHandle;
private FloatBuffer mTexVertices;
private FloatBuffer mPosVertices;
private int mViewWidth;
private int mViewHeight;
private int mTexWidth;
private int mTexHeight;
private static final String VERTEX_SHADER =
"attribute vec4 a_position;\n" +
"attribute vec2 a_texcoord;\n" +
"varying vec2 v_texcoord;\n" +
"void main() {\n" +
" gl_Position = a_position;\n" +
" v_texcoord = a_texcoord;\n" +
"}\n";
private static final String FRAGMENT_SHADER =
"precision mediump float;\n" +
"uniform sampler2D tex_sampler;\n" +
"varying vec2 v_texcoord;\n" +
"void main() {\n" +
" gl_FragColor = texture2D(tex_sampler, v_texcoord);\n" +
"}\n";
private static final float[] TEX_VERTICES = {
0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f
};
private static final float[] POS_VERTICES = {
-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f
};
private static final int FLOAT_SIZE_BYTES = 4;
public void init() {
// Create program
mProgram = GLToolbox.createProgram(VERTEX_SHADER, FRAGMENT_SHADER);
// Bind attributes and uniforms
mTexSamplerHandle = GLES20.glGetUniformLocation(mProgram,
"tex_sampler");
mTexCoordHandle = GLES20.glGetAttribLocation(mProgram, "a_texcoord");
mPosCoordHandle = GLES20.glGetAttribLocation(mProgram, "a_position");
// Setup coordinate buffers
mTexVertices = ByteBuffer.allocateDirect(
TEX_VERTICES.length * FLOAT_SIZE_BYTES)
.order(ByteOrder.nativeOrder()).asFloatBuffer();
mTexVertices.put(TEX_VERTICES).position(0);
mPosVertices = ByteBuffer.allocateDirect(
POS_VERTICES.length * FLOAT_SIZE_BYTES)
.order(ByteOrder.nativeOrder()).asFloatBuffer();
mPosVertices.put(POS_VERTICES).position(0);
}
public void tearDown() {
GLES20.glDeleteProgram(mProgram);
}
public void updateTextureSize(int texWidth, int texHeight) {
mTexWidth = texWidth;
mTexHeight = texHeight;
computeOutputVertices();
}
public void updateViewSize(int viewWidth, int viewHeight) {
mViewWidth = viewWidth;
mViewHeight = viewHeight;
computeOutputVertices();
}
public void renderTexture(int texId) {
// Bind default FBO
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
// Use our shader program
GLES20.glUseProgram(mProgram);
GLToolbox.checkGlError("glUseProgram");
// Set viewport
GLES20.glViewport(0, 0, mViewWidth, mViewHeight);
GLToolbox.checkGlError("glViewport");
// Disable blending
GLES20.glDisable(GLES20.GL_BLEND);
// Set the vertex attributes
GLES20.glVertexAttribPointer(mTexCoordHandle, 2, GLES20.GL_FLOAT, false,
0, mTexVertices);
GLES20.glEnableVertexAttribArray(mTexCoordHandle);
GLES20.glVertexAttribPointer(mPosCoordHandle, 2, GLES20.GL_FLOAT, false,
0, mPosVertices);
GLES20.glEnableVertexAttribArray(mPosCoordHandle);
GLToolbox.checkGlError("vertex attribute setup");
// Set the input texture
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLToolbox.checkGlError("glActiveTexture");
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texId);
GLToolbox.checkGlError("glBindTexture");
GLES20.glUniform1i(mTexSamplerHandle, 0);
// Draw
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
}
private void computeOutputVertices() {
if (mPosVertices != null) {
float imgAspectRatio = mTexWidth / (float)mTexHeight;
float viewAspectRatio = mViewWidth / (float)mViewHeight;
float relativeAspectRatio = viewAspectRatio / imgAspectRatio;
float x0, y0, x1, y1;
if (relativeAspectRatio > 1.0f) {
x0 = -1.0f / relativeAspectRatio;
y0 = -1.0f;
x1 = 1.0f / relativeAspectRatio;
y1 = 1.0f;
} else {
x0 = -1.0f;
y0 = -relativeAspectRatio;
x1 = 1.0f;
y1 = relativeAspectRatio;
}
float[] coords = new float[] { x0, y0, x1, y0, x0, y1, x1, y1 };
mPosVertices.put(coords).position(0);
}
}
}
================================================
FILE: src/org/cyanogenmod/focal/feats/TimerCapture.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.feats;
import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.speech.RecognitionListener;
import android.speech.RecognizerIntent;
import android.speech.SpeechRecognizer;
import android.util.Log;
import org.cyanogenmod.focal.CameraActivity;
import fr.xplod.focal.R;
import org.cyanogenmod.focal.SnapshotManager;
import org.cyanogenmod.focal.ui.ShutterButton;
import java.util.ArrayList;
/**
* Handles the timer before a capture, or the voice shutter feature
*/
public class TimerCapture extends CaptureTransformer implements RecognitionListener {
public final static String TAG = "TimerCapture";
public final static int VOICE_TIMER_VALUE = -1;
private int mTimer;
private CameraActivity mActivity;
private SpeechRecognizer mSpeechRecognizer;
private Intent mSpeechRecognizerIntent;
private AudioManager mAudioManager;
private String[] mShutterWords;
private boolean mIsMuted;
private boolean mIsInitialised;
private CountDownTimer mCountDown;
public TimerCapture(CameraActivity activity) {
super(activity.getCamManager(), activity.getSnapManager());
mActivity = activity;
mIsInitialised = false;
mIsMuted = false;
mTimer = 5;
}
/**
* Sets the amount of seconds before taking a shot, or VOICE_TIMER_VALUE for voice
* trigger commands
*
* @param seconds Number of seconds before taking a shots, or VOICE_TIMER_VALUE
*/
public void setTimer(int seconds) {
if (seconds == mTimer) return;
if (mTimer == VOICE_TIMER_VALUE) {
// We were in voice shutter mode, disable it
clearVoiceShutter();
}
mTimer = seconds;
if (seconds == VOICE_TIMER_VALUE) {
initializeVoiceShutter();
}
}
public int getTimer() {
return mTimer;
}
public void clearVoiceShutter() {
//Log.d(TAG,"Stopping speach recog - " + mSpeechActive + "/" + enable);
//mSpeechActive = false;
if (mIsMuted) {
mAudioManager.setStreamMute(AudioManager.STREAM_SYSTEM, false);
mIsMuted = false;
}
//mPhotoModule.updateVoiceShutterIndicator(false);
mSpeechRecognizer.cancel();
}
public void initializeVoiceShutter() {
if (!mIsInitialised) {
Context context = mCamManager.getContext();
if (context == null) {
Log.e(TAG, "Could not initialise voice shutter because of null context");
}
mSpeechRecognizer = SpeechRecognizer.createSpeechRecognizer(context);
mSpeechRecognizer.setRecognitionListener(this);
mSpeechRecognizerIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
mSpeechRecognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
mSpeechRecognizerIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE,
"org.cyanogenmod.voiceshutter");
mSpeechRecognizerIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 10);
mSpeechRecognizerIntent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true);
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
mShutterWords = context.getResources().getStringArray(
R.array.transformer_timer_voice_words);
}
// Turn it on
if (!mIsMuted) {
/* Avoid beeps when re-arming the listener */
mIsMuted = true;
mAudioManager.setStreamMute(AudioManager.STREAM_SYSTEM, true);
}
//Log.d(TAG,"Starting speach recog");
// TODO: mPhotoModule.updateVoiceShutterIndicator(true);
mSpeechRecognizer.startListening(mSpeechRecognizerIntent);
}
@Override
public void onShutterButtonClicked(ShutterButton button) {
if (mTimer > 0) {
mActivity.startTimerCountdown(mTimer * 1000);
mCountDown = new CountDownTimer(mTimer * 1000, 1000) {
@Override
public void onTick(long l) {
updateTimerIndicator();
}
@Override
public void onFinish() {
mSnapManager.queueSnapshot(true, 0);
mActivity.hideTimerCountdown();
}
};
mCountDown.start();
} else {
mSnapManager.queueSnapshot(true, 0);
}
}
@Override
public void onSnapshotShutter(SnapshotManager.SnapshotInfo info) {
}
@Override
public void onSnapshotPreview(SnapshotManager.SnapshotInfo info) {
}
@Override
public void onSnapshotProcessing(SnapshotManager.SnapshotInfo info) {
}
@Override
public void onSnapshotSaved(SnapshotManager.SnapshotInfo info) {
}
@Override
public void onMediaSavingStart() {
}
@Override
public void onMediaSavingDone() {
}
@Override
public void onVideoRecordingStart() {
}
@Override
public void onVideoRecordingStop() {
}
@Override
public void onReadyForSpeech(Bundle bundle) {
Log.d(TAG, "ON READY FOR SPEECH");
//mIsCountDownOn = true;
//mNoSpeechCountDown.start();
if (mIsMuted) {
mAudioManager.setStreamMute(AudioManager.STREAM_SYSTEM, false);
mIsMuted = false;
}
}
@Override
public void onBeginningOfSpeech() {
}
@Override
public void onRmsChanged(float v) {
}
@Override
public void onBufferReceived(byte[] bytes) {
}
@Override
public void onEndOfSpeech() {
}
@Override
public void onError(int error) {
Log.e(TAG, "error " + error);
mIsInitialised = false;
initializeVoiceShutter();
}
@Override
public void onResults(Bundle bundle) {
onPartialResults(bundle);
initializeVoiceShutter();
}
@Override
public void onPartialResults(Bundle partialResults) {
ArrayList data = partialResults.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION);
if (data == null) {
Log.e(TAG, "Null Partial Results");
return;
}
String str = "";
for (int i = 0; i < data.size(); i++) {
Log.d(TAG, "result " + data.get(i));
for (int f = 0; f < mShutterWords.length; f++) {
String[] resultWords = data.get(i).toString().split(" ");
for (int g = 0; g < resultWords.length; g++) {
if (mShutterWords[f].equalsIgnoreCase(resultWords[g])) {
Log.d(TAG, "matched to hotword! FIRE SHUTTER!");
mSnapManager.queueSnapshot(true, 0);
//mSpeechActive = false;
//enableSpeechRecognition(false, null);
}
}
}
str += data.get(i);
}
}
@Override
public void onEvent(int i, Bundle bundle) {
}
public void updateTimerIndicator() {
}
}
================================================
FILE: src/org/cyanogenmod/focal/pano/Mosaic.java
================================================
/*
* Copyright (C) 2011 The Android Open Source Project
* Copyright (C) 2013 Guillaume Lesniak
*
* Licensed under the Apache License, Version 2.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 org.cyanogenmod.focal.pano;
/**
* The Java interface to JNI calls regarding mosaic stitching.
*
* A high-level usage is:
*
* Mosaic mosaic = new Mosaic();
* mosaic.setSourceImageDimensions(width, height);
* mosaic.reset(blendType);
*
* while ((pixels = hasNextImage()) != null) {
* mosaic.setSourceImage(pixels);
* }
*
* mosaic.createMosaic(highRes);
* byte[] result = mosaic.getFinalMosaic();
*
*/
public class Mosaic {
/**
* In this mode, the images are stitched together in the same spatial arrangement as acquired
* i.e. if the user follows a curvy trajectory, the image boundary of the resulting mosaic will
* be curved in the same manner. This mode is useful if the user wants to capture a mosaic as
* if "painting" the scene using the smart-phone device and does not want any corrective warps
* to distort the captured images.
*/
public static final int BLENDTYPE_FULL = 0;
/**
* This mode is the same as BLENDTYPE_FULL except that the resulting mosaic is rotated
* to balance the first and last images to be approximately at the same vertical offset in the
* output mosaic. This is useful when acquiring a mosaic by a typical panning-like motion to
* remove a one-sided curve in the mosaic (typically due to the camera not staying horizontal
* during the video capture) and convert it to a more symmetrical "smiley-face" like output.
*/
public static final int BLENDTYPE_PAN = 1;
/**
* This mode compensates for typical "smiley-face" like output in longer mosaics and creates
* a rectangular mosaic with minimal black borders (by unwrapping the mosaic onto an imaginary
* cylinder). If the user follows a curved trajectory (instead of a perfect panning trajectory),
* the resulting mosaic here may suffer from some image distortions in trying to map the
* trajectory to a cylinder.
*/
public static final int BLENDTYPE_CYLINDERPAN = 2;
/**
* This mode is basically BLENDTYPE_CYLINDERPAN plus doing a rectangle cropping before returning
* the mosaic. The mode is useful for making the resulting mosaic have a rectangle shape.
*/
public static final int BLENDTYPE_HORIZONTAL =3;
/**
* This strip type will use the default thin strips where the strips are
* spaced according to the image capture rate.
*/
public static final int STRIPTYPE_THIN = 0;
/**
* This strip type will use wider strips for blending. The strip separation
* is controlled by a threshold on the native side. Since the strips are
* wider, there is an additional cross-fade blending step to make the seam
* boundaries smoother. Since this mode uses lesser image frames, it is
* computationally more efficient than the thin strip mode.
*/
public static final int STRIPTYPE_WIDE = 1;
/**
* Return flags returned by createMosaic() are one of the following.
*/
public static final int MOSAIC_RET_OK = 1;
public static final int MOSAIC_RET_ERROR = -1;
public static final int MOSAIC_RET_CANCELLED = -2;
public static final int MOSAIC_RET_LOW_TEXTURE = -3;
public static final int MOSAIC_RET_FEW_INLIERS = 2;
static {
System.loadLibrary("jni_mosaic2");
}
/**
* Allocate memory for the image frames at the given resolution.
*
* @param width width of the input frames in pixels
* @param height height of the input frames in pixels
*/
public native void allocateMosaicMemory(int width, int height);
/**
* Free memory allocated by allocateMosaicMemory.
*
*/
public native void freeMosaicMemory();
/**
* Pass the input image frame to the native layer. Each time the a new
* source image t is set, the transformation matrix from the first source
* image to t is computed and returned.
*
* @param pixels source image of NV21 format.
* @return Float array of length 11; first 9 entries correspond to the 3x3
* transformation matrix between the first frame and the passed frame;
* the 10th entry is the number of the passed frame, where the counting
* starts from 1; and the 11th entry is the returning code, whose value
* is one of those MOSAIC_RET_* returning flags defined above.
*/
public native float[] setSourceImage(byte[] pixels);
/**
* This is an alternative to the setSourceImage function above. This should
* be called when the image data is already on the native side in a fixed
* byte array. In implementation, this array is filled by the GL thread
* using glReadPixels directly from GPU memory (where it is accessed by
* an associated SurfaceTexture).
*
* @return Float array of length 11; first 9 entries correspond to the 3x3
* transformation matrix between the first frame and the passed frame;
* the 10th entry is the number of the passed frame, where the counting
* starts from 1; and the 11th entry is the returning code, whose value
* is one of those MOSAIC_RET_* returning flags defined above.
*/
public native float[] setSourceImageFromGPU();
/**
* Set the type of blending.
*
* @param type the blending type defined in the class. {BLENDTYPE_FULL,
* BLENDTYPE_PAN, BLENDTYPE_CYLINDERPAN, BLENDTYPE_HORIZONTAL}
*/
public native void setBlendingType(int type);
/**
* Set the type of strips to use for blending.
* @param type the blending strip type to use {STRIPTYPE_THIN,
* STRIPTYPE_WIDE}.
*/
public native void setStripType(int type);
/**
* Tell the native layer to create the final mosaic after all the input frame
* data have been collected.
* The case of generating high-resolution mosaic may take dozens of seconds to finish.
*
* @param value True means generating a high-resolution mosaic -
* which is based on the original images set in setSourceImage().
* False means generating a low-resolution version -
* which is based on 1/4 downscaled images from the original images.
* @return Returns a status code suggesting if the mosaic building was
* successful, in error, or was cancelled by the user.
*/
public native int createMosaic(boolean value);
/**
* Get the data for the created mosaic.
*
* @return Returns an integer array which contains the final mosaic in the ARGB_8888 format.
* The first MosaicWidth*MosaicHeight values contain the image data, followed by 2
* integers corresponding to the values MosaicWidth and MosaicHeight respectively.
*/
public native int[] getFinalMosaic();
/**
* Get the data for the created mosaic.
*
* @return Returns a byte array which contains the final mosaic in the NV21 format.
* The first MosaicWidth*MosaicHeight*1.5 values contain the image data, followed by
* 8 bytes which pack the MosaicWidth and MosaicHeight integers into 4 bytes each
* respectively.
*/
public native byte[] getFinalMosaicNV21();
/**
* Reset the state of the frame arrays which maintain the captured frame data.
* Also re-initializes the native mosaic object to make it ready for capturing a new mosaic.
*/
public native void reset();
/**
* Get the progress status of the mosaic computation process.
* @param hires Boolean flag to select whether to report progress of the
* low-res or high-res mosaicer.
* @param cancelComputation Boolean flag to allow cancelling the
* mosaic computation when needed from the GUI end.
* @return Returns a number from 0-100 where 50 denotes that the mosaic
* computation is 50% done.
*/
public native int reportProgress(boolean hires, boolean cancelComputation);
}
================================================
FILE: src/org/cyanogenmod/focal/pano/MosaicFrameProcessor.java
================================================
/*
* Copyright (C) 2011 The Android Open Source Project
* Copyright (C) 2013 Guillaume Lesniak
*
* Licensed under the Apache License, Version 2.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 org.cyanogenmod.focal.pano;
import android.util.Log;
/**
* Class to handle the processing of each frame by Mosaicer.
*/
public class MosaicFrameProcessor {
private static final String TAG = "MosaicFrameProcessor";
private static final int NUM_FRAMES_IN_BUFFER = 2;
private static final int MAX_NUMBER_OF_FRAMES = 200;
private static final int MOSAIC_RET_CODE_INDEX = 10;
private static final int FRAME_COUNT_INDEX = 9;
private static final int X_COORD_INDEX = 2;
private static final int Y_COORD_INDEX = 5;
private static final int HR_TO_LR_DOWNSAMPLE_FACTOR = 4;
private static final int WINDOW_SIZE = 3;
private Mosaic mMosaicer;
private boolean mIsMosaicMemoryAllocated = false;
private float mTranslationLastX;
private float mTranslationLastY;
private int mFillIn = 0;
private int mTotalFrameCount = 0;
private int mLastProcessFrameIdx = -1;
private int mCurrProcessFrameIdx = -1;
private boolean mFirstRun;
// Panning rate is in unit of percentage of image content translation per
// frame. Use moving average to calculate the panning rate.
private float mPanningRateX;
private float mPanningRateY;
private float[] mDeltaX = new float[WINDOW_SIZE];
private float[] mDeltaY = new float[WINDOW_SIZE];
private int mOldestIdx = 0;
private float mTotalTranslationX = 0f;
private float mTotalTranslationY = 0f;
private ProgressListener mProgressListener;
private int mPreviewWidth;
private int mPreviewHeight;
private int mPreviewBufferSize;
private static MosaicFrameProcessor sMosaicFrameProcessor; // singleton
public interface ProgressListener {
public void onProgress(boolean isFinished, float panningRateX, float panningRateY,
float progressX, float progressY);
}
public static MosaicFrameProcessor getInstance() {
if (sMosaicFrameProcessor == null) {
sMosaicFrameProcessor = new MosaicFrameProcessor();
}
return sMosaicFrameProcessor;
}
private MosaicFrameProcessor() {
mMosaicer = new Mosaic();
}
public void setProgressListener(ProgressListener listener) {
mProgressListener = listener;
}
public int reportProgress(boolean hires, boolean cancel) {
return mMosaicer.reportProgress(hires, cancel);
}
public void initialize(int previewWidth, int previewHeight, int bufSize) {
mPreviewWidth = previewWidth;
mPreviewHeight = previewHeight;
mPreviewBufferSize = bufSize;
setupMosaicer(mPreviewWidth, mPreviewHeight, mPreviewBufferSize);
setStripType(Mosaic.STRIPTYPE_WIDE);
reset();
}
public void clear() {
if (mIsMosaicMemoryAllocated) {
mMosaicer.freeMosaicMemory();
mIsMosaicMemoryAllocated = false;
}
synchronized (this) {
notify();
}
}
public boolean isMosaicMemoryAllocated() {
return mIsMosaicMemoryAllocated;
}
public void setStripType(int type) {
mMosaicer.setStripType(type);
}
private void setupMosaicer(int previewWidth, int previewHeight, int bufSize) {
Log.v(TAG, "setupMosaicer w, h=" + previewWidth + ',' + previewHeight + ',' + bufSize);
if (mIsMosaicMemoryAllocated) {
throw new RuntimeException("MosaicFrameProcessor in use!");
}
mIsMosaicMemoryAllocated = true;
mMosaicer.allocateMosaicMemory(previewWidth, previewHeight);
}
public void reset() {
// reset() can be called even if MosaicFrameProcessor is not initialized.
// Only counters will be changed.
mFirstRun = true;
mTotalFrameCount = 0;
mFillIn = 0;
mTotalTranslationX = 0;
mTranslationLastX = 0;
mTotalTranslationY = 0;
mTranslationLastY = 0;
mPanningRateX = 0;
mPanningRateY = 0;
mLastProcessFrameIdx = -1;
mCurrProcessFrameIdx = -1;
for (int i = 0; i < WINDOW_SIZE; ++i) {
mDeltaX[i] = 0f;
mDeltaY[i] = 0f;
}
mMosaicer.reset();
}
public int createMosaic(boolean highRes) {
return mMosaicer.createMosaic(highRes);
}
public byte[] getFinalMosaicNV21() {
return mMosaicer.getFinalMosaicNV21();
}
// Processes the last filled image frame through the mosaicer and
// updates the UI to show progress.
// When done, processes and displays the final mosaic.
public void processFrame() {
if (!mIsMosaicMemoryAllocated) {
// clear() is called and buffers are cleared, stop computation.
// This can happen when the onPause() is called in the activity, but still some frames
// are not processed yet and thus the callback may be invoked.
return;
}
mCurrProcessFrameIdx = mFillIn;
mFillIn = ((mFillIn + 1) % NUM_FRAMES_IN_BUFFER);
// Check that we are trying to process a frame different from the
// last one processed (useful if this class was running asynchronously)
if (mCurrProcessFrameIdx != mLastProcessFrameIdx) {
mLastProcessFrameIdx = mCurrProcessFrameIdx;
// TODO: make the termination condition regarding reaching
// MAX_NUMBER_OF_FRAMES solely determined in the library.
if (mTotalFrameCount < MAX_NUMBER_OF_FRAMES) {
// If we are still collecting new frames for the current mosaic,
// process the new frame.
calculateTranslationRate();
// Publish progress of the ongoing processing
if (mProgressListener != null) {
mProgressListener.onProgress(false, mPanningRateX, mPanningRateY,
mTranslationLastX * HR_TO_LR_DOWNSAMPLE_FACTOR / mPreviewWidth,
mTranslationLastY * HR_TO_LR_DOWNSAMPLE_FACTOR / mPreviewHeight);
}
} else {
if (mProgressListener != null) {
mProgressListener.onProgress(true, mPanningRateX, mPanningRateY,
mTranslationLastX * HR_TO_LR_DOWNSAMPLE_FACTOR / mPreviewWidth,
mTranslationLastY * HR_TO_LR_DOWNSAMPLE_FACTOR / mPreviewHeight);
}
}
}
}
public void calculateTranslationRate() {
float[] frameData = mMosaicer.setSourceImageFromGPU();
int ret_code = (int) frameData[MOSAIC_RET_CODE_INDEX];
mTotalFrameCount = (int) frameData[FRAME_COUNT_INDEX];
float translationCurrX = frameData[X_COORD_INDEX];
float translationCurrY = frameData[Y_COORD_INDEX];
if (mFirstRun) {
// First time: no need to update delta values.
mTranslationLastX = translationCurrX;
mTranslationLastY = translationCurrY;
mFirstRun = false;
return;
}
// Moving average: remove the oldest translation/deltaTime and
// add the newest translation/deltaTime in
int idx = mOldestIdx;
mTotalTranslationX -= mDeltaX[idx];
mTotalTranslationY -= mDeltaY[idx];
mDeltaX[idx] = Math.abs(translationCurrX - mTranslationLastX);
mDeltaY[idx] = Math.abs(translationCurrY - mTranslationLastY);
mTotalTranslationX += mDeltaX[idx];
mTotalTranslationY += mDeltaY[idx];
// The panning rate is measured as the rate of the translation percentage in
// image width/height. Take the horizontal panning rate for example, the image width
// used in finding the translation is (PreviewWidth / HR_TO_LR_DOWNSAMPLE_FACTOR).
// To get the horizontal translation percentage, the horizontal translation,
// (translationCurrX - mTranslationLastX), is divided by the
// image width. We then get the rate by dividing the translation percentage with the
// number of frames.
mPanningRateX = mTotalTranslationX /
(mPreviewWidth / HR_TO_LR_DOWNSAMPLE_FACTOR) / WINDOW_SIZE;
mPanningRateY = mTotalTranslationY /
(mPreviewHeight / HR_TO_LR_DOWNSAMPLE_FACTOR) / WINDOW_SIZE;
mTranslationLastX = translationCurrX;
mTranslationLastY = translationCurrY;
mOldestIdx = (mOldestIdx + 1) % WINDOW_SIZE;
}
}
================================================
FILE: src/org/cyanogenmod/focal/pano/MosaicPreviewRenderer.java
================================================
/*
* Copyright (C) 2011 The Android Open Source Project
* Copyright (C) 2013 Guillaume Lesniak
*
* Licensed under the Apache License, Version 2.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 org.cyanogenmod.focal.pano;
import android.graphics.SurfaceTexture;
import android.os.ConditionVariable;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.egl.EGLContext;
import javax.microedition.khronos.egl.EGLDisplay;
import javax.microedition.khronos.egl.EGLSurface;
import javax.microedition.khronos.opengles.GL10;
public class MosaicPreviewRenderer {
private static final String TAG = "MosaicPreviewRenderer";
private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
private static final boolean DEBUG = false;
private int mWidth; // width of the view in UI
private int mHeight; // height of the view in UI
private boolean mIsLandscape = true;
private final float[] mTransformMatrix = new float[16];
private ConditionVariable mEglThreadBlockVar = new ConditionVariable();
private HandlerThread mEglThread;
private EGLHandler mEglHandler;
private EGLConfig mEglConfig;
private EGLDisplay mEglDisplay;
private EGLContext mEglContext;
private EGLSurface mEglSurface;
private SurfaceTexture mMosaicOutputSurfaceTexture;
private SurfaceTexture mInputSurfaceTexture;
private EGL10 mEgl;
private GL10 mGl;
private class EGLHandler extends Handler {
public static final int MSG_INIT_EGL_SYNC = 0;
public static final int MSG_SHOW_PREVIEW_FRAME_SYNC = 1;
public static final int MSG_SHOW_PREVIEW_FRAME = 2;
public static final int MSG_ALIGN_FRAME_SYNC = 3;
public static final int MSG_RELEASE = 4;
public EGLHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_INIT_EGL_SYNC:
doInitGL();
mEglThreadBlockVar.open();
break;
case MSG_SHOW_PREVIEW_FRAME_SYNC:
doShowPreviewFrame();
mEglThreadBlockVar.open();
break;
case MSG_SHOW_PREVIEW_FRAME:
doShowPreviewFrame();
break;
case MSG_ALIGN_FRAME_SYNC:
doAlignFrame();
mEglThreadBlockVar.open();
break;
case MSG_RELEASE:
doRelease();
break;
}
}
private void doAlignFrame() {
mInputSurfaceTexture.updateTexImage();
mInputSurfaceTexture.getTransformMatrix(mTransformMatrix);
MosaicRenderer.setWarping(true);
// Call preprocess to render it to low-res and high-res RGB textures.
MosaicRenderer.preprocess(mTransformMatrix);
// Now, transfer the textures from GPU to CPU memory for processing
MosaicRenderer.transferGPUtoCPU();
MosaicRenderer.updateMatrix();
draw();
mEgl.eglSwapBuffers(mEglDisplay, mEglSurface);
}
private void doShowPreviewFrame() {
mInputSurfaceTexture.updateTexImage();
mInputSurfaceTexture.getTransformMatrix(mTransformMatrix);
MosaicRenderer.setWarping(false);
// Call preprocess to render it to low-res and high-res RGB textures.
MosaicRenderer.preprocess(mTransformMatrix);
MosaicRenderer.updateMatrix();
draw();
mEgl.eglSwapBuffers(mEglDisplay, mEglSurface);
}
private void doInitGL() {
// These are copied from GLSurfaceView
mEgl = (EGL10) EGLContext.getEGL();
mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
if (mEglDisplay == EGL10.EGL_NO_DISPLAY) {
throw new RuntimeException("eglGetDisplay failed");
}
int[] version = new int[2];
if (!mEgl.eglInitialize(mEglDisplay, version)) {
throw new RuntimeException("eglInitialize failed");
} else {
Log.v(TAG, "EGL version: " + version[0] + '.' + version[1]);
}
int[] attribList = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE };
mEglConfig = chooseConfig(mEgl, mEglDisplay);
mEglContext = mEgl.eglCreateContext(mEglDisplay, mEglConfig, EGL10.EGL_NO_CONTEXT,
attribList);
if (mEglContext == null || mEglContext == EGL10.EGL_NO_CONTEXT) {
throw new RuntimeException("failed to createContext");
}
mEglSurface = mEgl.eglCreateWindowSurface(
mEglDisplay, mEglConfig, mMosaicOutputSurfaceTexture, null);
if (mEglSurface == null || mEglSurface == EGL10.EGL_NO_SURFACE) {
throw new RuntimeException("failed to createWindowSurface");
}
if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) {
throw new RuntimeException("failed to eglMakeCurrent");
}
mGl = (GL10) mEglContext.getGL();
mInputSurfaceTexture = new SurfaceTexture(MosaicRenderer.init());
MosaicRenderer.reset(mWidth, mHeight, true);
}
private void doRelease() {
mEgl.eglDestroySurface(mEglDisplay, mEglSurface);
mEgl.eglDestroyContext(mEglDisplay, mEglContext);
mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE,
EGL10.EGL_NO_CONTEXT);
mEgl.eglTerminate(mEglDisplay);
mEglSurface = null;
mEglContext = null;
mEglDisplay = null;
releaseSurfaceTexture(mInputSurfaceTexture);
mEglThread.quit();
}
private void releaseSurfaceTexture(SurfaceTexture st) {
st.release();
}
// Should be called from other thread.
public void sendMessageSync(int msg) {
mEglThreadBlockVar.close();
sendEmptyMessage(msg);
mEglThreadBlockVar.block();
}
}
public MosaicPreviewRenderer(SurfaceTexture tex, int w, int h, boolean isLandscape) {
mMosaicOutputSurfaceTexture = tex;
mWidth = w;
mHeight = h;
mIsLandscape = isLandscape;
mEglThread = new HandlerThread("PanoramaRealtimeRenderer");
mEglThread.start();
mEglHandler = new EGLHandler(mEglThread.getLooper());
// We need to sync this because the generation of surface texture for input is
// done here and the client will continue with the assumption that the
// generation is completed.
mEglHandler.sendMessageSync(EGLHandler.MSG_INIT_EGL_SYNC);
}
public void release() {
mEglHandler.sendEmptyMessage(EGLHandler.MSG_RELEASE);
}
public void setLandscape(boolean landscape) {
MosaicRenderer.setIsLandscape(landscape);
}
public void showPreviewFrameSync() {
mEglHandler.sendMessageSync(EGLHandler.MSG_SHOW_PREVIEW_FRAME_SYNC);
}
public void showPreviewFrame() {
mEglHandler.sendEmptyMessage(EGLHandler.MSG_SHOW_PREVIEW_FRAME);
}
public void alignFrameSync() {
mEglHandler.sendMessageSync(EGLHandler.MSG_ALIGN_FRAME_SYNC);
}
public SurfaceTexture getInputSurfaceTexture() {
return mInputSurfaceTexture;
}
private void draw() {
MosaicRenderer.step();
}
private static void checkEglError(String prompt, EGL10 egl) {
int error;
while ((error = egl.eglGetError()) != EGL10.EGL_SUCCESS) {
Log.e(TAG, String.format("%s: EGL error: 0x%x", prompt, error));
}
}
private static final int EGL_OPENGL_ES2_BIT = 4;
private static final int[] CONFIG_SPEC = new int[] {
EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
EGL10.EGL_RED_SIZE, 8,
EGL10.EGL_GREEN_SIZE, 8,
EGL10.EGL_BLUE_SIZE, 8,
EGL10.EGL_NONE
};
private static EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) {
int[] numConfig = new int[1];
if (!egl.eglChooseConfig(display, CONFIG_SPEC, null, 0, numConfig)) {
throw new IllegalArgumentException("eglChooseConfig failed");
}
int numConfigs = numConfig[0];
if (numConfigs <= 0) {
throw new IllegalArgumentException("No configs match configSpec");
}
EGLConfig[] configs = new EGLConfig[numConfigs];
if (!egl.eglChooseConfig(
display, CONFIG_SPEC, configs, numConfigs, numConfig)) {
throw new IllegalArgumentException("eglChooseConfig#2 failed");
}
return configs[0];
}
}
================================================
FILE: src/org/cyanogenmod/focal/pano/MosaicProxy.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.pano;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.ImageFormat;
import android.graphics.Matrix;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.SurfaceTexture;
import android.graphics.YuvImage;
import android.hardware.Camera;
import android.media.ExifInterface;
import android.net.Uri;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.Gravity;
import android.view.TextureView;
import android.view.View;
import android.view.WindowManager;
import android.widget.FrameLayout;
import org.cyanogenmod.focal.CameraActivity;
import org.cyanogenmod.focal.SnapshotManager;
import org.cyanogenmod.focal.Storage;
import org.cyanogenmod.focal.Util;
import org.cyanogenmod.focal.feats.CaptureTransformer;
import org.cyanogenmod.focal.ui.PanoProgressBar;
import org.cyanogenmod.focal.ui.ShutterButton;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import fr.xplod.focal.R;
/**
* Nemesis interface to interact with Google's mosaic interface
* Strongly inspired from AOSP's PanoramaModule
*/
public class MosaicProxy extends CaptureTransformer
implements SurfaceTexture.OnFrameAvailableListener, TextureView.SurfaceTextureListener {
private static final String TAG = "CAM PanoModule";
public static final int DEFAULT_SWEEP_ANGLE = 360;
// The unit of speed is degrees per frame.
private static final float PANNING_SPEED_THRESHOLD = 2.5f;
private static final int MSG_LOW_RES_FINAL_MOSAIC_READY = 1;
private static final int MSG_GENERATE_FINAL_MOSAIC_ERROR = 2;
private static final int MSG_RESET_TO_PREVIEW = 3;
private static final int MSG_CLEAR_SCREEN_DELAY = 4;
private static final int CAPTURE_STATE_VIEWFINDER = 0;
private static final int CAPTURE_STATE_MOSAIC = 1;
private final int mIndicatorColor;
private final int mIndicatorColorFast;
private Runnable mOnFrameAvailableRunnable;
private MosaicFrameProcessor mMosaicFrameProcessor;
private MosaicPreviewRenderer mMosaicPreviewRenderer;
private boolean mMosaicFrameProcessorInitialized;
private float mHorizontalViewAngle;
private float mVerticalViewAngle;
private ShutterButton mShutterButton;
private int mCaptureState;
private FrameLayout mGLRootView;
private TextureView mGLSurfaceView;
private CameraActivity mActivity;
private Handler mMainHandler;
private SurfaceTexture mCameraTexture;
private SurfaceTexture mMosaicTexture;
private boolean mCancelComputation;
private long mTimeTaken;
private PanoProgressBar mPanoProgressBar;
private Matrix mProgressDirectionMatrix = new Matrix();
private float[] mProgressAngle = new float[2];
private int mPreviewWidth;
private int mPreviewHeight;
private boolean mThreadRunning;
final private Object mWaitObject = new Object();
private int mCurrentOrientation;
private class MosaicJpeg {
public MosaicJpeg(byte[] data, int width, int height) {
this.data = data;
this.width = width;
this.height = height;
this.isValid = true;
}
public MosaicJpeg() {
this.data = null;
this.width = 0;
this.height = 0;
this.isValid = false;
}
public final byte[] data;
public final int width;
public final int height;
public final boolean isValid;
}
public MosaicProxy(CameraActivity activity) {
super(activity.getCamManager(), activity.getSnapManager());
mActivity = activity;
mCaptureState = CAPTURE_STATE_VIEWFINDER;
mPanoProgressBar = activity.getPanoProgressBar();
mGLRootView = (FrameLayout) mActivity.findViewById(R.id.gl_renderer_container);
mGLSurfaceView = new TextureView(mActivity);
mGLRootView.addView(mGLSurfaceView);
mGLSurfaceView.setSurfaceTextureListener(this);
mMosaicFrameProcessor = MosaicFrameProcessor.getInstance();
Resources appRes = mActivity.getResources();
mIndicatorColor = appRes.getColor(R.color.pano_progress_indication);
mIndicatorColorFast = appRes.getColor(R.color.pano_progress_indication_fast);
mPanoProgressBar.setBackgroundColor(appRes.getColor(R.color.pano_progress_empty));
mPanoProgressBar.setDoneColor(appRes.getColor(R.color.pano_progress_done));
mPanoProgressBar.setIndicatorColor(mIndicatorColor);
mPanoProgressBar.setOnDirectionChangeListener(
new PanoProgressBar.OnDirectionChangeListener () {
@Override
public void onDirectionChange(int direction) {
if (mCaptureState == CAPTURE_STATE_MOSAIC) {
showDirectionIndicators(direction);
}
}
});
// This runs in UI thread.
mOnFrameAvailableRunnable = new Runnable() {
@Override
public void run() {
// Frames might still be available after the activity is paused.
// If we call onFrameAvailable after pausing, the GL thread will crash.
// if (mPaused) return;
if (mGLRootView.getVisibility() != View.VISIBLE) {
mMosaicPreviewRenderer.showPreviewFrameSync();
mGLRootView.setVisibility(View.VISIBLE);
} else {
if (mCaptureState == CAPTURE_STATE_VIEWFINDER) {
mMosaicPreviewRenderer.showPreviewFrame();
} else {
mMosaicPreviewRenderer.alignFrameSync();
mMosaicFrameProcessor.processFrame();
}
}
}
};
mMainHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_LOW_RES_FINAL_MOSAIC_READY:
//onBackgroundThreadFinished();
//showFinalMosaic((Bitmap) msg.obj);
Util.fadeOut(mGLRootView);
mActivity.displayOverlayBitmap((Bitmap) msg.obj);
saveHighResMosaic();
break;
case MSG_GENERATE_FINAL_MOSAIC_ERROR:
CameraActivity.notify(
mActivity.getString(R.string.pano_panorama_rendering_failed), 2000);
resetToPreview();
break;
case MSG_RESET_TO_PREVIEW:
resetToPreview();
mThreadRunning = false;
break;
case MSG_CLEAR_SCREEN_DELAY:
mActivity.getWindow().clearFlags(WindowManager.LayoutParams.
FLAG_KEEP_SCREEN_ON);
break;
}
}
};
// Initialization
Camera.Parameters params = mActivity.getCamManager().getParameters();
if (params != null) {
mHorizontalViewAngle = params.getHorizontalViewAngle();
mVerticalViewAngle = params.getVerticalViewAngle();
} else {
mHorizontalViewAngle = 50;
mHorizontalViewAngle = 30;
}
int pixels = mActivity.getResources().getInteger(R.integer.config_panoramaDefaultWidth)
* mActivity.getResources().getInteger(R.integer.config_panoramaDefaultHeight);
if (params != null) {
Point size = Util.findBestPanoPreviewSize(
params.getSupportedPreviewSizes(), true, true, pixels);
mPreviewWidth = size.y;
mPreviewHeight = size.x;
} else {
mPreviewWidth = 480;
mPreviewHeight = 640;
}
FrameLayout.LayoutParams layoutParams =
(FrameLayout.LayoutParams) mGLSurfaceView.getLayoutParams();
layoutParams.width = FrameLayout.LayoutParams.WRAP_CONTENT;
layoutParams.height = FrameLayout.LayoutParams.WRAP_CONTENT;
layoutParams.gravity = Gravity.CENTER;
mGLSurfaceView.setLayoutParams(layoutParams);
layoutParams = (FrameLayout.LayoutParams) mGLRootView.getLayoutParams();
layoutParams.width = FrameLayout.LayoutParams.WRAP_CONTENT;
layoutParams.height = FrameLayout.LayoutParams.WRAP_CONTENT;
layoutParams.gravity = Gravity.CENTER;
mGLSurfaceView.setLayoutParams(layoutParams);
}
// This function will be called upon the first camera frame is available.
private void resetToPreview() {
mCaptureState = CAPTURE_STATE_VIEWFINDER;
if (mShutterButton != null) {
mShutterButton.setImageResource(R.drawable.btn_shutter_photo);
}
mCamManager.setLockSetup(false);
Util.fadeIn(mShutterButton);
Util.fadeIn(mGLRootView);
mActivity.hideOverlayBitmap();
Util.fadeOut(mPanoProgressBar);
mMosaicFrameProcessor.reset();
mCameraTexture.setOnFrameAvailableListener(this);
setupProgressDirectionMatrix();
mCamManager.setRenderToTexture(mCameraTexture);
}
/**
* Call this when you're done using MosaicProxy, to remove views added and shutdown
* threads.
*/
public void tearDown() {
mGLRootView.removeView(mGLSurfaceView);
mMosaicFrameProcessor.clear();
}
private void configMosaicPreview() {
boolean isLandscape = false;
Log.d(TAG, "isLandscape ? " + isLandscape +
" (orientation: " + mCamManager.getOrientation() + ")");
int viewWidth = mGLRootView.getMeasuredWidth();
int viewHeight = mGLRootView.getMeasuredHeight();
mMosaicPreviewRenderer = new MosaicPreviewRenderer(mMosaicTexture, viewWidth,
viewHeight, isLandscape);
mCameraTexture = mMosaicPreviewRenderer.getInputSurfaceTexture();
mCameraTexture.setDefaultBufferSize(mPreviewWidth, mPreviewHeight);
mCameraTexture.setOnFrameAvailableListener(this);
mMainHandler.post(new Runnable() {
@Override
public void run() {
mActivity.getCamManager().setRenderToTexture(mCameraTexture);
}
});
mMosaicTexture.setDefaultBufferSize(viewWidth, viewHeight);
}
@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
/* This function may be called by some random thread,
* so let's be safe and jump back to ui thread.
* No OpenGL calls can be done here. */
mActivity.runOnUiThread(mOnFrameAvailableRunnable);
}
@Override
public void onShutterButtonClicked(ShutterButton button) {
mShutterButton = button;
if (mCaptureState == CAPTURE_STATE_MOSAIC) {
stopCapture(false);
Util.fadeOut(button);
} else {
startCapture();
button.setImageResource(R.drawable.btn_shutter_stop);
}
}
@Override
public void onSnapshotShutter(SnapshotManager.SnapshotInfo info) {
}
@Override
public void onSnapshotPreview(SnapshotManager.SnapshotInfo info) {
}
@Override
public void onSnapshotProcessing(SnapshotManager.SnapshotInfo info) {
}
@Override
public void onSnapshotSaved(SnapshotManager.SnapshotInfo info) {
}
@Override
public void onMediaSavingStart() {
}
@Override
public void onMediaSavingDone() {
}
@Override
public void onVideoRecordingStart() {
}
@Override
public void onVideoRecordingStop() {
}
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int i, int i2) {
mMosaicTexture = surfaceTexture;
initMosaicFrameProcessorIfNeeded();
configMosaicPreview();
}
public int getPreviewBufSize() {
PixelFormat pixelInfo = new PixelFormat();
PixelFormat.getPixelFormatInfo(PixelFormat.RGB_888, pixelInfo);
// TODO: remove this extra 32 byte after the driver bug is fixed.
return (mPreviewWidth * mPreviewHeight * pixelInfo.bitsPerPixel / 8) + 32;
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int i, int i2) {
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
return false;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
}
public void startCapture() {
Log.v(TAG, "Starting Panorama capture");
// Reset values so we can do this again.
mCancelComputation = false;
mTimeTaken = System.currentTimeMillis();
// mShutterButton.setImageResource(R.drawable.btn_shutter_recording);
mCaptureState = CAPTURE_STATE_MOSAIC;
//mCaptureIndicator.setVisibility(View.VISIBLE);
//showDirectionIndicators(PanoProgressBar.DIRECTION_NONE);
mPanoProgressBar.setDoneColor(mActivity.getResources().getColor(R.color.pano_progress_done));
mPanoProgressBar.setIndicatorColor(mIndicatorColor);
boolean isLandscape = (mCamManager.getOrientation() != -90);
Log.d(TAG, "isLandscape ? " + isLandscape + " (orientation: "
+ mCamManager.getOrientation() + ")");
mMosaicPreviewRenderer.setLandscape(isLandscape);
mCurrentOrientation = mActivity.getCamManager().getOrientation();
if (mCurrentOrientation == -90) {
mCurrentOrientation = 90;
}
if (mCurrentOrientation < 0) {
mCurrentOrientation += 360;
}
mMosaicFrameProcessor.setProgressListener(new MosaicFrameProcessor.ProgressListener() {
@Override
public void onProgress(boolean isFinished, float panningRateX, float panningRateY,
float progressX, float progressY) {
float accumulatedHorizontalAngle = progressX * mHorizontalViewAngle;
float accumulatedVerticalAngle = progressY * mVerticalViewAngle;
if (isFinished
|| (Math.abs(accumulatedHorizontalAngle) >= DEFAULT_SWEEP_ANGLE)
|| (Math.abs(accumulatedVerticalAngle) >= DEFAULT_SWEEP_ANGLE)) {
Util.fadeOut(mShutterButton);
stopCapture(false);
} else {
float panningRateXInDegree = panningRateX * mHorizontalViewAngle;
float panningRateYInDegree = panningRateY * mVerticalViewAngle;
updateProgress(panningRateXInDegree, panningRateYInDegree,
accumulatedHorizontalAngle, accumulatedVerticalAngle);
}
}
});
mPanoProgressBar.reset();
// TODO: calculate the indicator width according to different devices to reflect the actual
// angle of view of the camera device.
mPanoProgressBar.setIndicatorWidth(20);
mPanoProgressBar.setMaxProgress(DEFAULT_SWEEP_ANGLE);
mPanoProgressBar.setVisibility(View.VISIBLE);
Util.fadeIn(mPanoProgressBar);
//mDeviceOrientationAtCapture = mDeviceOrientation;
//keepScreenOn();
//mActivity.getOrientationManager().lockOrientation();
setupProgressDirectionMatrix();
}
private void stopCapture(boolean aborted) {
mCaptureState = CAPTURE_STATE_VIEWFINDER;
//mCaptureIndicator.setVisibility(View.GONE);
//hideTooFastIndication();
//hideDirectionIndicators();
mMosaicFrameProcessor.setProgressListener(null);
//stopCameraPreview();
mCameraTexture.setOnFrameAvailableListener(null);
mPanoProgressBar.setDoneColor(mActivity.getResources().getColor(R.color.pano_saving_done));
mPanoProgressBar.setIndicatorColor(mActivity.getResources().getColor(R.color.pano_saving_indication));
if (!aborted && !mThreadRunning) {
//mRotateDialog.showWaitingDialog(mPreparePreviewString);
// Hide shutter button, shutter icon, etc when waiting for
// panorama to stitch
//mActivity.hideUI();
runInBackground(new Thread() {
@Override
public void run() {
MosaicJpeg jpeg = generateFinalMosaic(false);
if (jpeg != null && jpeg.isValid) {
Bitmap bitmap = null;
bitmap = BitmapFactory.decodeByteArray(jpeg.data, 0, jpeg.data.length);
mMainHandler.sendMessage(mMainHandler.obtainMessage(
MSG_LOW_RES_FINAL_MOSAIC_READY, bitmap));
} else {
CameraActivity.notify(
mActivity.getString(R.string.pano_panorama_rendering_failed), 2000);
mMainHandler.sendMessage(mMainHandler.obtainMessage(
MSG_RESET_TO_PREVIEW));
}
}
});
}
}
/**
* Generate the final mosaic image.
*
* @param highRes flag to indicate whether we want to get a high-res version.
* @return a MosaicJpeg with its isValid flag set to true if successful; null if the generation
* process is cancelled; and a MosaicJpeg with its isValid flag set to false if there
* is an error in generating the final mosaic.
*/
public MosaicJpeg generateFinalMosaic(boolean highRes) {
int mosaicReturnCode = mMosaicFrameProcessor.createMosaic(highRes);
if (mosaicReturnCode == Mosaic.MOSAIC_RET_CANCELLED) {
return null;
} else if (mosaicReturnCode == Mosaic.MOSAIC_RET_ERROR) {
return new MosaicJpeg();
}
byte[] imageData = mMosaicFrameProcessor.getFinalMosaicNV21();
if (imageData == null) {
Log.e(TAG, "getFinalMosaicNV21() returned null.");
return new MosaicJpeg();
}
int len = imageData.length - 8;
int width = (imageData[len + 0] << 24) + ((imageData[len + 1] & 0xFF) << 16)
+ ((imageData[len + 2] & 0xFF) << 8) + (imageData[len + 3] & 0xFF);
int height = (imageData[len + 4] << 24) + ((imageData[len + 5] & 0xFF) << 16)
+ ((imageData[len + 6] & 0xFF) << 8) + (imageData[len + 7] & 0xFF);
Log.v(TAG, "ImLength = " + (len) + ", W = " + width + ", H = " + height);
if (width <= 0 || height <= 0) {
CameraActivity.notify(
mActivity.getString(R.string.pano_panorama_rendering_failed), 2000);
Log.e(TAG, "width|height <= 0!!, len = " + (len) + ", W = " + width + ", H = " +
height);
return new MosaicJpeg();
}
YuvImage yuvimage = new YuvImage(imageData, ImageFormat.NV21, width, height, null);
ByteArrayOutputStream out = new ByteArrayOutputStream();
yuvimage.compressToJpeg(new Rect(0, 0, width, height), 100, out);
try {
out.close();
} catch (Exception e) {
Log.e(TAG, "Exception in storing final mosaic", e);
CameraActivity.notify(
mActivity.getString(R.string.pano_panorama_rendering_failed), 2000);
return new MosaicJpeg();
}
return new MosaicJpeg(out.toByteArray(), width, height);
}
void setupProgressDirectionMatrix() {
int degrees = Util.getDisplayRotation(mActivity);
int cameraId = 0; //CameraHolder.instance().getBackCameraId();
int orientation = 0; // TODO //Util.getDisplayOrientation(degrees, cameraId);
mProgressDirectionMatrix.reset();
mProgressDirectionMatrix.postRotate(orientation);
}
public void saveHighResMosaic() {
CameraActivity.notify(mActivity.getString(R.string.pano_panorama_rendering), 3000);
runInBackground(new Thread() {
@Override
public void run() {
//mPartialWakeLock.acquire();
MosaicJpeg jpeg;
try {
jpeg = generateFinalMosaic(true);
} finally {
//mPartialWakeLock.release();
}
if (jpeg == null) { // Cancelled by user.
mMainHandler.sendEmptyMessage(MSG_RESET_TO_PREVIEW);
} else if (!jpeg.isValid) { // Error when generating mosaic.
mMainHandler.sendEmptyMessage(MSG_GENERATE_FINAL_MOSAIC_ERROR);
} else {
Uri uri = savePanorama(jpeg.data, jpeg.width, jpeg.height, mCurrentOrientation);
if (uri != null) {
Util.broadcastNewPicture(mActivity, uri);
mActivity.getReviewDrawer().updateFromGallery(true, 0);
}
mMainHandler.sendMessage(
mMainHandler.obtainMessage(MSG_RESET_TO_PREVIEW));
}
}
});
reportProgress();
}
private Uri savePanorama(byte[] jpegData, int width, int height, int orientation) {
if (jpegData != null) {
String filename = PanoUtil.createName(
mActivity.getResources().getString(R.string.pano_file_name_format), mTimeTaken);
String filePath = Storage.getStorage().writeFile(filename, jpegData);
// Add Exif tags.
try {
ExifInterface exif = new ExifInterface(filePath);
/*exif.setAttribute(ExifInterface.TAG_GPS_DATESTAMP,
mGPSDateStampFormat.format(mTimeTaken));
exif.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP,
mGPSTimeStampFormat.format(mTimeTaken));
exif.setAttribute(ExifInterface.TAG_DATETIME,
mDateTimeStampFormat.format(mTimeTaken));*/
exif.setAttribute(ExifInterface.TAG_ORIENTATION,
getExifOrientation(orientation));
exif.saveAttributes();
} catch (IOException e) {
Log.e(TAG, "Cannot set EXIF for " + filePath, e);
}
int jpegLength = (int) (new File(filePath).length());
return Storage.getStorage().addImage(mActivity.getContentResolver(), filename,
mTimeTaken, null, orientation, jpegLength, filePath, width, height);
}
return null;
}
private static String getExifOrientation(int orientation) {
orientation = (orientation + 360) % 360;
switch (orientation) {
case 0:
return String.valueOf(ExifInterface.ORIENTATION_NORMAL);
case 90:
return String.valueOf(ExifInterface.ORIENTATION_ROTATE_90);
case 180:
return String.valueOf(ExifInterface.ORIENTATION_ROTATE_180);
case 270:
return String.valueOf(ExifInterface.ORIENTATION_ROTATE_270);
default:
throw new AssertionError("invalid: " + orientation);
}
}
private void initMosaicFrameProcessorIfNeeded() {
//if (mPaused || mThreadRunning) return;
if (!mMosaicFrameProcessorInitialized) {
mMosaicFrameProcessor.initialize(
mPreviewWidth, mPreviewHeight, getPreviewBufSize());
mMosaicFrameProcessorInitialized = true;
}
}
private void updateProgress(float panningRateXInDegree, float panningRateYInDegree,
float progressHorizontalAngle, float progressVerticalAngle) {
mGLRootView.invalidate();
if ((Math.abs(panningRateXInDegree) > PANNING_SPEED_THRESHOLD)
|| (Math.abs(panningRateYInDegree) > PANNING_SPEED_THRESHOLD)) {
showTooFastIndication();
} else {
hideTooFastIndication();
}
// progressHorizontalAngle and progressVerticalAngle are relative to the
// camera. Convert them to UI direction.
mProgressAngle[0] = progressHorizontalAngle;
mProgressAngle[1] = -progressVerticalAngle;
mProgressDirectionMatrix.mapPoints(mProgressAngle);
int angleInMajorDirection =
(Math.abs(mProgressAngle[0]) > Math.abs(mProgressAngle[1]))
? (int) mProgressAngle[0]
: (int) mProgressAngle[1];
mPanoProgressBar.setProgress((angleInMajorDirection));
}
/**
* Report saving progress
*/
public void reportProgress() {
mPanoProgressBar.reset();
mPanoProgressBar.setRightIncreasing(true);
mPanoProgressBar.setMaxProgress(100);
Thread t = new Thread() {
@Override
public void run() {
while (mThreadRunning) {
final int progress = mMosaicFrameProcessor.reportProgress(
true, mCancelComputation);
try {
synchronized (mWaitObject) {
mWaitObject.wait(50);
}
} catch (InterruptedException e) {
throw new RuntimeException("Panorama reportProgress failed", e);
}
// Update the progress bar
mActivity.runOnUiThread(new Runnable() {
@Override
public void run() {
mPanoProgressBar.setProgress(progress);
}
});
}
}
};
t.start();
}
private void runInBackground(Thread t) {
mThreadRunning = true;
t.start();
}
private void showTooFastIndication() {
//mTooFastPrompt.setVisibility(View.VISIBLE);
// The PreviewArea also contains the border for "too fast" indication.
//mPreviewArea.setVisibility(View.VISIBLE);
mPanoProgressBar.setIndicatorColor(mIndicatorColorFast);
//mLeftIndicator.setEnabled(true);
//mRightIndicator.setEnabled(true);
}
private void hideTooFastIndication() {
//mTooFastPrompt.setVisibility(View.GONE);
// We set "INVISIBLE" instead of "GONE" here because we need mPreviewArea to have layout
// information so we can know the size and position for mCameraScreenNail.
//mPreviewArea.setVisibility(View.INVISIBLE);
mPanoProgressBar.setIndicatorColor(mIndicatorColor);
//mLeftIndicator.setEnabled(false);
//mRightIndicator.setEnabled(false);
}
private void hideDirectionIndicators() {
/*mLeftIndicator.setVisibility(View.GONE);
mRightIndicator.setVisibility(View.GONE);*/
}
private void showDirectionIndicators(int direction) {
/*switch (direction) {
case PanoProgressBar.DIRECTION_NONE:
mLeftIndicator.setVisibility(View.VISIBLE);
mRightIndicator.setVisibility(View.VISIBLE);
break;
case PanoProgressBar.DIRECTION_LEFT:
mLeftIndicator.setVisibility(View.VISIBLE);
mRightIndicator.setVisibility(View.GONE);
break;
case PanoProgressBar.DIRECTION_RIGHT:
mLeftIndicator.setVisibility(View.GONE);
mRightIndicator.setVisibility(View.VISIBLE);
break;
}*/
}
}
================================================
FILE: src/org/cyanogenmod/focal/pano/MosaicRenderer.java
================================================
/*
* Copyright (C) 2011 The Android Open Source Project
* Copyright (C) 2013 Guillaume Lesniak
*
* Licensed under the Apache License, Version 2.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 org.cyanogenmod.focal.pano;
/**
* The Java interface to JNI calls regarding mosaic preview rendering.
*
*/
public class MosaicRenderer {
static {
System.loadLibrary("jni_mosaic2");
}
/**
* Function to be called in onSurfaceCreated() to initialize
* the GL context, load and link the shaders and create the
* program. Returns a texture ID to be used for SurfaceTexture.
*
* @return textureID the texture ID of the newly generated texture to
* be assigned to the SurfaceTexture object.
*/
public static native int init();
/**
* Pass the drawing surface's width and height to initialize the
* renderer viewports and FBO dimensions.
*
* @param width width of the drawing surface in pixels.
* @param height height of the drawing surface in pixels.
* @param isLandscapeOrientation is the orientation of the activity layout in landscape.
*/
public static native void reset(int width, int height, boolean isLandscapeOrientation);
/**
* Changes the orientation of the stitching
*
* @param isLandscape
*/
public static native void setIsLandscape(boolean isLandscape);
/**
* Calling this function will render the SurfaceTexture to a new 2D texture
* using the provided STMatrix.
*
* @param stMatrix texture coordinate transform matrix obtained from the
* Surface texture
*/
public static native void preprocess(float[] stMatrix);
/**
* This function calls glReadPixels to transfer both the low-res and high-res
* data from the GPU memory to the CPU memory for further processing by the
* mosaicing library.
*/
public static native void transferGPUtoCPU();
/**
* Function to be called in onDrawFrame() to update the screen with
* the new frame data.
*/
public static native void step();
/**
* Call this function when a new low-res frame has been processed by
* the mosaicing library. This will tell the renderer library to
* update its texture and warping transformation. Any calls to step()
* after this call will use the new image frame and transformation data.
*/
public static native void updateMatrix();
/**
* This function allows toggling between showing the input image data
* (without applying any warp) and the warped image data. For running
* the renderer as a viewfinder, we set the flag to false. To see the
* preview mosaic, we set the flag to true.
*
* @param flag boolean flag to set the warping to true or false.
*/
public static native void setWarping(boolean flag);
}
================================================
FILE: src/org/cyanogenmod/focal/pano/PanoUtil.java
================================================
/*
* Copyright (C) 2011 The Android Open Source Project
* Copyright (C) 2013 Guillaume Lesniak
*
* Licensed under the Apache License, Version 2.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 org.cyanogenmod.focal.pano;
import java.text.SimpleDateFormat;
import java.util.Date;
public class PanoUtil {
public static String createName(String format, long dateTaken) {
Date date = new Date(dateTaken);
SimpleDateFormat dateFormat = new SimpleDateFormat(format);
return dateFormat.format(date);
}
// TODO: Add comments about the range of these two arguments.
public static double calculateDifferenceBetweenAngles(
double firstAngle, double secondAngle) {
double difference1 = (secondAngle - firstAngle) % 360;
if (difference1 < 0) {
difference1 += 360;
}
double difference2 = (firstAngle - secondAngle) % 360;
if (difference2 < 0) {
difference2 += 360;
}
return Math.min(difference1, difference2);
}
public static void decodeYUV420SPQuarterRes(int[] rgb, byte[] yuv420sp, int width, int height) {
final int frameSize = width * height;
for (int j = 0, ypd = 0; j < height; j += 4) {
int uvp = frameSize + (j >> 1) * width, u = 0, v = 0;
for (int i = 0; i < width; i += 4, ypd++) {
int y = (0xff & (yuv420sp[j * width + i])) - 16;
if (y < 0) {
y = 0;
}
if ((i & 1) == 0) {
v = (0xff & yuv420sp[uvp++]) - 128;
u = (0xff & yuv420sp[uvp++]) - 128;
uvp += 2; // Skip the UV values for the 4 pixels skipped in between
}
int y1192 = 1192 * y;
int r = (y1192 + 1634 * v);
int g = (y1192 - 833 * v - 400 * u);
int b = (y1192 + 2066 * u);
if (r < 0) {
r = 0;
} else if (r > 262143) {
r = 262143;
}
if (g < 0) {
g = 0;
} else if (g > 262143) {
g = 262143;
}
if (b < 0) {
b = 0;
} else if (b > 262143) {
b = 262143;
}
rgb[ypd] = 0xff000000 | ((r << 6) & 0xff0000) | ((g >> 2) & 0xff00) |
((b >> 10) & 0xff);
}
}
}
}
================================================
FILE: src/org/cyanogenmod/focal/picsphere/Capture3DRenderer.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.picsphere;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.SurfaceTexture;
import android.opengl.GLES11Ext;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import android.opengl.GLUtils;
import android.opengl.Matrix;
import android.util.Log;
import org.cyanogenmod.focal.CameraManager;
import fr.xplod.focal.R;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
/**
* Manages the 3D rendering of the sphere capture mode, using gyroscope to
* orientate the camera and displays the preview at the center.
*
* TODO: Fallback for non-GLES2 devices?
*/
public class Capture3DRenderer implements GLSurfaceView.Renderer {
public final static String TAG = "Capture3DRenderer";
private final CameraManager mCamManager;
private List mSnapshots;
private List mDots;
private ReentrantLock mListBusy;
private SensorFusion mSensorFusion;
private Quaternion mCameraQuat;
private Skybox mSkyBox;
private float[] mViewMatrix = new float[16];
private float[] mProjectionMatrix = new float[16];
private FloatBuffer mVertexBuffer;
private FloatBuffer m43VertexBuffer;
private FloatBuffer mTexCoordBuffer;
private final static float SNAPSHOT_SCALE = 65.5f;
private final static float RATIO = 4.0f/3.0f;
private final static float DISTANCE = 135.0f;
// x, y,
private final float mVertexData[] =
{
-SNAPSHOT_SCALE, -SNAPSHOT_SCALE,
-SNAPSHOT_SCALE, SNAPSHOT_SCALE,
SNAPSHOT_SCALE, SNAPSHOT_SCALE,
SNAPSHOT_SCALE, -SNAPSHOT_SCALE
};
private final float m43VertexData[] =
{
-SNAPSHOT_SCALE *RATIO, -SNAPSHOT_SCALE,
-SNAPSHOT_SCALE *RATIO, SNAPSHOT_SCALE,
SNAPSHOT_SCALE *RATIO, SNAPSHOT_SCALE,
SNAPSHOT_SCALE *RATIO, -SNAPSHOT_SCALE
};
// u,v
private final float mTexCoordData[] =
{
0.0f, 0.0f,
0.0f, 1.0f,
1.0f, 1.0f,
1.0f, 0.0f
};
private final static int CAMERA = 0;
private final static int SNAPSHOT = 1;
private int[] mProgram = new int[2];
private int[] mVertexShader = new int[2];
private int[] mFragmentShader = new int[2];
private int[] mPositionHandler = new int[2];
private int[] mTexCoordHandler = new int[2];
private int[] mTextureHandler = new int[2];
private int[] mAlphaHandler = new int[2];
private int[] mMVPMatrixHandler = new int[2];
private SurfaceTexture mCameraSurfaceTex;
private int mCameraTextureId;
private Snapshot mCameraBillboard;
private Snapshot mViewfinderBillboard;
private Context mContext;
private Quaternion mTempQuaternion;
private float[] mMVPMatrix = new float[16];
private class Skybox {
private float DIST = SNAPSHOT_SCALE;
private Snapshot[] mFaces = new Snapshot[6];
private int FACE_NORTH = 0;
private int FACE_WEST = 1;
private int FACE_SOUTH = 2;
private int FACE_EAST = 3;
private int FACE_UP = 4;
private int FACE_DOWN = 5;
public Skybox() {
mFaces[FACE_NORTH] = new Snapshot(false);
mFaces[FACE_NORTH].mModelMatrix = matrixFromEuler(0, 90, 0, 0, 0, DIST);
mFaces[FACE_NORTH].setTexture(BitmapFactory.decodeResource(mContext.getResources(),
R.drawable.picsphere_sky_fr));
mFaces[FACE_SOUTH] = new Snapshot(false);
mFaces[FACE_SOUTH].mModelMatrix = matrixFromEuler(0, 90, 0, 0, 0, -DIST);
mFaces[FACE_SOUTH].setTexture(BitmapFactory.decodeResource(mContext.getResources(),
R.drawable.picsphere_sky_bk));
mFaces[FACE_WEST] = new Snapshot(false);
mFaces[FACE_WEST].mModelMatrix = matrixFromEuler(0, 90, 90, 0, 0, DIST);
mFaces[FACE_WEST].setTexture(BitmapFactory.decodeResource(mContext.getResources(),
R.drawable.picsphere_sky_lt));
mFaces[FACE_EAST] = new Snapshot(false);
mFaces[FACE_EAST].mModelMatrix = matrixFromEuler(0, 90, 270, 0, 0, DIST);
mFaces[FACE_EAST].setTexture(BitmapFactory.decodeResource(mContext.getResources(),
R.drawable.picsphere_sky_rt));
mFaces[FACE_UP] = new Snapshot(false);
mFaces[FACE_UP].mModelMatrix = matrixFromEuler(90, 0, 270, 0, 0, DIST);
mFaces[FACE_UP].setTexture(BitmapFactory.decodeResource(mContext.getResources(),
R.drawable.picsphere_sky_up));
mFaces[FACE_DOWN] = new Snapshot(false);
mFaces[FACE_DOWN].mModelMatrix = matrixFromEuler(-90, 0, 90, 0, 0, DIST);
mFaces[FACE_DOWN].setTexture(BitmapFactory.decodeResource(mContext.getResources(),
R.drawable.picsphere_sky_dn));
}
public void draw() {
for (int i = 0; i < mFaces.length; i++) {
if (mFaces[i] != null) {
mFaces[i].draw();
}
}
}
}
/**
* Stores the information about each snapshot displayed in the sphere
*/
private class Snapshot {
private float[]mModelMatrix;
private int mTextureData;
private Bitmap mBitmapToLoad;
private boolean mIsFourToThree;
private int mMode;
private boolean mIsVisible = true;
private float mAlpha = 1.0f;
private float mAutoAlphaX;
private float mAutoAlphaY;
public Snapshot() {
mIsFourToThree = true;
mMode = SNAPSHOT;
}
public Snapshot(boolean isFourToThree) {
mIsFourToThree = isFourToThree;
mMode = SNAPSHOT;
}
public void setVisible(boolean visible) {
mIsVisible = visible;
}
/**
* Sets whether to use the CAMERA shaders or the SNAPSHOT shaders
* @param mode CAMERA or SNAPSHOT
*/
public void setMode(int mode) {
mMode = mode;
}
public void setTexture(Bitmap tex) {
mBitmapToLoad = tex;
}
public void setTextureId(int id) {
mTextureData = id;
}
public void setAlpha(float alpha) {
mAlpha = alpha;
}
public void setAutoAlphaAngle(float x, float y) {
mAutoAlphaX = x;
mAutoAlphaY = y;
}
public float getAutoAlphaX() {
return mAutoAlphaX;
}
public float getAutoAlphaY() {
return mAutoAlphaY;
}
private void loadTexture() {
// Load the snapshot bitmap as a texture to bind to our GLES20 program
int texture[] = new int[1];
GLES20.glGenTextures(1, texture, 0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture[0]);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, mBitmapToLoad, 0);
//tex.recycle();
if(texture[0] == 0){
Log.e(TAG, "Unable to attribute texture to quad");
}
mTextureData = texture[0];
mBitmapToLoad = null;
}
public void draw() {
if (!mIsVisible) return;
if (mBitmapToLoad != null) {
loadTexture();
}
GLES20.glUseProgram(mProgram[mMode]);
if (mIsFourToThree) {
m43VertexBuffer.position(0);
} else {
mVertexBuffer.position(0);
}
mTexCoordBuffer.position(0);
GLES20.glEnableVertexAttribArray(mTexCoordHandler[mMode]);
GLES20.glEnableVertexAttribArray(mPositionHandler[mMode]);
if (mIsFourToThree) {
GLES20.glVertexAttribPointer(mPositionHandler[mMode],
2, GLES20.GL_FLOAT, false, 8, m43VertexBuffer);
} else {
GLES20.glVertexAttribPointer(mPositionHandler[mMode],
2, GLES20.GL_FLOAT, false, 8, mVertexBuffer);
}
GLES20.glVertexAttribPointer(mTexCoordHandler[mMode], 2,
GLES20.GL_FLOAT, false, 8, mTexCoordBuffer);
// This multiplies the view matrix by the model matrix, and stores the
// result in the MVP matrix (which currently contains model * view).
Matrix.multiplyMM(mMVPMatrix, 0, mViewMatrix, 0, mModelMatrix, 0);
// This multiplies the modelview matrix by the projection matrix, and stores
// the result in the MVP matrix (which now contains model * view * projection).
Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mMVPMatrix, 0);
// Pass in the combined matrix.
GLES20.glUniformMatrix4fv(mMVPMatrixHandler[mMode], 1, false, mMVPMatrix, 0);
GLES20.glUniform1f(mAlphaHandler[mMode], mAlpha);
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureData);
GLES20.glUniform1i(mTextureHandler[mMode], 0);
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, 4);
}
}
/**
* Initialize the model data.
*/
public Capture3DRenderer(Context context, CameraManager cameraManager) {
mSnapshots = new ArrayList();
mDots = new ArrayList();
mCamManager = cameraManager;
mListBusy = new ReentrantLock();
mSensorFusion = new SensorFusion(context);
mCameraQuat = new Quaternion();
mContext = context;
mTempQuaternion = new Quaternion();
// Position the dots every 40°
for (int x = 0; x < 360; x += 360/12) {
for (int y = 0; y < 360; y += 360/12) {
createDot(x, y);
}
}
}
private void createDot(float rx, float ry) {
Snapshot dot = new Snapshot(false);
dot.setTexture(BitmapFactory.decodeResource(mContext.getResources(),
R.drawable.ic_picsphere_marker));
dot.mModelMatrix = matrixFromEuler(rx, 0, ry, 0, 0, 100);
Matrix.scaleM(dot.mModelMatrix, 0, 0.1f, 0.1f, 0.1f);
dot.setAutoAlphaAngle(rx, ry);
mDots.add(dot);
}
private float[] matrixFromEuler(float rx, float ry, float rz, float tx, float ty, float tz) {
Quaternion quat = new Quaternion();
quat.fromEuler(rx,ry,rz);
float[] matrix = quat.getMatrix();
Matrix.translateM(matrix, 0, tx, ty, tz);
return matrix;
}
@Override
public void onSurfaceCreated(GL10 glUnused, EGLConfig config) {
// Set the background clear color to gray.
GLES20.glClearColor(0.5f, 0.5f, 0.5f, 0.5f);
// Initialize plane vertex data and texcoords data
ByteBuffer bb_data = ByteBuffer.allocateDirect(mVertexData.length * 4);
bb_data.order(ByteOrder.nativeOrder());
ByteBuffer bb_43data = ByteBuffer.allocateDirect(m43VertexData.length * 4);
bb_43data.order(ByteOrder.nativeOrder());
ByteBuffer bb_texture = ByteBuffer.allocateDirect(mTexCoordData.length * 4);
bb_texture.order(ByteOrder.nativeOrder());
mVertexBuffer = bb_data.asFloatBuffer();
mVertexBuffer.put(mVertexData);
mVertexBuffer.position(0);
m43VertexBuffer = bb_43data.asFloatBuffer();
m43VertexBuffer.put(m43VertexData);
m43VertexBuffer.position(0);
mTexCoordBuffer = bb_texture.asFloatBuffer();
mTexCoordBuffer.put(mTexCoordData);
mTexCoordBuffer.position(0);
// Simple GLSL vertex/fragment, as GLES2 doesn't have the classical fixed pipeline
final String vertexShader =
"uniform mat4 u_MVPMatrix; \n"
+ "attribute vec4 a_Position; \n"
+ "attribute vec2 a_TexCoordinate;\n"
+ "varying vec2 v_TexCoordinate; \n"
+ "void main() \n"
+ "{ \n"
+ " v_TexCoordinate = a_TexCoordinate;\n"
+ " gl_Position = u_MVPMatrix * a_Position; \n"
+ "} \n";
final String fragmentShader =
"precision mediump float; \n"
+ "uniform sampler2D u_Texture; \n"
+ "varying vec2 v_TexCoordinate; \n"
+ "uniform float f_Alpha;\n"
+ "void main() \n"
+ "{ \n"
+ " gl_FragColor = texture2D(u_Texture, v_TexCoordinate);\n"
+ " gl_FragColor.a = gl_FragColor.a * f_Alpha;"
+ "} \n";
// As the camera preview is stored in the OES external slot, we need a different shader
final String camPreviewShader = "#extension GL_OES_EGL_image_external : require\n"
+ "precision mediump float; \n"
+ "uniform samplerExternalOES u_Texture; \n"
+ "varying vec2 v_TexCoordinate; \n"
+ "uniform float f_Alpha;\n"
+ "void main() \n"
+ "{ \n"
+ " gl_FragColor = texture2D(u_Texture, v_TexCoordinate);\n"
+ " gl_FragColor.a = gl_FragColor.a * f_Alpha;"
+ "} \n";
mVertexShader[CAMERA] = compileShader(GLES20.GL_VERTEX_SHADER, vertexShader);
mFragmentShader[CAMERA] = compileShader(GLES20.GL_FRAGMENT_SHADER, camPreviewShader);
mVertexShader[SNAPSHOT] = compileShader(GLES20.GL_VERTEX_SHADER, vertexShader);
mFragmentShader[SNAPSHOT] = compileShader(GLES20.GL_FRAGMENT_SHADER, fragmentShader);
// create the program and bind the shader attributes
for (int i = 0; i < 2; i++) {
mProgram[i] = GLES20.glCreateProgram();
GLES20.glAttachShader(mProgram[i], mFragmentShader[i]);
GLES20.glAttachShader(mProgram[i], mVertexShader[i]);
GLES20.glLinkProgram(mProgram[i]);
int[] linkStatus = new int[1];
GLES20.glGetProgramiv(mProgram[i], GLES20.GL_LINK_STATUS, linkStatus, 0);
if (linkStatus[0] == 0) {
throw new RuntimeException("Error linking shaders");
}
mPositionHandler[i] = GLES20.glGetAttribLocation(mProgram[i], "a_Position");
mTexCoordHandler[i] = GLES20.glGetAttribLocation(mProgram[i], "a_TexCoordinate");
mMVPMatrixHandler[i] = GLES20.glGetUniformLocation(mProgram[i], "u_MVPMatrix");
mTextureHandler[i] = GLES20.glGetUniformLocation(mProgram[i], "u_Texture");
mAlphaHandler[i] = GLES20.glGetUniformLocation(mProgram[i], "f_Alpha");
}
mSkyBox = new Skybox();
initCameraBillboard();
}
private void initCameraBillboard() {
int texture[] = new int[1];
GLES20.glGenTextures(1, texture, 0);
mCameraTextureId = texture[0];
if (mCameraTextureId == 0) {
throw new RuntimeException("CAMERA TEXTURE ID == 0");
}
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mCameraTextureId);
// Can't do mipmapping with camera source
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER,
GLES20.GL_LINEAR);
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER,
GLES20.GL_LINEAR);
// Clamp to edge is the only option
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S,
GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T,
GLES20.GL_CLAMP_TO_EDGE);
mCameraSurfaceTex = new SurfaceTexture(mCameraTextureId);
mCameraSurfaceTex.setDefaultBufferSize(640, 480);
mCameraBillboard = new Snapshot();
mCameraBillboard.setTextureId(mCameraTextureId);
mCameraBillboard.setMode(CAMERA);
mCamManager.setRenderToTexture(mCameraSurfaceTex);
// Setup viewfinder billboard
mViewfinderBillboard = new Snapshot(false);
mViewfinderBillboard.setTexture(BitmapFactory.decodeResource(mContext.getResources(),
R.drawable.ic_picsphere_viewfinder));
}
@Override
public void onSurfaceChanged(GL10 glUnused, int width, int height) {
// Set the OpenGL viewport to the same size as the surface.
GLES20.glViewport(0, 0, width, height);
// We use here a field of view of 40, which is mostly fine for a camera app representation
final float hfov = 90f;
// Create a new perspective projection matrix. The height will stay the same
// while the width will vary as per aspect ratio.
final float ratio = 640.0f / 480.0f;
final float near = 0.1f;
final float far = 1500.0f;
final float left = (float) Math.tan(hfov * Math.PI / 360.0f) * near;
final float right = -left;
final float bottom = ratio * right / 1.0f;
final float top = ratio * left / 1.0f;
Matrix.frustumM(mProjectionMatrix, 0, left, right, bottom, top, near, far);
}
@Override
public void onDrawFrame(GL10 glUnused) {
mCameraSurfaceTex.updateTexImage();
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA);
GLES20.glEnable(GLES20.GL_BLEND);
// Update camera view matrix
float[] orientation = mSensorFusion.getFusedOrientation();
// Convert angles to degrees
float rX = (float) (orientation[1] * 180.0f/Math.PI);
float rY = (float) (orientation[0] * 180.0f/Math.PI);
float rZ = (float) (orientation[2] * 180.0f/Math.PI);
// Update quaternion from euler angles out of orientation
mCameraQuat.fromEuler( rX, 180.0f-rZ, rY);
mCameraQuat = mCameraQuat.getConjugate();
mCameraQuat.normalise();
mViewMatrix = mCameraQuat.getMatrix();
// Update camera billboard
mCameraBillboard.mModelMatrix = mCameraQuat.getMatrix();
Matrix.invertM(mCameraBillboard.mModelMatrix, 0, mCameraBillboard.mModelMatrix, 0);
Matrix.translateM(mCameraBillboard.mModelMatrix, 0, 0.0f, 0.0f, -DISTANCE);
Matrix.rotateM(mCameraBillboard.mModelMatrix, 0, -90, 0, 0, 1);
mViewfinderBillboard.mModelMatrix = Arrays.copyOf(mCameraBillboard.mModelMatrix,
mCameraBillboard.mModelMatrix.length);
Matrix.scaleM(mViewfinderBillboard.mModelMatrix, 0, 0.25f, 0.25f, 0.25f);
// Draw all teh things
// First the skybox, then the marker dots, then the snapshots
mSkyBox.draw();
mCameraBillboard.draw();
mListBusy.lock();
for (Snapshot snap : mSnapshots) {
snap.draw();
}
mListBusy.unlock();
for (Snapshot dot : mDots) {
// Set alpha based on camera distance to the point
float dX = dot.getAutoAlphaX() - (rX + 180.0f);
float dY = dot.getAutoAlphaY() - rY;
dX = (dX + 180.0f) % 360.0f - 180.0f;
dot.setAlpha(1.0f - Math.abs(dX)/180.0f * 8.0f);
dot.draw();
}
mViewfinderBillboard.draw();
}
public void setCamPreviewVisible(boolean visible) {
mCameraBillboard.setVisible(visible);
}
public void onPause() {
if (mSensorFusion != null) {
mSensorFusion.onPauseOrStop();
}
}
public void onResume() {
if (mSensorFusion != null) {
mSensorFusion.onResume();
}
}
public void setCameraOrientation(float rX, float rY, float rZ) {
// Convert angles to degrees
rX = (float) (rX * 180.0f/Math.PI);
rY = (float) (rY * 180.0f/Math.PI);
// Update quaternion from euler angles out of orientation and set it as view matrix
mCameraQuat.fromEuler(rY, 0.0f, rX);
mViewMatrix = mCameraQuat.getConjugate().getMatrix();
}
public Vector3 getAngleAsVector() {
float[] orientation = mSensorFusion.getFusedOrientation();
// Convert angles to degrees
float rX = (float) (orientation[0] * 180.0f/Math.PI);
float rY = (float) (orientation[1] * 180.0f/Math.PI);
float rZ = (float) (orientation[2] * 180.0f/Math.PI);
return new Vector3(rX, rY, rZ);
}
/**
* Helper function to compile a shader.
*
* @param shaderType The shader type.
* @param shaderSource The shader source code.
* @return An OpenGL handle to the shader.
*/
public static int compileShader(final int shaderType, final String shaderSource) {
int shaderHandle = GLES20.glCreateShader(shaderType);
if (shaderHandle != 0) {
// Pass in the shader source.
GLES20.glShaderSource(shaderHandle, shaderSource);
// Compile the shader.
GLES20.glCompileShader(shaderHandle);
// Get the compilation status.
final int[] compileStatus = new int[1];
GLES20.glGetShaderiv(shaderHandle, GLES20.GL_COMPILE_STATUS, compileStatus, 0);
// If the compilation failed, delete the shader.
if (compileStatus[0] == 0) {
Log.e(TAG, "Error compiling shader: " + GLES20.glGetShaderInfoLog(shaderHandle));
GLES20.glDeleteShader(shaderHandle);
shaderHandle = 0;
}
}
if (shaderHandle == 0) {
throw new RuntimeException("Error creating shader.");
}
return shaderHandle;
}
/**
* Adds a snapshot to the sphere
*/
public void addSnapshot(final Bitmap image) {
Snapshot snap = new Snapshot();
snap.mModelMatrix = Arrays.copyOf(mViewMatrix, mViewMatrix.length);
Matrix.invertM(snap.mModelMatrix, 0, snap.mModelMatrix, 0);
Matrix.translateM(snap.mModelMatrix, 0, 0.0f, 0.0f, -DISTANCE);
Matrix.rotateM(snap.mModelMatrix, 0, -90, 0, 0, 1);
snap.setTexture(image);
mListBusy.lock();
mSnapshots.add(snap);
mListBusy.unlock();
}
/**
* Removes the last taken snapshot
*/
public void removeLastPicture() {
mListBusy.lock();
if (mSnapshots.size() > 0) {
mSnapshots.remove(mSnapshots.size()-1);
}
mListBusy.unlock();
}
/**
* Clear sphere's snapshots
*/
public void clearSnapshots() {
mListBusy.lock();
mSnapshots.clear();
mListBusy.unlock();
}
}
================================================
FILE: src/org/cyanogenmod/focal/picsphere/PicSphere.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.picsphere;
import android.content.Context;
import android.database.Cursor;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Environment;
import android.provider.MediaStore;
import android.util.Log;
import org.cyanogenmod.focal.PopenHelper;
import org.cyanogenmod.focal.SnapshotManager;
import org.cyanogenmod.focal.Util;
import org.cyanogenmod.focal.XMPHelper;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.List;
/**
* This class handles the data needed for a PicSphere to happen, as well as the current
* status of the processing.
*
* You shouldn't instantiate the awesomeness directly, but rather go through PicSphereManager.
*/
public class PicSphere {
public final static String TAG = "PicSphere";
private String mPathPrefix;
private List mPictures;
private Context mContext;
private File mTempPath;
private String mProjectFile;
private SnapshotManager mSnapManager;
private Uri mOutputUri;
private String mOutputTitle;
private List mProgressListeners;
private int mRenderProgress = 0;
private int mOrientation;
private float mHorizontalAngle;
public final static int STEP_PTOGEN = 1;
public final static int STEP_PTOVAR = 2;
public final static int STEP_CPFIND = 3;
public final static int STEP_PTCLEAN = 4;
public final static int STEP_AUTOOPTIMISER = 5;
public final static int STEP_PANOMODIFY = 6;
public final static int STEP_NONA = 7;
public final static int STEP_ENBLEND = 8;
public final static int STEP_TOTAL = 9;
private final static int EXIT_SUCCESS = 0;
private final static String PANO_PROJECT_FILE = "project.pto";
private final static int PANO_PROJECTION_MODE = 2; // 2=equirectangular, 0=rectilinear
private final static int PANO_THREADS = 4;
private final static int PANO_CPFIND_SIEVE1SIZE = 10;
private final static int PANO_CPFIND_SIEVE2WIDTH = 5;
private final static int PANO_CPFIND_SIEVE2HEIGHT = 5;
private final static int PANO_CPFIND_MINMATCHES = 3;
private final static int PANO_BLEND_COMPRESSION = 90;
public interface ProgressListener {
public void onRenderStart(PicSphere sphere);
public void onStepChange(PicSphere sphere, int newStep);
public void onRenderDone(PicSphere sphere);
}
private class PicSphereImage {
private Uri mRealPathUri;
private Uri mContentUri;
private Vector3 mAngle;
public PicSphereImage(Uri realPath, Uri contentUri, Vector3 angle) {
mRealPathUri = realPath;
mContentUri = contentUri;
mAngle = angle;
}
public Uri getRealPath() {
return mRealPathUri;
}
public Uri getUri() {
return mContentUri;
}
public Vector3 getAngle() {
return mAngle;
}
}
protected PicSphere(Context context, SnapshotManager snapMan) {
mPictures = new ArrayList();
mProgressListeners = new ArrayList();
mContext = context;
mSnapManager = snapMan;
}
public void addProgressListener(ProgressListener listener) {
mProgressListeners.add(listener);
}
/**
* Adds a picture to the sphere
* @param pic The URI of the picture
*/
public void addPicture(Uri pic, Vector3 angle) {
PicSphereImage image = new PicSphereImage(
Uri.fromFile(new File(Util.getRealPathFromURI(mContext, pic))),
pic,
angle);
mPictures.add(image);
}
/**
* Removes the last picture of the sphere
*/
public void removeLastPicture() {
if (mPictures.size() > 0) {
mPictures.remove(mPictures.size()-1);
}
}
/**
* @return The number of pictures in the sphere
*/
public int getPicturesCount() {
return mPictures.size();
}
/**
* Sets the horizontal angle of the camera. AOSP don't check values are real values, and
* OEMs don't seem to care much about returning a real value (sony reports 360°...), so we
* pass 45° by default.
*
* @param angle The horizontal angle, in degrees
*/
public void setHorizontalAngle(float angle) {
mHorizontalAngle = angle;
}
/**
* @return The current rendering progress, or -1 if it's not rendering or done.
*/
public int getRenderProgress() {
return mRenderProgress;
}
/**
* Renders the sphere
*/
protected boolean render(int orientation) {
mOrientation = (orientation + 360) % 360;
for (ProgressListener listener : mProgressListeners) {
listener.onRenderStart(this);
}
// Prepare a temporary directory
File appFilesDir = mContext.getFilesDir();
mPathPrefix = appFilesDir.getAbsolutePath() + "/";
String tempPathStr = appFilesDir.getAbsolutePath() + "/" + System.currentTimeMillis();
mTempPath = new File(tempPathStr);
if (!mTempPath.mkdir()) {
Log.e(TAG, "Couldn't create temporary path");
return false;
}
mProjectFile = mTempPath + "/" + PANO_PROJECT_FILE;
// Wait till all images are saved and accessible
boolean allSaved = false;
while (!allSaved) {
allSaved = true;
for (PicSphereImage pic : mPictures) {
File file = new File(pic.getRealPath().getPath());
if (!file.exists() || !file.canRead()) {
allSaved = false;
// Wait a bit until we retry
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
break;
}
}
}
mRenderProgress = 1;
// Process our images
try {
doPtoGen();
doPtoVar();
doCpFind();
doPtclean();
doAutoOptimiser();
doPanoModify();
doNona();
doEnblend();
} catch (Exception ex) {
Log.e(TAG, "Unable to process: ", ex);
for (ProgressListener listener : mProgressListeners) {
listener.onRenderDone(this);
}
removeTempFiles();
return false;
}
for (ProgressListener listener : mProgressListeners) {
listener.onRenderDone(this);
}
removeTempFiles();
mRenderProgress = -1;
return true;
}
/**
* Remove the temporary files used by the app, and the source pictures of the sphere
*/
private void removeTempFiles() {
// Remove source pictures and temporary path
for (PicSphereImage uri : mPictures) {
List segments = uri.getUri().getPathSegments();
if (segments != null && segments.size() > 0) {
Util.removeFromGallery(mContext.getContentResolver(),
Integer.parseInt(segments.get(segments.size()-1)));
}
}
run("rm -r " + mTempPath.getPath());
}
/**
* Run a command on the system shell
* @param command The command to run
* @throws IOException
*/
private boolean run(String command) {
// HACK: We cd to /storage/emulated/0/DCIM/Camera/ because cpfind tries to
// find it in the current path instead of the actual path provided in the .pto. I'm
// leaving this here as a hack until I fix it properly in cpfind source
String dcimPath = "/sdcard/DCIM/Camera";
String fullCommand = String.format("PATH=%s; LD_LIBRARY_PATH=%s; cd %s; %s 2>&1",
mPathPrefix+":/system/bin",
mPathPrefix+":"+mPathPrefix+"/../lib/:/system/lib",
dcimPath,
command);
Log.v(TAG, "Running: " + fullCommand);
if (PopenHelper.run(fullCommand) != EXIT_SUCCESS) {
Log.e(TAG, "Command return code is not EXIT_SUCCESS!");
return false;
}
return true;
}
/**
* Notify the ProgressListeners of a step change
* @param step The new step
*/
private void notifyStep(int step) {
int progressPerStep = 100 / PicSphere.STEP_TOTAL;
mRenderProgress = step * progressPerStep;
for (ProgressListener listener : mProgressListeners) {
listener.onStepChange(this, step);
}
}
/**
* Most of hugin tools take a pto file as input and output. So the first step is to create this
* pto file. For this purpose use pto_gen.
*
* @return Success status
* @throws IOException
*/
private void doPtoGen() throws IOException {
Log.d(TAG, "PtoGen...");
notifyStep(STEP_PTOGEN);
// Build a list of files
String filesStr = "";
for (PicSphereImage picture : mPictures) {
filesStr += " " + picture.getRealPath().getPath();
}
if (!run(String.format("pto_gen -f %f -p %d -o %s %s",
mHorizontalAngle, PANO_PROJECTION_MODE,
mProjectFile, filesStr))) {
throw new IOException("PtoGen failed");
}
Log.d(TAG, "PtoGen... done");
}
/**
* Since we know the angle of each image thanks to the gyroscope, we're going to set them
* in our pto file before going on to matching the pictures edges.
*
* @return Success status
* @throws IOException
*/
private void doPtoVar() throws IOException {
Log.d(TAG, "PtoVar...");
notifyStep(STEP_PTOVAR);
int index = 0;
boolean referenceYawSet = false;
float referenceYaw = 0.0f;
for (PicSphereImage picture : mPictures) {
// We stored camera angle into x, y and z respectively as degrees
Vector3 a = picture.getAngle();
// We now convert them to fit pitch, yaw and roll values as expected by Hugin
// TODO: Is it orientation independent? Portrait/landscape?
// Pitch: If you shoot a multi row panorama you will have to tilt the camera up or down.
// The angle the optical axis of the camera is tilted up (positive value) or down
// (negative value) is the pitch of the image. The pitch of the zenith image (shot
// straight upwards) is 90° f.e.
float pitch = -a.y;
// Yaw: If shooting a panorama you rotate the camera horizontally (around a vertical
// axis, TrY). You will treat one image as an anchor image which is considered to have
// yaw = 0. The angle you rotated your camera relatively to this image is its Yaw value.
float yaw = a.x + 180.0f;
if (!referenceYawSet) {
referenceYaw = yaw;
referenceYawSet = true;
yaw = 0.0f;
} else {
yaw = (yaw - referenceYaw) + ((yaw - referenceYaw < 0) ? 360 : 0);
}
// Roll: Roll is the angle of camera rotation around its optical axis. In normal
// landscape orientation, the sensor returns a pitch or -180/180 degrees. Hugin expects
// that angle to be zero, so we clamp it
float roll = (a.z + 180.0f) % 360.0f;
Log.e(TAG, "Pitch/Yaw/Roll: " + pitch + " ; " + yaw + " ; " + roll);
if (!run(String.format("pto_var -o %s --set p%d=%f,y%d=%f,r%d=%f %s",
mProjectFile,
index,
pitch,
index,
yaw,
index,
roll,
mProjectFile))) {
throw new IOException("PtoVar failed");
}
index++;
}
}
/**
* We then look for control points and matching pairs in the images, using CpFind. Note that we
* can enable celeste cleaning in cpfind, but we need the celeste reference database. Also, it's
* probably a bit slower and not worth it for what we're doing.
*
* @return Success status
* @throws IOException
*/
private boolean doCpFind() throws IOException {
Log.d(TAG, "CpFind...");
notifyStep(STEP_CPFIND);
if (!run(String.format("cpfind --sieve1size %d --sieve2width %d --sieve2height %d -n %d -v " +
"--prealigned --minmatches %d -o " +
"%s %s",
PANO_CPFIND_SIEVE1SIZE, PANO_CPFIND_SIEVE2WIDTH, PANO_CPFIND_SIEVE2HEIGHT,
PANO_THREADS, PANO_CPFIND_MINMATCHES, mProjectFile, mProjectFile))) {
throw new IOException("CpFind failed");
}
Log.d(TAG, "CpFind... done");
return true;
}
/**
* You could go ahead and optimise this project file straight away, but this can be a bit hit
* and miss. First it is a good idea to clean up the control points.
*
* @return Success status
* @throws IOException
*/
private boolean doPtclean() throws IOException {
Log.d(TAG, "Ptclean...");
notifyStep(STEP_PTCLEAN);
if (!run(String.format("ptclean -o %s %s", mProjectFile, mProjectFile))) {
throw new IOException("PtClean failed");
}
Log.d(TAG, "Ptclean... done");
return true;
}
/**
* Up to now, the project file simply contains an image list and control points, the images are
* not yet aligned, you can do this by optimising geometric parameters with the autooptimiser
* tool
*
* @return Success status
* @throws IOException
*/
private boolean doAutoOptimiser() throws IOException {
Log.d(TAG, "AutoOptimiser...");
notifyStep(STEP_AUTOOPTIMISER);
if (!run(String.format("autooptimiser -v %f -a -s -l -m -o %s %s", mHorizontalAngle,
mProjectFile, mProjectFile))) {
throw new IOException("AutoOptimiser failed");
}
Log.d(TAG, "AutoOptimiser... done");
return true;
}
/**
* Crop and straighten the panorama to fit a rectangular image
*
* @return Success status
* @throws IOException
*/
private boolean doPanoModify() throws IOException {
Log.d(TAG, "PanoModify...");
notifyStep(STEP_PANOMODIFY);
// Google PhotoSphere (both Google Gallery viewer and Google+ viewer, even if the last one
// is a bit more permissive) must have a 2:1 image to work properly. For now, we left this
// on AUTO to have a clean final image. They seem to view fine on the Google+ Web viewer,
// and we'll have to come up with our own Android viewer anyway at some point.
String canvas = "AUTO";
String crop = "AUTO";
if (!run(String.format("pano_modify --crop=%s --canvas=%s -o %s %s",
crop, canvas, mProjectFile, mProjectFile))) {
throw new IOException("Pano_Modify failed");
}
Log.d(TAG, "PanoModify... done");
return true;
}
/**
* The hugin tool for remapping and distorting the photos into the final panorama frame is nona,
* it uses the .pto project file as a set of instructions
*
* @return Success status
* @throws IOException
*/
private boolean doNona() throws IOException {
Log.d(TAG, "Nona...");
notifyStep(STEP_NONA);
if (!run(String.format("nona -t %d -o %s/project -m TIFF_m %s",
PANO_THREADS, mTempPath, mProjectFile))) {
throw new IOException("Nona failed");
}
Log.d(TAG, "Nona... done");
return true;
}
/**
* nona can do rudimentary assembly of the remapped images, but a much better tool for this
* is enblend, feed it the images, it will pick seam lines and blend the overlapping areas.
* We use its multithreaded friend, multiblend, which is much faster than enblend for the
* same final quality.
*
* @return Success status
* @throws IOException
*/
private void doEnblend() throws IOException {
Log.d(TAG, "Enblend...");
notifyStep(STEP_ENBLEND);
String jpegPath = mTempPath + "/final.jpg";
// Multiblend can take a lot of RAM when blending a lot of high resolution pictures. We
// prepare the system for that.
System.gc();
// As we are running through a sh shell, we can use * instead of writing the full
// images list.
run(String.format("multiblend --wideblend --compression=%d -o %s %s/project*.tif",
PANO_BLEND_COMPRESSION, jpegPath, mTempPath));
// Apply PhotoSphere EXIF tags on the final jpeg
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;
BitmapFactory.decodeFile(mTempPath + "/final.jpg", opts);
if (!doExifTagging(opts.outWidth, opts.outHeight)) {
throw new IOException("Cannot tag EXIF info");
}
// Save it to gallery
// XXX: This needs opening the output byte array... Isn't there any way to update
// gallery data without having to reload/save the JPEG file? Because we have it already,
// we could just move it.
byte[] jpegData;
RandomAccessFile f = null;
try {
f = new RandomAccessFile(jpegPath, "r");
// Get and check length
long longlength = f.length();
int length = (int) longlength;
if (length != longlength)
throw new IOException("File size >= 2 GB");
// Read file and return data
jpegData = new byte[length];
f.readFully(jpegData);
} catch (Exception e) {
Log.e(TAG, "Couldn't access final file, did rendering fail?");
throw new IOException("Cannot access final file");
} finally {
if (f != null) {
f.close();
}
}
if (opts.outWidth > 0 && opts.outHeight > 0) {
mSnapManager.prepareNamerUri(opts.outWidth, opts.outHeight);
mOutputUri = mSnapManager.getNamerUri();
mOutputTitle = mSnapManager.getNamerTitle();
Log.i(TAG, "PicSphere size: " + opts.outWidth + "x" + opts.outHeight);
mSnapManager.saveImage(mOutputUri, mOutputTitle, opts.outWidth, opts.outWidth, 0, jpegData);
} else {
Log.e(TAG, "Invalid output image size: " + opts.outWidth + "x" + opts.outHeight);
throw new IOException("Invalid output image size: " + opts.outWidth + "x" + opts.outHeight);
}
Log.d(TAG, "Enblend... done");
}
private boolean doExifTagging(int width, int height) throws IOException {
Log.d(TAG, "XMP metadata tagging...");
try {
XMPHelper xmp = new XMPHelper();
xmp.writeXmpToFile(mTempPath + "/final.jpg", generatePhotoSphereXMP(width,
height, mPictures.size()));
} catch (Exception e) {
Log.e(TAG, "Couldn't access final file, did rendering fail?");
return false;
}
Log.d(TAG, "XMP metadata tagging... done");
return true;
}
/**
* Generate PhotoSphere XMP data file to apply on panorama images
* https://developers.google.com/photo-sphere/metadata/
*
* @param width The width of the panorama
* @param height The height of the panorama
* @return RDF XML XMP metadata
*/
public String generatePhotoSphereXMP(int width, int height, int sourceImageCount) {
return "\n" +
"\n" +
" True\n" +
" CyanogenMod Focal\n" +
" CyanogenMod Focal with Hugin\n" +
" equirectangular\n" +
" 350.0\n" +
" 90.0\n" +
" 45.0\n" +
" "+mOrientation+"\n" +
" "+mHorizontalAngle+"\n" +
" 0\n" +
" 0\n" +
" "+(width)+"\n" +
" "+(height)+"\n" +
" "+width+"\n" +
" "+height+"\n" +
//" 2012-11-07T21:03:13.465Z\n" +
//" 2012-11-07T21:04:10.897Z\n" +
" "+sourceImageCount+"\n" +
" False\n" +
"";
}
}
================================================
FILE: src/org/cyanogenmod/focal/picsphere/PicSphereCaptureTransformer.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.picsphere;
import android.hardware.Camera;
import android.util.Log;
import org.cyanogenmod.focal.CameraActivity;
import fr.xplod.focal.R;
import org.cyanogenmod.focal.SnapshotManager;
import org.cyanogenmod.focal.feats.CaptureTransformer;
import org.cyanogenmod.focal.ui.ShutterButton;
/**
* Capture Transformer for PicSphere that will store all shots to feed them to a new PicSphere
* created by PicSphereManager
*/
public class PicSphereCaptureTransformer extends CaptureTransformer {
public final static String TAG = "PicSphereCaptureTransformer";
private PicSphereManager mPicSphereManager;
private PicSphere mPicSphere;
private CameraActivity mContext;
private Vector3 mLastShotAngle;
public PicSphereCaptureTransformer(CameraActivity context) {
super(context.getCamManager(), context.getSnapManager());
mContext = context;
mPicSphereManager = context.getPicSphereManager();
}
public void removeLastPicture() {
if (mPicSphere == null) {
return;
}
mPicSphere.removeLastPicture();
mPicSphereManager.getRenderer().removeLastPicture();
if (mPicSphere.getPicturesCount() == 0) {
mContext.setPicSphereUndoVisible(false);
mPicSphereManager.getRenderer().setCamPreviewVisible(true);
}
}
@Override
public void onShutterButtonClicked(ShutterButton button) {
if (mPicSphere == null) {
// Initialize a new sphere
mPicSphere = mPicSphereManager.createPicSphere();
Camera.Parameters params = mCamManager.getParameters();
float horizontalAngle = 0;
if (params != null) {
horizontalAngle = mCamManager.getParameters().getHorizontalViewAngle();
}
// In theory, drivers should return a proper value for horizontal angle. However,
// some careless OEMs put "0" or "360" to pass CTS, so we just check if the value
// seems legit, otherwise we put 45° as it's the angle of most phone lenses.
if (horizontalAngle < 30 || horizontalAngle > 70) {
horizontalAngle = 45;
}
mPicSphere.setHorizontalAngle(horizontalAngle);
}
mSnapManager.setBypassProcessing(true);
mSnapManager.queueSnapshot(true, 0);
mPicSphereManager.getRenderer().setCamPreviewVisible(false);
// Notify how to finish a sphere
if (mPicSphere != null && mPicSphere.getPicturesCount() == 0) {
CameraActivity.notify(mContext.getString(R.string.ps_long_press_to_stop), 4000);
}
}
@Override
public void onShutterButtonLongPressed(ShutterButton button) {
if (mPicSphere != null) {
if (mPicSphere.getPicturesCount() <= 1) {
CameraActivity.notify(mCamManager.getContext()
.getString(R.string.picsphere_need_two_pics), 2000);
return;
}
mPicSphereManager.startRendering(mPicSphere, mContext.getOrientation());
mPicSphereManager.getRenderer().clearSnapshots();
mPicSphere = null;
mContext.setPicSphereUndoVisible(false);
}
}
@Override
public void onSnapshotShutter(SnapshotManager.SnapshotInfo info) {
mPicSphereManager.getRenderer().addSnapshot(info.mThumbnail);
mLastShotAngle = mPicSphereManager.getRenderer().getAngleAsVector();
}
@Override
public void onSnapshotPreview(SnapshotManager.SnapshotInfo info) {
}
@Override
public void onSnapshotProcessing(SnapshotManager.SnapshotInfo info) {
}
@Override
public void onSnapshotSaved(SnapshotManager.SnapshotInfo info) {
if (mPicSphere != null) {
mPicSphere.addPicture(info.mUri, mLastShotAngle);
mContext.setPicSphereUndoVisible(true);
mContext.setHelperText("");
} else {
Log.e(TAG, "No current PicSphere");
}
}
@Override
public void onMediaSavingStart() {
}
@Override
public void onMediaSavingDone() {
}
@Override
public void onVideoRecordingStart() {
}
@Override
public void onVideoRecordingStop() {
}
}
================================================
FILE: src/org/cyanogenmod/focal/picsphere/PicSphereManager.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.picsphere;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.res.AssetManager;
import android.os.Handler;
import android.os.IBinder;
import android.util.Log;
import android.view.TextureView;
import android.widget.FrameLayout;
import org.cyanogenmod.focal.CameraActivity;
import fr.xplod.focal.R;
import org.cyanogenmod.focal.SnapshotManager;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
/**
* This class manages the interaction and processing of Picture Spheres ("PicSphere"). PicSphere
* is basically an open-source clone of Google's PhotoSphere, using Hugin open-source panorama
* stitching libraries.
*
* Can you feel the awesomeness?
*/
public class PicSphereManager implements PicSphere.ProgressListener {
public final static String TAG = "PicSphereManager";
private List mPicSpheres;
private CameraActivity mContext;
private SnapshotManager mSnapManager;
private Capture3DRenderer mCapture3DRenderer;
private PicSphereRenderingService mBoundService;
private FrameLayout mGLRootView;
private TextureView mGLSurfaceView;
private Handler mHandler;
private boolean mIsBound;
private ServiceConnection mServiceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
// This is called when the connection with the service has been
// established, giving us the service object we can use to
// interact with the service. Because we have bound to a explicit
// service that we know is running in our own process, we can
// cast its IBinder to a concrete class and directly access it.
mBoundService = ((PicSphereRenderingService.LocalBinder)service).getService();
}
public void onServiceDisconnected(ComponentName className) {
// This is called when the connection with the service has been
// unexpectedly disconnected -- that is, its process crashed.
// Because it is running in our same process, we should never
// see this happen.
mBoundService = null;
}
};
public PicSphereManager(CameraActivity context, SnapshotManager snapMan) {
mContext = context;
mSnapManager = snapMan;
mPicSpheres = new ArrayList();
mHandler = new Handler();
mIsBound = false;
doBindService();
new Thread() {
public void run() { copyBinaries(); }
}.start();
}
/**
* Creates an empty PicSphere
* @return A PicSphere that is empty
*/
public PicSphere createPicSphere() {
PicSphere sphere = new PicSphere(mContext, mSnapManager);
mPicSpheres.add(sphere);
return sphere;
}
public void clearSpheres() {
mPicSpheres.clear();
}
/**
* Returns the 3D renderer context for PicSphere capture mode
*/
public Capture3DRenderer getRenderer() {
if (mCapture3DRenderer == null) {
// Initialize the 3D renderer
mCapture3DRenderer = new Capture3DRenderer(mContext, mContext.getCamManager());
// We route the camera preview to our texture
mGLRootView = (FrameLayout) mContext.findViewById(R.id.gl_renderer_container);
}
return mCapture3DRenderer;
}
/**
* Returns the progress of the currently rendering picsphere
* @return The percentage of progress, or -1 if no picsphere is rendering
*/
public int getRenderingProgress() {
if (mPicSpheres.size() > 0) {
return mPicSpheres.get(0).getRenderProgress();
}
return -1;
}
/**
* Starts rendering the provided PicSphere in a thread, and handles a service
* that will keep on processing even once Nemesis is closed
*
* @param sphere The PicSphere to render
*/
public void startRendering(final PicSphere sphere, final int orientation) {
// Notify toast
CameraActivity.notify(mContext.getString(R.string.picsphere_toast_background_render), 2500);
if (mIsBound && mBoundService != null) {
sphere.addProgressListener(this);
mBoundService.render(sphere, orientation);
} else {
doBindService();
mHandler.postDelayed(new Runnable() {
public void run() {
startRendering(sphere, orientation);
}
}, 500);
}
}
void doBindService() {
// Establish a connection with the service. We use an explicit
// class name because we want a specific service implementation that
// we know will be running in our own process (and thus won't be
// supporting component replacement by other applications).
Log.v(TAG, "Binding PicSphere rendering service");
mContext.bindService(new Intent(mContext, PicSphereRenderingService.class),
mServiceConnection, Context.BIND_AUTO_CREATE);
mIsBound = true;
}
public void tearDown() {
if (mCapture3DRenderer != null) {
mCapture3DRenderer.onPause();
mCapture3DRenderer = null;
}
mPicSpheres.clear();
if (mIsBound) {
// Detach our existing connection.
mContext.unbindService(mServiceConnection);
mIsBound = false;
}
}
public void onPause() {
if (mCapture3DRenderer != null) {
mCapture3DRenderer.onPause();
}
tearDown();
mPicSpheres.clear();
}
public void onResume() {
if (mCapture3DRenderer != null) {
mCapture3DRenderer.onResume();
}
}
public int getSpheresCount() {
return mPicSpheres.size();
}
/**
* Copy the required binaries and libraries to the data folder
* @return
*/
private boolean copyBinaries() {
boolean result = true;
try {
String files[] = {
"autooptimiser", "pto_gen", "cpfind", "multiblend", "enfuse", "nona", "pano_modify",
"ptclean", "tiffinfo", "align_image_stack", "pto_var",
"libexiv2.so", "libglib-2.0.so", "libgmodule-2.0.so", "libgobject-2.0.so",
"libgthread-2.0.so", "libjpeg.so", "libpano13.so", "libtiff.so", "libtiffdecoder.so",
"libvigraimpex.so"
};
final AssetManager am = mContext.getAssets();
for (String file : files) {
InputStream is = am.open("picsphere/" + file);
// Create the output file
File dir = mContext.getFilesDir();
File outFile = new File(dir, file);
if (outFile.exists())
outFile.delete();
OutputStream os = new FileOutputStream(outFile);
// Copy the file
byte[] buffer = new byte[1024];
int read;
while ((read = is.read(buffer)) != -1) {
os.write(buffer, 0, read);
}
if (!outFile.getName().endsWith(".so") && !outFile.setExecutable(true)) {
result = false;
}
os.flush();
os.close();
is.close();
}
} catch (Exception e) {
Log.e(TAG, "Error copying libraries and binaries", e);
result = false;
}
return result;
}
@Override
public void onRenderStart(PicSphere sphere) {
}
@Override
public void onStepChange(PicSphere sphere, int newStep) {
if (CameraActivity.getCameraMode() == CameraActivity.CAMERA_MODE_PICSPHERE) {
mContext.setHelperText(String.format(mContext.getString(
R.string.picsphere_rendering_progress), sphere.getRenderProgress()));
}
}
@Override
public void onRenderDone(PicSphere sphere) {
mPicSpheres.remove(sphere);
if (CameraActivity.getCameraMode() == CameraActivity.CAMERA_MODE_PICSPHERE) {
mContext.setHelperText(mContext.getString(R.string.picsphere_start_hint));
}
if (mCapture3DRenderer != null) {
mCapture3DRenderer.setCamPreviewVisible(true);
}
}
}
================================================
FILE: src/org/cyanogenmod/focal/picsphere/PicSphereRenderingService.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.picsphere;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;
import fr.xplod.focal.R;
/**
* Service that handles PicSphere rendering outside
* the app context (as the rendering thread would get killed)
*/
public class PicSphereRenderingService extends Service implements PicSphere.ProgressListener {
private NotificationManager mNM;
// Unique Identification Number for the Notification.
// We use it on Notification start, and to cancel it.
private int NOTIFICATION = 1337;
private boolean mHasFailed = false;
private int mRenderQueue = 0;
/**
* Class for clients to access. Because we know this service always
* runs in the same process as its clients, we don't need to deal with
* IPC.
*/
public class LocalBinder extends Binder {
PicSphereRenderingService getService() {
return PicSphereRenderingService.this;
}
}
@Override
public void onCreate() {
mNM = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i("LocalService", "Received start id " + startId + ": " + intent);
// We want this service to continue running until it is explicitly
// stopped, so return sticky.
return START_STICKY;
}
@Override
public void onDestroy() {
// Cancel the persistent notification.
mNM.cancel(NOTIFICATION);
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
// This is the object that receives interactions from clients. See
// RemoteService for a more complete example.
private final IBinder mBinder = new LocalBinder();
public void render(final PicSphere sphere, final int orientation) {
sphere.addProgressListener(this);
mRenderQueue++;
new Thread() {
public void run() {
if (!sphere.render(orientation)) {
mHasFailed = true;
mNM.notify(NOTIFICATION,
buildFailureNotification(getString(R.string.picsphere_failed),
getString(R.string.picsphere_failed_details)));
}
mRenderQueue--;
if (mRenderQueue == 0) {
// We have no more PicSpheres to render, so we can stop the service. Otherwise,
// we leave it on for other spheres.
PicSphereRenderingService.this.stopSelf();
}
}
}.start();
}
@Override
public void onRenderStart(PicSphere sphere) {
// Display a notification
mNM.notify(NOTIFICATION, buildProgressNotification(0,
getString(R.string.picsphere_step_preparing)));
mHasFailed = false;
}
@Override
public void onStepChange(PicSphere sphere, int newStep) {
int progressPerStep = 100 / PicSphere.STEP_TOTAL;
int progress = newStep * progressPerStep;
String text = "";
switch (newStep) {
case PicSphere.STEP_PTOGEN:
text = getString(R.string.picsphere_step_ptogen);
break;
case PicSphere.STEP_PTOVAR:
text = getString(R.string.picsphere_step_ptovar);
break;
case PicSphere.STEP_CPFIND:
text = getString(R.string.picsphere_step_cpfind);
break;
case PicSphere.STEP_PTCLEAN:
text = getString(R.string.picsphere_step_ptclean);
break;
case PicSphere.STEP_AUTOOPTIMISER:
text = getString(R.string.picsphere_step_autooptimiser);
break;
case PicSphere.STEP_PANOMODIFY:
text = getString(R.string.picsphere_step_panomodify);
break;
case PicSphere.STEP_NONA:
text = getString(R.string.picsphere_step_nona);
break;
case PicSphere.STEP_ENBLEND:
text = getString(R.string.picsphere_step_enblend);
break;
}
mNM.notify(NOTIFICATION, buildProgressNotification(progress, text));
}
@Override
public void onRenderDone(PicSphere sphere) {
if (!mHasFailed) {
mNM.cancel(NOTIFICATION);
}
}
private Notification buildProgressNotification(int percentage, String text) {
Notification.Builder mBuilder =
new Notification.Builder(this)
.setSmallIcon(R.drawable.ic_launcher)
.setContentTitle(getString(R.string.picsphere_notif_title))
.setContentText(percentage+"% ("+text+")")
.setOngoing(true);
return mBuilder.build();
}
private Notification buildFailureNotification(String title, String text) {
Notification.Builder mBuilder =
new Notification.Builder(this)
.setSmallIcon(R.drawable.ic_launcher)
.setContentTitle(title)
.setContentText(text)
.setOngoing(false);
return mBuilder.build();
}
}
================================================
FILE: src/org/cyanogenmod/focal/picsphere/Quaternion.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.picsphere;
import android.opengl.Matrix;
/**
* 3D Maths - Quaternion
* Inspired from http://content.gpwiki.org/index.php/OpenGL%3aTutorials%3aUsing_Quaternions_to_represent_rotation
*/
public class Quaternion {
public float x;
public float y;
public float z;
public float w = 1.0f;
public final static float PIOVER180 = (float) (Math.PI / 180.0f);
private final static float TOLERANCE = 0.00001f;
public Quaternion() {
}
public Quaternion(Quaternion o) {
this.x = o.x;
this.y = o.y;
this.z = o.z;
this.w = o.w;
}
public Quaternion(float x, float y, float z, float w) {
this.x = x;
this.y = y;
this.z = z;
this.w = w;
}
public void fromEuler(float pitch, float yaw, float roll) {
// Basically we create 3 Quaternions, one for pitch, one for yaw, one for roll
// and multiply those together.
// the calculation below does the same, just shorter
float p = pitch * PIOVER180 / 2.0f;
float y = yaw * PIOVER180 / 2.0f;
float r = roll * PIOVER180 / 2.0f;
float sinp = (float) Math.sin(p);
float siny = (float) Math.sin(y);
float sinr = (float) Math.sin(r);
float cosp = (float) Math.cos(p);
float cosy = (float) Math.cos(y);
float cosr = (float) Math.cos(r);
this.x = sinr * cosp * cosy - cosr * sinp * siny;
this.y = cosr * sinp * cosy + sinr * cosp * siny;
this.z = cosr * cosp * siny - sinr * sinp * cosy;
this.w = cosr * cosp * cosy + sinr * sinp * siny;
normalise();
}
// Convert to Matrix
public float[] getMatrix() {
float x2 = x * x;
float y2 = y * y;
float z2 = z * z;
float xy = x * y;
float xz = x * z;
float yz = y * z;
float wx = w * x;
float wy = w * y;
float wz = w * z;
// This calculation would be a lot more complicated for non-unit length quaternions
// Note: The constructor of Matrix4 expects the Matrix in column-major format like
// expected by OpenGL
return new float[]{1.0f - 2.0f * (y2 + z2), 2.0f * (xy - wz), 2.0f * (xz + wy), 0.0f,
2.0f * (xy + wz), 1.0f - 2.0f * (x2 + z2), 2.0f * (yz - wx), 0.0f,
2.0f * (xz - wy), 2.0f * (yz + wx), 1.0f - 2.0f * (x2 + y2), 0.0f,
0.0f, 0.0f, 0.0f, 1.0f};
}
public Quaternion getConjugate() {
return new Quaternion(-x, -y, -z, w);
}
// Multiplying q1 with q2 applies the rotation q2 to q1
public Quaternion multiply(Quaternion rq) {
// the constructor takes its arguments as (x, y, z, w)
return new Quaternion(w * rq.x + x * rq.w + y * rq.z - z * rq.y,
w * rq.y + y * rq.w + z * rq.x - x * rq.z,
w * rq.z + z * rq.w + x * rq.y - y * rq.x,
w * rq.w - x * rq.x - y * rq.y - z * rq.z);
}
// normalising a quaternion works similar to a vector. This method will not do anything
// if the quaternion is close enough to being unit-length. define TOLERANCE as something
// small like 0.00001f to get accurate results
public void normalise() {
// Don't normalize if we don't have to
float mag2 = w * w + x * x + y * y + z * z;
if (Math.abs(mag2) > TOLERANCE && Math.abs(mag2 - 1.0f) > TOLERANCE) {
float mag = (float) Math.sqrt(mag2);
w /= mag;
x /= mag;
y /= mag;
z /= mag;
}
}
}
================================================
FILE: src/org/cyanogenmod/focal/picsphere/SensorFusion.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
* Copyright (C) 2012 Paul Lawitzki
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.picsphere;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import java.util.Timer;
import java.util.TimerTask;
public class SensorFusion implements SensorEventListener {
private SensorManager mSensorManager = null;
float[] mOrientation = new float[3];
float[] mRotationMatrix;
public SensorFusion(Context context) {
// get sensorManager and initialise sensor listeners
mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
initListeners();
}
public void onPauseOrStop() {
mSensorManager.unregisterListener(this);
}
public void onResume() {
// restore the sensor listeners when user resumes the application.
initListeners();
}
// This function registers sensor listeners for the accelerometer, magnetometer and gyroscope.
public void initListeners(){
mSensorManager.registerListener(this,
mSensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR),
20000);
}
@Override
public void onSensorChanged(SensorEvent event) {
if (mRotationMatrix == null) {
mRotationMatrix = new float[16];
}
// Get rotation matrix from angles
SensorManager.getRotationMatrixFromVector(mRotationMatrix, event.values);
// Remap the axes
SensorManager.remapCoordinateSystem(mRotationMatrix, SensorManager.AXIS_MINUS_Z,
SensorManager.AXIS_X, mRotationMatrix);
// Get back a remapped orientation vector
SensorManager.getOrientation(mRotationMatrix, mOrientation);
}
@Override
public void onAccuracyChanged(Sensor sensor, int i) {
}
public float[] getFusedOrientation() {
return mOrientation;
}
public float[] getRotationMatrix() {
return mRotationMatrix;
}
}
================================================
FILE: src/org/cyanogenmod/focal/picsphere/Vector3.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.picsphere;
/**
* 3D Maths - 3 component vector
*/
public class Vector3 {
public float x;
public float y;
public float z;
public Vector3() {
}
public Vector3(float x, float y, float z) {
this.x = x;
this.y = y;
this.z = z;
}
public Vector3(Vector3 other) {
this.x = other.x;
this.y = other.y;
this.z = other.z;
}
/**
* Returns the length of the vector
*/
public float length() {
return (float) Math.sqrt(x * x + y * y + z * z);
}
public void normalise() {
final float length = length();
if(length != 0) {
y = x/length;
y = y/length;
z = z/length;
}
}
public Vector3 multiply(Quaternion quat) {
Vector3 vn = new Vector3(this);
vn.normalise();
Quaternion vecQuat = new Quaternion(),
resQuat = new Quaternion();
vecQuat.x = vn.x;
vecQuat.y = vn.y;
vecQuat.z = vn.z;
vecQuat.w = 0.0f;
resQuat = vecQuat.multiply(quat.getConjugate());
resQuat = quat.multiply(resQuat);
return new Vector3(resQuat.x, resQuat.y, resQuat.z);
}
}
================================================
FILE: src/org/cyanogenmod/focal/ui/CenteredSeekBar.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.ui;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.RectF;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import android.widget.ImageView;
import fr.xplod.focal.R;
import java.math.BigDecimal;
/**
* Seek bar that starts from the middle
* Based on RangeSeekBar at https://code.google.com/p/range-seek-bar/
*/
public class CenteredSeekBar extends ImageView {
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Bitmap thumbImage = BitmapFactory.decodeResource(
getResources(), R.drawable.seek_thumb_normal);
private final Bitmap thumbPressedImage = BitmapFactory.decodeResource(
getResources(), R.drawable.seek_thumb_pressed);
private final float thumbWidth = thumbImage.getWidth();
private final float thumbHalfWidth = 0.5f * thumbWidth;
private final float thumbHalfHeight = 0.5f * thumbImage.getHeight();
private final float lineHeight = 0.3f * thumbHalfHeight;
private final float padding = thumbHalfWidth;
private final Integer absoluteMinValue, absoluteMaxValue;
private final NumberType numberType;
private final double absoluteMinValuePrim, absoluteMaxValuePrim;
private double normalizedValue = 0d;
private Thumb pressedThumb = null;
private boolean notifyWhileDragging = false;
private OnCenteredSeekBarChangeListener listener;
/**
* Default color of a {@link CenteredSeekBar}, #FF33B5E5.
* This is also known as "Ice Cream Sandwich" blue.
*/
public static final int DEFAULT_COLOR = Color.argb(0xFF, 0x33, 0xB5, 0xE5);
/**
* An invalid pointer id.
*/
public static final int INVALID_POINTER_ID = 255;
// Localized constants from MotionEvent for compatibility
// with API < 8 "Froyo".
public static final int ACTION_POINTER_UP = 0x6, ACTION_POINTER_INDEX_MASK = 0x0000ff00,
ACTION_POINTER_INDEX_SHIFT = 8;
private float mDownMotionX;
private int mActivePointerId = INVALID_POINTER_ID;
/**
* On touch, this offset plus the scaled value from the position of the
* touch will form the progress value. Usually 0.
*/
float mTouchProgressOffset;
private int mScaledTouchSlop;
private boolean mIsDragging;
/**
* Creates a new RangeSeekBar.
*
* @param absoluteMinValue
* The minimum value of the selectable range.
* @param absoluteMaxValue
* The maximum value of the selectable range.
* @param context
* @throws IllegalArgumentException
* Will be thrown if min/max value type is not one of Long, Double,
* Integer, Float, Short, Byte or BigDecimal.
*/
public CenteredSeekBar(Integer absoluteMinValue, Integer absoluteMaxValue,
Context context) throws IllegalArgumentException {
super(context);
this.absoluteMinValue = absoluteMinValue;
this.absoluteMaxValue = absoluteMaxValue;
absoluteMinValuePrim = absoluteMinValue.doubleValue();
absoluteMaxValuePrim = absoluteMaxValue.doubleValue();
numberType = NumberType.fromNumber(absoluteMinValue);
// Make RangeSeekBar focusable. This solves focus handling issues in case
// EditText widgets are being used along with the RangeSeekBar within ScollViews.
setFocusable(true);
setFocusableInTouchMode(true);
init();
}
public CenteredSeekBar(Context context, AttributeSet attrs) {
super(context);
this.absoluteMinValue = 0;
this.absoluteMaxValue = 10;
absoluteMinValuePrim = absoluteMinValue.doubleValue();
absoluteMaxValuePrim = absoluteMaxValue.doubleValue();
numberType = NumberType.fromNumber(absoluteMinValue);
// Make RangeSeekBar focusable. This solves focus handling issues in case
// EditText widgets are being used along with the RangeSeekBar within ScollViews.
setFocusable(true);
setFocusableInTouchMode(true);
init();
}
private final void init() {
mScaledTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
}
public boolean isNotifyWhileDragging() {
return notifyWhileDragging;
}
/**
* Should the widget notify the listener callback while the user
* is still dragging a thumb? Default is false.
*
* @param flag
*/
public void setNotifyWhileDragging(boolean flag) {
this.notifyWhileDragging = flag;
}
/**
* Returns the absolute minimum value of the range that has
* been set at construction time.
*
* @return The absolute minimum value of the range.
*/
public Integer getAbsoluteMinValue() {
return absoluteMinValue;
}
/**
* Returns the absolute maximum value of the range that has
* been set at construction time.
*
* @return The absolute maximum value of the range.
*/
public Integer getAbsoluteMaxValue() {
return absoluteMaxValue;
}
/**
* Returns the currently selected value.
*
* @return The currently selected value.
*/
public Integer getSelectedValue() {
return normalizedToValue(normalizedValue);
}
/**
* Sets the currently selected minimum value.
* The widget will be invalidated and redrawn.
*
* @param value
* The Integer value to set the minimum value to.
* Will be clamped to given absolute minimum/maximum range.
*/
public void setSelectedMinValue(Integer value) {
// In case absoluteMinValue == absoluteMaxValue, avoid division by zero when normalizing.
if (0 == (absoluteMaxValuePrim - absoluteMinValuePrim)) {
setNormalizedValue(0d);
} else {
setNormalizedValue(valueToNormalized(value));
}
}
/**
* Registers given listener callback to notify about changed selected values.
*
* @param listener
* The listener to notify about changed selected values.
*/
public void setOnCenteredSeekBarChangeListener(OnCenteredSeekBarChangeListener listener) {
this.listener = listener;
}
/**
* Handles thumb selection and movement.
* Notifies listener callback on certain events.
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!isEnabled()) {
return false;
}
int pointerIndex;
final int action = event.getAction();
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
// Remember where the motion event started
mActivePointerId = event.getPointerId(event.getPointerCount() - 1);
pointerIndex = event.findPointerIndex(mActivePointerId);
mDownMotionX = event.getX(pointerIndex);
pressedThumb = evalPressedThumb(mDownMotionX);
// Only handle thumb presses
if (pressedThumb == null) {
return super.onTouchEvent(event);
}
setPressed(true);
invalidate();
onStartTrackingTouch();
trackTouchEvent(event);
attemptClaimDrag();
break;
case MotionEvent.ACTION_MOVE:
if (pressedThumb != null) {
if (mIsDragging) {
trackTouchEvent(event);
} else {
// Scroll to follow the motion event
pointerIndex = event.findPointerIndex(mActivePointerId);
final float x = event.getX(pointerIndex);
if (Math.abs(x - mDownMotionX) > mScaledTouchSlop) {
setPressed(true);
invalidate();
onStartTrackingTouch();
trackTouchEvent(event);
attemptClaimDrag();
}
}
if (notifyWhileDragging && listener != null) {
listener.OnCenteredSeekBarValueChanged(this, getSelectedValue());
}
}
break;
case MotionEvent.ACTION_UP:
if (mIsDragging) {
trackTouchEvent(event);
onStopTrackingTouch();
setPressed(false);
} else {
// Touch up when we never crossed the touch slop threshold
// should be interpreted as a tap-seek to that location.
onStartTrackingTouch();
trackTouchEvent(event);
onStopTrackingTouch();
}
pressedThumb = null;
invalidate();
if (listener != null) {
listener.OnCenteredSeekBarValueChanged(this, getSelectedValue());
}
break;
case MotionEvent.ACTION_POINTER_DOWN:
final int index = event.getPointerCount() - 1;
// Final int index = ev.getActionIndex();
mDownMotionX = event.getX(index);
mActivePointerId = event.getPointerId(index);
invalidate();
break;
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(event);
invalidate();
break;
case MotionEvent.ACTION_CANCEL:
if (mIsDragging) {
onStopTrackingTouch();
setPressed(false);
}
invalidate(); // see above explanation
break;
}
return true;
}
private final void onSecondaryPointerUp(MotionEvent ev) {
final int pointerIndex = (ev.getAction() & ACTION_POINTER_INDEX_MASK)
>> ACTION_POINTER_INDEX_SHIFT;
final int pointerId = ev.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
// This was our active pointer going up. Choose
// a new active pointer and adjust accordingly.
// TODO: Make this decision more intelligent.
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mDownMotionX = ev.getX(newPointerIndex);
mActivePointerId = ev.getPointerId(newPointerIndex);
}
}
private final void trackTouchEvent(MotionEvent event) {
final int pointerIndex = event.findPointerIndex(mActivePointerId);
final float x = event.getX(pointerIndex);
setNormalizedValue(screenToNormalized(x));
setSelectedMinValue(normalizedToValue(normalizedValue));
}
/**
* Tries to claim the user's drag motion, and requests disallowing any ancestors
* from stealing events in the drag.
*/
private void attemptClaimDrag() {
if (getParent() != null) {
getParent().requestDisallowInterceptTouchEvent(true);
}
}
/**
* This is called when the user has started touching this widget.
*/
void onStartTrackingTouch() {
mIsDragging = true;
}
/**
* This is called when the user either releases his touch or the touch is canceled.
*/
void onStopTrackingTouch() {
mIsDragging = false;
}
/**
* Ensures correct size of the widget.
*/
@Override
protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getMaxWidth(), getMaxHeight());
}
/**
* Draws the widget on the given canvas.
*/
@Override
protected synchronized void onDraw(Canvas canvas) {
super.onDraw(canvas);
// draw seek bar background line
final RectF rect = new RectF(padding, 0.5f * (getHeight() - lineHeight),
getWidth() - padding, 0.5f * (getHeight() + lineHeight));
paint.setStyle(Style.FILL);
paint.setColor(Color.GRAY);
paint.setAntiAlias(true);
canvas.drawRect(rect, paint);
// Draw seek bar active range line
if (normalizedValue > valueToNormalized(absoluteMaxValue) / 2) {
rect.left = normalizedToScreen(valueToNormalized(absoluteMaxValue) / 2);
rect.right = normalizedToScreen(normalizedValue);
} else {
rect.left = normalizedToScreen(normalizedValue);
rect.right = normalizedToScreen(valueToNormalized(absoluteMaxValue) / 2);
}
// Fill color
paint.setColor(DEFAULT_COLOR);
canvas.drawRect(rect, paint);
// Draw a small ball at the middle
canvas.drawCircle(normalizedToScreen(valueToNormalized(absoluteMaxValue) / 2),
getHeight() / 2.0f - 2.0f, 8.0f, paint);
// Draw thumb
drawThumb(normalizedToScreen(normalizedValue), Thumb.MIN.equals(pressedThumb), canvas);
}
/**
* Overridden to save instance state when device orientation changes.
* This method is called automatically if you assign an id to the
* RangeSeekBar widget using the {@link #setId(int)} method.
* Other members of this class than the normalized min and
* max values don't need to be saved.
*/
@Override
protected Parcelable onSaveInstanceState() {
final Bundle bundle = new Bundle();
bundle.putParcelable("SUPER", super.onSaveInstanceState());
bundle.putDouble("MIN", normalizedValue);
return bundle;
}
/**
* Overridden to restore instance state when device orientation changes.
* This method is called automatically if you assign an id to the
* RangeSeekBar widget using the {@link #setId(int)} method.
*/
@Override
protected void onRestoreInstanceState(Parcelable parcel) {
final Bundle bundle = (Bundle) parcel;
super.onRestoreInstanceState(bundle.getParcelable("SUPER"));
normalizedValue = bundle.getDouble("MIN");
}
/**
* Draws the "normal" resp. "pressed" thumb image on specified x-coordinate.
*
* @param screenCoord
* The x-coordinate in screen space where to draw the image.
* @param pressed
* Is the thumb currently in "pressed" state?
* @param canvas
* The canvas to draw upon.
*/
private void drawThumb(float screenCoord, boolean pressed, Canvas canvas) {
canvas.drawBitmap(pressed ? thumbPressedImage : thumbImage, screenCoord
- thumbHalfWidth, (float) ((0.5f * getMeasuredHeight()) - thumbHalfHeight), paint);
}
/**
* Decides which (if any) thumb is touched by the given x-coordinate.
*
* @param touchX
* The x-coordinate of a touch event in screen space.
* @return The pressed thumb or null if none has been touched.
*/
private Thumb evalPressedThumb(float touchX) {
Thumb result = null;
boolean minThumbPressed = isInThumbRange(touchX, normalizedValue);
if (minThumbPressed) {
result = Thumb.MIN;
}
return result;
}
/**
* Decides if given x-coordinate in screen space needs to be interpreted as
* "within" the normalized thumb x-coordinate.
*
* @param touchX
* The x-coordinate in screen space to check.
* @param normalizedThumbValue
* The normalized x-coordinate of the thumb to check.
* @return true if x-coordinate is in thumb range, false otherwise.
*/
private boolean isInThumbRange(float touchX, double normalizedThumbValue) {
return Math.abs(touchX - normalizedToScreen(normalizedThumbValue)) <= thumbHalfWidth;
}
/**
* Sets normalized min value to value so that 0 <= value <= normalized max value <= 1.
* The View will get invalidated when calling this method.
*
* @param value
* The new normalized min value to set.
*/
public void setNormalizedValue(double value) {
normalizedValue = Math.max(0d, Math.min(1d, Math.min(value,
valueToNormalized(absoluteMaxValue))));
invalidate();
}
/**
* Converts a normalized value to a Integer object in the value
* space between absolute minimum and maximum.
*
* @param normalized
* @return
*/
@SuppressWarnings("unchecked")
private Integer normalizedToValue(double normalized) {
return (Integer) numberType.toNumber(absoluteMinValuePrim + normalized
* (absoluteMaxValuePrim - absoluteMinValuePrim));
}
/**
* Converts the given Integer value to a normalized double.
*
* @param value
* The Integer value to normalize.
* @return The normalized double.
*/
private double valueToNormalized(Integer value) {
if (0 == absoluteMaxValuePrim - absoluteMinValuePrim) {
// prevent division by zero, simply return 0.
return 0d;
}
return (value.doubleValue() - absoluteMinValuePrim)
/ (absoluteMaxValuePrim - absoluteMinValuePrim);
}
/**
* Converts a normalized value into screen space.
*
* @param normalizedCoord
* The normalized value to convert.
* @return The converted value in screen space.
*/
private float normalizedToScreen(double normalizedCoord) {
return (float) (padding + normalizedCoord * (getWidth() - 2 * padding));
}
/**
* Converts screen space x-coordinates into normalized values.
*
* @param screenCoord
* The x-coordinate in screen space to convert.
* @return The normalized value.
*/
private double screenToNormalized(float screenCoord) {
int width = getWidth();
if (width <= 2 * padding) {
// prevent division by zero, simply return 0.
return 0d;
}
else {
double result = (screenCoord - padding) / (width - 2 * padding);
return Math.min(1d, Math.max(0d, result));
}
}
/**
* Callback listener interface to notify about changed range values.
*
* @author Stephan Tittel (stephan.tittel@kom.tu-darmstadt.de)
*
*/
public interface OnCenteredSeekBarChangeListener {
public void OnCenteredSeekBarValueChanged(CenteredSeekBar bar, Integer minValue);
}
/**
* Thumb constants (min and max).
*/
private static enum Thumb {
MIN
};
/**
* Utility enumaration used to convert between Numbers and doubles.
*
* @author Stephan Tittel (stephan.tittel@kom.tu-darmstadt.de)
*
*/
private static enum NumberType {
LONG, DOUBLE, INTEGER, FLOAT, SHORT, BYTE, BIG_DECIMAL;
public static NumberType fromNumber(E value)
throws IllegalArgumentException {
if (value instanceof Long) {
return LONG;
}
if (value instanceof Double) {
return DOUBLE;
}
if (value instanceof Integer) {
return INTEGER;
}
if (value instanceof Float) {
return FLOAT;
}
if (value instanceof Short) {
return SHORT;
}
if (value instanceof Byte) {
return BYTE;
}
if (value instanceof BigDecimal) {
return BIG_DECIMAL;
}
throw new IllegalArgumentException("Integer class '"
+ value.getClass().getName() + "' is not supported");
}
public Integer toNumber(double value) {
return (int)value;
}
}
}
================================================
FILE: src/org/cyanogenmod/focal/ui/CircleTimerView.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
* Copyright (C) 2012 The Android Open Source Project
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.ui;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.os.SystemClock;
import android.util.AttributeSet;
import android.view.View;
import fr.xplod.focal.R;
public class CircleTimerView extends View {
private int mRedColor;
private int mWhiteColor;
private long mIntervalTime = 0;
private long mIntervalStartTime = -1;
private long mMarkerTime = -1;
private long mCurrentIntervalTime = 0;
private long mAccumulatedTime = 0;
private boolean mPaused = false;
private boolean mAnimate = false;
private static float mCircleXCenterLeftPadding = 0;
private static float mStrokeSize = 4;
private static float mDiamondStrokeSize = 12;
private static float mMarkerStrokeSize = 2;
private final Paint mPaint = new Paint();
private final Paint mFill = new Paint();
private final RectF mArcRect = new RectF();
private float mRectHalfWidth = 6f;
private Resources mResources;
private float mRadiusOffset; // amount to remove from radius to account for markers on circle
private float mScreenDensity;
// Class has 2 modes:
// Timer mode - counting down. in this mode the animation is counter-clockwise and stops at 0
// Stop watch mode - counting up - in this mode the animation is clockwise and will keep the
// animation until stopped.
private boolean mTimerMode = true; // default is timer mode
public CircleTimerView(Context context) {
this(context, null);
}
public CircleTimerView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
setAlpha(0);
}
public void setIntervalTime(long t) {
mIntervalTime = t;
postInvalidate();
}
public void setMarkerTime(long t) {
mMarkerTime = t;
postInvalidate();
}
public void reset() {
mIntervalStartTime = -1;
mMarkerTime = -1;
postInvalidate();
}
public void startIntervalAnimation() {
mIntervalStartTime = SystemClock.elapsedRealtime();
mAnimate = true;
invalidate();
mPaused = false;
}
public void stopIntervalAnimation() {
mAnimate = false;
mIntervalStartTime = -1;
mAccumulatedTime = 0;
}
public boolean isAnimating() {
return (mIntervalStartTime != -1);
}
public void pauseIntervalAnimation() {
mAnimate = false;
mAccumulatedTime += SystemClock.elapsedRealtime() - mIntervalStartTime;
mPaused = true;
}
public void abortIntervalAnimation() {
mAnimate = false;
}
public void setPassedTime(long time, boolean drawRed) {
// The onDraw() method checks if mIntervalStartTime has been set before drawing any red.
// Without drawRed, mIntervalStartTime should not be set here at all, and would remain at -1
// when the state is reconfigured after exiting and re-entering the application.
// If the timer is currently running, this drawRed will not be set, and will have no effect
// because mIntervalStartTime will be set when the thread next runs.
// When the timer is not running, mIntervalStartTime will not be set upon loading the state,
// and no red will be drawn, so drawRed is used to force onDraw() to draw the red portion,
// despite the timer not running.
mCurrentIntervalTime = mAccumulatedTime = time;
if (drawRed) {
mIntervalStartTime = SystemClock.elapsedRealtime();
}
postInvalidate();
}
/**
* Calculate the amount by which the radius of a CircleTimerView should
* be offset by the any of the extra painted objects.
*/
public static float calculateRadiusOffset(
float strokeSize, float diamondStrokeSize, float markerStrokeSize) {
return Math.max(strokeSize, Math.max(diamondStrokeSize, markerStrokeSize));
}
private void init(Context c) {
mResources = c.getResources();
mCircleXCenterLeftPadding = (mResources.getDimension(R.dimen.timer_circle_width)
- mResources.getDimension(R.dimen.timer_circle_diameter)) / 2;
mStrokeSize = mResources.getDimension(R.dimen.circletimer_circle_size);
mDiamondStrokeSize = mResources.getDimension(R.dimen.circletimer_diamond_size);
mMarkerStrokeSize = mResources.getDimension(R.dimen.circletimer_marker_size);
mRadiusOffset = calculateRadiusOffset(
mStrokeSize, mDiamondStrokeSize, mMarkerStrokeSize);
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.STROKE);
mWhiteColor = mResources.getColor(R.color.clock_white);
mRedColor = mResources.getColor(R.color.clock_red);
mScreenDensity = mResources.getDisplayMetrics().density;
mFill.setAntiAlias(true);
mFill.setStyle(Paint.Style.FILL);
mFill.setColor(mWhiteColor);
mRectHalfWidth = mDiamondStrokeSize / 2f;
}
public void setTimerMode(boolean mode) {
mTimerMode = mode;
}
@Override
public void onDraw(Canvas canvas) {
int xCenter = getWidth() / 2 + 1;
int yCenter = getHeight() / 2;
mPaint.setStrokeWidth(mStrokeSize);
float radius = Math.min(xCenter, yCenter) - mRadiusOffset;
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
xCenter = (int) (radius + mRadiusOffset);
if (mTimerMode) {
xCenter += mCircleXCenterLeftPadding;
}
}
if (mIntervalStartTime == -1) {
// Just draw a complete red circle, no white arc needed
mPaint.setColor(mRedColor);
canvas.drawCircle (xCenter, yCenter, radius, mPaint);
if (mTimerMode) {
drawWhiteDiamond(canvas, 0f, xCenter, yCenter, radius);
}
} else {
if (mAnimate) {
mCurrentIntervalTime = SystemClock.elapsedRealtime()
- mIntervalStartTime + mAccumulatedTime;
}
// Draw a combination of red and white arcs to create a circle
mArcRect.top = yCenter - radius;
mArcRect.bottom = yCenter + radius;
mArcRect.left = xCenter - radius;
mArcRect.right = xCenter + radius;
float redPercent = (float)mCurrentIntervalTime / (float)mIntervalTime;
// Prevent timer from doing more than one full circle
redPercent = (redPercent > 1 && mTimerMode) ? 1 : redPercent;
float whitePercent = 1 - (redPercent > 1 ? 1 : redPercent);
// Draw white arc here
mPaint.setColor(mWhiteColor);
if (mTimerMode){
canvas.drawArc (mArcRect, 270, - redPercent * 360 , false, mPaint);
} else {
canvas.drawArc (mArcRect, 270, + redPercent * 360 , false, mPaint);
}
// Draw red arc here
mPaint.setStrokeWidth(mStrokeSize);
mPaint.setColor(mRedColor);
if (mTimerMode) {
canvas.drawArc(mArcRect, 270, + whitePercent * 360, false, mPaint);
} else {
canvas.drawArc(mArcRect, 270 + (1 - whitePercent) * 360,
whitePercent * 360, false, mPaint);
}
if (mMarkerTime != -1 && radius > 0 && mIntervalTime != 0) {
mPaint.setStrokeWidth(mMarkerStrokeSize);
float angle = (float)(mMarkerTime % mIntervalTime) / (float)mIntervalTime * 360;
// Draw 2dips thick marker the formula to draw the marker 1 unit thick is:
// 180 / (radius * Math.PI) after that we have to scale it by the screen density
canvas.drawArc (mArcRect, 270 + angle, mScreenDensity *
(float) (360 / (radius * Math.PI)) , false, mPaint);
}
drawWhiteDiamond(canvas, redPercent, xCenter, yCenter, radius);
}
if (mAnimate) {
invalidate();
}
}
protected void drawWhiteDiamond(
Canvas canvas, float degrees, int xCenter, int yCenter, float radius) {
mPaint.setColor(mWhiteColor);
float diamondPercent;
if (mTimerMode) {
diamondPercent = 270 - degrees * 360;
} else {
diamondPercent = 270 + degrees * 360;
}
canvas.save();
final double diamondRadians = Math.toRadians(diamondPercent);
canvas.translate(xCenter + (float) (radius * Math.cos(diamondRadians)),
yCenter + (float) (radius * Math.sin(diamondRadians)));
canvas.rotate(diamondPercent + 45f);
canvas.drawRect(-mRectHalfWidth, -mRectHalfWidth, mRectHalfWidth, mRectHalfWidth, mFill);
canvas.restore();
}
}
================================================
FILE: src/org/cyanogenmod/focal/ui/ExposureHudRing.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.ui;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import org.cyanogenmod.focal.CameraManager;
import fr.xplod.focal.R;
/**
* Exposure ring HUD that lets user select exposure metering point
*/
public class ExposureHudRing extends HudRing {
private CameraManager mCamManager;
private long mTimeLastSet = 0;
private final static long SET_INTERVAL = 100;
public ExposureHudRing(Context context) {
super(context);
}
public ExposureHudRing(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ExposureHudRing(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
setImageResource(R.drawable.hud_exposure_ring);
}
public void setManagers(CameraManager camMan) {
mCamManager = camMan;
}
/**
* Centers the exposure ring on the x,y coordinates provided
* and sets the focus to this position
*
* @param x
* @param y
*/
public void setPosition(float x, float y) {
setX(x - getWidth() / 2.0f);
setY(y - getHeight() / 2.0f);
applyExposurePoint();
}
private void applyExposurePoint() {
ViewGroup parent = (ViewGroup) getParent();
if (parent == null) return;
// We swap X/Y as we have a landscape preview in portrait mode
float centerPointX = getY() + getHeight() / 2.0f;
float centerPointY = parent.getWidth() - (getX() + getWidth() / 2.0f);
centerPointX *= 1000.0f / parent.getHeight();
centerPointY *= 1000.0f / parent.getWidth();
centerPointX = (centerPointX - 500.0f) * 2.0f;
centerPointY = (centerPointY - 500.0f) * 2.0f;
mTimeLastSet = System.currentTimeMillis();
mCamManager.setExposurePoint((int) centerPointX, (int) centerPointY);
}
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
super.onTouch(view, motionEvent);
if (motionEvent.getActionMasked() == MotionEvent.ACTION_MOVE) {
if (System.currentTimeMillis() - mTimeLastSet > SET_INTERVAL) {
applyExposurePoint();
}
} else if (motionEvent.getActionMasked() == MotionEvent.ACTION_UP) {
applyExposurePoint();
}
return true;
}
}
================================================
FILE: src/org/cyanogenmod/focal/ui/FocusHudRing.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.ui;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import org.cyanogenmod.focal.CameraManager;
import org.cyanogenmod.focal.FocusManager;
import fr.xplod.focal.R;
/**
* Focus ring HUD that lets user select focus point (tap to focus)
*/
public class FocusHudRing extends HudRing {
private CameraManager mCamManager;
private FocusManager mFocusManager;
public FocusHudRing(Context context) {
super(context);
}
public FocusHudRing(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FocusHudRing(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
setFocusImage(true);
}
public void setFocusImage(boolean success) {
if (success) {
setImageResource(R.drawable.hud_focus_ring_success);
} else {
setImageResource(R.drawable.hud_focus_ring_fail);
}
}
public void setManagers(CameraManager camMan, FocusManager focusMan) {
mCamManager = camMan;
mFocusManager = focusMan;
}
/**
* Centers the focus ring on the x,y coordinates provided
* and sets the focus to this position
*
* @param x
* @param y
*/
public void setPosition(float x, float y) {
setX(x - getWidth() / 2.0f);
setY(y - getHeight() / 2.0f);
applyFocusPoint();
}
private void applyFocusPoint() {
ViewGroup parent = (ViewGroup) getParent();
if (parent == null) return;
// We swap X/Y as we have a landscape preview in portrait mode
float centerPointX = getY() + getHeight() / 2.0f;
float centerPointY = parent.getWidth() - (getX() + getWidth() / 2.0f);
centerPointX *= 1000.0f / parent.getHeight();
centerPointY *= 1000.0f / parent.getWidth();
centerPointX = (centerPointX - 500.0f) * 2.0f;
centerPointY = (centerPointY - 500.0f) * 2.0f;
// The CamManager might be null if users try to tap the preview area, when the
// camera is actually not yet ready
if (mCamManager != null) {
mCamManager.setFocusPoint((int) centerPointX, (int) centerPointY);
}
}
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
super.onTouch(view, motionEvent);
if (motionEvent.getActionMasked() == MotionEvent.ACTION_UP) {
applyFocusPoint();
if (mFocusManager != null) {
mFocusManager.refocus();
}
}
return true;
}
}
================================================
FILE: src/org/cyanogenmod/focal/ui/HudRing.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.ui;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.DecelerateInterpolator;
import android.widget.ImageView;
/**
* Represents a ring that can be dragged on the screen
*/
public class HudRing extends ImageView implements View.OnTouchListener {
private float mLastX;
private float mLastY;
private final static float IDLE_ALPHA = 0.25f;
private final static int ANIMATION_DURATION = 120;
public HudRing(Context context) {
super(context);
}
public HudRing(Context context, AttributeSet attrs) {
super(context, attrs);
}
public HudRing(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
setOnTouchListener(this);
setAlpha(IDLE_ALPHA);
}
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
mLastX = motionEvent.getRawX();
mLastY = motionEvent.getRawY();
/* mOffsetX = motionEvent.getX() - motionEvent.getRawX();
mOffsetY = motionEvent.getY() - motionEvent.getRawY();*/
animatePressDown();
} else if (motionEvent.getAction() == MotionEvent.ACTION_MOVE) {
setX(getX() + (motionEvent.getRawX() - mLastX));
setY(getY() + (motionEvent.getRawY() - mLastY));
mLastX = motionEvent.getRawX();
mLastY = motionEvent.getRawY();
} else if (motionEvent.getActionMasked() == MotionEvent.ACTION_UP) {
animatePressUp();
}
return true;
}
public void animatePressDown() {
animate().alpha(1.0f).setDuration(ANIMATION_DURATION).start();
}
public void animatePressUp() {
animate().alpha(IDLE_ALPHA).rotation(0).setDuration(ANIMATION_DURATION).start();
}
public void animateWorking(long duration) {
animate().rotationBy(45.0f).setDuration(duration).setInterpolator(
new DecelerateInterpolator()).start();
}
}
================================================
FILE: src/org/cyanogenmod/focal/ui/Notifier.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.ui;
import android.content.Context;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.widget.LinearLayout;
import android.widget.TextView;
import fr.xplod.focal.R;
import org.cyanogenmod.focal.Util;
/**
* Little notifier that comes in from the side of the screen
* to tell the user about something quickly
*/
public class Notifier extends LinearLayout {
private TextView mTextView;
private Handler mHandler;
private int mOrientation;
private Runnable mFadeOutRunnable = new Runnable() {
@Override
public void run() {
fadeOut();
}
};
private float mTargetX;
private float mTargetY;
public Notifier(Context context) {
super(context);
initialize();
}
public Notifier(Context context, AttributeSet attrs) {
super(context, attrs);
initialize();
}
public Notifier(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initialize();
}
private void initialize() {
mHandler = new Handler();
}
public void notifyOrientationChanged(int orientation) {
mOrientation = orientation;
if (mTextView == null) return;
if (orientation % 180 != 0) {
// We translate to fit orientation properly within the screen
animate().rotation(orientation).setDuration(200).x(mTargetX + mTextView
.getMeasuredHeight() / 2).y(mTargetY - mTextView.getMeasuredWidth() / 2)
.setInterpolator(new DecelerateInterpolator()).start();
} else {
animate().rotation(orientation).setDuration(200).x(mTargetX)
.y(mTargetY).setInterpolator(new DecelerateInterpolator()).start();
}
}
/**
* Shows the provided {@param text} during {@param durationMs} milliseconds with
* a nice animation.
*
* @param text
* @param durationMs
* @note This method must be ran in UI thread!
*/
public void notify(final String text, final long durationMs) {
mHandler.post(new Runnable() {
public void run() {
mHandler.removeCallbacks(mFadeOutRunnable);
mTextView = (TextView) findViewById(R.id.notifier_text);
mTextView.setText(text);
setAlpha(0.0f);
setX(Util.getScreenSize(null).x*1.0f/3.0f);
setY(Util.getScreenSize(null).y*2.0f/3.0f);
mTargetX = getX();
mTargetY = getY();
notifyOrientationChanged(mOrientation);
fadeIn();
mHandler.postDelayed(mFadeOutRunnable, durationMs);
}
});
}
/**
* Shows the provided {@param text} during {@param durationMs} milliseconds at the
* provided {@param x} and {@param y} position with a nice animation.
*
* @param text The text to display
* @param durationMs How long should we display it
* @param x The X position
* @param y The Y position
*/
public void notify(final String text, final long durationMs, final float x, final float y) {
mHandler.post(new Runnable() {
public void run() {
mHandler.removeCallbacks(mFadeOutRunnable);
mTextView = (TextView) findViewById(R.id.notifier_text);
mTextView.setText(text);
setAlpha(0.0f);
setX(x);
setY(y);
mTargetX = getX();
mTargetY = getY();
notifyOrientationChanged(mOrientation);
fadeIn();
mHandler.postDelayed(mFadeOutRunnable, durationMs);
}
});
}
private void fadeIn() {
animate().setDuration(300).alpha(1.0f).setInterpolator(
new AccelerateInterpolator()).start();
}
private void fadeOut() {
animate().setDuration(300).alpha(0.0f).setInterpolator(
new DecelerateInterpolator()).start();
}
}
================================================
FILE: src/org/cyanogenmod/focal/ui/PanoProgressBar.java
================================================
/*
* Copyright (C) 2011 The Android Open Source Project
* Copyright (C) 2013 Guillaume Lesniak
*
* Licensed under the Apache License, Version 2.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 org.cyanogenmod.focal.ui;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.widget.ImageView;
public class PanoProgressBar extends ImageView {
@SuppressWarnings("unused")
private static final String TAG = "PanoProgressBar";
public static final int DIRECTION_NONE = 0;
public static final int DIRECTION_LEFT = 1;
public static final int DIRECTION_RIGHT = 2;
private float mProgress = 0;
private float mMaxProgress = 0;
private float mLeftMostProgress = 0;
private float mRightMostProgress = 0;
private float mProgressOffset = 0;
private float mIndicatorWidth = 0;
private int mDirection = 0;
private final Paint mBackgroundPaint = new Paint();
private final Paint mDoneAreaPaint = new Paint();
private final Paint mIndicatorPaint = new Paint();
private float mWidth;
private float mHeight;
private RectF mDrawBounds;
private OnDirectionChangeListener mListener = null;
public interface OnDirectionChangeListener {
public void onDirectionChange(int direction);
}
public PanoProgressBar(Context context, AttributeSet attrs) {
super(context, attrs);
mDoneAreaPaint.setStyle(Paint.Style.FILL);
mDoneAreaPaint.setAlpha(0xff);
mBackgroundPaint.setStyle(Paint.Style.FILL);
mBackgroundPaint.setAlpha(0xff);
mIndicatorPaint.setStyle(Paint.Style.FILL);
mIndicatorPaint.setAlpha(0xff);
mDrawBounds = new RectF();
}
public void setOnDirectionChangeListener(OnDirectionChangeListener l) {
mListener = l;
}
private void setDirection(int direction) {
if (mDirection != direction) {
mDirection = direction;
if (mListener != null) {
mListener.onDirectionChange(mDirection);
}
invalidate();
}
}
public int getDirection() {
return mDirection;
}
@Override
public void setBackgroundColor(int color) {
mBackgroundPaint.setColor(color);
invalidate();
}
public void setDoneColor(int color) {
mDoneAreaPaint.setColor(color);
invalidate();
}
public void setIndicatorColor(int color) {
mIndicatorPaint.setColor(color);
invalidate();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
mWidth = w;
mHeight = h;
mDrawBounds.set(0, 0, mWidth, mHeight);
}
public void setMaxProgress(int progress) {
mMaxProgress = progress;
}
public void setIndicatorWidth(float w) {
mIndicatorWidth = w;
invalidate();
}
public void setRightIncreasing(boolean rightIncreasing) {
if (rightIncreasing) {
mLeftMostProgress = 0;
mRightMostProgress = 0;
mProgressOffset = 0;
setDirection(DIRECTION_RIGHT);
} else {
mLeftMostProgress = mWidth;
mRightMostProgress = mWidth;
mProgressOffset = mWidth;
setDirection(DIRECTION_LEFT);
}
invalidate();
}
public void setProgress(int progress) {
// The panning direction will be decided after user pan more
// than 10 degrees in one direction.
if (mDirection == DIRECTION_NONE) {
if (progress > 10) {
setRightIncreasing(true);
} else if (progress < -10) {
setRightIncreasing(false);
}
}
// mDirection might be modified by setRightIncreasing() above. Need to check again.
if (mDirection != DIRECTION_NONE) {
mProgress = progress * mWidth / mMaxProgress + mProgressOffset;
// Value bounds
mProgress = Math.min(mWidth, Math.max(0, mProgress));
if (mDirection == DIRECTION_RIGHT) {
// The right most progress is adjusted.
mRightMostProgress = Math.max(mRightMostProgress, mProgress);
}
if (mDirection == DIRECTION_LEFT) {
// The left most progress is adjusted.
mLeftMostProgress = Math.min(mLeftMostProgress, mProgress);
}
invalidate();
}
}
public void reset() {
mProgress = 0;
mProgressOffset = 0;
setDirection(DIRECTION_NONE);
invalidate();
}
@Override
protected void onDraw(Canvas canvas) {
// the background
canvas.drawRect(mDrawBounds, mBackgroundPaint);
if (mDirection != DIRECTION_NONE) {
// the progress area
canvas.drawRect(mLeftMostProgress, mDrawBounds.top, mRightMostProgress,
mDrawBounds.bottom, mDoneAreaPaint);
// the indication bar
float l;
float r;
if (mDirection == DIRECTION_RIGHT) {
l = Math.max(mProgress - mIndicatorWidth, 0f);
r = mProgress;
} else {
l = mProgress;
r = Math.min(mProgress + mIndicatorWidth, mWidth);
}
canvas.drawRect(l, mDrawBounds.top, r, mDrawBounds.bottom, mIndicatorPaint);
}
// Draw the mask image on the top for shaping.
super.onDraw(canvas);
}
}
================================================
FILE: src/org/cyanogenmod/focal/ui/PreviewFrameLayout.java
================================================
/*
* Copyright (C) 2009 The Android Open Source Project
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.ui;
import android.content.Context;
import android.content.res.Configuration;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.RelativeLayout;
import org.cyanogenmod.focal.Util;
/**
* A layout which handles the preview aspect ratio.
*/
public class PreviewFrameLayout extends RelativeLayout {
public static final String TAG = "CAM_preview";
/**
* A callback to be invoked when the preview frame's size changes.
*/
public interface OnSizeChangedListener {
public void onSizeChanged(int width, int height);
}
private double mAspectRatio;
private int mPreviewWidth;
private int mPreviewHeight;
private OnSizeChangedListener mListener;
public PreviewFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
setAspectRatio(4.0 / 3.0);
}
public void setAspectRatio(double ratio) {
if (ratio <= 0.0) {
throw new IllegalArgumentException();
}
ratio = 1 / ratio;
if (mAspectRatio != ratio) {
mAspectRatio = ratio;
requestLayout();
}
}
public void setPreviewSize(int width, int height) {
mPreviewWidth = height;
mPreviewHeight = width;
requestLayout();
}
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
// Scale the preview while keeping the aspect ratio
int fullWidth = Util.getScreenSize(null).x;
int fullHeight = Util.getScreenSize(null).y;
if (fullWidth == 0 || mPreviewWidth == 0) {
setMeasuredDimension(mPreviewWidth, mPreviewHeight);
return;
}
setMeasuredDimension(fullWidth, fullHeight);
// Ask children to follow the new preview dimension.
super.onMeasure(MeasureSpec.makeMeasureSpec((int) fullWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec((int) fullHeight, MeasureSpec.EXACTLY));
}
/**
* Called when this view should assign a size and position to all of its children.
* @param changed
* @param l
* @param t
* @param r
* @param b
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if(changed && getChildCount() > 0) {
final View child = getChildAt(0);
// Scale the preview while keeping the aspect ratio
int fullWidth = Util.getScreenSize(null).x;
int fullHeight = Util.getScreenSize(null).y;
if (fullWidth == 0 || mPreviewWidth == 0) {
setMeasuredDimension(fullWidth, fullHeight);
return;
}
float ratio = Math.min((float) fullHeight / (float) mPreviewHeight,
(float) fullWidth / (float) mPreviewWidth);
float width = mPreviewWidth * ratio;
float height = mPreviewHeight * ratio;
// Center the child SurfaceView within the parent.
if (child != null) {
Log.v(TAG, "Layout: (" + (int) ((fullWidth - width)) / 2 + ", "
+ (int) ((fullHeight - height)) / 2 + ", " + (int) ((fullWidth
+ width)) / 2 + ", " + (int) ((fullHeight + height)) / 2 + ")");
child.layout((int) ((fullWidth - width)) / 2, (int) ((fullHeight - height))
/ 2, (int) ((fullWidth + width)) / 2, (int) ((fullHeight + height)) / 2);
}
}
}
public void setOnSizeChangedListener(OnSizeChangedListener listener) {
mListener = listener;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
if (mListener != null) mListener.onSizeChanged(w, h);
}
}
================================================
FILE: src/org/cyanogenmod/focal/ui/ReviewDrawer.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.ui;
import android.animation.Animator;
import android.content.ActivityNotFoundException;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Handler;
import android.provider.MediaStore;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import org.cyanogenmod.focal.CameraActivity;
import fr.xplod.focal.R;
import org.cyanogenmod.focal.Util;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* This class handles the review drawer that can be opened by swiping down
*/
public class ReviewDrawer extends RelativeLayout {
public final static String TAG = "ReviewDrawer";
private final static long DRAWER_TOGGLE_DURATION = 400;
private final static float MIN_REMOVE_THRESHOLD = 10.0f;
private final static String GALLERY_CAMERA_BUCKET = "Camera";
private List mImages;
private Handler mHandler;
private ImageListAdapter mImagesListAdapter;
private int mReviewedImageId;
private int mCurrentOrientation;
private final Object mImagesLock = new Object();
private boolean mIsOpen;
private ViewPager mViewPager;
public ReviewDrawer(Context context) {
super(context);
initialize();
}
public ReviewDrawer(Context context, AttributeSet attrs) {
super(context, attrs);
initialize();
}
public ReviewDrawer(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initialize();
}
private void initialize() {
mHandler = new Handler();
mImages = new ArrayList();
// Default hidden
setAlpha(0.0f);
mHandler.post(new Runnable() {
@Override
public void run() {
setTranslationY(-getMeasuredHeight());
}
});
// Setup the list adapter
mImagesListAdapter = new ImageListAdapter();
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
// Load pictures or videos from gallery
if (CameraActivity.getCameraMode() == CameraActivity.CAMERA_MODE_VIDEO) {
updateFromGallery(false, 0);
} else {
updateFromGallery(true, 0);
}
// Make sure drawer is initially closed
setTranslationY(-99999);
ImageButton editImageButton = (ImageButton) findViewById(R.id.button_retouch);
editImageButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
editInGallery(mReviewedImageId);
}
});
ImageButton openImageButton = (ImageButton) findViewById(R.id.button_open_in_gallery);
openImageButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
openInGallery(mReviewedImageId);
}
});
mViewPager = (ViewPager) findViewById(R.id.reviewed_image);
mViewPager.setAdapter(mImagesListAdapter);
mViewPager.setPageMargin((int) Util.dpToPx(getContext(), 8));
mViewPager.setPageTransformer(true, new ZoomOutPageTransformer());
mViewPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int i, float v, int i2) {
}
@Override
public void onPageSelected(int i) {
mReviewedImageId = mImages.get(i);
}
@Override
public void onPageScrollStateChanged(int i) {
}
});
}
/**
* Clears the list of images and reload it from the Gallery (MediaStore)
* This method is threaded!
*
* @param images True to get images, false to get videos
* @param scrollPos Position to scroll to, or 0 to get latest image
*/
public void updateFromGallery(final boolean images, final int scrollPos) {
new Thread() {
public void run() {
updateFromGallerySynchronous(images, scrollPos);
}
}.start();
}
/**
* Clears the list of images and reload it from the Gallery (MediaStore)
* This method is synchronous, see updateFromGallery for the threaded one.
*
* @param images True to get images, false to get videos
* @param scrollPos Position to scroll to, or 0 to get latest image
*/
public void updateFromGallerySynchronous(final boolean images, final int scrollPos) {
mHandler.post(new Runnable() {
@Override
public void run() {
synchronized (mImagesLock) {
mImages.clear();
mImagesListAdapter.notifyDataSetChanged();
}
String[] columns;
String orderBy;
if (images) {
columns = new String[]{MediaStore.Images.Media.DATA, MediaStore.Images.Media._ID};
orderBy = MediaStore.Images.Media.DATE_TAKEN + " ASC";
} else {
columns = new String[]{MediaStore.Video.Media.DATA, MediaStore.Video.Media._ID};
orderBy = MediaStore.Video.Media.DATE_TAKEN + " ASC";
}
// Select only the images that has been taken from the Camera
Context ctx = getContext();
if (ctx == null) return;
ContentResolver cr = ctx.getContentResolver();
if (cr == null) {
Log.e(TAG, "No content resolver!");
return;
}
Cursor cursor;
if (images) {
cursor = cr.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, columns,
MediaStore.Images.Media.BUCKET_DISPLAY_NAME + " LIKE ?",
new String[]{GALLERY_CAMERA_BUCKET}, orderBy);
} else {
cursor = cr.query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, columns,
MediaStore.Video.Media.BUCKET_DISPLAY_NAME + " LIKE ?",
new String[]{GALLERY_CAMERA_BUCKET}, orderBy);
}
if (cursor == null) {
Log.e(TAG, "Null cursor from MediaStore!");
return;
}
final int imageColumnIndex = cursor.getColumnIndex(images ?
MediaStore.Images.Media._ID : MediaStore.Video.Media._ID);
for (int i = 0; i < cursor.getCount(); i++) {
cursor.moveToPosition(i);
int id = cursor.getInt(imageColumnIndex);
if (mReviewedImageId <= 0) {
mReviewedImageId = id;
}
addImageToList(id);
mImagesListAdapter.notifyDataSetChanged();
}
cursor.close();
if (scrollPos < mImages.size()) {
mViewPager.setCurrentItem(scrollPos+1, false);
mViewPager.setCurrentItem(scrollPos, true);
}
}
});
}
/**
* Queries the Media service for image orientation
*
* @param id The id of the gallery image
* @return The orientation of the image, or 0 if it failed
*/
public int getCameraPhotoOrientation(final int id) {
if (CameraActivity.getCameraMode() == CameraActivity.CAMERA_MODE_VIDEO) {
return 0;
}
Uri uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI.buildUpon()
.appendPath(Integer.toString(id)).build();
String[] orientationColumn = new String[] {
MediaStore.Images.ImageColumns.ORIENTATION
};
int orientation = 0;
Cursor cur = getContext().getContentResolver().query(uri,
orientationColumn, null, null, null);
if (cur != null && cur.moveToFirst()) {
orientation = cur.getInt(cur.getColumnIndex(orientationColumn[0]));
}
if (cur != null) {
cur.close();
cur = null;
}
return orientation;
}
/**
* Add an image at the head of the image ribbon
*
* @param id The id of the image from the MediaStore
*/
public void addImageToList(final int id) {
mImagesListAdapter.addImage(id);
mHandler.post(new Runnable() {
@Override
public void run() {
mImagesListAdapter.notifyDataSetChanged();
}
});
}
public void scrollToLatestImage() {
mHandler.post(new Runnable() {
@Override
public void run() {
mViewPager.setCurrentItem(0);
}
});
}
public void notifyOrientationChanged(final int orientation) {
mCurrentOrientation = orientation;
}
private void openInGallery(final int imageId) {
if (imageId > 0) {
Uri uri;
if (CameraActivity.getCameraMode() == CameraActivity.CAMERA_MODE_VIDEO) {
uri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI.buildUpon()
.appendPath(Integer.toString(imageId)).build();
} else {
uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI.buildUpon()
.appendPath(Integer.toString(imageId)).build();
}
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
try {
Context ctx = getContext();
if (ctx != null) {
ctx.startActivity(intent);
}
} catch (ActivityNotFoundException e) {
CameraActivity.notify(getContext().getString(R.string.no_video_player), 2000);
}
}
}
private void editInGallery(final int imageId) {
if (imageId > 0) {
Intent editIntent = new Intent(Intent.ACTION_EDIT);
// Get URI
Uri uri = null;
if (CameraActivity.getCameraMode() == CameraActivity.CAMERA_MODE_VIDEO) {
uri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI.buildUpon()
.appendPath(Integer.toString(imageId)).build();
} else {
uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI.buildUpon()
.appendPath(Integer.toString(imageId)).build();
}
// Start gallery edit activity
editIntent.setDataAndType(uri, "image/*");
editIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
Context ctx = getContext();
if (ctx != null) {
ctx.startActivity(Intent.createChooser(editIntent, null));
}
}
}
/**
* Sets the review drawer to temporary hide, by reducing alpha to a very low
* level
*
* @param enabled To hide or not to hide, that is the question
*/
public void setTemporaryHide(final boolean enabled) {
float alpha = (enabled ? 0.2f : 1.0f);
animate().alpha(alpha).setDuration(200).start();
}
/**
* Returns whether or not the review drawer is FULLY open (ie. not in
* quick review mode)
*/
public boolean isOpen() {
return mIsOpen;
}
/**
* Normally opens the review drawer (animation)
*/
public void open() {
//mReviewedImage.setVisibility(View.VISIBLE);
openImpl(1.0f);
}
private void openImpl(final float alpha) {
mIsOpen = true;
setVisibility(View.VISIBLE);
animate().setDuration(DRAWER_TOGGLE_DURATION).setInterpolator(new AccelerateInterpolator())
.translationY(0.0f).setListener(null).alpha(alpha).start();
}
/**
* Normally closes the review drawer (animation)
*/
public void close() {
mIsOpen = false;
animate().setDuration(DRAWER_TOGGLE_DURATION).setInterpolator(new DecelerateInterpolator())
.translationY(-getMeasuredHeight()).alpha(0.0f)
.setListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationEnd(Animator animator) {
setVisibility(View.GONE);
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
}).start();
}
/**
* Slide the review drawer of the specified distance on the X axis
*
* @param distance The distance to slide
*/
public void slide(final float distance) {
float finalPos = getTranslationY() + distance;
if (finalPos > 0) {
finalPos = 0;
}
setTranslationY(finalPos);
if (getAlpha() == 0.0f) {
setVisibility(View.VISIBLE);
setAlpha(1.0f);
}
}
public void clampSliding() {
if (getTranslationY() < -getMeasuredHeight() / 2) {
close();
} else {
openImpl(getAlpha());
}
}
/**
* Removes the currently reviewed image from the
* internal memory.
*/
public void removeReviewedImage() {
Util.removeFromGallery(getContext().getContentResolver(), mReviewedImageId);
int position = mViewPager.getCurrentItem();
if (CameraActivity.getCameraMode() == CameraActivity.CAMERA_MODE_VIDEO) {
updateFromGallery(false, position);
} else {
updateFromGallery(true, position);
}
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
if (mImages.size() > 0) {
int imageId = mImages.get(0);
mReviewedImageId = imageId;
}
}
}, 300);
// XXX: Undo popup
}
/**
* Open the drawer in Quick Review mode
*/
public void openQuickReview() {
openImpl(0.5f);
}
/**
* Adapter responsible for showing the images in the list of the review drawer
*/
private class ImageListAdapter extends android.support.v4.view.PagerAdapter {
private Map mViewsToId;
public ImageListAdapter() {
mViewsToId = new HashMap();
}
public void addImage(int id) {
synchronized (mImagesLock) {
mImages.add(0, id);
}
}
@Override
public int getItemPosition(Object object) {
ImageView img = (ImageView) object;
if (mImages.indexOf(mViewsToId.get(img)) >= 0) {
return mImages.indexOf(mViewsToId.get(img));
} else {
return POSITION_NONE;
}
}
@Override
public int getCount() {
return mImages.size();
}
@Override
public boolean isViewFromObject(View view, Object o) {
return view == o;
}
@Override
public Object instantiateItem(ViewGroup container, final int position) {
final ImageView imageView = new ImageView(getContext());
container.addView(imageView);
ViewGroup.LayoutParams params = imageView.getLayoutParams();
params.width = ViewGroup.LayoutParams.WRAP_CONTENT;
params.height = ViewGroup.LayoutParams.WRAP_CONTENT;
imageView.setLayoutParams(params);
imageView.setOnTouchListener(new ThumbnailTouchListener(imageView));
new Thread() {
public void run() {
int imageId = -1;
synchronized (mImagesLock) {
try {
imageId = mImages.get(position);
} catch (IndexOutOfBoundsException e) {
return;
}
}
mViewsToId.put(imageView, imageId);
final Bitmap thumbnail = CameraActivity.getCameraMode() ==
CameraActivity.CAMERA_MODE_VIDEO ? (MediaStore.Video.Thumbnails
.getThumbnail(getContext().getContentResolver(),
mImages.get(position), MediaStore.Video.Thumbnails.MINI_KIND, null))
: (MediaStore.Images.Thumbnails.getThumbnail(getContext()
.getContentResolver(), imageId,
MediaStore.Images.Thumbnails.MINI_KIND, null));
final int rotation = getCameraPhotoOrientation(imageId);
mHandler.post(new Runnable() {
@Override
public void run() {
imageView.setImageBitmap(thumbnail);
imageView.setRotation(rotation);
}
});
}
}.start();
return imageView;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
// Remove viewpager_item.xml from ViewPager
container.removeView((ImageView) object);
}
}
/**
* Pager transition animation
*/
public class ZoomOutPageTransformer implements ViewPager.PageTransformer {
private final float MIN_SCALE = 0.85f;
private final float MIN_ALPHA = 0.5f;
public void transformPage(View view, float position) {
int pageWidth = view.getWidth();
int pageHeight = view.getHeight();
if (position < -1) { // [-Infinity,-1)
// This page is way off-screen to the left.
view.setAlpha(0);
} else if (position <= 1) { // [-1,1]
// Modify the default slide transition to shrink the page as well
float scaleFactor = Math.max(MIN_SCALE, 1 - Math.abs(position));
float vertMargin = pageHeight * (1 - scaleFactor) / 2;
float horzMargin = pageWidth * (1 - scaleFactor) / 2;
if (position < 0) {
view.setTranslationX(horzMargin - vertMargin / 2);
} else {
view.setTranslationX(-horzMargin + vertMargin / 2);
}
// Scale the page down (between MIN_SCALE and 1)
view.setScaleX(scaleFactor);
view.setScaleY(scaleFactor);
// Fade the page relative to its size.
view.setAlpha(MIN_ALPHA +
(scaleFactor - MIN_SCALE) /
(1 - MIN_SCALE) * (1 - MIN_ALPHA));
} else { // (1,+Infinity]
// This page is way off-screen to the right.
view.setAlpha(0);
}
}
}
public class ThumbnailTouchListener implements OnTouchListener {
private final GestureDetector mGestureDetector;
private ImageView mImageView;
private GestureDetector.SimpleOnGestureListener mListener =
new GestureDetector.SimpleOnGestureListener() {
private final float DRIFT_THRESHOLD = 80.0f;
private final int SWIPE_THRESHOLD_VELOCITY = 800;
@Override
public boolean onDown(MotionEvent motionEvent) {
return true;
}
@Override
public void onShowPress(MotionEvent motionEvent) {
}
@Override
public boolean onSingleTapUp(MotionEvent motionEvent) {
return false;
}
@Override
public boolean onScroll(MotionEvent ev1, MotionEvent ev2, float vX, float vY) {
if (Math.abs(ev2.getX() - ev1.getX()) > DRIFT_THRESHOLD
&& Math.abs(mImageView.getTranslationY()) < MIN_REMOVE_THRESHOLD) {
return false;
}
mImageView.setTranslationY(ev2.getRawY() - ev1.getRawY());
float alpha = Math.max(0.0f, 1.0f - Math.abs(mImageView.getTranslationY()
/ mImageView.getMeasuredHeight())*2.0f);
mImageView.setAlpha(alpha);
return true;
}
@Override
public void onLongPress(MotionEvent motionEvent) {
}
@Override
public boolean onFling(MotionEvent ev1, MotionEvent ev2, float vX, float vY) {
if (Math.abs(ev2.getX() - ev1.getX()) > DRIFT_THRESHOLD) {
return false;
}
if (Math.abs(vY) > SWIPE_THRESHOLD_VELOCITY) {
mImageView.animate().translationY(-mImageView.getHeight()).alpha(0.0f)
.setDuration(300).start();
removeReviewedImage();
return true;
}
return false;
}
};
public ThumbnailTouchListener(ImageView iv) {
mImageView = iv;
mGestureDetector = new GestureDetector(getContext(), mListener);
}
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
if (motionEvent.getActionMasked() == MotionEvent.ACTION_UP) {
if (mImageView.getTranslationY() > mImageView.getMeasuredHeight()*0.5f) {
mImageView.animate().translationY(-mImageView.getHeight()).alpha(0.0f)
.setDuration(300).start();
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
mImageView.setTranslationY(0.0f);
mImageView.setTranslationX(mImageView.getWidth());
mImageView.animate().translationX(0.0f).alpha(1.0f).start();
}
}, 400);
removeReviewedImage();
} else {
mImageView.animate().translationY(0).alpha(1.0f)
.setDuration(300).start();
}
}
return mGestureDetector.onTouchEvent(motionEvent);
}
}
}
================================================
FILE: src/org/cyanogenmod/focal/ui/RuleOfThirds.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.ui;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
/**
* Shows Rule of Thirds helper lines on the screen
*/
public class RuleOfThirds extends View {
private Paint mPaint;
public RuleOfThirds(Context context) {
super(context);
}
public RuleOfThirds(Context context, AttributeSet attrs) {
super(context, attrs);
}
public RuleOfThirds(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
protected void onDraw(Canvas canvas) {
if (mPaint == null) {
mPaint = new Paint();
}
mPaint.setARGB(255, 255, 255, 255);
int width = getMeasuredWidth();
int height = getMeasuredHeight();
canvas.drawLine(width / 3, 0, width / 3, height, mPaint);
canvas.drawLine(width * 2 / 3, 0, width * 2 / 3, height, mPaint);
canvas.drawLine(0, height / 3, width, height / 3, mPaint);
canvas.drawLine(0, height * 2 / 3, width, height * 2 / 3, mPaint);
}
}
================================================
FILE: src/org/cyanogenmod/focal/ui/SavePinger.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.ui;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.drawable.BitmapDrawable;
import android.util.AttributeSet;
import android.view.View;
import fr.xplod.focal.R;
public class SavePinger extends View {
public final static String TAG = "SavePinger";
// Just the circles pulsing
public final static int PING_MODE_SIMPLE = 0;
// Circles pulsing + save icon
public final static int PING_MODE_SAVE = 1;
// Circles pulsing + enhancer icon
public final static int PING_MODE_ENHANCER = 2;
private ValueAnimator mFadeAnimator;
private ValueAnimator mConstantAnimator;
private float mFadeProgress;
private Paint mPaint;
private long mRingTime[] = new long[CIRCLES_COUNT];
private long mLastTime;
private Bitmap mSaveIcon;
private Bitmap mEnhanceIcon;
private int mOrientation;
private int mPingMode;
private final static int CIRCLES_COUNT = 3;
private float mRingRadius;
public SavePinger(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initialize();
}
public SavePinger(Context context, AttributeSet attrs) {
super(context, attrs);
initialize();
}
public SavePinger(Context context) {
super(context);
initialize();
}
private void initialize() {
mPaint = new Paint();
setLayerType(LAYER_TYPE_SOFTWARE, mPaint);
mPaint.setShadowLayer(2.0f, 0.0f, 0.0f, 0xFF444444);
// start hidden
mFadeProgress = 0.0f;
mSaveIcon = ((BitmapDrawable) getResources()
.getDrawable(R.drawable.ic_save)).getBitmap();
mEnhanceIcon = ((BitmapDrawable) getResources()
.getDrawable(R.drawable.ic_enhancing)).getBitmap();
mFadeAnimator = new ValueAnimator();
mFadeAnimator.setDuration(1500);
mFadeAnimator.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator arg0) {
mFadeProgress = (Float) arg0.getAnimatedValue();
invalidate();
}
});
mConstantAnimator = new ValueAnimator();
mConstantAnimator.setDuration(1000);
mConstantAnimator.setRepeatMode(ValueAnimator.INFINITE);
mConstantAnimator.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator arg0) {
invalidate();
}
});
mConstantAnimator.setFloatValues(0, 1);
mConstantAnimator.start();
mLastTime = System.currentTimeMillis();
for (int i = 0; i < CIRCLES_COUNT; i++) {
mRingTime[i] = i * -500;
}
}
public void setPingMode(int mode) {
mPingMode = mode;
}
public void startSaving() {
mFadeAnimator.setFloatValues(0, 1);
mFadeAnimator.start();
}
public void stopSaving() {
mFadeAnimator.setFloatValues(1, 0);
mFadeAnimator.start();
}
public void notifyOrientationChanged(int angle) {
mOrientation = angle;
}
@Override
public void onDraw(Canvas canvas) {
mRingRadius = getWidth() * 0.5f;
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(8.0f);
long systemTime = System.currentTimeMillis();
long deltaMs = systemTime - mLastTime;
for (int i = 0; i < CIRCLES_COUNT; i++) {
mRingTime[i] += deltaMs * 0.2f;
if (mRingTime[i] < 0) continue;
float circleValue = mRingTime[i] / 255.0f;
float ringProgress = circleValue * mRingRadius;
if (circleValue > 1) circleValue = 1;
mPaint.setARGB((int) ((255.0f - 255.0f * circleValue) * mFadeProgress * 0.5f),
51, 181, 229);
canvas.drawCircle(getWidth() / 2, getHeight() / 2, ringProgress, mPaint);
if (circleValue == 1) {
mRingTime[i] = 0;
}
}
if (mPingMode != PING_MODE_SIMPLE) {
int alpha = (int) (((Math.cos((double) systemTime / 200.0) + 1.0f) / 2.0f) * 255.0f);
canvas.save();
canvas.translate(getWidth() / 2, getHeight() / 2);
canvas.rotate(mOrientation);
mPaint.setARGB((int) (alpha * mFadeProgress), 255, 255, 255);
if (mPingMode == PING_MODE_SAVE) {
canvas.drawBitmap(mSaveIcon, -mSaveIcon.getWidth() / 2,
-mSaveIcon.getHeight() / 2, mPaint);
} else if (mPingMode == PING_MODE_ENHANCER) {
canvas.drawBitmap(mEnhanceIcon, -mEnhanceIcon.getWidth() / 2,
-mEnhanceIcon.getHeight() / 2, mPaint);
}
canvas.restore();
}
mLastTime = systemTime;
if (getAlpha() > 0.0) {
invalidate();
}
}
}
================================================
FILE: src/org/cyanogenmod/focal/ui/ShutterButton.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.ui;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.ImageView;
public class ShutterButton extends ImageView {
public final static String TAG = "ShutterButton";
private boolean mSlideOpen = false;
/**
* Interface that notifies the CameraActivity that
* the shutter button has been slided (and should show the
* pad ring), or that there is a motionevent handled by this
* View that should belong to the SwitchRingPad
*/
public interface ShutterSlideListener {
public void onSlideOpen();
public void onSlideClose();
public void onShutterButtonPressed();
public boolean onMotionEvent(MotionEvent ev);
}
private float mDownX;
private float mDownY;
private ShutterSlideListener mListener;
public ShutterButton(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public ShutterButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ShutterButton(Context context) {
super(context);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
mDownX = event.getRawX();
mDownY = event.getRawY();
mListener.onShutterButtonPressed();
} else if (event.getActionMasked() == MotionEvent.ACTION_MOVE) {
if ((event.getRawY() - mDownY < -getHeight() / 2 || Math.abs(event.getRawX()
- mDownX) > getWidth() / 2) && mListener != null) {
if (!mSlideOpen) {
mListener.onSlideOpen();
mSlideOpen = true;
}
} else {
if (mSlideOpen) {
mListener.onSlideClose();
mSlideOpen = false;
}
}
}
boolean listenerResult = false;
if (mListener != null && mSlideOpen) {
listenerResult = mListener.onMotionEvent(event);
}
return (super.onTouchEvent(event) || listenerResult);
}
public void setSlideListener(ShutterSlideListener listener) {
mListener = listener;
}
}
================================================
FILE: src/org/cyanogenmod/focal/ui/SideBar.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.ui;
import android.content.Context;
import android.hardware.Camera;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.widget.HorizontalScrollView;
import android.widget.ScrollView;
import org.cyanogenmod.focal.CameraActivity;
import org.cyanogenmod.focal.CameraCapabilities;
import fr.xplod.focal.R;
import org.cyanogenmod.focal.widgets.WidgetBase;
public class SideBar extends ScrollView {
public final static String TAG = "SideBar";
public final static int SLIDE_ANIMATION_DURATION_MS = 300;
private final static float BAR_MARGIN = 0;
private CameraCapabilities mCapabilities;
private ViewGroup mToggleContainer;
private boolean mIsOpen;
public SideBar(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initialize();
}
public SideBar(Context context, AttributeSet attrs) {
super(context, attrs);
initialize();
}
public SideBar(Context context) {
super(context);
initialize();
}
/**
* Setup the bar
*/
private void initialize() {
this.setBackgroundColor(getResources().getColor(R.color.widget_background));
mIsOpen = true;
}
/**
* Check the capabilities of the device, and populate the sidebar
* with the toggle buttons.
*/
public void checkCapabilities(CameraActivity activity, ViewGroup widgetsContainer) {
mToggleContainer = (ViewGroup) this.getChildAt(0);
ViewGroup shortcutsContainer = (ViewGroup) activity.findViewById(R.id.shortcuts_container);
if (mCapabilities != null) {
mToggleContainer.removeAllViews();
shortcutsContainer.removeAllViews();
mCapabilities = null;
}
mCapabilities = new CameraCapabilities(activity);
Camera.Parameters params = activity.getCamManager().getParameters();
if (params != null) {
mCapabilities.populateSidebar(params, mToggleContainer,
shortcutsContainer, widgetsContainer);
} else {
Log.e(TAG, "Parameters were null when capabilities were checked");
}
}
/**
* Notify the bar of the rotation
*
* @param target Target orientation
*/
public void notifyOrientationChanged(float target) {
mToggleContainer = (ViewGroup) this.getChildAt(0);
int buttonsCount = mToggleContainer.getChildCount();
for (int i = 0; i < buttonsCount; i++) {
View child = mToggleContainer.getChildAt(i);
child.animate().rotation(target)
.setDuration(200).setInterpolator(new DecelerateInterpolator()).start();
}
}
/**
* Slides the sidebar off the provided distance, and clamp it at either
* sides of the screen (out/in)
*
* @param distance The distance to translate the bar
*/
public void slide(float distance) {
float finalX = this.getTranslationX() + distance;
if (finalX > 0)
finalX = 0;
else if (finalX < -getWidth())
finalX = -getWidth();
this.setTranslationX(finalX);
}
/**
* Clamp the sliding position of the bar. Basically, if the
* bar is half-visible, it will animate it in its visible position,
* and vice-versa.
*/
public void clampSliding() {
if (this.getTranslationX() < 0) {
slideClose();
} else {
slideOpen();
}
}
/**
* @return Whether or not the sidebar is open
*/
public boolean isOpen() {
return mIsOpen;
}
/**
* Smoothly close the sidebar
*/
public void slideClose() {
this.animate().translationX(-this.getWidth())
.setDuration(SLIDE_ANIMATION_DURATION_MS).start();
mIsOpen = false;
}
/**
* Smoothly open the sidebar
*/
public void slideOpen() {
this.animate().translationX(0)
.setDuration(SLIDE_ANIMATION_DURATION_MS).start();
mIsOpen = true;
}
}
================================================
FILE: src/org/cyanogenmod/focal/ui/SwitchRingPad.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.ui;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.RectF;
import android.graphics.drawable.BitmapDrawable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import fr.xplod.focal.R;
import org.cyanogenmod.focal.Util;
public class SwitchRingPad extends View implements AnimatorUpdateListener {
public interface RingPadListener {
public void onButtonActivated(int eventId);
}
private float mEdgePadding;
private int mButtonSize;
private float mRingRadius;
public final static int BUTTON_CAMERA = 1;
public final static int BUTTON_VIDEO = 2;
public final static int BUTTON_PANO = 3;
public final static int BUTTON_PICSPHERE = 4;
public final static int BUTTON_SWITCHCAM = 5;
private final static int SLOT_RIGHT = 4;
private final static int SLOT_MIDRIGHT = 3;
private final static int SLOT_MID = 2;
private final static int SLOT_MIDLEFT = 1;
private final static int SLOT_LEFT = 0;
private final static int SLOT_MAX = 5;
private final static int RING_ANIMATION_DURATION_MS = 150;
private PadButton[] mButtons;
private Paint mPaint;
private ValueAnimator mAnimator;
private float mOpenProgress;
private boolean mIsOpen;
public float mTargetOrientation;
public float mCurrentOrientation;
private RingPadListener mListener;
private float mHintProgress;
private ValueAnimator mHintAnimator;
private class PadButton {
public Bitmap mNormalBitmap;
public Bitmap mHoverBitmap;
public boolean mIsHovering;
public int mEventId;
public float mLastDrawnX;
public float mLastDrawnY;
public String mHintText;
public float mHintTextAlpha = 0.0f;
public boolean mHintAnimationDirection = false;
}
public SwitchRingPad(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initialize();
}
public SwitchRingPad(Context context, AttributeSet attrs) {
super(context, attrs);
initialize();
}
public SwitchRingPad(Context context) {
super(context);
initialize();
}
private Bitmap getDrawable(int resId) {
Bitmap bmp = ((BitmapDrawable) getResources().getDrawable(resId)).getBitmap();
mButtonSize = bmp.getWidth();
return bmp;
}
public void animateHint() {
mHintAnimator = new ValueAnimator();
mHintAnimator.setDuration(1500);
mHintAnimator.setFloatValues(0, 1);
mHintAnimator.setStartDelay(500);
mHintAnimator.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator arg0) {
mHintProgress = (Float) arg0.getAnimatedValue();
invalidate();
}
});
mHintAnimator.start();
}
private void initialize() {
mIsOpen = false;
mPaint = new Paint();
animateHint();
mAnimator = new ValueAnimator();
mAnimator.setDuration(RING_ANIMATION_DURATION_MS);
mAnimator.setInterpolator(new DecelerateInterpolator());
mAnimator.addUpdateListener(this);
mEdgePadding = getResources().getDimension(R.dimen.ringpad_edge_spacing);
mRingRadius = getResources().getDimension(R.dimen.ringpad_radius);
mButtons = new PadButton[SLOT_MAX];
// Camera pad button
Resources res = getResources();
addRingPad(getDrawable(R.drawable.btn_ring_camera_normal),
getDrawable(R.drawable.btn_ring_camera_hover),
BUTTON_CAMERA, SLOT_LEFT, res.getString(R.string.mode_photo));
// Panorama pad button
addRingPad(getDrawable(R.drawable.btn_ring_pano_normal),
getDrawable(R.drawable.btn_ring_pano_hover),
BUTTON_PANO, SLOT_MIDLEFT, res.getString(R.string.mode_panorama));
// Video pad button
addRingPad(getDrawable(R.drawable.btn_ring_video_normal),
getDrawable(R.drawable.btn_ring_video_hover),
BUTTON_VIDEO, SLOT_MID, res.getString(R.string.mode_video));
// PictureSphere pad button
addRingPad(getDrawable(R.drawable.btn_ring_picsphere_normal),
getDrawable(R.drawable.btn_ring_picsphere_hover),
BUTTON_PICSPHERE, SLOT_MIDRIGHT, res.getString(R.string.mode_picsphere));
// Switch Cam pad button
addRingPad(getDrawable(R.drawable.btn_ring_switchcam_normal),
getDrawable(R.drawable.btn_ring_switchcam_hover),
BUTTON_SWITCHCAM, SLOT_RIGHT, res.getString(R.string.mode_switchcam));
}
public void setListener(RingPadListener listener) {
mListener = listener;
}
@Override
protected void onDraw(Canvas canvas) {
if (mOpenProgress == 0 && mHintProgress == 1)
return;
if (mPaint == null) {
mPaint = new Paint();
}
// Get the size dimensions regardless of orientation
final Point screenSize = Util.getScreenSize(null);
final int width = Math.min(screenSize.x, screenSize.y);
final int height = Math.max(screenSize.x, screenSize.y);
// Draw the ping animation
final float buttonOffset = Util.dpToPx(getContext(), 12);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(4.0f);
mPaint.setARGB((int) (255.0f - 255.0f * mHintProgress), 255, 255, 255);
canvas.drawCircle(width / 2, height - mEdgePadding + buttonOffset, mHintProgress
* mRingRadius, mPaint);
canvas.drawCircle(width / 2, height - mEdgePadding + buttonOffset, mHintProgress
* mRingRadius * 0.66f, mPaint);
canvas.drawCircle(width / 2, height - mEdgePadding + buttonOffset, mHintProgress
* mRingRadius * 0.33f, mPaint);
final float ringRadius = mRingRadius * mOpenProgress;
// Draw the inner circle (dark)
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(0x88888888);
canvas.drawCircle(width / 2, height - mEdgePadding, ringRadius, mPaint);
// Draw the outline stroke
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(4.0f);
mPaint.setColor(0x88DDDDDD);
canvas.drawCircle(width / 2, height - mEdgePadding, ringRadius, mPaint);
// Draw the actual pad buttons
for (int i = 0; i < SLOT_MAX; i++) {
mPaint.setAlpha((int) (255.0f * mOpenProgress));
PadButton button = mButtons[i];
if (button == null) continue;
final float radAngle = (float) ((i * (180.0f / 4.0f) + 90.0f) * Math.PI / 180.0f);
final float y = (float) (height + ringRadius * Math.cos(radAngle) - mButtonSize);
// We remove the button edge
final float x = (float) (width / 2 - button.mNormalBitmap.getWidth() / 2
- ringRadius * Math.sin(radAngle));
canvas.save();
canvas.translate(x + button.mNormalBitmap.getWidth() / 2,
y + button.mNormalBitmap.getWidth() / 2);
canvas.rotate(mCurrentOrientation);
if (button.mIsHovering) {
canvas.drawBitmap(button.mHoverBitmap, -button.mNormalBitmap.getWidth() / 2,
-button.mNormalBitmap.getWidth() / 2, mPaint);
} else {
canvas.drawBitmap(button.mNormalBitmap, -button.mNormalBitmap.getWidth() / 2,
-button.mNormalBitmap.getWidth() / 2, mPaint);
}
canvas.restore();
if (mOpenProgress == 1.0f) {
animateAlpha(button.mIsHovering, button);
} else {
animateAlpha(false, button);
}
if ((button.mIsHovering || button.mHintTextAlpha > 0.0f) && mOpenProgress == 1.0f) {
// Draw hint text
int alpha = (int) (255 * (button.mHintTextAlpha));
mPaint.setStyle(Paint.Style.FILL);
mPaint.setTextSize(36);
mPaint.setTextAlign(Paint.Align.CENTER);
mPaint.setShadowLayer(12, 0, 0, 0xEE333333 * ((alpha & 0xFF) << 24));
mPaint.setAlpha(alpha);
float measureText = mPaint.measureText(button.mHintText);
canvas.save();
canvas.translate(x + button.mNormalBitmap.getWidth() / 2 - mPaint.getTextSize() / 2,
y - measureText / 2 - Util.dpToPx(getContext(), 4));
canvas.rotate(mCurrentOrientation);
canvas.drawText(button.mHintText, 0, 0, mPaint);
canvas.restore();
}
button.mLastDrawnX = x;
button.mLastDrawnY = y;
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!mIsOpen) return false;
if (event.getActionMasked() == MotionEvent.ACTION_MOVE) {
for (int i = 0; i < SLOT_MAX; i++) {
PadButton button = mButtons[i];
if (button == null) {
continue;
}
RectF btnRect = new RectF(button.mLastDrawnX, button.mLastDrawnY,
button.mLastDrawnX + button.mNormalBitmap.getWidth(),
button.mLastDrawnY + button.mNormalBitmap.getHeight());
if (btnRect.contains(event.getRawX(), event.getRawY())) {
button.mIsHovering = true;
} else {
button.mIsHovering = false;
}
}
} else if (event.getActionMasked() == MotionEvent.ACTION_UP) {
animateClose();
for (int i = 0; i < SLOT_MAX; i++) {
PadButton button = mButtons[i];
if (button == null) continue;
if (button.mIsHovering && mListener != null) {
mListener.onButtonActivated(button.mEventId);
}
}
return false;
}
invalidate();
return super.onTouchEvent(event);
}
private void animateAlpha(boolean in, final PadButton button) {
if (button.mHintAnimationDirection == in) {
return;
}
button.mHintAnimationDirection = in;
ValueAnimator anim = new ValueAnimator();
anim.setDuration(200);
anim.setFloatValues(in ? 0 : 1, in ? 1 : 0);
anim.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator arg0) {
button.mHintTextAlpha = (Float) arg0.getAnimatedValue();
invalidate();
}
});
anim.setInterpolator(new AccelerateDecelerateInterpolator());
anim.start();
}
public void notifyOrientationChanged(float orientation) {
mTargetOrientation = orientation;
ValueAnimator anim = new ValueAnimator();
anim.setDuration(200);
anim.setFloatValues(mCurrentOrientation, orientation);
anim.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator arg0) {
mCurrentOrientation = (Float) arg0.getAnimatedValue();
invalidate();
}
});
anim.setInterpolator(new AccelerateDecelerateInterpolator());
anim.start();
}
public boolean isOpen() {
return mIsOpen;
}
public void animateOpen() {
if (mIsOpen) {
return;
}
mAnimator.cancel();
mAnimator.setFloatValues(0, 1);
mAnimator.start();
mIsOpen = true;
}
public void animateClose() {
if (!mIsOpen) {
return;
}
mAnimator.cancel();
mAnimator.setFloatValues(1, 0);
mAnimator.start();
mIsOpen = false;
}
public void addRingPad(Bitmap iconNormal, Bitmap iconHover,
int eventId, int slot, String hint) {
mButtons[slot] = new PadButton();
mButtons[slot].mNormalBitmap = iconNormal;
mButtons[slot].mHoverBitmap = iconHover;
mButtons[slot].mEventId = eventId;
mButtons[slot].mHintText = hint;
}
@Override
public void onAnimationUpdate(ValueAnimator animator) {
mOpenProgress = (Float) animator.getAnimatedValue();
invalidate();
}
}
================================================
FILE: src/org/cyanogenmod/focal/ui/ThumbnailFlinger.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.ui;
import android.animation.Animator;
import android.content.Context;
import android.util.AttributeSet;
import android.view.ViewGroup;
import android.view.animation.AccelerateInterpolator;
import android.widget.ImageView;
/**
* View designed to show and fade the preview of a snapshot
* after it was taken.
*/
public class ThumbnailFlinger extends ImageView {
private final static int FADE_IN_DURATION_MS = 250;
private final static int FADE_OUT_DURATION_MS = 250;
public ThumbnailFlinger(Context context) {
super(context);
}
public ThumbnailFlinger(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ThumbnailFlinger(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public void doAnimation() {
// Setup original values
this.clearAnimation();
setScaleX(0.8f);
setScaleY(0.8f);
setAlpha(0.0f);
setTranslationX(0.0f);
setTranslationY(0.0f);
// First step of animation: fade in quick
animate().alpha(1.0f).scaleX(1.0f).scaleY(1.0f).setInterpolator(
new AccelerateInterpolator()).setDuration(FADE_IN_DURATION_MS)
.setListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationEnd(Animator animator) {
// Second step: fade out and scale and slide
animate().alpha(0.0f).scaleY(0.7f).scaleX(0.7f).translationYBy(
-getHeight()*1.5f).setStartDelay(100).setDuration(
FADE_OUT_DURATION_MS).setListener(
new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationEnd(Animator animator) {
((ViewGroup) getParent()).removeView(ThumbnailFlinger.this);
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
}).start();
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
}).start();
}
}
================================================
FILE: src/org/cyanogenmod/focal/ui/WidgetRenderer.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.ui;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.widget.FrameLayout;
import fr.xplod.focal.R;
import org.cyanogenmod.focal.Util;
import org.cyanogenmod.focal.widgets.WidgetBase;
import java.util.ArrayList;
import java.util.List;
public class WidgetRenderer extends FrameLayout {
public final static String TAG = "WidgetRenderer";
private static float WIDGETS_MARGIN;
private List mOpenWidgets;
private float mTotalHeight;
private float mSpacing;
private int mOrientation;
private float mWidgetDragStartPoint;
private boolean mIsHidden;
public WidgetRenderer(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initialize();
}
public WidgetRenderer(Context context, AttributeSet attrs) {
super(context, attrs);
initialize();
}
public WidgetRenderer(Context context) {
super(context);
initialize();
}
private void initialize() {
mOpenWidgets = new ArrayList();
WIDGETS_MARGIN = Util.dpToPx(getContext(), 32);
mTotalHeight = WIDGETS_MARGIN;
mSpacing = getResources().getDimension(R.dimen.widget_spacing);
}
/**
* Rotate the contents of open widgets
*
* @param orientation
*/
public void notifyOrientationChanged(int orientation) {
mOrientation = orientation;
for (int i = 0; i < mOpenWidgets.size(); i++) {
mOpenWidgets.get(i).notifyOrientationChanged(orientation, false);
}
}
/**
* Notifies the renderer a widget has been pressed.
*
* @param widget The widget that has been pressed
*/
public void widgetPressed(WidgetBase.WidgetContainer widget) {
mWidgetDragStartPoint = widget.getFinalY();
}
/**
* Notifies the renderer a widget has been moved. The renderer
* will then move the other widgets accordingly if needed.
*
* @param widget The widget that has been moved
*/
public void widgetMoved(WidgetBase.WidgetContainer widget) {
// Don't move widget if it was just a small tap
if (Math.abs(widget.getY() - mWidgetDragStartPoint) < 40.0f) return;
if (mOpenWidgets.size() == 0) return;
boolean isFirst = (mOpenWidgets.get(0) == widget);
// Check if we overlap the top of a widget
for (int i = 0; i < mOpenWidgets.size(); i++) {
WidgetBase.WidgetContainer tested = mOpenWidgets.get(i);
if (tested == widget) {
continue;
}
if (widget.getY() < tested.getFinalY() + tested.getMeasuredHeight()) {
// Don't try to go before the first if we're already it
if (isFirst && widget.getY() + widget.getHeight() < tested.getFinalY()
- tested.getHeight() / 2)
break;
// Move the widget in our list
mOpenWidgets.remove(widget);
mOpenWidgets.add(i, widget);
reorderWidgets(widget);
break;
}
}
}
/**
* Reorder the widgets and clamp their position after a widget
* has been dropped.
*
* @param widget
*/
public void widgetDropped(WidgetBase.WidgetContainer widget) {
reorderWidgets(null);
}
public void reorderWidgets(WidgetBase.WidgetContainer ignore) {
mTotalHeight = WIDGETS_MARGIN;
for (int i = 0; i < mOpenWidgets.size(); i++) {
WidgetBase.WidgetContainer widget = mOpenWidgets.get(i);
if (widget != ignore) {
widget.setYSmooth(mTotalHeight);
}
mTotalHeight += widget.getMeasuredHeight() + mSpacing;
}
}
/**
* Notifies the renderer a widget has been opened and needs to be
* positioned.
*
* @param widget Widget opened
*/
public void widgetOpened(final WidgetBase.WidgetContainer widget) {
if (mOpenWidgets.contains(widget)) {
Log.w(TAG, "Widget was already rendered!");
return;
}
mOpenWidgets.add(widget);
// Make sure the widget is properly oriented
widget.notifyOrientationChanged(mOrientation, true);
// Position it properly
float finalY = mTotalHeight;
widget.setYSmooth(finalY);
mTotalHeight += widget.getMeasuredHeight() + mSpacing;
reorderWidgets(null);
}
/**
* Notifies the renderer a widget has been closed and its
* space can be reclaimed
*
* @param widget Widget closed
*/
public void widgetClosed(WidgetBase.WidgetContainer widget) {
widget.setYSmooth(widget.getFinalY() - Util.dpToPx(getContext(), 8));
mOpenWidgets.remove(widget);
// Reposition all the widgets
reorderWidgets(null);
}
/**
* Close all the widgets currently opened
*/
public void closeAllWidgets() {
for (int i = 0; i < mOpenWidgets.size(); i++) {
mOpenWidgets.get(i).getWidgetBase().close();
}
}
public void notifySidebarSlideStatus(float distance) {
float finalX = getTranslationX() + distance;
FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) getLayoutParams();
if (finalX > params.leftMargin) {
finalX = params.leftMargin;
} else if (finalX < -params.leftMargin) {
finalX = -params.leftMargin;
}
setTranslationX(finalX);
}
public void notifySidebarSlideClose() {
FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) getLayoutParams();
animate().translationX(-params.leftMargin).setDuration(
SideBar.SLIDE_ANIMATION_DURATION_MS).start();
}
public void notifySidebarSlideOpen() {
FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) getLayoutParams();
animate().translationX(params.leftMargin).setDuration(
SideBar.SLIDE_ANIMATION_DURATION_MS).start();
}
public void hideWidgets() {
mIsHidden = true;
for (int i = 0; i < mOpenWidgets.size(); i++) {
WidgetBase.WidgetContainer widget = mOpenWidgets.get(i);
widget.animate().translationXBy(-widget.getWidth()).setDuration(300).alpha(0)
.setInterpolator(new AccelerateInterpolator()).start();
}
}
public void restoreWidgets() {
mIsHidden = false;
for (int i = 0; i < mOpenWidgets.size(); i++) {
WidgetBase.WidgetContainer widget = mOpenWidgets.get(i);
widget.animate().translationXBy(widget.getWidth()).setDuration(300).alpha(1)
.setInterpolator(new DecelerateInterpolator()).start();
}
}
public boolean isHidden() {
return mIsHidden;
}
public int getWidgetsCount() {
return mOpenWidgets.size();
}
}
================================================
FILE: src/org/cyanogenmod/focal/ui/showcase/AnimationUtils.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
* Copyright (C) 2012 Alex Curran
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.ui.showcase;
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.os.Handler;
import android.view.View;
public class AnimationUtils {
private static final int DEFAULT_DURATION = 300;
private static final String ALPHA = "alpha";
private static final float INVISIBLE = 0f;
private static final float VISIBLE = 1f;
private static final String COORD_X = "x";
private static final String COORD_Y = "y";
private static final int INSTANT = 0;
public interface AnimationStartListener {
void onAnimationStart();
}
public interface AnimationEndListener {
void onAnimationEnd();
}
public static float getX(View view) {
return view.getX();
}
public static float getY(View view) {
return view.getY();
}
public static void hide(View view) {
view.setAlpha(INVISIBLE);
}
public static ObjectAnimator createFadeInAnimation(Object target,
final AnimationStartListener listener) {
return createFadeInAnimation(target, DEFAULT_DURATION, listener);
}
public static ObjectAnimator createFadeInAnimation(Object target, int duration,
final AnimationStartListener listener) {
ObjectAnimator oa = ObjectAnimator.ofFloat(target, ALPHA, VISIBLE);
oa.setDuration(duration).addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
listener.onAnimationStart();
}
@Override
public void onAnimationEnd(Animator animator) {
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
return oa;
}
public static ObjectAnimator createFadeOutAnimation(Object target,
final AnimationEndListener listener) {
return createFadeOutAnimation(target, DEFAULT_DURATION, listener);
}
public static ObjectAnimator createFadeOutAnimation(Object target, int duration,
final AnimationEndListener listener) {
ObjectAnimator oa = ObjectAnimator.ofFloat(target, ALPHA, INVISIBLE);
oa.setDuration(duration).addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationEnd(Animator animator) {
listener.onAnimationEnd();
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
return oa;
}
public static AnimatorSet createMovementAnimation(View view, float canvasX, float canvasY,
float offsetStartX, float offsetStartY, float offsetEndX, float offsetEndY,
final AnimationEndListener listener) {
hide(view);
ObjectAnimator alphaIn = ObjectAnimator.ofFloat(
view, ALPHA, INVISIBLE, VISIBLE).setDuration(500);
ObjectAnimator setUpX = ObjectAnimator.ofFloat(
view, COORD_X, canvasX + offsetStartX).setDuration(INSTANT);
ObjectAnimator setUpY = ObjectAnimator.ofFloat(
view, COORD_Y, canvasY + offsetStartY).setDuration(INSTANT);
ObjectAnimator moveX = ObjectAnimator.ofFloat(
view, COORD_X, canvasX + offsetEndX).setDuration(1000);
ObjectAnimator moveY = ObjectAnimator.ofFloat(
view, COORD_Y, canvasY + offsetEndY).setDuration(1000);
moveX.setStartDelay(1000);
moveY.setStartDelay(1000);
ObjectAnimator alphaOut = ObjectAnimator.ofFloat(
view, ALPHA, INVISIBLE).setDuration(500);
alphaOut.setStartDelay(2500);
AnimatorSet as = new AnimatorSet();
as.play(setUpX).with(setUpY).before(alphaIn).before(moveX).with(moveY).before(alphaOut);
Handler handler = new Handler();
Runnable runnable = new Runnable() {
@Override
public void run() {
listener.onAnimationEnd();
}
};
handler.postDelayed(runnable, 3000);
return as;
}
public static AnimatorSet createMovementAnimation(View view, float x, float y) {
ObjectAnimator alphaIn = ObjectAnimator.ofFloat(
view, ALPHA, INVISIBLE, VISIBLE).setDuration(500);
ObjectAnimator setUpX = ObjectAnimator.ofFloat(view, COORD_X, x).setDuration(INSTANT);
ObjectAnimator setUpY = ObjectAnimator.ofFloat(view, COORD_Y, y).setDuration(INSTANT);
AnimatorSet as = new AnimatorSet();
as.play(setUpX).with(setUpY).before(alphaIn);
return as;
}
}
================================================
FILE: src/org/cyanogenmod/focal/ui/showcase/ShowcaseView.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
* Copyright (C) 2012 Alex Curran
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.ui.showcase;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.text.DynamicLayout;
import android.text.Layout;
import android.text.TextPaint;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.Button;
import android.widget.RelativeLayout;
import fr.xplod.focal.R;
import java.lang.reflect.Field;
import static org.cyanogenmod.focal.ui.showcase.AnimationUtils.AnimationEndListener;
import static org.cyanogenmod.focal.ui.showcase.AnimationUtils.AnimationStartListener;
/**
* A view which allows you to showcase areas of your app with an explanation.
*/
public class ShowcaseView extends RelativeLayout implements View.OnClickListener,
View.OnTouchListener {
public static final int TYPE_NO_LIMIT = 0;
public static final int TYPE_ONE_SHOT = 1;
public static final int INSERT_TO_DECOR = 0;
public static final int INSERT_TO_VIEW = 1;
public static final int ITEM_ACTION_HOME = 0;
public static final int ITEM_TITLE = 1;
public static final int ITEM_SPINNER = 2;
public static final int ITEM_ACTION_ITEM = 3;
public static final int ITEM_ACTION_OVERFLOW = 6;
public static final int INNER_CIRCLE_RADIUS = 94;
private static final String PREFS_SHOWCASE_INTERNAL = "showcase_internal";
private final Button mEndButton;
private final String buttonText;
private float showcaseX = -1;
private float showcaseY = -1;
private float showcaseRadius = -1;
private float metricScale = 1.0f;
private float legacyShowcaseX = -1;
private float legacyShowcaseY = -1;
private boolean isRedundant = false;
private boolean hasCustomClickListener = false;
private ConfigOptions mOptions;
private Paint mPaintTitle, mEraser;
private TextPaint mPaintDetail;
private int backColor;
private Drawable showcase;
private View mHandy;
private OnShowcaseEventListener mEventListener;
private Rect voidedArea;
private String mTitleText, mSubText;
private int detailTextColor = -1;
private int titleTextColor = -1;
private DynamicLayout mDynamicTitleLayout;
private DynamicLayout mDynamicDetailLayout;
private float[] mBestTextPosition;
private boolean mAlteredText = false;
private float scaleMultiplier = 1f;
private int mOrientation;
public ShowcaseView(Context context) {
this(context, null, R.styleable.CustomTheme_showcaseViewStyle);
}
public ShowcaseView(Context context, AttributeSet attrs) {
this(context, attrs, R.styleable.CustomTheme_showcaseViewStyle);
}
public ShowcaseView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// Get the attributes for the ShowcaseView
final TypedArray styled = context.getTheme().obtainStyledAttributes(
attrs, R.styleable.ShowcaseView, R.attr.showcaseViewStyle, R.style.ShowcaseView);
backColor = styled.getInt(R.styleable.ShowcaseView_sv_backgroundColor,
Color.argb(128, 80, 80, 80));
detailTextColor = styled.getColor(R.styleable.ShowcaseView_sv_detailTextColor, Color.WHITE);
titleTextColor = styled.getColor(R.styleable.ShowcaseView_sv_titleTextColor,
Color.parseColor("#49C0EC"));
buttonText = styled.getString(R.styleable.ShowcaseView_sv_buttonText);
styled.recycle();
metricScale = getContext().getResources().getDisplayMetrics().density;
mEndButton = (Button) LayoutInflater.from(context).inflate(R.layout.showcase_button, null);
ConfigOptions options = new ConfigOptions();
options.showcaseId = getId();
setConfigOptions(options);
}
/**
* Quick method to insert a ShowcaseView into an Activity
*
* @param viewToShowcase View to showcase
* @param activity Activity to insert into
* @param title Text to show as a title. Can be null.
* @param detailText More detailed text. Can be null.
* @param options A set of options to customise the ShowcaseView
* @return the created ShowcaseView instance
*/
public static ShowcaseView insertShowcaseView(View viewToShowcase, Activity activity,
String title, String detailText, ConfigOptions options) {
ShowcaseView sv = new ShowcaseView(activity);
if (options != null)
sv.setConfigOptions(options);
if (sv.getConfigOptions().insert == INSERT_TO_DECOR) {
((ViewGroup) activity.getWindow().getDecorView()).addView(sv);
} else {
((ViewGroup) activity.findViewById(android.R.id.content)).addView(sv);
}
sv.setShowcaseView(viewToShowcase);
sv.setText(title, detailText);
return sv;
}
/**
* Quick method to insert a ShowcaseView into an Activity
*
* @param viewToShowcase View to showcase
* @param activity Activity to insert into
* @param title Text to show as a title. Can be null.
* @param detailText More detailed text. Can be null.
* @param options A set of options to customise the ShowcaseView
* @return the created ShowcaseView instance
*/
public static ShowcaseView insertShowcaseView(View viewToShowcase, Activity activity,
int title, int detailText, ConfigOptions options) {
ShowcaseView sv = new ShowcaseView(activity);
if (options != null)
sv.setConfigOptions(options);
if (sv.getConfigOptions().insert == INSERT_TO_DECOR) {
((ViewGroup) activity.getWindow().getDecorView()).addView(sv);
} else {
((ViewGroup) activity.findViewById(android.R.id.content)).addView(sv);
}
sv.setShowcaseView(viewToShowcase);
sv.setText(title, detailText);
return sv;
}
public static ShowcaseView insertShowcaseView(int showcaseViewId, Activity activity,
String title, String detailText, ConfigOptions options) {
View v = activity.findViewById(showcaseViewId);
if (v != null) {
return insertShowcaseView(v, activity, title, detailText, options);
}
return null;
}
public static ShowcaseView insertShowcaseView(int showcaseViewId, Activity activity,
int title, int detailText, ConfigOptions options) {
View v = activity.findViewById(showcaseViewId);
if (v != null) {
return insertShowcaseView(v, activity, title, detailText, options);
}
return null;
}
public static ShowcaseView insertShowcaseView(float x, float y, Activity activity,
String title, String detailText, ConfigOptions options) {
ShowcaseView sv = new ShowcaseView(activity);
if (options != null)
sv.setConfigOptions(options);
if (sv.getConfigOptions().insert == INSERT_TO_DECOR) {
((ViewGroup) activity.getWindow().getDecorView()).addView(sv);
} else {
((ViewGroup) activity.findViewById(android.R.id.content)).addView(sv);
}
sv.setShowcasePosition(x, y);
sv.setText(title, detailText);
return sv;
}
public static ShowcaseView insertShowcaseView(float x, float y, Activity activity,
int title, int detailText, ConfigOptions options) {
ShowcaseView sv = new ShowcaseView(activity);
if (options != null)
sv.setConfigOptions(options);
if (sv.getConfigOptions().insert == INSERT_TO_DECOR) {
((ViewGroup) activity.getWindow().getDecorView()).addView(sv);
} else {
((ViewGroup) activity.findViewById(android.R.id.content)).addView(sv);
}
sv.setShowcasePosition(x, y);
sv.setText(title, detailText);
return sv;
}
public static ShowcaseView insertShowcaseView(View showcase, Activity activity) {
return insertShowcaseView(showcase, activity, null, null, null);
}
/**
* Quickly insert a ShowcaseView into an Activity, highlighting an item.
*
* @param type the type of item to showcase (can be ITEM_ACTION_HOME, ITEM_TITLE_OR_SPINNER, ITEM_ACTION_ITEM or ITEM_ACTION_OVERFLOW)
* @param itemId the ID of an Action item to showcase (only required for ITEM_ACTION_ITEM
* @param activity Activity to insert the ShowcaseView into
* @param title Text to show as a title. Can be null.
* @param detailText More detailed text. Can be null.
* @param options A set of options to customise the ShowcaseView
* @return the created ShowcaseView instance
*/
public static ShowcaseView insertShowcaseViewWithType(int type, int itemId, Activity activity,
String title, String detailText, ConfigOptions options) {
ShowcaseView sv = new ShowcaseView(activity);
if (options != null)
sv.setConfigOptions(options);
if (sv.getConfigOptions().insert == INSERT_TO_DECOR) {
((ViewGroup) activity.getWindow().getDecorView()).addView(sv);
} else {
((ViewGroup) activity.findViewById(android.R.id.content)).addView(sv);
}
sv.setShowcaseItem(type, itemId, activity);
sv.setText(title, detailText);
return sv;
}
/**
* Quickly insert a ShowcaseView into an Activity, highlighting an item.
*
* @param type the type of item to showcase (can be ITEM_ACTION_HOME, ITEM_TITLE_OR_SPINNER, ITEM_ACTION_ITEM or ITEM_ACTION_OVERFLOW)
* @param itemId the ID of an Action item to showcase (only required for ITEM_ACTION_ITEM
* @param activity Activity to insert the ShowcaseView into
* @param title Text to show as a title. Can be null.
* @param detailText More detailed text. Can be null.
* @param options A set of options to customise the ShowcaseView
* @return the created ShowcaseView instance
*/
public static ShowcaseView insertShowcaseViewWithType(int type, int itemId, Activity activity,
int title, int detailText, ConfigOptions options) {
ShowcaseView sv = new ShowcaseView(activity);
if (options != null)
sv.setConfigOptions(options);
if (sv.getConfigOptions().insert == INSERT_TO_DECOR) {
((ViewGroup) activity.getWindow().getDecorView()).addView(sv);
} else {
((ViewGroup) activity.findViewById(android.R.id.content)).addView(sv);
}
sv.setShowcaseItem(type, itemId, activity);
sv.setText(title, detailText);
return sv;
}
public static ShowcaseView insertShowcaseView(float x, float y, Activity activity) {
return insertShowcaseView(x, y, activity, null, null, null);
}
private void init() {
boolean hasShot = getContext().getSharedPreferences(PREFS_SHOWCASE_INTERNAL,
Context.MODE_PRIVATE).getBoolean("hasShot" + getConfigOptions().showcaseId, false);
if (hasShot && mOptions.shotType == TYPE_ONE_SHOT) {
// The showcase has already been shot once, so we don't need to do anything
setVisibility(View.GONE);
isRedundant = true;
return;
}
showcase = getContext().getResources().getDrawable(R.drawable.cling);
showcaseRadius = metricScale * INNER_CIRCLE_RADIUS;
PorterDuffXfermode mBlender = new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY);
setOnTouchListener(this);
mPaintTitle = new Paint();
mPaintTitle.setColor(titleTextColor);
mPaintTitle.setShadowLayer(2.0f, 0f, 2.0f, Color.DKGRAY);
mPaintTitle.setTextSize(24 * metricScale);
mPaintTitle.setAntiAlias(true);
mPaintDetail = new TextPaint();
mPaintDetail.setColor(detailTextColor);
mPaintDetail.setShadowLayer(2.0f, 0f, 2.0f, Color.DKGRAY);
mPaintDetail.setTextSize(16 * metricScale);
mPaintDetail.setAntiAlias(true);
mEraser = new Paint();
mEraser.setColor(0xFFFFFF);
mEraser.setAlpha(0);
mEraser.setXfermode(mBlender);
mEraser.setAntiAlias(true);
if (!mOptions.noButton && mEndButton.getParent() == null) {
RelativeLayout.LayoutParams lps = (LayoutParams) generateDefaultLayoutParams();
lps.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
lps.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
int margin = ((Number) (metricScale * 12)).intValue();
lps.setMargins(margin, margin, margin, margin);
lps.height = LayoutParams.WRAP_CONTENT;
lps.width = LayoutParams.WRAP_CONTENT;
mEndButton.setLayoutParams(lps);
mEndButton.setText(buttonText != null ? buttonText : getResources().getString(R.string.ok));
if (!hasCustomClickListener) {
mEndButton.setOnClickListener(this);
}
addView(mEndButton);
}
}
public void notifyOrientationChanged(int orientation) {
if (mEndButton != null) {
mEndButton.animate().rotation(orientation).setDuration(300).start();
}
if (mHandy != null) {
mHandy.animate().rotation(orientation).setDuration(300).start();
}
mOrientation = orientation;
invalidate();
}
/**
* Set the view to showcase
*
* @param view The {@link View} to showcase.
*/
public void setShowcaseView(final View view) {
if (isRedundant || view == null) {
isRedundant = true;
return;
}
isRedundant = false;
view.post(new Runnable() {
@Override
public void run() {
init();
if (mOptions.insert == INSERT_TO_VIEW) {
showcaseX = (float) (view.getLeft() + view.getWidth() / 2);
showcaseY = (float) (view.getTop() + view.getHeight() / 2);
} else {
int[] coordinates = new int[2];
view.getLocationInWindow(coordinates);
showcaseX = (float) (coordinates[0] + view.getWidth() / 2);
showcaseY = (float) (coordinates[1] + view.getHeight() / 2);
}
invalidate();
}
});
}
/**
* Set a specific position to showcase
*
* @param x X co-ordinate
* @param y Y co-ordinate
*/
public void setShowcasePosition(float x, float y) {
if (isRedundant) {
return;
}
showcaseX = x;
showcaseY = y;
init();
invalidate();
}
public void setShowcaseItem(final int itemType, final int actionItemId,
final Activity activity) {
post(new Runnable() {
@Override
public void run() {
View homeButton = activity.findViewById(android.R.id.home);
if (homeButton == null) {
// Thanks to @hameno for this
int homeId = activity.getResources().getIdentifier(
"abs__home", "id", activity.getPackageName());
if (homeId != 0) {
homeButton = activity.findViewById(homeId);
}
}
if (homeButton == null)
throw new RuntimeException("insertShowcaseViewWithType cannot be used when the theme " +
"has no ActionBar");
ViewParent p = homeButton.getParent().getParent(); //ActionBarView
if (!p.getClass().getName().contains("ActionBarView")) {
String previousP = p.getClass().getName();
p = p.getParent();
String throwP = p.getClass().getName();
if (!p.getClass().getName().contains("ActionBarView"))
throw new IllegalStateException("Cannot find ActionBarView for " +
"Activity, instead found " + previousP + " and " + throwP);
}
Class abv = p.getClass(); //ActionBarView class
Class absAbv = abv.getSuperclass(); //AbsActionBarView class
switch (itemType) {
case ITEM_ACTION_HOME:
setShowcaseView(homeButton);
break;
case ITEM_SPINNER:
showcaseSpinner(p, abv);
break;
case ITEM_TITLE:
showcaseTitle(p, abv);
break;
case ITEM_ACTION_ITEM:
case ITEM_ACTION_OVERFLOW:
showcaseActionItem(p, absAbv, itemType, actionItemId);
break;
default:
Log.e("TAG", "Unknown item type");
}
}
});
}
private void showcaseActionItem(ViewParent p, Class absAbv, int itemType, int actionItemId) {
try {
Field mAmpField = absAbv.getDeclaredField("mActionMenuPresenter");
mAmpField.setAccessible(true);
Object mAmp = mAmpField.get(p);
if (itemType == ITEM_ACTION_OVERFLOW) {
// Finds the overflow button associated with the ActionMenuPresenter
Field mObField = mAmp.getClass().getDeclaredField("mOverflowButton");
mObField.setAccessible(true);
View mOb = (View) mObField.get(mAmp);
if (mOb != null)
setShowcaseView(mOb);
} else {
// Want an ActionItem, so find it
Field mAmvField = mAmp.getClass().getSuperclass().getDeclaredField("mMenuView");
mAmvField.setAccessible(true);
Object mAmv = mAmvField.get(mAmp);
Field mChField;
if (mAmv.getClass().toString().contains("com.actionbarsherlock")) {
// There are thousands of superclasses to traverse up
// Have to get superclasses because mChildren is private
mChField = mAmv.getClass().getSuperclass().getSuperclass()
.getSuperclass().getSuperclass().getDeclaredField("mChildren");
} else
mChField = mAmv.getClass().getSuperclass().getSuperclass()
.getDeclaredField("mChildren");
mChField.setAccessible(true);
Object[] mChs = (Object[]) mChField.get(mAmv);
for (Object mCh : mChs) {
if (mCh != null) {
View v = (View) mCh;
if (v.getId() == actionItemId)
setShowcaseView(v);
}
}
}
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (NullPointerException npe) {
throw new RuntimeException("insertShowcaseViewWithType() must be called " +
"after or during onCreateOptionsMenu() of the host Activity");
}
}
private void showcaseSpinner(ViewParent p, Class abv) {
try {
Field mSpinnerField = abv.getDeclaredField("mSpinner");
mSpinnerField.setAccessible(true);
View mSpinnerView = (View) mSpinnerField.get(p);
if (mSpinnerView != null) {
setShowcaseView(mSpinnerView);
}
} catch (NoSuchFieldException e) {
Log.e("TAG", "Failed to find actionbar spinner", e);
} catch (IllegalAccessException e) {
Log.e("TAG", "Failed to access actionbar spinner", e);
}
}
private void showcaseTitle(ViewParent p, Class abv) {
try {
Field mTitleViewField = abv.getDeclaredField("mTitleView");
mTitleViewField.setAccessible(true);
View titleView = (View) mTitleViewField.get(p);
if (titleView != null) {
setShowcaseView(titleView);
}
} catch (NoSuchFieldException e) {
Log.e("TAG", "Failed to find actionbar title", e);
} catch (IllegalAccessException e) {
Log.e("TAG", "Failed to access actionbar title", e);
}
}
/**
* Set the shot method of the showcase - only once or no limit
*
* @param shotType either TYPE_ONE_SHOT or TYPE_NO_LIMIT
* @deprecated Use the option in {@link ConfigOptions} instead.
*/
@Deprecated
public void setShotType(int shotType) {
if (shotType == TYPE_NO_LIMIT || shotType == TYPE_ONE_SHOT) {
mOptions.shotType = shotType;
}
}
/**
* Decide whether touches outside the showcased circle should be ignored or not
*
* @param block true to block touches, false otherwise. By default, this is true.
* @deprecated Use the option in {@link ConfigOptions} instead.
*/
@Deprecated
public void blockNonShowcasedTouches(boolean block) {
mOptions.block = block;
}
/**
* Override the standard button click event
*
* @param listener Listener to listen to on click events
*/
public void overrideButtonClick(OnClickListener listener) {
if (isRedundant) {
return;
}
if (mEndButton != null) {
mEndButton.setOnClickListener(listener != null ? listener : this);
}
hasCustomClickListener = true;
}
public void setOnShowcaseEventListener(OnShowcaseEventListener listener) {
mEventListener = listener;
}
@Override
protected void dispatchDraw(Canvas canvas) {
if (showcaseX < 0 || showcaseY < 0 || isRedundant) {
super.dispatchDraw(canvas);
return;
}
Bitmap b = Bitmap.createBitmap(getMeasuredWidth(),
getMeasuredHeight(), Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(b);
//Draw the semi-transparent background
c.drawColor(backColor);
//Draw to the scale specified
Matrix mm = new Matrix();
mm.postScale(scaleMultiplier, scaleMultiplier, showcaseX, showcaseY);
c.setMatrix(mm);
//Erase the area for the ring
c.drawCircle(showcaseX, showcaseY, showcaseRadius, mEraser);
boolean recalculateText = makeVoidedRect() || mAlteredText;
mAlteredText = false;
showcase.setBounds(voidedArea);
showcase.draw(c);
canvas.drawBitmap(b, 0, 0, null);
// Clean up, as we no longer require these items.
try {
c.setBitmap(null);
} catch (NullPointerException npe) {
//TODO why does this NPE happen?
npe.printStackTrace();
}
b.recycle();
b = null;
if (!TextUtils.isEmpty(mTitleText) || !TextUtils.isEmpty(mSubText)) {
if (recalculateText) {
mBestTextPosition = getBestTextPosition(canvas.getWidth(), canvas.getHeight());
}
if (!TextUtils.isEmpty(mTitleText)) {
//TODO: use a dynamic detail layout
canvas.save();
float width = mPaintTitle.measureText(mTitleText);
canvas.rotate(mOrientation, mBestTextPosition[0] + width / 2.0f,
mBestTextPosition[1] + mPaintTitle.getTextSize() / 2.0f);
canvas.drawText(mTitleText, mBestTextPosition[0], mBestTextPosition[1], mPaintTitle);
canvas.restore();
}
if (!TextUtils.isEmpty(mSubText)) {
canvas.save();
if (recalculateText)
mDynamicDetailLayout = new DynamicLayout(mSubText, mPaintDetail,
((Number) mBestTextPosition[2]).intValue(),
Layout.Alignment.ALIGN_NORMAL, 1.2f, 1.0f, true);
if (mOrientation % 180 == 0) {
canvas.translate(mBestTextPosition[0], mBestTextPosition[1]);
} else {
canvas.rotate(mOrientation, mDynamicDetailLayout.getWidth() / 2,
mDynamicDetailLayout.getHeight() / 2);
}
mDynamicDetailLayout.draw(canvas);
canvas.restore();
}
}
super.dispatchDraw(canvas);
}
/**
* Calculates the best place to position text
*
* @param canvasW width of the screen
* @param canvasH height of the screen
* @return
*/
private float[] getBestTextPosition(int canvasW, int canvasH) {
float spaceTop = voidedArea.top;
float spaceBottom = canvasH - voidedArea.bottom - 64 * metricScale; //64dip considers the OK button
return new float[]{24 * metricScale, spaceTop > spaceBottom ? 128
* metricScale : 24 * metricScale + voidedArea.bottom, canvasW - 48 * metricScale};
}
/**
* Creates a {@link Rect} which represents the area the showcase covers.
* Used to calculate where best to place the text.
*
* @return true if voidedArea has changed, false otherwise.
*/
private boolean makeVoidedRect() {
// This if statement saves resources by not recalculating voidedArea
// if the X & Y coordinates haven't changed
if (voidedArea == null || (showcaseX != legacyShowcaseX || showcaseY != legacyShowcaseY)) {
int cx = (int) showcaseX, cy = (int) showcaseY;
int dw = showcase.getIntrinsicWidth();
int dh = showcase.getIntrinsicHeight();
voidedArea = new Rect(cx - dw / 2, cy - dh / 2, cx + dw / 2, cy + dh / 2);
legacyShowcaseX = showcaseX;
legacyShowcaseY = showcaseY;
return true;
}
return false;
}
public void animateGesture(float offsetStartX, float offsetStartY,
float offsetEndX, float offsetEndY) {
mHandy = ((LayoutInflater) getContext().getSystemService(
Context.LAYOUT_INFLATER_SERVICE)).inflate(R.layout.handy, null);
addView(mHandy);
mHandy.setRotation(mOrientation);
moveHand(offsetStartX, offsetStartY, offsetEndX, offsetEndY, new AnimationEndListener() {
@Override
public void onAnimationEnd() {
removeView(mHandy);
}
});
}
private void moveHand(float offsetStartX, float offsetStartY, float offsetEndX,
float offsetEndY, AnimationEndListener listener) {
AnimationUtils.createMovementAnimation(mHandy, showcaseX, showcaseY,
offsetStartX, offsetStartY,
offsetEndX, offsetEndY,
listener).start();
}
@Override
public void onClick(View view) {
// If the type is set to one-shot, store that it has shot
if (mOptions.shotType == TYPE_ONE_SHOT) {
SharedPreferences internal = getContext().getSharedPreferences(
PREFS_SHOWCASE_INTERNAL, Context.MODE_PRIVATE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
internal.edit().putBoolean(
"hasShot" + getConfigOptions().showcaseId, true).apply();
} else {
internal.edit().putBoolean(
"hasShot" + getConfigOptions().showcaseId, true).commit();
}
}
hide();
}
public void hide() {
if (mEventListener != null) {
mEventListener.onShowcaseViewHide(this);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
fadeOutShowcase();
} else {
setVisibility(View.GONE);
}
}
private void fadeOutShowcase() {
AnimationUtils.createFadeOutAnimation(this, new AnimationEndListener() {
@Override
public void onAnimationEnd() {
setVisibility(View.GONE);
}
}).start();
}
public void show() {
if (mEventListener != null) {
mEventListener.onShowcaseViewShow(this);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
fadeInShowcase();
} else {
setVisibility(View.VISIBLE);
}
}
private void fadeInShowcase() {
AnimationUtils.createFadeInAnimation(this, new AnimationStartListener() {
@Override
public void onAnimationStart() {
setVisibility(View.VISIBLE);
}
}).start();
}
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
float xDelta = Math.abs(motionEvent.getRawX() - showcaseX);
float yDelta = Math.abs(motionEvent.getRawY() - showcaseY);
double distanceFromFocus = Math.sqrt(Math.pow(xDelta, 2) + Math.pow(yDelta, 2));
if (mOptions.hideOnClickOutside && distanceFromFocus > showcaseRadius) {
this.hide();
return true;
}
return mOptions.block && distanceFromFocus > showcaseRadius;
}
public void setShowcaseIndicatorScale(float scaleMultiplier) {
this.scaleMultiplier = scaleMultiplier;
}
public ShowcaseView setTextColors(int titleTextColor, int detailTextColor) {
this.titleTextColor = titleTextColor;
this.detailTextColor = detailTextColor;
if (mPaintTitle != null) {
mPaintTitle.setColor(titleTextColor);
}
if (mPaintDetail != null) {
mPaintDetail.setColor(detailTextColor);
}
invalidate();
return this;
}
public void setText(int titleTextResId, int subTextResId) {
String titleText = getContext().getResources().getString(titleTextResId);
String subText = getContext().getResources().getString(subTextResId);
setText(titleText, subText);
}
public void setText(String titleText, String subText) {
mTitleText = titleText;
mSubText = subText;
mAlteredText = true;
invalidate();
}
/**
* Get the ghostly gesture hand for custom gestures
*
* @return a View representing the ghostly hand
*/
public View getHand() {
final View mHandy = ((LayoutInflater) getContext().getSystemService(
Context.LAYOUT_INFLATER_SERVICE)).inflate(R.layout.handy, null);
addView(mHandy);
AnimationUtils.hide(mHandy);
return mHandy;
}
/**
* Point to a specific view
*
* @param view The {@link View} to Showcase
*/
public void pointTo(View view) {
float x = AnimationUtils.getX(view) + view.getWidth() / 2;
float y = AnimationUtils.getY(view) + view.getHeight() / 2;
pointTo(x, y);
}
/**
* Point to a specific point on the screen
*
* @param x X-coordinate to point to
* @param y Y-coordinate to point to
*/
public void pointTo(float x, float y) {
AnimationUtils.createMovementAnimation(mHandy, x, y).start();
}
private ConfigOptions getConfigOptions() {
// Make sure that this method never returns null
if (mOptions == null) return mOptions = new ConfigOptions();
return mOptions;
}
private void setConfigOptions(ConfigOptions options) {
mOptions = options;
}
public interface OnShowcaseEventListener {
public void onShowcaseViewHide(ShowcaseView showcaseView);
public void onShowcaseViewShow(ShowcaseView showcaseView);
}
public static class ConfigOptions {
public boolean block = true, noButton = false;
public int showcaseId = 0;
public int shotType = TYPE_NO_LIMIT;
public int insert = INSERT_TO_DECOR;
public boolean hideOnClickOutside = false;
}
}
================================================
FILE: src/org/cyanogenmod/focal/ui/showcase/ShowcaseViewBuilder.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
* Copyright (C) 2012 Alex Curran
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.ui.showcase;
import android.app.Activity;
import android.view.View;
public class ShowcaseViewBuilder {
private final ShowcaseView showcaseView;
public ShowcaseViewBuilder(Activity activity) {
this.showcaseView = new ShowcaseView(activity);
}
public ShowcaseViewBuilder(ShowcaseView showcaseView) {
this.showcaseView = showcaseView;
}
public ShowcaseViewBuilder(Activity activity, int showcaseLayoutViewId) {
this.showcaseView = (ShowcaseView) activity.getLayoutInflater()
.inflate(showcaseLayoutViewId, null);
}
public ShowcaseViewBuilder setShowcaseView(View view) {
showcaseView.setShowcaseView(view);
return this;
}
public ShowcaseViewBuilder setShowcasePosition(float x, float y) {
showcaseView.setShowcasePosition(x, y);
return this;
}
public ShowcaseViewBuilder setShowcaseItem(int itemType,
int actionItemId, Activity activity) {
showcaseView.setShowcaseItem(itemType, actionItemId, activity);
return this;
}
public ShowcaseViewBuilder setShowcaseIndicatorScale(float scale) {
showcaseView.setShowcaseIndicatorScale(scale);
return this;
}
public ShowcaseViewBuilder overrideButtonClick(View.OnClickListener listener) {
showcaseView.overrideButtonClick(listener);
return this;
}
public ShowcaseViewBuilder animateGesture(float offsetStartX, float offsetStartY,
float offsetEndX, float offsetEndY) {
showcaseView.animateGesture(offsetStartX, offsetStartY, offsetEndX, offsetEndY);
return this;
}
public ShowcaseViewBuilder setTextColors(int titleTextColor, int detailTextColor) {
showcaseView.setTextColors(titleTextColor, detailTextColor);
return this;
}
public ShowcaseViewBuilder setText(String titleText, String subText) {
showcaseView.setText(titleText, subText);
return this;
}
public ShowcaseViewBuilder setText(int titleText, int subText) {
showcaseView.setText(titleText, subText);
return this;
}
public ShowcaseViewBuilder pointTo(View view) {
showcaseView.pointTo(view);
return this;
}
public ShowcaseViewBuilder pointTo(float x, float y) {
showcaseView.pointTo(x, y);
return this;
}
public ShowcaseView build() {
return showcaseView;
}
}
================================================
FILE: src/org/cyanogenmod/focal/ui/showcase/ShowcaseViews.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
* Copyright (C) 2012 Alex Curran
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.ui.showcase;
import android.app.Activity;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;
public class ShowcaseViews {
private final List views = new ArrayList();
private final Activity activity;
private final int showcaseTemplateId;
private OnShowcaseAcknowledged showcaseAcknowledgedListener = new OnShowcaseAcknowledged() {
@Override
public void onShowCaseAcknowledged(ShowcaseView showcaseView) {
//DEFAULT LISTENER - DOESN'T DO ANYTHING!
}
};
public interface OnShowcaseAcknowledged {
void onShowCaseAcknowledged(ShowcaseView showcaseView);
}
public ShowcaseViews(Activity activity, int showcaseTemplateLayout) {
this.activity = activity;
this.showcaseTemplateId = showcaseTemplateLayout;
}
public ShowcaseViews(Activity activity, int showcaseTemplateLayout, OnShowcaseAcknowledged acknowledgedListener) {
this(activity, showcaseTemplateLayout);
this.showcaseAcknowledgedListener = acknowledgedListener;
}
public void addView(ItemViewProperties properties) {
ShowcaseView showcaseView = new ShowcaseViewBuilder(activity, showcaseTemplateId).setShowcaseItem(properties.itemType, properties.id, activity)
.setText(properties.titleResId, properties.messageResId)
.setShowcaseIndicatorScale(properties.scale)
.build();
showcaseView.overrideButtonClick(createShowcaseViewDismissListener(showcaseView));
views.add(showcaseView);
}
private View.OnClickListener createShowcaseViewDismissListener(final ShowcaseView showcaseView) {
return new View.OnClickListener() {
@Override
public void onClick(View v) {
showcaseView.hide();
if (views.isEmpty()) {
showcaseAcknowledgedListener.onShowCaseAcknowledged(showcaseView);
} else {
show();
}
}
};
}
public void show() {
if (views.isEmpty()) {
return;
}
final ShowcaseView view = views.get(0);
((ViewGroup) activity.getWindow().getDecorView()).addView(view);
views.remove(0);
}
public boolean hasViews() {
return !views.isEmpty();
}
public static class ItemViewProperties {
public static final int ID_SPINNER = 0;
public static final int ID_TITLE = 1;
public static final int ID_OVERFLOW = 2;
private static final float DEFAULT_SCALE = 1f;
protected final int titleResId;
protected final int messageResId;
protected final int id;
protected final int itemType;
protected final float scale;
public ItemViewProperties(int id, int titleResId, int messageResId, int itemType) {
this(id, titleResId, messageResId, itemType, DEFAULT_SCALE);
}
public ItemViewProperties(int id, int titleResId, int messageResId, int itemType, float scale) {
this.id = id;
this.titleResId = titleResId;
this.messageResId = messageResId;
this.itemType = itemType;
this.scale = scale;
}
}
}
================================================
FILE: src/org/cyanogenmod/focal/widgets/AutoExposureWidget.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.widgets;
import android.content.Context;
import android.hardware.Camera;
import org.cyanogenmod.focal.CameraManager;
import fr.xplod.focal.R;
/**
* Auto Exposure Widget, manages the auto-exposure measurement method
*/
public class AutoExposureWidget extends SimpleToggleWidget {
private static final String KEY_AUTOEXPOSURE = "auto-exposure";
public AutoExposureWidget(CameraManager cam, Context context) {
super(cam, context, KEY_AUTOEXPOSURE, R.drawable.ic_widget_autoexposure);
inflateFromXml(R.array.widget_autoexposure_values, R.array.widget_autoexposure_icons,
R.array.widget_autoexposure_hints);
getToggleButton().setHintText(R.string.widget_autoexposure);
restoreValueFromStorage(KEY_AUTOEXPOSURE);
}
@Override
public boolean isSupported(Camera.Parameters params) {
return super.isSupported(params) && mCamManager.isExposureAreaSupported();
}
}
================================================
FILE: src/org/cyanogenmod/focal/widgets/BurstModeWidget.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.widgets;
import android.content.res.Resources;
import android.hardware.Camera;
import android.view.View;
import org.cyanogenmod.focal.CameraActivity;
import fr.xplod.focal.R;
import org.cyanogenmod.focal.feats.BurstCapture;
/**
* Burst-shooting mode widget
*/
public class BurstModeWidget extends WidgetBase implements View.OnClickListener {
private WidgetOptionButton mBtnOff;
private WidgetOptionButton mBtn5;
private WidgetOptionButton mBtn10;
private WidgetOptionButton mBtn15;
private WidgetOptionButton mBtnInf;
private WidgetOptionButton mPreviousMode;
private CameraActivity mCameraActivity;
private BurstCapture mTransformer;
private final static String DRAWABLE_TAG = "nemesis-burst-mode";
public BurstModeWidget(CameraActivity activity) {
super(activity.getCamManager(), activity, R.drawable.ic_widget_burst);
mCameraActivity = activity;
// Create options
// XXX: Move that into an XML
mBtnOff = new WidgetOptionButton(R.drawable.ic_widget_burst_off, activity);
mBtn5 = new WidgetOptionButton(R.drawable.ic_widget_burst_5, activity);
mBtn10 = new WidgetOptionButton(R.drawable.ic_widget_burst_10, activity);
mBtn15 = new WidgetOptionButton(R.drawable.ic_widget_burst_15, activity);
mBtnInf = new WidgetOptionButton(R.drawable.ic_widget_burst_inf, activity);
getToggleButton().setHintText(R.string.widget_burstmode);
final Resources res = getWidget().getResources();
mBtn5.setHintText(String.format(res.getString(R.string.widget_burstmode_count_shots), 5));
mBtn10.setHintText(String.format(res.getString(R.string.widget_burstmode_count_shots), 10));
mBtn15.setHintText(String.format(res.getString(R.string.widget_burstmode_count_shots), 15));
mBtnOff.setHintText(R.string.widget_burstmode_off);
mBtnInf.setHintText(R.string.widget_burstmode_infinite);
addViewToContainer(mBtnOff);
addViewToContainer(mBtn5);
addViewToContainer(mBtn10);
addViewToContainer(mBtn15);
addViewToContainer(mBtnInf);
mBtnOff.setOnClickListener(this);
mBtn5.setOnClickListener(this);
mBtn10.setOnClickListener(this);
mBtn15.setOnClickListener(this);
mBtnInf.setOnClickListener(this);
mPreviousMode = mBtnOff;
mPreviousMode.activeImage(DRAWABLE_TAG + "=off");
mTransformer = new BurstCapture(activity);
}
@Override
public boolean isSupported(Camera.Parameters params) {
// Burst mode is supported by everything. If we are in photo mode that is.
if (CameraActivity.getCameraMode() == CameraActivity.CAMERA_MODE_PHOTO) {
return true;
} else {
return false;
}
}
@Override
public void onClick(View view) {
mPreviousMode.resetImage();
if (view == mBtnOff) {
// Disable the transformer
mCameraActivity.setCaptureTransformer(null);
mBtnOff.activeImage(DRAWABLE_TAG + "=off");
mPreviousMode = mBtnOff;
} else if (view == mBtn5) {
mTransformer.setBurstCount(5);
mCameraActivity.setCaptureTransformer(mTransformer);
mBtn5.activeImage(DRAWABLE_TAG + "=5");
mPreviousMode = mBtn5;
} else if (view == mBtn10) {
mTransformer.setBurstCount(10);
mCameraActivity.setCaptureTransformer(mTransformer);
mBtn10.activeImage(DRAWABLE_TAG + "=10");
mPreviousMode = mBtn10;
} else if (view == mBtn15) {
mTransformer.setBurstCount(15);
mCameraActivity.setCaptureTransformer(mTransformer);
mBtn15.activeImage(DRAWABLE_TAG + "=15");
mPreviousMode = mBtn15;
} else if (view == mBtnInf) {
// Infinite burst count
mTransformer.setBurstCount(0);
mCameraActivity.setCaptureTransformer(mTransformer);
mBtnInf.activeImage(DRAWABLE_TAG + "=inf");
mPreviousMode = mBtnInf;
}
}
}
================================================
FILE: src/org/cyanogenmod/focal/widgets/EffectWidget.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.widgets;
import android.content.Context;
import android.os.Build;
import android.util.Log;
import org.cyanogenmod.focal.CameraManager;
import fr.xplod.focal.R;
public class EffectWidget extends SimpleToggleWidget {
private static final String KEY_EFFECT = "effect";
public EffectWidget(CameraManager cam, Context context) {
super(cam, context, KEY_EFFECT, R.drawable.ic_widget_effect);
inflateFromXml(R.array.widget_effects_values, R.array.widget_effects_icons,
R.array.widget_effects_hints);
getToggleButton().setHintText(R.string.widget_effect);
restoreValueFromStorage(KEY_EFFECT);
}
@Override
public boolean filterDeviceSpecific(String value) {
if (Build.DEVICE.equals("mako")) {
if (value.equals("whiteboard") || value.equals("blackboard")) {
// Hello Google, why do you report whiteboard/blackboard supported (in -values),
// when they are not? :(
return false;
}
}
return true;
}
}
================================================
FILE: src/org/cyanogenmod/focal/widgets/EnhancementsWidget.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.widgets;
import android.content.Context;
import android.hardware.Camera;
import android.util.Log;
import android.view.View;
import android.widget.SeekBar;
import org.cyanogenmod.focal.CameraManager;
import fr.xplod.focal.R;
import org.cyanogenmod.focal.SettingsStorage;
import org.cyanogenmod.focal.ui.CenteredSeekBar;
/**
* Contrast/sharpness/saturation setting widget (Qualcomm)
*/
public class EnhancementsWidget extends WidgetBase {
private static final String KEY_CONTRAST_PARAMETER = "contrast";
private static final String KEY_MAX_CONTRAST_PARAMETER = "max-contrast";
private static final String KEY_SHARPNESS_PARAMETER = "sharpness";
private static final String KEY_MAX_SHARPNESS_PARAMETER = "max-sharpness";
private static final String KEY_SATURATION_PARAMETER = "saturation";
private static final String KEY_MAX_SATURATION_PARAMETER = "max-saturation";
private static final int ROW_CONTRAST = 0;
private static final int ROW_SHARPNESS = 1;
private static final int ROW_SATURATION = 2;
private static final int ROW_COUNT = 3;
private WidgetOptionSeekBar[] mSeekBar;
private WidgetOptionLabel[] mValueLabel;
private class SeekBarListener implements SeekBar.OnSeekBarChangeListener {
private String mKey;
public SeekBarListener(String key) {
mKey = key;
}
@Override
public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
setValue(mKey, i);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
}
public EnhancementsWidget(CameraManager cam, Context context) {
super(cam, context, R.drawable.ic_widget_contrast);
// Add views in the widget
mSeekBar = new WidgetOptionSeekBar[ROW_COUNT];
mValueLabel = new WidgetOptionLabel[ROW_COUNT];
for (int i = 0; i < ROW_COUNT; i++) {
int max = 0, val = 0;
switch (i) {
case ROW_CONTRAST:
max = getValue(KEY_MAX_CONTRAST_PARAMETER);
val = getValue(KEY_CONTRAST_PARAMETER);
break;
case ROW_SATURATION:
max = getValue(KEY_MAX_SATURATION_PARAMETER);
val = getValue(KEY_SATURATION_PARAMETER);
break;
case ROW_SHARPNESS:
max = getValue(KEY_MAX_SHARPNESS_PARAMETER);
val = getValue(KEY_SHARPNESS_PARAMETER);
break;
}
mSeekBar[i] = new WidgetOptionSeekBar(context);
mValueLabel[i] = new WidgetOptionLabel(context);
mSeekBar[i].setMax(max);
mSeekBar[i].setProgress(val);
setViewAt(i, 2, 1, 3, mSeekBar[i]);
setViewAt(i, 1, 1, 1, mValueLabel[i]);
if (i == ROW_CONTRAST) {
setViewAt(i, 0, 1, 1, new WidgetOptionImage(
R.drawable.ic_widget_contrast, context));
} else if (i == ROW_SATURATION) {
setViewAt(i, 0, 1, 1, new WidgetOptionImage(
R.drawable.ic_widget_saturation, context));
} else if (i == ROW_SHARPNESS) {
setViewAt(i, 0, 1, 1, new WidgetOptionImage(
R.drawable.ic_widget_sharpness, context));
}
}
mSeekBar[ROW_CONTRAST].setOnSeekBarChangeListener(
new SeekBarListener(KEY_CONTRAST_PARAMETER));
mSeekBar[ROW_SHARPNESS].setOnSeekBarChangeListener(
new SeekBarListener(KEY_SHARPNESS_PARAMETER));
mSeekBar[ROW_SATURATION].setOnSeekBarChangeListener(
new SeekBarListener(KEY_SATURATION_PARAMETER));
mValueLabel[ROW_CONTRAST].setText(restoreValueFromStorage(KEY_CONTRAST_PARAMETER));
mValueLabel[ROW_SHARPNESS].setText(restoreValueFromStorage(KEY_SHARPNESS_PARAMETER));
mValueLabel[ROW_SATURATION].setText(restoreValueFromStorage(KEY_SATURATION_PARAMETER));
getToggleButton().setHintText(R.string.widget_enhancements);
}
@Override
public boolean isSupported(Camera.Parameters params) {
if (params.get(KEY_CONTRAST_PARAMETER) != null
|| params.get(KEY_SATURATION_PARAMETER) != null
|| params.get(KEY_SHARPNESS_PARAMETER) != null) {
return true;
} else {
return false;
}
}
public int getValue(String key) {
Camera.Parameters params = mCamManager.getParameters();
String value = params.get(key);
if (value != null) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
Log.e(TAG, value + " is not a valid number, returning 0");
return 0;
}
} else {
return 0;
}
}
public void setValue(String key, int value) {
String valueStr = Integer.toString(value);
mCamManager.setParameterAsync(key, valueStr);
if (key.equals(KEY_CONTRAST_PARAMETER)) {
mValueLabel[ROW_CONTRAST].setText(valueStr);
SettingsStorage.storeCameraSetting(getWidget().getContext(),
mCamManager.getCurrentFacing(),KEY_CONTRAST_PARAMETER, valueStr);
} else if (key.equals(KEY_SHARPNESS_PARAMETER)) {
mValueLabel[ROW_SHARPNESS].setText(valueStr);
SettingsStorage.storeCameraSetting(getWidget().getContext(),
mCamManager.getCurrentFacing(), KEY_SHARPNESS_PARAMETER, valueStr);
} else if (key.equals(KEY_SATURATION_PARAMETER)) {
mValueLabel[ROW_SATURATION].setText(valueStr);
SettingsStorage.storeCameraSetting(getWidget().getContext(),
mCamManager.getCurrentFacing(), KEY_SATURATION_PARAMETER, valueStr);
}
}
}
================================================
FILE: src/org/cyanogenmod/focal/widgets/ExposureCompensationWidget.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.widgets;
import android.content.Context;
import android.hardware.Camera;
import org.cyanogenmod.focal.CameraManager;
import fr.xplod.focal.R;
import org.cyanogenmod.focal.SettingsStorage;
import org.cyanogenmod.focal.ui.CenteredSeekBar;
/**
* Exposure compensation setup widget
*/
public class ExposureCompensationWidget extends WidgetBase implements
CenteredSeekBar.OnCenteredSeekBarChangeListener {
private static final String KEY_PARAMETER = "exposure-compensation";
private static final String KEY_MAX_PARAMETER = "max-exposure-compensation";
private static final String KEY_MIN_PARAMETER = "min-exposure-compensation";
private WidgetOptionCenteredSeekBar mSeekBar;
private WidgetOptionLabel mValueLabel;
public ExposureCompensationWidget(CameraManager cam, Context context) {
super(cam, context, R.drawable.ic_widget_exposure);
// Add views in the widget
mSeekBar = new WidgetOptionCenteredSeekBar(getMinExposureValue(),
getMaxExposureValue(), context);
mSeekBar.setNotifyWhileDragging(true);
mValueLabel = new WidgetOptionLabel(context);
addViewToContainer(mSeekBar);
addViewToContainer(mValueLabel);
mValueLabel.setText(restoreValueFromStorage(KEY_PARAMETER));
mSeekBar.setSelectedMinValue(Integer.parseInt(restoreValueFromStorage(KEY_PARAMETER)));
mSeekBar.setOnCenteredSeekBarChangeListener(this);
getToggleButton().setHintText(R.string.widget_exposure_compensation);
}
@Override
public boolean isSupported(Camera.Parameters params) {
return params != null && params.get(KEY_PARAMETER) != null;
}
public int getExposureValue() {
try {
return Integer.parseInt(mCamManager.getParameters().get(KEY_PARAMETER));
} catch (Exception e) {
return 0;
}
}
public int getMinExposureValue() {
try {
return Integer.parseInt(mCamManager.getParameters().get(KEY_MIN_PARAMETER));
} catch (Exception e) {
return 0;
}
}
public int getMaxExposureValue() {
try {
return Integer.parseInt(mCamManager.getParameters().get(KEY_MAX_PARAMETER));
} catch (Exception e) {
return 0;
}
}
public void setExposureValue(int value) {
if (getExposureValue() == value) return;
String valueStr = Integer.toString(value);
mCamManager.setParameterAsync(KEY_PARAMETER, valueStr);
SettingsStorage.storeCameraSetting(mWidget.getContext(), mCamManager.getCurrentFacing(),
KEY_PARAMETER, valueStr);
mValueLabel.setText(valueStr);
}
@Override
public void OnCenteredSeekBarValueChanged(CenteredSeekBar bar, Integer minValue) {
setExposureValue(minValue);
}
}
================================================
FILE: src/org/cyanogenmod/focal/widgets/FlashWidget.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.widgets;
import android.content.Context;
import org.cyanogenmod.focal.CameraManager;
import fr.xplod.focal.R;
/**
* Flash Widget, manages the flash settings
*/
public class FlashWidget extends SimpleToggleWidget {
private static final String KEY_REDEYE_REDUCTION = "redeye-reduction";
private static final String KEY_FLASH_MODE = "flash-mode";
public FlashWidget(CameraManager cam, Context context) {
super(cam, context, KEY_FLASH_MODE, R.drawable.ic_widget_flash_on);
inflateFromXml(R.array.widget_flash_values, R.array.widget_flash_icons,
R.array.widget_flash_hints);
getToggleButton().setHintText(R.string.widget_flash);
restoreValueFromStorage(KEY_FLASH_MODE);
}
/**
* When the flash is enabled, try to enable red-eye reduction (qualcomm)
*
* @param value The value set to the key
*/
@Override
public void onValueSet(String value) {
if (value.equals("on") || value.equals("auto")) {
if (mCamManager.getParameters().get(KEY_REDEYE_REDUCTION) != null) {
mCamManager.setParameterAsync(KEY_REDEYE_REDUCTION, "enable");
}
} else {
if (mCamManager.getParameters().get(KEY_REDEYE_REDUCTION) != null) {
mCamManager.setParameterAsync(KEY_REDEYE_REDUCTION, "disable");
}
}
}
}
================================================
FILE: src/org/cyanogenmod/focal/widgets/HdrWidget.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.widgets;
import android.content.Context;
import android.hardware.Camera;
import org.cyanogenmod.focal.CameraManager;
import fr.xplod.focal.R;
import java.util.List;
public class HdrWidget extends SimpleToggleWidget {
private final static String KEY_PARAMETER = "ae-bracket-hdr";
public HdrWidget(CameraManager cam, Context context) {
super(cam, context, "ae-bracket-hdr", R.drawable.ic_widget_hdr);
getToggleButton().setHintText(R.string.widget_hdr);
// Reminder: AOSP's HDR mode is scene-mode, so we did put that in scene-mode
// Here, it's for qualcomm's ae-bracket-hdr param. We filter out scene-mode
// HDR in priority though, for devices like the Nexus 4 which reports
// ae-bracket-hdr, but doesn't use it.
Camera.Parameters params = cam.getParameters();
if (params == null) {
return;
}
List sceneModes = params.getSupportedSceneModes();
if (sceneModes != null && !sceneModes.contains("hdr")) {
addValue("Off", R.drawable.ic_widget_hdr_off, context.getString(R.string.disabled));
addValue("HDR", R.drawable.ic_widget_hdr_on, context.getString(R.string.enabled));
addValue("AE-Bracket", R.drawable.ic_widget_hdr_aebracket,
context.getString(R.string.widget_hdr_aebracket));
restoreValueFromStorage(KEY_PARAMETER);
}
}
}
================================================
FILE: src/org/cyanogenmod/focal/widgets/IsoWidget.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.widgets;
import android.content.Context;
import org.cyanogenmod.focal.CameraManager;
import fr.xplod.focal.R;
/**
* ISO widget, sets ISO sensitivity value
*/
public class IsoWidget extends SimpleToggleWidget {
private static final String KEY_ISO = "iso";
public IsoWidget(CameraManager cam, Context context) {
super(cam, context, KEY_ISO, R.drawable.ic_widget_iso);
inflateFromXml(R.array.widget_iso_values, R.array.widget_iso_icons,
R.array.widget_iso_hints);
getToggleButton().setHintText(R.string.widget_iso);
restoreValueFromStorage(KEY_ISO);
}
}
================================================
FILE: src/org/cyanogenmod/focal/widgets/SceneModeWidget.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.widgets;
import android.content.Context;
import org.cyanogenmod.focal.CameraManager;
import fr.xplod.focal.R;
public class SceneModeWidget extends SimpleToggleWidget {
private static final String KEY_SCENEMODE = "scene-mode";
public SceneModeWidget(CameraManager cam, Context context) {
super(cam, context, KEY_SCENEMODE, R.drawable.ic_widget_scenemode);
inflateFromXml(R.array.widget_scenemode_values, R.array.widget_scenemode_icons,
R.array.widget_scenemode_hints);
getToggleButton().setHintText(R.string.widget_scenemode);
restoreValueFromStorage(KEY_SCENEMODE);
}
}
================================================
FILE: src/org/cyanogenmod/focal/widgets/SettingsWidget.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.widgets;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.hardware.Camera;
import android.media.CamcorderProfile;
import android.view.View;
import android.view.ViewGroup;
import android.widget.NumberPicker;
import org.cyanogenmod.focal.CameraActivity;
import org.cyanogenmod.focal.CameraCapabilities;
import org.cyanogenmod.focal.CameraManager;
import fr.xplod.focal.R;
import org.cyanogenmod.focal.SettingsStorage;
import org.cyanogenmod.focal.SnapshotManager;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* Overflow settings widget
*/
public class SettingsWidget extends WidgetBase {
private static final String TAG = "SettingsWidget";
private static final String DRAWABLE_KEY_EXPO_RING = "_Nemesis_ExposureRing=true";
private static final String DRAWABLE_KEY_AUTO_ENHANCE = "_Nemesis_AutoEnhance=true";
private static final String DRAWABLE_KEY_RULE_OF_THIRDS = "_Nemesis_RuleOfThirds=true";
private static final String KEY_SHOW_EXPOSURE_RING = "ShowExposureRing";
private static final String KEY_ENABLE_AUTO_ENHANCE = "AutoEnhanceEnabled";
private static final String KEY_ENABLE_RULE_OF_THIRDS = "RuleOfThirdsEnabled";
private WidgetOptionButton mResolutionButton;
private WidgetOptionButton mToggleExposureRing;
private WidgetOptionButton mToggleAutoEnhancer;
private WidgetOptionButton mToggleWidgetsButton;
private WidgetOptionButton mToggleRuleOfThirds;
private CameraActivity mContext;
private CameraCapabilities mCapabilities;
private List mResolutionsName;
private List mVideoResolutions;
private List mResolutions;
private AlertDialog mResolutionDialog;
private AlertDialog mWidgetsDialog;
private NumberPicker mNumberPicker;
private int mInitialOrientation = -1;
private int mOrientation;
private final Comparator super Camera.Size> mResolutionsSorter =
new Comparator() {
@Override
public int compare(Camera.Size size, Camera.Size size2) {
if (size.width * size.height > size2.width * size2.height) {
return -1;
} else {
return 1;
}
}
};
private View.OnClickListener mExpoRingClickListener = new View.OnClickListener() {
@Override
public void onClick(View view) {
mContext.setExposureRingVisible(!mContext.isExposureRingVisible());
if (mContext.isExposureRingVisible()) {
mToggleExposureRing.activeImage(DRAWABLE_KEY_EXPO_RING);
} else {
mToggleExposureRing.resetImage();
}
SettingsStorage.storeAppSetting(mContext, KEY_SHOW_EXPOSURE_RING,
mContext.isExposureRingVisible() ? "1" : "0");
}
};
private View.OnClickListener mRuleOfThirdsClickListener = new View.OnClickListener() {
@Override
public void onClick(View view) {
View rot = mContext.findViewById(R.id.rule_of_thirds);
if (rot.getVisibility() == View.GONE) {
mToggleRuleOfThirds.activeImage(DRAWABLE_KEY_RULE_OF_THIRDS);
rot.setVisibility(View.VISIBLE);
} else {
mToggleRuleOfThirds.resetImage();
rot.setVisibility(View.GONE);
}
SettingsStorage.storeAppSetting(mContext, KEY_ENABLE_RULE_OF_THIRDS,
rot.getVisibility() == View.GONE ? "0" : "1");
}
};
private View.OnClickListener mAutoEnhanceClickListener = new View.OnClickListener() {
@Override
public void onClick(View view) {
SnapshotManager snapMan = mContext.getSnapManager();
snapMan.setAutoEnhance(!snapMan.getAutoEnhance());
if (snapMan.getAutoEnhance()) {
mToggleAutoEnhancer.activeImage(DRAWABLE_KEY_AUTO_ENHANCE);
} else {
mToggleAutoEnhancer.resetImage();
}
SettingsStorage.storeAppSetting(mContext, KEY_ENABLE_AUTO_ENHANCE,
snapMan.getAutoEnhance() ? "1" : "0");
}
};
private View.OnClickListener mResolutionClickListener = new View.OnClickListener() {
@Override
public void onClick(View view) {
mInitialOrientation = -1;
mNumberPicker = new NumberPicker(mContext);
String[] names = new String[mResolutionsName.size()];
mResolutionsName.toArray(names);
if (names.length > 0) {
mNumberPicker.setDisplayedValues(names);
mNumberPicker.setMinValue(0);
mNumberPicker.setMaxValue(names.length - 1);
} else {
mNumberPicker.setMinValue(0);
mNumberPicker.setMaxValue(0);
}
mNumberPicker.setWrapSelectorWheel(false);
mNumberPicker.setDescendantFocusability(NumberPicker.FOCUS_BLOCK_DESCENDANTS);
mNumberPicker.setFormatter(new NumberPicker.Formatter() {
@Override
public String format(int i) {
return mResolutionsName.get(i);
}
});
if (CameraActivity.getCameraMode() == CameraActivity.CAMERA_MODE_VIDEO){
// TODO set correct menu selection also for video
String actualSz = getActualProfileResolution();
if (mVideoResolutions != null) {
for (int i = 0; i < mVideoResolutions.size(); i++) {
if (mVideoResolutions.get(i).equals(actualSz)) {
mNumberPicker.setValue(i);
break;
}
}
}
} else {
Camera.Size actualSz = mCamManager.getParameters().getPictureSize();
if (mResolutions != null) {
for (int i = 0; i < mResolutions.size(); i++) {
if (mResolutions.get(i).equals(actualSz)) {
mNumberPicker.setValue(i);
break;
}
}
}
}
AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
builder.setView(mNumberPicker);
builder.setTitle(null);
builder.setCancelable(false);
builder.setPositiveButton(mContext.getString(R.string.ok),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
mInitialOrientation = -1;
if (CameraActivity.getCameraMode() == CameraActivity.CAMERA_MODE_PHOTO
|| CameraActivity.getCameraMode()
== CameraActivity.CAMERA_MODE_PICSPHERE) {
// Set picture size
Camera.Size size = mResolutions.get(mNumberPicker.getValue());
// TODO: only one method
mCamManager.setPictureSize(""+size.width+"x"+size.height);
if (CameraActivity.getCameraMode() == CameraActivity.CAMERA_MODE_PHOTO) {
SettingsStorage.storeCameraSetting(mContext, mCamManager
.getCurrentFacing(), "picture-size",
""+size.width+"x"+size.height);
} else {
SettingsStorage.storeCameraSetting(mContext,
mCamManager.getCurrentFacing(), "picsphere-picture-size",
""+size.width+"x"+size.height);
}
} else if (CameraActivity.getCameraMode() == CameraActivity.CAMERA_MODE_VIDEO) {
// Set video size
String size = mVideoResolutions.get(mNumberPicker.getValue());
applyVideoResolution(size);
String[] splat = size.split("x");
int width = Integer.parseInt(splat[0]);
int height = Integer.parseInt(splat[1]);
SettingsStorage.storeCameraSetting(mContext, mCamManager.getCurrentFacing(),
"video-size", ""+width+"x"+height);
}
}
});
mResolutionDialog = builder.create();
mResolutionDialog.show();
((ViewGroup)mNumberPicker.getParent().getParent().getParent())
.animate().rotation(mOrientation).setDuration(300).start();
}
};
public SettingsWidget(CameraActivity context, CameraCapabilities capabilities) {
super(context.getCamManager(), context, R.drawable.ic_widget_settings);
mContext = context;
mCapabilities = capabilities;
CameraManager cam = context.getCamManager();
getToggleButton().setHintText(R.string.widget_settings);
if (CameraActivity.getCameraMode() == CameraActivity.CAMERA_MODE_PHOTO
|| CameraActivity.getCameraMode() == CameraActivity.CAMERA_MODE_PICSPHERE) {
// Get the available photo size. Unlike AOSP app, we don't
// store manually each resolution in an XML, but we calculate it directly
// from the width and height of the picture size.
mResolutions = cam.getParameters().getSupportedPictureSizes();
mResolutionsName = new ArrayList();
DecimalFormat df = new DecimalFormat();
df.setMaximumFractionDigits(1);
df.setMinimumFractionDigits(0);
df.setDecimalSeparatorAlwaysShown(false);
Collections.sort(mResolutions, mResolutionsSorter);
for (Camera.Size size : mResolutions) {
float megapixels = size.width * size.height / 1000000.0f;
mResolutionsName.add(df.format(megapixels) +
"MP (" + size.width + "x" + size.height + ")");
}
// Restore picture size if we have any
String resolution = "";
if (CameraActivity.getCameraMode() == CameraActivity.CAMERA_MODE_PHOTO) {
resolution = SettingsStorage.getCameraSetting(context,
mCamManager.getCurrentFacing(), "picture-size", ""+mResolutions
.get(0).width+"x"+mResolutions.get(0).height);
} else {
resolution = SettingsStorage.getCameraSetting(context,
mCamManager.getCurrentFacing(), "picsphere-picture-size", "640x480");
}
mCamManager.setPictureSize(resolution);
} else if (CameraActivity.getCameraMode() == CameraActivity.CAMERA_MODE_VIDEO) {
mResolutions = cam.getParameters().getSupportedVideoSizes();
mResolutionsName = new ArrayList();
mVideoResolutions = new ArrayList();
if (mResolutions != null) {
// We support a fixed set of video resolutions (pretty much like AOSP)
// because we need to take the CamcorderProfile (media_profiles.xml), which
// has a fixed set of values...
for (Camera.Size size : mResolutions) {
if (size.width == 1920 && size.height == 1080
|| size.width == 1920 && size.height == 1088) {
mResolutionsName.add(mContext.getString(R.string.video_res_1080p));
mVideoResolutions.add("1920x1080");
} else if (size.width == 1280 && size.height == 720) {
mResolutionsName.add(mContext.getString(R.string.video_res_720p));
mVideoResolutions.add("1280x720");
} else if (size.width == 720 && size.height == 480) {
mResolutionsName.add(mContext.getString(R.string.video_res_480p));
mVideoResolutions.add("720x480");
} else if (size.width == 352 && size.height == 288) {
mResolutionsName.add(mContext.getString(R.string.video_res_mms));
mVideoResolutions.add("352x288");
}
}
} else if (mResolutions == null || mResolutions.size() == 0) {
// We detected no compatible video resolution! We add default ones.
mResolutionsName.add(mContext.getString(R.string.video_res_1080p));
mVideoResolutions.add("1920x1080");
mResolutionsName.add(mContext.getString(R.string.video_res_720p));
mVideoResolutions.add("1280x720");
mResolutionsName.add(mContext.getString(R.string.video_res_480p));
mVideoResolutions.add("720x480");
}
// TODO: Restore video size if we have any
String resolution = SettingsStorage.getCameraSetting(context,
mCamManager.getCurrentFacing(), "video-size", mVideoResolutions.get(0));
applyVideoResolution(resolution);
}
mResolutionButton = new WidgetOptionButton(
R.drawable.ic_widget_settings_resolution, context);
mResolutionButton.setOnClickListener(mResolutionClickListener);
mResolutionButton.setHintText(mContext.getString(R.string.widget_settings_picture_size));
addViewToContainer(mResolutionButton);
if (mCamManager.isExposureAreaSupported()
&& CameraActivity.getCameraMode() != CameraActivity.CAMERA_MODE_PICSPHERE) {
mToggleExposureRing = new WidgetOptionButton(
R.drawable.ic_widget_settings_exposurering, context);
mToggleExposureRing.setHintText(mContext.getString(
R.string.widget_settings_exposure_ring));
mToggleExposureRing.setOnClickListener(mExpoRingClickListener);
// Restore exposure ring state
if (SettingsStorage.getAppSetting(mContext, KEY_SHOW_EXPOSURE_RING, "0").equals("1")) {
mContext.setExposureRingVisible(true);
mToggleExposureRing.activeImage(DRAWABLE_KEY_EXPO_RING);
} else {
mContext.setExposureRingVisible(false);
}
addViewToContainer(mToggleExposureRing);
}
// Toggle auto enhancer
if (CameraActivity.getCameraMode() != CameraActivity.CAMERA_MODE_PICSPHERE) {
mToggleAutoEnhancer = new WidgetOptionButton(R.drawable.ic_enhancing, context);
mToggleAutoEnhancer.setOnClickListener(mAutoEnhanceClickListener);
mToggleAutoEnhancer.setHintText(mContext.getString(
R.string.widget_settings_autoenhance));
// Restore auto enhancer state
if (SettingsStorage.getAppSetting(mContext, KEY_ENABLE_AUTO_ENHANCE, "0").equals("1")) {
mContext.getSnapManager().setAutoEnhance(true);
mToggleAutoEnhancer.activeImage(DRAWABLE_KEY_AUTO_ENHANCE);
} else {
if (mContext.getSnapManager() != null) {
mContext.getSnapManager().setAutoEnhance(false);
}
}
addViewToContainer(mToggleAutoEnhancer);
// Toggle rule of thirds
mToggleRuleOfThirds = new WidgetOptionButton(
R.drawable.ic_widget_settings_rulethirds, context);
mToggleRuleOfThirds.setOnClickListener(mRuleOfThirdsClickListener);
mToggleRuleOfThirds.setHintText(mContext.getString(
R.string.widget_settings_ruleofthirds));
// Restore rule of thirds visibility state
if (SettingsStorage.getAppSetting(mContext,
KEY_ENABLE_RULE_OF_THIRDS, "0").equals("1")) {
mContext.findViewById(R.id.rule_of_thirds).setVisibility(View.VISIBLE);
mToggleRuleOfThirds.activeImage(KEY_ENABLE_RULE_OF_THIRDS);
} else {
mContext.findViewById(R.id.rule_of_thirds).setVisibility(View.GONE);
mToggleRuleOfThirds.resetImage();
}
addViewToContainer(mToggleRuleOfThirds);
}
// Choose widgets to appear
mToggleWidgetsButton = new WidgetOptionButton(
R.drawable.ic_widget_settings_widgets, context);
mToggleWidgetsButton.setHintText(mContext.getString(
R.string.widget_settings_choose_widgets_button));
mToggleWidgetsButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
openWidgetsToggleDialog();
}
});
addViewToContainer(mToggleWidgetsButton);
}
@Override
public boolean isSupported(Camera.Parameters params) {
int mode = CameraActivity.getCameraMode();
return (mode == CameraActivity.CAMERA_MODE_PHOTO || mode == CameraActivity.CAMERA_MODE_VIDEO
|| mode == CameraActivity.CAMERA_MODE_PICSPHERE);
}
@Override
public void notifyOrientationChanged(int orientation) {
if (mInitialOrientation == -1) mInitialOrientation = orientation;
if (mOrientation == orientation) return;
mOrientation = orientation;
if (mResolutionDialog != null) {
((ViewGroup) mNumberPicker.getParent().getParent().getParent().getParent())
.animate().rotation(orientation - mInitialOrientation).setDuration(300).start();
}
if (mWidgetsDialog != null) {
((ViewGroup) mWidgetsDialog.getListView().getParent()
.getParent().getParent().getParent()).animate().rotation(orientation
- mInitialOrientation).setDuration(300).start();
}
}
private void applyVideoResolution(String resolution) {
if (resolution.equals("1920x1080")) {
mContext.getSnapManager().setVideoProfile(
CamcorderProfile.get(CamcorderProfile.QUALITY_1080P));
} else if (resolution.equals("1280x720")) {
mContext.getSnapManager().setVideoProfile(
CamcorderProfile.get(CamcorderProfile.QUALITY_720P));
} else if (resolution.equals("720x480")) {
mContext.getSnapManager().setVideoProfile(
CamcorderProfile.get(CamcorderProfile.QUALITY_480P));
} else if (resolution.equals("352x288")) {
mContext.getSnapManager().setVideoProfile(
CamcorderProfile.get(CamcorderProfile.QUALITY_CIF));
}
}
private String getActualProfileResolution() {
CamcorderProfile actualProfile = mContext.getSnapManager().getVideoProfile();
return "" + actualProfile.videoFrameWidth + "x" + actualProfile.videoFrameHeight;
}
private void openWidgetsToggleDialog() {
final List widgets = mCapabilities.getWidgets();
List widgetsName = new ArrayList();
// Construct a list of widgets
for (WidgetBase widget : widgets) {
if (widget.getClass().getName().contains("SettingsWidget")) continue;
widgetsName.add(widget.getToggleButton().getHintText());
}
CharSequence[] items = widgetsName.toArray(new CharSequence[widgetsName.size()]);
AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
builder.setTitle(mContext.getString(R.string.widget_settings_choose_widgets));
builder.setMultiChoiceItems(items, null,
new DialogInterface.OnMultiChoiceClickListener() {
// indexSelected contains the index of item (of which checkbox checked)
@Override
public void onClick(DialogInterface dialog, int indexSelected,
boolean isChecked) {
widgets.get(indexSelected).setHidden(!isChecked);
}
})
// Set the action buttons
.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
// Store the widget visibility status in SharedPreferences, using the
// widget class name as key
for (WidgetBase widget : widgets) {
if (widget.getClass().getName().contains("SettingsWidget")) {
continue;
}
SettingsStorage.storeVisibilitySetting(mContext,
widget.getClass().getName(), !widget.isHidden());
}
}
})
.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
// Restore visibility status from storage
for (WidgetBase widget : widgets) {
if (widget.getClass().getName().contains("SettingsWidget")) {
continue;
}
widget.setHidden(!SettingsStorage.getVisibilitySetting(
mContext, widget.getClass().getName()));
}
}
});
mWidgetsDialog = builder.create();
mWidgetsDialog.show();
for (int i = 0; i < widgets.size(); i++) {
mWidgetsDialog.getListView().setItemChecked(i, !widgets.get(i).isHidden());
}
((ViewGroup)mWidgetsDialog.getListView().getParent().getParent().getParent())
.animate().rotation(mOrientation).setDuration(300).start();
}
}
================================================
FILE: src/org/cyanogenmod/focal/widgets/ShutterSpeedWidget.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.widgets;
import android.content.Context;
import android.hardware.Camera;
import android.view.View;
import org.cyanogenmod.focal.CameraManager;
import org.cyanogenmod.focal.SettingsStorage;
import fr.xplod.focal.R;
/**
* Shutter speed setup widget
*/
public class ShutterSpeedWidget extends WidgetBase {
// XXX We use shutter-speed here because the wrapper sets the ae-mode
// according to the value of shutter speed as well as ISO.
// What is done here (0-12, auto) is sony-specific anyway, so it
// makes sense to keep it this way
private static final String KEY_PARAMETER = "sony-shutter-speed";
private static final String KEY_MAX_PARAMETER = "sony-max-shutter-speed";
private static final String KEY_MIN_PARAMETER = "sony-min-shutter-speed";
private static final String KEY_AUTO_VALUE = "auto";
private WidgetOptionButton mMinusButton;
private WidgetOptionButton mPlusButton;
private WidgetOptionButton mAutoButton;
private WidgetOptionLabel mValueLabel;
private class MinusClickListener implements View.OnClickListener {
@Override
public void onClick(View view) {
if (getShutterSpeedRawValue().equals(KEY_AUTO_VALUE)) {
setShutterSpeedValue(getMaxShutterSpeedValue());
} else {
setShutterSpeedValue(Math.max(getShutterSpeedValue() - 1, getMinShutterSpeedValue()));
}
}
}
private class PlusClickListener implements View.OnClickListener {
@Override
public void onClick(View view) {
if (getShutterSpeedRawValue().equals(KEY_AUTO_VALUE)) {
setShutterSpeedValue(getMinShutterSpeedValue());
} else {
setShutterSpeedValue(Math.min(getShutterSpeedValue() + 1, getMaxShutterSpeedValue()));
}
}
}
private class AutoClickListener implements View.OnClickListener {
@Override
public void onClick(View view) {
setAutoShutterSpeed();
}
}
public ShutterSpeedWidget(CameraManager cam, Context context) {
super(cam, context, R.drawable.ic_widget_shutterspeed);
// Add views in the widget
mMinusButton = new WidgetOptionButton(R.drawable.ic_widget_timer_minus, context);
mPlusButton = new WidgetOptionButton(R.drawable.ic_widget_timer_plus, context);
mAutoButton = new WidgetOptionButton(R.drawable.ic_widget_iso_auto, context);
mValueLabel = new WidgetOptionLabel(context);
mMinusButton.setOnClickListener(new MinusClickListener());
mPlusButton.setOnClickListener(new PlusClickListener());
mAutoButton.setOnClickListener(new AutoClickListener());
addViewToContainer(mMinusButton);
addViewToContainer(mValueLabel);
addViewToContainer(mPlusButton);
addViewToContainer(mAutoButton);
mValueLabel.setText(getShutterSpeedDisplayValue(restoreValueFromStorage(KEY_PARAMETER)));
if ((restoreValueFromStorage(KEY_PARAMETER) != null) &&
(restoreValueFromStorage(KEY_PARAMETER).equals(KEY_AUTO_VALUE))) {
mAutoButton.activeImage(KEY_PARAMETER + ":" + KEY_AUTO_VALUE);
}
getToggleButton().setHintText(R.string.widget_shutter_speed);
}
@Override
public boolean isSupported(Camera.Parameters params) {
return params.get(KEY_PARAMETER) != null;
}
public int getShutterSpeedValue() {
int retVal = 0;
String val = mCamManager.getParameters().get(KEY_PARAMETER);
if (val.equals(KEY_AUTO_VALUE) ){
retVal = getMaxShutterSpeedValue() + 1;
}
else {
retVal = Integer.parseInt(mCamManager.getParameters().get(KEY_PARAMETER));
}
return retVal;
}
public String getShutterSpeedRawValue() {
String val = mCamManager.getParameters().get(KEY_PARAMETER);
return val;
}
public int getMinShutterSpeedValue() {
return Integer.parseInt(mCamManager.getParameters().get(KEY_MIN_PARAMETER));
}
public int getMaxShutterSpeedValue() {
return Integer.parseInt(mCamManager.getParameters().get(KEY_MAX_PARAMETER));
}
public void setShutterSpeedValue(int value) {
String valueStr = Integer.toString(value);
mCamManager.setParameterAsync(KEY_PARAMETER, valueStr);
SettingsStorage.storeCameraSetting(mWidget.getContext(), mCamManager.getCurrentFacing(),
KEY_PARAMETER, valueStr);
mValueLabel.setText(getShutterSpeedDisplayValue(valueStr));
mAutoButton.resetImage();
}
public void setAutoShutterSpeed() {
mCamManager.setParameterAsync(KEY_PARAMETER, KEY_AUTO_VALUE);
SettingsStorage.storeCameraSetting(mWidget.getContext(), mCamManager.getCurrentFacing(),
KEY_PARAMETER, "auto");
mValueLabel.setText(getShutterSpeedDisplayValue(KEY_AUTO_VALUE));
mAutoButton.activeImage(KEY_PARAMETER + ":" + KEY_AUTO_VALUE);
}
public String getShutterSpeedDisplayValue(String value) {
String[] values = mWidget.getContext().getResources().getStringArray(
R.array.widget_shutter_speed_display_values);
if (value == null || value.equals(KEY_AUTO_VALUE)) {
return values[0];
} else {
return values[Integer.parseInt(value) + 1];
}
}
}
================================================
FILE: src/org/cyanogenmod/focal/widgets/SimpleToggleWidget.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.widgets;
import android.content.Context;
import android.content.res.TypedArray;
import android.hardware.Camera;
import android.os.Build;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import org.cyanogenmod.focal.CameraActivity;
import org.cyanogenmod.focal.CameraManager;
import org.cyanogenmod.focal.SettingsStorage;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* Base for simple widgets simply changing a parameter
* (eg. flash=on/off/auto, iso=100/200/300/500/800) from
* a simple button.
*/
public class SimpleToggleWidget extends WidgetBase implements OnClickListener {
public final static String TAG = "SimpleToggleWidget";
private String mKey;
private boolean mVideoOnly;
private Map mButtonsValues;
private Context mContext;
private WidgetOptionButton mActiveButton;
public SimpleToggleWidget(CameraManager cam, Context context, int sidebarIcon) {
super(cam, context, sidebarIcon);
mButtonsValues = new HashMap();
mContext = context;
}
public SimpleToggleWidget(CameraManager cam, Context context, String key, int sidebarIcon) {
super(cam, context, sidebarIcon);
mButtonsValues = new HashMap();
mContext = context;
mKey = key;
}
/**
* Defines or redefines the key used for the settings
*
* @param key
*/
protected void setKey(String key) {
mKey = key;
}
/**
* Sets whether or not this setting applies only in video mode
*
* @param videoOnly
*/
protected void setVideoOnly(boolean videoOnly) {
mVideoOnly = videoOnly;
}
/**
* Add a value that can be toggled to the widget
* If the HAL reports a [key]-values array, it will check and filter
* the values against this array. Otherwise, all the values are added
* to the list.
*
* You might want to check for device-specific values that aren't
* reported in -values in the child class before doing addValue
*
* @param value The value that the key of this widget can take
* @param resId The icon that represents it
* @param hint The hint text that appears when long-pressing the button
*/
public void addValue(String value, int resId, String hint) {
Camera.Parameters params = mCamManager.getParameters();
if (params == null) return;
String values = params.get(mKey + "-values");
// If we don't have a -values provided, or if it contains the value, add it.
if ((values == null || Arrays.asList(values.split(",")).contains(value))
&& filterDeviceSpecific(value)) {
WidgetBase.WidgetOptionButton button =
new WidgetBase.WidgetOptionButton(resId, mContext);
button.setOnClickListener(this);
button.setHintText(hint);
mButtonsValues.put(button, value);
addViewToContainer(button);
String currentValue = params.get(mKey);
if (currentValue != null && currentValue.equals(value)) {
setButtonActivated(button, value);
}
} else {
Log.v(TAG, "Device doesn't support " + value + " for setting " + mKey);
}
}
/**
* Updates the currently selected button based on the current Camera parameters
*/
public void updateSelectedButton() {
String value = getCameraValue(mKey);
updateSelectedButton(value);
}
public void updateSelectedButton(String value) {
Set buttons = mButtonsValues.keySet();
for (WidgetOptionButton btn : buttons) {
if (mButtonsValues.get(btn).equals(value)) {
Log.v(TAG, "Set button activated for value " + value);
setButtonActivated(btn, value);
return;
}
}
setButtonActivated(null, value);
}
/**
* Restore the camera parameter from the stored settings, and update the selected
* button (method of WidgetBase overridden in SimpleToggleWidget)
* @param key
*/
@Override
public String restoreValueFromStorage(String key) {
String value = super.restoreValueFromStorage(key);
updateSelectedButton(value);
return value;
}
/**
* This method can be overridden by each widgets. If some keys doesn't
* have a "-values" array, we can filter eventual device-specific incompatibilities
* in this method.
*
* @param value The value tested for support
* @return true if the value is supported, false if it's not
*/
public boolean filterDeviceSpecific(String value) {
return true;
}
/**
* This method checks if the Key provided in the constructor
* is supported (as per {@link WidgetBase} docs), by checking
* if the key is in the Camera.Parameters array.
*/
@Override
public boolean isSupported(Camera.Parameters params) {
if (CameraActivity.getCameraMode() != CameraActivity.CAMERA_MODE_VIDEO && mVideoOnly) {
// This setting is only in video mode
return false;
}
if (mButtonsValues.isEmpty() || mButtonsValues.size() == 1) {
// There's only one setting, or it's empty, so not worth showing an icon/menu
return false;
}
if (params.get(mKey) != null) {
Log.v(TAG, "The device supports '" + mKey + "'");
String values = params.get(mKey + "-values");
if (values != null) {
Log.v(TAG, "... and has these possible values: " + values);
if (!values.contains(",")) {
// There is only one value, no need to show that button
return false;
}
} else {
Log.w(TAG, "... but we don't know the possible values for " + mKey);
}
return true;
} else {
Log.d(TAG, "The device doesn't support '" + mKey + "'");
return false;
}
}
@Override
public void onClick(View v) {
// We check what button was pressed, and enable it
WidgetBase.WidgetOptionButton button = (WidgetBase.WidgetOptionButton) v;
String value = mButtonsValues.get(button);
if (value != null) {
SettingsStorage.storeCameraSetting(mContext,
mCamManager.getCurrentFacing(), mKey, value);
mCamManager.setParameterAsync(mKey, value);
setButtonActivated(button, value);
} else {
Log.e(TAG, "Unknown value for this button!");
}
}
private void setButtonActivated(WidgetOptionButton button, String value) {
if (mActiveButton != null) {
mActiveButton.resetImage();
}
mActiveButton = button;
if (button != null) {
mActiveButton.activeImage(mKey + "=" + value);
}
}
/**
* Inflate the buttons and icons from the provided values, icons and hints
* arrays.
*
* @param valuesArray
* @param iconsArray
* @param hintsArray
*/
public void inflateFromXml(int valuesArray, int iconsArray, int hintsArray) {
String[] values = mContext.getResources().getStringArray(valuesArray);
String[] hints = mContext.getResources().getStringArray(hintsArray);
TypedArray icons = mContext.getResources().obtainTypedArray(iconsArray);
for (int i = 0; i < values.length; i++) {
addValue(values[i], icons.getResourceId(i, -1), hints[i]);
}
}
/**
* This method can be overridden by a child class, to do special actions
* when a value is set.
*
* @param value The value set to the key
*/
public void onValueSet(String value) {
}
/**
* This checks if the widget is enabled
* This is basically a hash (key, value pair)
* @return true if it enabled
*/
public static boolean isWidgetEnabled(Context context,
CameraManager mCamManager,
String selectedKey, String value){
String out = SettingsStorage.getCameraSetting(context,
mCamManager.getCurrentFacing(), selectedKey, "null");
return out.equals(value);
}
}
================================================
FILE: src/org/cyanogenmod/focal/widgets/SkinToneWidget.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.widgets;
import android.content.Context;
import android.hardware.Camera;
import android.view.View;
import org.cyanogenmod.focal.CameraManager;
import fr.xplod.focal.R;
import org.cyanogenmod.focal.SettingsStorage;
/**
* Skin Tone Enhancement widget (Qualcomm)
* Most info (values, etc) from
* https://codeaurora.org/cgit/quic/la/platform/packages/apps/Camera/commit/?id=9d48710275915ac0672cb7c1bc1b9aa14d70e8b0
*/
public class SkinToneWidget extends WidgetBase {
private static final String KEY_PARAMETER = "skinToneEnhancement";
private static final int MIN_SCE_FACTOR = -10;
private static final int MAX_SCE_FACTOR = +10;
private WidgetOptionButton mMinusButton;
private WidgetOptionButton mPlusButton;
private WidgetOptionLabel mValueLabel;
private class MinusClickListener implements View.OnClickListener {
@Override
public void onClick(View view) {
setToneValue(Math.max(getToneValue() - 1, MIN_SCE_FACTOR));
}
}
private class PlusClickListener implements View.OnClickListener {
@Override
public void onClick(View view) {
setToneValue(Math.min(getToneValue() + 1, MAX_SCE_FACTOR));
}
}
public SkinToneWidget(CameraManager cam, Context context) {
super(cam, context, R.drawable.ic_widget_skintone);
// Add views in the widget
mMinusButton = new WidgetOptionButton(R.drawable.ic_widget_timer_minus, context);
mPlusButton = new WidgetOptionButton(R.drawable.ic_widget_timer_plus, context);
mValueLabel = new WidgetOptionLabel(context);
mMinusButton.setOnClickListener(new MinusClickListener());
mPlusButton.setOnClickListener(new PlusClickListener());
addViewToContainer(mMinusButton);
addViewToContainer(mValueLabel);
addViewToContainer(mPlusButton);
restoreValueFromStorage(KEY_PARAMETER);
mValueLabel.setText(Integer.toString(getToneValue()));
getToggleButton().setHintText(R.string.widget_skintone);
}
@Override
public boolean isSupported(Camera.Parameters params) {
return params.get(KEY_PARAMETER) != null;
}
public int getToneValue() {
Camera.Parameters params = mCamManager.getParameters();
String value = params.get(KEY_PARAMETER);
if (value != null) {
return Integer.parseInt(value);
} else {
return 0;
}
}
public void setToneValue(int value) {
String valueStr = Integer.toString(value);
mCamManager.setParameterAsync(KEY_PARAMETER, valueStr);
SettingsStorage.storeCameraSetting(mWidget.getContext(), mCamManager.getCurrentFacing(),
KEY_PARAMETER, valueStr);
mValueLabel.setText(valueStr);
}
}
================================================
FILE: src/org/cyanogenmod/focal/widgets/SoftwareHdrWidget.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.widgets;
import android.hardware.Camera;
import android.view.View;
import org.cyanogenmod.focal.CameraActivity;
import fr.xplod.focal.R;
import org.cyanogenmod.focal.feats.SoftwareHdrCapture;
import java.util.List;
/**
* Software HDR widget
* This is distinct from the hardware HDR to avoid putting everything in one same class
* and have to hack around the XML inflation. This widget only shows up if neither HDR
* scene-mode is present and there is no AE-Bracket-HDR setting. If there are new
* methods of having HDR, you'll have to add them in arrays.xml.
*/
public class SoftwareHdrWidget extends WidgetBase implements View.OnClickListener {
private CameraActivity mContext;
private WidgetOptionButton mBtnOn;
private WidgetOptionButton mBtnOff;
private WidgetOptionButton mPreviousMode;
private SoftwareHdrCapture mTransformer;
private final static String DRAWABLE_TAG = "nemesis-soft-hdr";
public SoftwareHdrWidget(CameraActivity activity) {
super(activity.getCamManager(), activity, R.drawable.ic_widget_hdr);
mContext = activity;
mBtnOn = new WidgetOptionButton(R.drawable.ic_widget_hdr_on, mContext);
mBtnOff = new WidgetOptionButton(R.drawable.ic_widget_hdr_off, mContext);
mBtnOn.setOnClickListener(this);
mBtnOff.setOnClickListener(this);
addViewToContainer(mBtnOn);
addViewToContainer(mBtnOff);
mPreviousMode = mBtnOff;
mPreviousMode.activeImage(DRAWABLE_TAG + "=off");
mTransformer = new SoftwareHdrCapture(mContext);
getToggleButton().setHintText(R.string.widget_softwarehdr);
}
@Override
public boolean isSupported(Camera.Parameters params) {
// Software HDR only in photo mode!
if (CameraActivity.getCameraMode() != CameraActivity.CAMERA_MODE_PHOTO) {
mTransformer.tearDown();
return false;
}
// We hide the software HDR widget if either we have scene-mode=hdr,
// or any of the HDR setting key defined
List sceneModes = params.getSupportedSceneModes();
if (sceneModes != null && sceneModes.contains("hdr")) {
mTransformer.tearDown();
return false;
}
String[] keys = mContext.getResources().getStringArray(R.array.hardware_hdr_keys);
for (String key : keys) {
if (params.get(key) != null) {
mTransformer.tearDown();
return false;
}
}
return true;
}
@Override
public void onClick(View view) {
if (view == mBtnOn) {
mPreviousMode.resetImage();
mBtnOn.activeImage(DRAWABLE_TAG + "=on");
mPreviousMode = mBtnOn;
mContext.setCaptureTransformer(mTransformer);
} else if (view == mBtnOff) {
mPreviousMode.resetImage();
mBtnOff.activeImage(DRAWABLE_TAG + "=off");
mPreviousMode = mBtnOff;
mContext.setCaptureTransformer(null);
}
}
public void render() {
}
}
================================================
FILE: src/org/cyanogenmod/focal/widgets/TimerModeWidget.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.widgets;
import android.hardware.Camera;
import android.view.View;
import org.cyanogenmod.focal.CameraActivity;
import fr.xplod.focal.R;
import org.cyanogenmod.focal.feats.TimerCapture;
/**
* Timer mode and voice shutter widget
*/
public class TimerModeWidget extends WidgetBase implements View.OnClickListener {
private final static String DRAWABLE_TAG = "nemesis-timer-mode";
private final static int TIMER_MIN_VALUE = 1;
private final static int TIMER_MAX_VALUE = 60;
private WidgetOptionButton mBtnToggle;
private WidgetOptionButton mBtnVoice;
private WidgetOptionButton mBtnPlus;
private WidgetOptionButton mBtnMinus;
private WidgetOptionLabel mLabel;
private CameraActivity mCameraActivity;
private TimerCapture mTransformer;
private boolean mIsEnabled;
public TimerModeWidget(CameraActivity activity) {
super(activity.getCamManager(), activity, R.drawable.ic_widget_timer);
mCameraActivity = activity;
mIsEnabled = false;
// Create options
// XXX: Move that into an XML
mBtnToggle = new WidgetOptionButton(R.drawable.ic_widget_timer, activity);
mBtnVoice = new WidgetOptionButton(R.drawable.ic_widget_timer_voice, activity);
mBtnMinus = new WidgetOptionButton(R.drawable.ic_widget_timer_minus, activity);
mBtnPlus = new WidgetOptionButton(R.drawable.ic_widget_timer_plus, activity);
mLabel = new WidgetOptionLabel(activity);
addViewToContainer(mBtnVoice);
addViewToContainer(mBtnToggle);
addViewToContainer(mBtnMinus);
addViewToContainer(mLabel);
addViewToContainer(mBtnPlus);
mBtnToggle.setOnClickListener(this);
mBtnVoice.setOnClickListener(this);
mBtnMinus.setOnClickListener(this);
mBtnPlus.setOnClickListener(this);
mTransformer = new TimerCapture(activity);
mLabel.setText(Integer.toString(mTransformer.getTimer()));
getToggleButton().setHintText(R.string.widget_timermode);
}
@Override
public boolean isSupported(Camera.Parameters params) {
// Timer mode is supported by everything. If we are in photo mode that is.
if (CameraActivity.getCameraMode() == CameraActivity.CAMERA_MODE_PHOTO) {
return true;
} else {
return false;
}
}
private void turnOn() {
if (!mIsEnabled || mTransformer.getTimer() == TimerCapture.VOICE_TIMER_VALUE) {
mCameraActivity.setCaptureTransformer(mTransformer);
mBtnToggle.activeImage(DRAWABLE_TAG + "=on");
mIsEnabled = true;
if (mTransformer.getTimer() == TimerCapture.VOICE_TIMER_VALUE) {
mTransformer.setTimer(5); // some default value
mBtnVoice.resetImage();
}
}
}
private void turnOff(boolean nullizeTransformer) {
if (mIsEnabled) {
if (nullizeTransformer) {
mCameraActivity.setCaptureTransformer(null);
}
mBtnToggle.resetImage();
mIsEnabled = false;
}
}
@Override
public void onClick(View view) {
if (view == mBtnToggle) {
// Toggle the transformer
if (mIsEnabled && mTransformer.getTimer() != TimerCapture.VOICE_TIMER_VALUE) {
turnOff(true);
} else {
turnOn();
}
} else if (view == mBtnMinus) {
turnOn();
mTransformer.setTimer(clampTimer(mTransformer.getTimer()-1));
mLabel.setText(Integer.toString(mTransformer.getTimer()));
} else if (view == mBtnPlus) {
turnOn();
mTransformer.setTimer(clampTimer(mTransformer.getTimer()+1));
mLabel.setText(Integer.toString(mTransformer.getTimer()));
} else if (view == mBtnVoice) {
if (mIsEnabled && mTransformer.getTimer() == TimerCapture.VOICE_TIMER_VALUE) {
mBtnVoice.resetImage();
turnOff(true);
return;
} else if (mIsEnabled && mTransformer.getTimer() != TimerCapture.VOICE_TIMER_VALUE) {
mBtnToggle.resetImage();
}
mTransformer.setTimer(TimerCapture.VOICE_TIMER_VALUE);
mCameraActivity.setCaptureTransformer(mTransformer);
mBtnVoice.activeImage(DRAWABLE_TAG + "=voice");
mIsEnabled = true;
}
}
private int clampTimer(int value) {
value = Math.max(TIMER_MIN_VALUE, value);
value = Math.min(TIMER_MAX_VALUE, value);
return value;
}
}
================================================
FILE: src/org/cyanogenmod/focal/widgets/VideoFrWidget.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.widgets;
import android.content.Context;
import org.cyanogenmod.focal.CameraManager;
import fr.xplod.focal.R;
public class VideoFrWidget extends SimpleToggleWidget {
public VideoFrWidget(CameraManager cam, Context context) {
super(cam, context, "video-hfr", R.drawable.ic_widget_videofr);
// For video only
setVideoOnly(true);
// TODO: Needs filtering depending on resolution (see video-hfr-size)
addValue("off", R.drawable.ic_widget_videofr_30, "30 FPS");
addValue("60", R.drawable.ic_widget_videofr_60, "60 FPS");
addValue("90", R.drawable.ic_widget_videofr_90, "90 FPS");
addValue("120", R.drawable.ic_widget_videofr_120, "120 FPS");
getToggleButton().setHintText(R.string.widget_videofr);
}
}
================================================
FILE: src/org/cyanogenmod/focal/widgets/VideoHdrWidget.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.widgets;
import android.content.Context;
import android.hardware.Camera;
import org.cyanogenmod.focal.CameraManager;
import fr.xplod.focal.R;
/**
* Video HDR widget
* - On non-sony QCOM devices, video hdr is toggled by "video-hdr"
* - On Sony devices, video hdr is toggled by "sony-video-hdr"
*/
public class VideoHdrWidget extends SimpleToggleWidget {
private static final String KEY_QCOM_VIDEO_HDR = "video-hdr";
private static final String KEY_SONY_VIDEO_HDR = "sony-video-hdr";
public VideoHdrWidget(CameraManager cam, Context context) {
super(cam, context, R.drawable.ic_widget_placeholder); // TODO: Icon, video hdr
setVideoOnly(true);
// We cannot inflate from XML here, because there are device-specific keys and values
Camera.Parameters params = mCamManager.getParameters();
if (params == null) {
return;
}
if (params.get(KEY_SONY_VIDEO_HDR) != null) {
// Use Sony values
setKey(KEY_SONY_VIDEO_HDR);
addValue("off", R.drawable.ic_widget_hdr_off, context.getString(R.string.disabled));
addValue("on", R.drawable.ic_widget_hdr_on, context.getString(R.string.enabled));
} else if (params.get(KEY_QCOM_VIDEO_HDR) != null) {
// Use Qcom values
setKey(KEY_QCOM_VIDEO_HDR);
addValue("0", R.drawable.ic_widget_hdr_off, context.getString(R.string.disabled));
addValue("1", R.drawable.ic_widget_hdr_on, context.getString(R.string.enabled));
}
getToggleButton().setHintText(R.string.widget_videohdr);
}
}
================================================
FILE: src/org/cyanogenmod/focal/widgets/WhiteBalanceWidget.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.widgets;
import android.content.Context;
import org.cyanogenmod.focal.CameraManager;
import fr.xplod.focal.R;
/**
* White Balance Widget, manages the wb settings
*/
public class WhiteBalanceWidget extends SimpleToggleWidget {
private static final String KEY_WHITEBALANCE = "whitebalance";
public WhiteBalanceWidget(CameraManager cam, Context context) {
super(cam, context, KEY_WHITEBALANCE, R.drawable.ic_widget_wb_auto);
inflateFromXml(R.array.widget_whitebalance_values, R.array.widget_whitebalance_icons,
R.array.widget_whitebalance_hints);
getToggleButton().setHintText(R.string.widget_whitebalance);
restoreValueFromStorage(KEY_WHITEBALANCE);
}
}
================================================
FILE: src/org/cyanogenmod/focal/widgets/WidgetBase.java
================================================
/*
* Copyright (C) 2013 Guillaume Lesniak
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.cyanogenmod.focal.widgets;
import android.animation.Animator;
import android.animation.Animator.AnimatorListener;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.drawable.BitmapDrawable;
import android.hardware.Camera;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewPropertyAnimator;
import android.view.animation.DecelerateInterpolator;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.GridLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.SeekBar;
import android.widget.TextView;
import org.cyanogenmod.focal.BitmapFilter;
import org.cyanogenmod.focal.CameraActivity;
import org.cyanogenmod.focal.CameraManager;
import fr.xplod.focal.R;
import org.cyanogenmod.focal.SettingsStorage;
import org.cyanogenmod.focal.Util;
import org.cyanogenmod.focal.ui.CenteredSeekBar;
import org.cyanogenmod.focal.ui.WidgetRenderer;
/**
* Base class for settings widget. Each setting widget
* will extend this class.
*/
public abstract class WidgetBase {
public final static String TAG = "WidgetBase";
protected CameraManager mCamManager;
protected WidgetToggleButton mToggleButton;
protected WidgetToggleButton mShortcutButton;
protected WidgetContainer mWidget;
private int mWidgetMaxWidth;
private boolean mIsOpen;
private final static int WIDGET_FADE_DURATION_MS = 200;
private final static float WIDGET_FADE_SCALE = 0.75f;
private class ToggleClickListener implements View.OnClickListener {
@Override
public void onClick(View v) {
if (isOpen()) {
WidgetBase.this.close();
} else {
WidgetBase.this.open();
}
}
}
public WidgetBase(CameraManager cam, Context context, int iconResId) {
mCamManager = cam;
mWidget = new WidgetContainer(context);
mToggleButton = new WidgetToggleButton(context);
mShortcutButton = new WidgetToggleButton(context);
mShortcutButton.setShortcut(true);
mIsOpen = false;
// Setup the toggle button
mToggleButton.setCompoundDrawablesWithIntrinsicBounds(0, iconResId, 0, 0);
mShortcutButton.setCompoundDrawablesWithIntrinsicBounds(0, iconResId, 0, 0);
mWidget.setIconId(iconResId);
mWidgetMaxWidth = context.getResources().getInteger(R.integer.widget_default_max_width);
setHidden(!SettingsStorage.getVisibilitySetting(
context, this.getClass().getName()));
}
/**
* This method returns whether or not the widget settings are
* supported by the camera HAL or not.
*
* This method must be overridden by each widget, in order to hide
* widgets that are not supported by the Camera device.
*
* @param params The Camera parameters provided by HAL
* @return true if the widget is supported by the device
*/
public abstract boolean isSupported(Camera.Parameters params);
/**
* Add a view to the container widget
*
* @param v The view to add
*/
public void addViewToContainer(View v) {
if (mWidget.getGrid().getRowCount() * mWidget.getGrid().getColumnCount() < mWidget.getGrid().getChildCount() + 1) {
if (mWidget.getGrid().getColumnCount() == mWidgetMaxWidth) {
// Add a new line instead of a column to fit the max width
mWidget.getGrid().setRowCount(mWidget.getGrid().getRowCount() + 1);
} else {
mWidget.getGrid().setColumnCount(mWidget.getGrid().getColumnCount() + 1);
}
}
mWidget.getGrid().addView(v);
}
/**
* Force the size of the grid container to the specified
* row and column count. The views can then be added more precisely
* to the container using setViewAt()
*
* @param row
* @param column
*/
public void forceGridSize(int row, int column) {
mWidget.getGrid().setColumnCount(column);
mWidget.getGrid().setRowCount(row);
}
/**
* Sets the view v at the specific row/column combo slot.
* Pretty much useful only if you used forceGridSize
*
* @param row
* @param column
* @param v
*/
public void setViewAt(int row, int column, int rowSpan, int colSpan, View v) {
// Do some safety checks first
if (mWidget.getGrid().getRowCount() < row + rowSpan) {
mWidget.getGrid().setRowCount(row + rowSpan);
}
if (mWidget.getGrid().getColumnCount() < column + colSpan) {
mWidget.getGrid().setColumnCount(column + colSpan);
}
mWidget.getGrid().addView(v);
GridLayout.LayoutParams layoutParams = (GridLayout.LayoutParams) v.getLayoutParams();
layoutParams.columnSpec = GridLayout.spec(column, colSpan);
layoutParams.rowSpec = GridLayout.spec(row, rowSpan);
layoutParams.setGravity(Gravity.FILL);
layoutParams.width = colSpan * v.getMinimumWidth();
v.setLayoutParams(layoutParams);
}
public WidgetToggleButton getToggleButton() {
return mToggleButton;
}
public WidgetToggleButton getShortcutButton() {
return mShortcutButton;
}
public WidgetContainer getWidget() {
return mWidget;
}
public boolean isOpen() {
return mIsOpen;
}
/**
* Restores the value of this widget from the database to the Camera's preferences
*/
public String restoreValueFromStorage(String key) {
Camera.Parameters params = mCamManager.getParameters();
if (params == null) {
return "";
}
String currentValue = params.get(key);
if (currentValue != null) {
String storageValue = SettingsStorage.getCameraSetting(mWidget.getContext(),
mCamManager.getCurrentFacing(), key, currentValue);
mCamManager.setParameterAsync(key, storageValue);
return storageValue;
}
return currentValue;
}
/**
* @param key The name of the parameter to get
* @return The parameter value, or null if it doesn't exist
*/
public String getCameraValue(String key) {
Camera.Parameters params = mCamManager.getParameters();
return params.get(key);
}
/**
* Notifies the WidgetBase that the orientation has changed. WidgetBase doesn't
* do anything, but specific widgets might need orientation information (such as
* SettingsWidget to rotate the dialog).
*
* @param orientation The screen orientation
*/
public void notifyOrientationChanged(int orientation) {
}
/**
* Opens the widget, show the fade in animation
*/
public void open() {
WidgetRenderer parent = (WidgetRenderer) mWidget.getParent();
if (parent == null) return;
mWidget.setVisibility(View.VISIBLE);
mWidget.invalidate();
mIsOpen = true;
parent.widgetOpened(mWidget);
// Set initial widget state
mWidget.setAlpha(0.0f);
mWidget.setScaleX(WIDGET_FADE_SCALE);
mWidget.setScaleY(WIDGET_FADE_SCALE);
mWidget.setX(0);
mWidget.setY(Util.dpToPx(mWidget.getContext(), 8));
mWidget.animate().alpha(1.0f).scaleX(1.0f).scaleY(1.0f)
.setDuration(WIDGET_FADE_DURATION_MS).start();
mShortcutButton.toggleBackground(true);
mToggleButton.toggleBackground(true);
}
public void close() {
WidgetRenderer parent = (WidgetRenderer) mWidget.getParent();
if (parent != null) {
parent.widgetClosed(mWidget);
}
mWidget.animate().alpha(0.0f).scaleX(WIDGET_FADE_SCALE).scaleY(WIDGET_FADE_SCALE)
.setDuration(WIDGET_FADE_DURATION_MS).start();
mShortcutButton.toggleBackground(false);
mToggleButton.toggleBackground(false);
mIsOpen = false;
}
public void setHidden(boolean hide) {
mToggleButton.setVisibility(hide ? View.GONE : View.VISIBLE);
}
public boolean isHidden() {
return mToggleButton.getVisibility() == View.GONE;
}
/**
* Represents the button that toggles the widget, in the
* side bar.
*/
public class WidgetToggleButton extends Button {
private float mDownX;
private float mDownY;
private boolean mCancelOpenOnDown;
private String mHintText;
private boolean mIsShortcut = false;
public WidgetToggleButton(Context context, AttributeSet attrs,
int defStyle) {
super(context, attrs, android.R.style.Widget_DeviceDefault_Button_Borderless_Small);
initialize();
}
public WidgetToggleButton(Context context, AttributeSet attrs) {
super(context, attrs, android.R.style.Widget_DeviceDefault_Button_Borderless_Small);
initialize();
}
public WidgetToggleButton(Context context) {
super(context, null, android.R.style.Widget_DeviceDefault_Button_Borderless_Small);
initialize();
}
private void initialize() {
Resources res = getResources();
if (res == null) return;
this.setClickable(true);
int pad = res.getDimensionPixelSize(R.dimen.widget_toggle_button_padding);
this.setPadding(pad, pad, pad, pad);
this.setOnClickListener(new ToggleClickListener());
this.setOnLongClickListener(new LongClickListener());
setTextSize(11);
setGravity(Gravity.CENTER);
setLines(2);
int pxSize = (int) Util.dpToPx(getContext(), 82);
setMaxWidth(pxSize);
setMinWidth(pxSize);
}
public void setHintText(String text) {
mHintText = text;
if (!mIsShortcut) {
setText(text);
}
}
public void setHintText(int resId) {
mHintText = getResources().getString(resId);
if (!mIsShortcut) {
setText(mHintText);
}
}
public String getHintText() {
return mHintText;
}
public void toggleBackground(boolean active) {
if (active) {
this.setBackgroundColor(getResources().getColor(R.color.widget_toggle_active));
} else {
this.setBackgroundColor(0);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// to dispatch click / long click event, we do it first
if (event.getActionMasked() == MotionEvent.ACTION_UP && mCancelOpenOnDown) {
toggleBackground(mIsOpen);
return true;
} else {
super.onTouchEvent(event);
}
// Handle the background color on touch
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
this.setBackgroundColor(getResources().getColor(R.color.widget_toggle_pressed));
mCancelOpenOnDown = false;
mDownX = event.getX();
mDownY = event.getY();
} else if (event.getActionMasked() == MotionEvent.ACTION_UP) {
// State is not updated yet, but after ACTION_UP is received the button
// will likely be clicked and its state will change.
toggleBackground(!mIsOpen);
} else if (event.getActionMasked() == MotionEvent.ACTION_MOVE) {
if (!mIsOpen && (Math.abs(event.getX() - mDownX) > 8
|| Math.abs(event.getY() - mDownY) > 8)) {
toggleBackground(false);
}
}
return true;
}
public void setShortcut(boolean b) {
mIsShortcut = b;
if (b) {
setText("");
int pxSize = (int) Util.dpToPx(getContext(), 72);
setMaxWidth(pxSize);
setMinWidth(pxSize);
setMaxHeight(pxSize / 2);
}
}
private class LongClickListener implements OnLongClickListener {
@Override
public boolean onLongClick(View view) {
mCancelOpenOnDown = true;
if (mHintText != null && !mHintText.isEmpty()) {
CameraActivity.notify(mHintText, 2000, getX(), Util.getScreenSize(null).y
- getHeight() - getBottom());
}
toggleBackground(mIsOpen);
return true;
}
}
}
/**
* Interface that all widget options must implement. Give info
* about the layout of the widget.
*/
public interface WidgetOption {
/**
* Returns the number of columns occupied by the widget.
* A button typically occupies 1 column, but specific widgets
* like text widget might occupy two columns in some cases.
*
* @return Number of columns
*/
public int getColSpan();
/**
* Notifies the widget that its orientation changed
*
* @param orientation The new angle
*/
public void notifyOrientationChanged(int orientation);
}
/**
* Represents a standard label (text view) put inside
* a {@link WidgetContainer}.
*/
public class WidgetOptionLabel extends TextView implements WidgetOption {
public WidgetOptionLabel(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initialize();
}
public WidgetOptionLabel(Context context, AttributeSet attrs) {
super(context, attrs);
initialize();
}
public WidgetOptionLabel(Context context) {
super(context);
initialize();
}
private void initialize() {
setMinimumWidth(getResources().getDimensionPixelSize(
R.dimen.widget_option_button_size));
setMinimumHeight(getResources().getDimensionPixelSize(
R.dimen.widget_option_button_size));
int padding = getResources().getDimensionPixelSize(
R.dimen.widget_option_button_padding);
setPadding(padding, padding, padding, padding);
setGravity(Gravity.CENTER);
setTextSize(getResources().getInteger(R.integer.widget_option_label_font_size));
}
@Override
public void onFinishInflate() {
super.onFinishInflate();
GridLayout.LayoutParams params = (GridLayout.LayoutParams) this.getLayoutParams();
params.setGravity(Gravity.CENTER);
}
public void setSmall(boolean small) {
if (small) {
setTextSize(getResources().getInteger(
R.integer.widget_option_label_font_size_small));
} else {
setTextSize(getResources().getInteger(R.integer.widget_option_label_font_size));
}
}
@Override
public int getColSpan() {
// TODO: Return a different colspan if label larger
return 1;
}
@Override
public void notifyOrientationChanged(int orientation) {
}
}
/**
* Represents a non-clickable imageview put inside
* a {@link WidgetContainer}.
*/
public class WidgetOptionImage extends ImageView implements WidgetOption {
public WidgetOptionImage(int resId, Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initialize(resId);
}
public WidgetOptionImage(int resId, Context context, AttributeSet attrs) {
super(context, attrs);
initialize(resId);
}
public WidgetOptionImage(int resId, Context context) {
super(context);
initialize(resId);
}
private void initialize(int resId) {
Resources res = getResources();
if (res == null) return;
setMinimumWidth(res.getDimensionPixelSize(
R.dimen.widget_option_button_size) / 2);
setMaxWidth(getMinimumWidth());
int padding = res.getDimensionPixelSize(
R.dimen.widget_option_button_padding);
setPadding(padding, padding, padding, padding);
setImageResource(resId);
}
@Override
public int getColSpan() {
return 1;
}
@Override
public void notifyOrientationChanged(int orientation) {
}
}
/**
* Represents a standard setting button put inside
* a {@link WidgetContainer}.
* Note that you're not forced to use exclusively buttons,
* TextViews through WidgetOptionLabel can also be added (for Timer for instance)
*/
public class WidgetOptionButton extends Button implements WidgetOption,
View.OnLongClickListener {
private float mTouchOffset = 0.0f;
private BitmapDrawable mOriginalDrawable;
private String mHintText;
public WidgetOptionButton(int resId, Context context, AttributeSet attrs,
int defStyle) {
super(context, attrs, android.R.style.Widget_DeviceDefault_Button_Borderless_Small);
initialize(resId);
}
public WidgetOptionButton(int resId, Context context, AttributeSet attrs) {
super(context, attrs, android.R.style.Widget_DeviceDefault_Button_Borderless_Small);
initialize(resId);
}
public WidgetOptionButton(int resId, Context context) {
super(context, null, android.R.style.Widget_DeviceDefault_Button_Borderless_Small);
initialize(resId);
}
public void resetImage() {
this.setCompoundDrawablesRelativeWithIntrinsicBounds(null, mOriginalDrawable, null, null);
}
public void setHintText(String hint) {
mHintText = hint;
this.setText(mHintText);
}
public void setHintText(int resId) {
Resources res = getResources();
if (res == null) return;
mHintText = res.getString(resId);
this.setText(mHintText);
}
public void activeImage(String key) {
Resources res = getResources();
if (res == null) return;
this.setCompoundDrawablesRelativeWithIntrinsicBounds(null,
new BitmapDrawable(res,
BitmapFilter.getSingleton().getGlow(key,
res.getColor(R.color.widget_option_active),
mOriginalDrawable.getBitmap())), null, null);
}
private void initialize(int resId) {
Resources res = getResources();
if (res == null) return;
mOriginalDrawable = (BitmapDrawable) res.getDrawable(resId);
this.setCompoundDrawablesRelativeWithIntrinsicBounds(null, mOriginalDrawable, null, null);
this.setGravity(Gravity.CENTER);
this.setLines(2);
int pxHeight = (int) Util.dpToPx(getContext(), 56);
int pxWidth = (int) Util.dpToPx(getContext(), 64);
this.setMinimumWidth(pxWidth);
this.setMinimumHeight(pxHeight);
this.setMaxWidth(pxWidth);
this.setMaxHeight(pxHeight);
this.setSelected(true);
this.setFadingEdgeLength((int) Util.dpToPx(getContext(), 6));
this.setHorizontalFadingEdgeEnabled(true);
this.setTextSize(10);
setOnLongClickListener(this);
}
@Override
public int getColSpan() {
return 1;
}
@Override
public void notifyOrientationChanged(int orientation) {
}
@Override
public boolean onLongClick(View view) {
if (mHintText != null && mHintText.length() > 0) {
CameraActivity.notify(mHintText, 2000, Util.dpToPx(getContext(), 8)
+ getX() + mWidget.getX(), Util.dpToPx(getContext(), 32)
+ Util.dpToPx(getContext(), 4) * mHintText.length());
return true;
} else {
return false;
}
}
}
/**
* Represents a centered seek bar put inside
* a {@link WidgetContainer}.
*/
public class WidgetOptionCenteredSeekBar extends CenteredSeekBar implements WidgetOption {
public WidgetOptionCenteredSeekBar(Context context, AttributeSet attrs) {
super(context, attrs);
initialize();
}
public WidgetOptionCenteredSeekBar(Integer min, Integer max, Context context) {
super(min, max, context);
initialize();
}
private void initialize() {
Resources res = getResources();
if (res == null) return;
setMaxWidth(res.getDimensionPixelSize(
R.dimen.widget_option_button_size) * 3);
setMaxHeight(res.getDimensionPixelSize(R.dimen.widget_option_button_size));
// Set padding
int pad = res.getDimensionPixelSize(R.dimen.widget_container_padding);
this.setPadding(pad, pad, pad, pad);
}
@Override
public void onFinishInflate() {
super.onFinishInflate();
GridLayout.LayoutParams params = (GridLayout.LayoutParams) this.getLayoutParams();
if (params != null) {
params.setGravity(Gravity.CENTER);
}
}
@Override
public int getColSpan() {
return 3;
}
@Override
public void notifyOrientationChanged(int orientation) {
}
}
/**
* Represents a normal seek bar put inside
* a {@link WidgetContainer}.
*/
public class WidgetOptionSeekBar extends SeekBar implements WidgetOption {
public WidgetOptionSeekBar(Context context, AttributeSet attrs) {
super(context, attrs);
initialize();
}
public WidgetOptionSeekBar(Context context) {
super(context);
initialize();
}
private void initialize() {
Resources res = getResources();
if (res == null) return;
// Set padding
int pad = res.getDimensionPixelSize(R.dimen.widget_container_padding) * 2;
this.setPadding(pad, pad, pad, pad);
this.setMinimumHeight(res.getDimensionPixelSize(
R.dimen.widget_option_button_size) * 3);
}
@Override
public void onFinishInflate() {
super.onFinishInflate();
GridLayout.LayoutParams params = (GridLayout.LayoutParams) this.getLayoutParams();
if (params != null) {
params.setMargins(120, 120, 120, 120);
this.setLayoutParams(params);
}
}
@Override
public int getColSpan() {
return 3;
}
@Override
public void notifyOrientationChanged(int orientation) {
}
}
/**
* Represents the floating window of a widget.
*/
public class WidgetContainer extends FrameLayout {
private float mTouchOffsetY = 0.0f;
private float mTouchOffsetX = 0.0f;
private float mTouchDownX = 0.0f;
private float mTouchDownY = 0.0f;
private float mTargetY = 0.0f;
private ViewGroup mRootView;
private GridLayout mGridView;
private ImageView mWidgetIcon;
private Button mPinButton;
private ImageButton mResetButton;
private ViewPropertyAnimator mRootAnimator;
public WidgetContainer(Context context, AttributeSet attrs,
int defStyle) {
super(context, attrs, defStyle);
initialize();
}
public WidgetContainer(Context context, AttributeSet attrs) {
super(context, attrs);
initialize();
}
public WidgetContainer(Context context) {
super(context);
initialize();
}
/**
* Animate the movement on X
*
* @param y
*/
public void setYSmooth(float y) {
if (mTargetY != y) {
mRootAnimator.cancel();
mRootAnimator.y(y).setDuration(100).start();
mTargetY = y;
}
}
/**
* Returns the final X position, after the animation is done
*
* @return final X position
*/
public float getFinalY() {
return mTargetY;
}
public void forceFinalY(float y) {
mTargetY = y;
}
public WidgetBase getWidgetBase() {
return WidgetBase.this;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean handle = false;
if (getAlpha() == 0.0f) return false;
if (event.getAction() == MotionEvent.ACTION_DOWN) {
handle = true;
mTouchOffsetY = getY() - event.getRawY();
mTouchOffsetX = getX() - event.getRawX();
mTouchDownX = event.getRawX();
mTouchDownY = event.getRawY();
WidgetRenderer parent = (WidgetRenderer) mWidget.getParent();
parent.widgetPressed(mWidget);
} else if (event.getAction() == MotionEvent.ACTION_MOVE) {
float driftX = Math.abs(event.getRawX() - mTouchDownX);
float driftY = Math.abs(event.getRawY() - mTouchDownY);
if (driftX < driftY) {
setX(0);
setY(event.getRawY() + mTouchOffsetY);
} else {
setY(mTargetY);
setX(event.getRawX() + mTouchOffsetX);
}
setAlpha(1 - driftX / getWidth());
WidgetRenderer parent = (WidgetRenderer) mWidget.getParent();
parent.widgetMoved(mWidget);
forceFinalY(getY());
handle = true;
} else if (event.getAction() == MotionEvent.ACTION_UP) {
WidgetRenderer parent = (WidgetRenderer) mWidget.getParent();
parent.widgetDropped(mWidget);
if (event.getRawX() - mTouchDownX > getWidth() / 2) {
animate().x(getWidth()).alpha(0).start();
close();
mToggleButton.toggleBackground(false);
} else {
animate().x(0).alpha(1).start();
}
}
if (event.getAction() == MotionEvent.ACTION_UP) {
return handle;
} else {
return (super.onTouchEvent(event) || handle);
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
FrameLayout.LayoutParams layoutParam =
(FrameLayout.LayoutParams) this.getLayoutParams();
if (layoutParam != null) {
layoutParam.width = FrameLayout.LayoutParams.WRAP_CONTENT;
layoutParam.height = FrameLayout.LayoutParams.WRAP_CONTENT;
}
}
private void initialize() {
Context context = getContext();
if (context == null) {
Log.e(TAG, "Null context when initializing widget");
return;
}
Resources res = getResources();
if (res == null) {
Log.e(TAG, "Null resources when initializing widget");
return;
}
// Inflate our widget layout
LayoutInflater inflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mRootView = (ViewGroup) inflater.inflate(R.layout.widget_container, this);
if (mRootView == null) {
Log.e(TAG, "Error inflating the widget layout XML");
return;
}
// Grab all the views
mGridView = (GridLayout) mRootView.findViewById(R.id.widget_options_container);
mWidgetIcon = (ImageView) mRootView.findViewById(R.id.widget_icon);
mPinButton = (Button) mRootView.findViewById(R.id.btn_pin_widget);
//mResetButton = (ImageButton) mRootView.findViewById(R.id.btn_reset_widget);
mGridView.setColumnOrderPreserved(true);
mGridView.setRowCount(1);
mGridView.setColumnCount(0);
mGridView.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
// We consume all events of the scrollview to avoid dragging the
// widget by scrolling
mGridView.onTouchEvent(motionEvent);
return true;
}
});
mPinButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
Resources res = getResources();
if (res == null) return;
// Toggle shortcut (pinned) status
if (SettingsStorage.getShortcutSetting(getContext(),
WidgetBase.this.getClass().getCanonicalName())) {
// It was a shortcut, remove it
SettingsStorage.storeShortcutSetting(getContext(),
WidgetBase.this.getClass().getCanonicalName(), false);
mPinButton.setBackground(res.getDrawable(R.drawable.btn_pin_widget_inactive));
mShortcutButton.setVisibility(View.GONE);
mToggleButton.setVisibility(View.VISIBLE);
} else {
// Add it
SettingsStorage.storeShortcutSetting(getContext(),
WidgetBase.this.getClass().getCanonicalName(), true);
mPinButton.setBackground(res.getDrawable(R.drawable.ic_pin_widget_active));
mShortcutButton.setVisibility(View.VISIBLE);
mToggleButton.setVisibility(View.GONE);
}
}
});
// We default the window invisible, so we must respect the status
// obtained after the "close" animation
setAlpha(0.0f);
setScaleX(WIDGET_FADE_SCALE);
setScaleY(WIDGET_FADE_SCALE);
setVisibility(View.VISIBLE);
// Set padding
int pad = getResources().getDimensionPixelSize(R.dimen.widget_container_padding);
this.setPadding(pad, pad, pad, pad);
mRootAnimator = mRootView.animate();
if (mRootAnimator != null) {
mRootAnimator.setListener(new AnimatorListener() {
@Override
public void onAnimationCancel(Animator arg0) {
}
@Override
public void onAnimationEnd(Animator arg0) {
if (!mIsOpen) {
WidgetContainer.this.setVisibility(View.GONE);
}
}
@Override
public void onAnimationRepeat(Animator arg0) {
}
@Override
public void onAnimationStart(Animator arg0) {
WidgetContainer.this.setVisibility(View.VISIBLE);
}
});
}
}
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
super.onMeasure(widthSpec, heightSpec);
if (!isOpen()) {
setVisibility(View.GONE);
} else {
setVisibility(View.VISIBLE);
}
}
public void notifyOrientationChanged(int orientation, boolean fastforward) {
int buttonsCount = mGridView.getChildCount();
for (int i = 0; i < buttonsCount; i++) {
View child = mGridView.getChildAt(i);
WidgetOption option = (WidgetOption) child;
// Don't rotate seekbars
if (child instanceof WidgetOptionCenteredSeekBar) {
continue;
}
if (child instanceof WidgetOptionSeekBar) {
continue;
}
if (option != null) {
option.notifyOrientationChanged(orientation);
}
if (fastforward) {
child.setRotation(orientation);
} else {
child.animate().rotation(orientation).setDuration(200).setInterpolator(
new DecelerateInterpolator()).start();
}
}
WidgetBase.this.notifyOrientationChanged(orientation);
}
public GridLayout getGrid() {
return mGridView;
}
public void setIconId(int iconResId) {
mWidgetIcon.setImageResource(iconResId);
}
}
}