Merge pull request #2 from SimpleMobileTools/master

sync
This commit is contained in:
WanderMax 2021-01-23 20:29:04 +08:00 committed by GitHub
commit cc54d9c607
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
611 changed files with 43744 additions and 7253 deletions

23
.editorconfig Normal file
View file

@ -0,0 +1,23 @@
# Copying and distribution of this file, with or without modification,
# are permitted in any medium without royalty provided this notice is
# preserved. This file is offered as-is, without any warranty.
# Names of contributors must not be used to endorse or promote products
# derived from this file without specific prior written permission.
# EditorConfig
# http://EditorConfig.org
# top-most EditorConfig file
root = true
# LF end-of-line, insert an empty new line and UTF-8
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_style = space
indent_size = 4
continuation_indent_size = 8
[*.xml]
continuation_indent_size = 4

3
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1,3 @@
github: [tibbi]
patreon: tiborkaputa
custom: ["https://www.paypal.com/paypalme/simplemobiletools", "https://www.simplemobiletools.com/donate"]

8
.gitignore vendored
View file

@ -1,10 +1,12 @@
*.iml
*.aab
.gradle
/local.properties
/gradle.properties
/.idea/
.DS_Store
/build
/captures
release.keystore
signing.properties
keystore.jks
keystore.properties
/app/src/proprietary/assets/pesdk_license
/app/src/proprietary/assets/vesdk_license

File diff suppressed because it is too large Load diff

681
LICENSE
View file

@ -1,13 +1,674 @@
Copyright 2016 SimpleMobileTools
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
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
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
https://www.apache.org/licenses/LICENSE-2.0
Preamble
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.
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. 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
them 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 prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. 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.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey 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;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If 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 convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU 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 that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
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.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
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.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
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
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
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 3 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, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program 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, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU 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. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

124
README.md
View file

@ -1,36 +1,114 @@
# Simple Gallery
<img alt="Logo" src="app/src/main/res/mipmap-xxxhdpi/ic_launcher.png" width="80" />
<img alt="Logo" src="fastlane/metadata/android/en-US/images/icon.png" width="120" />
A gallery for viewing photos and videos.
Simple Gallery Pro is a highly customizable lightweight gallery loved by millions of people for its great user experience. Organize and edit your photos, recover deleted files with the recycle bin, protect & hide files and easily view a huge variety of different photo and video formats including RAW, SVG, GIF, panoramic and much more.
A simple tool usable for viewing photos and videos. Items can be sorted by date, size, name both ascending or descending, photos can be zoomed in. Media files are shown in multiple columns depending on the size of the display, you can change the column count by pinch gestures. They can be renamed, shared, deleted, copied, moved. Images can also be cropped, rotated, flipped or set as Wallpaper directly from the app.
-------------------------------------------------
SIMPLE GALLERY PRO FEATURES
-------------------------------------------------
The Gallery is also offered for third party usage for previewing images / videos, adding attachments at email clients etc. It's perfect for everyday usage.
• A beautiful modern gallery with no ads or popups
Contains no ads or unnecessary permissions. It is fully opensource, provides customizable colors.
• Photo editor crop, rotate, resize, draw, filters & more
This app is just one piece of a bigger series of apps. You can find the rest of them at http://www.simplemobiletools.com
• No internet access needed, giving you more privacy, security and stability
<a href='https://play.google.com/store/apps/details?id=com.simplemobiletools.gallery'><img src='http://simplemobiletools.github.io/assets/public/google-play.png' alt='Get it on Google Play' height='45' /></a>
<a href='https://f-droid.org/app/com.simplemobiletools.gallery'><img src='http://simplemobiletools.github.io/assets/public/f-droid.png' alt='Get it on F-Droid' height='45' /></a>
• No unnecessary permissions required
<img alt="App image" src="screenshots/app.jpg" width="250" />
<img alt="App image" src="screenshots/app_2.jpg" width="250" />
<img alt="App image" src="screenshots/app_5.jpg" width="250" />
• Quickly search images, videos & other files
License
-------
Copyright 2017 SimpleMobileTools
• Open and view many different photo and video types (RAW, SVG, panoramic, GIF etc)
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
• A variety of intuitive gestures to easily edit, rotate & organize files
https://www.apache.org/licenses/LICENSE-2.0
• Lots of ways to filter, group and sort files
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.
• Customize the appearance of Simple Gallery Pro
• Available in over 30 languages
• Mark files as favorites for quick access
• Protect your photos & videos with a pattern, pin or fingerprint
• Use pin, pattern & fingerprint to protect the app launch or specific functions too
• Recover deleted photos & videos from the recycle bin
• Toggle visibility of files to hide photos & videos
• Create a customizable slideshow of your files with many options
• View detailed information of your files (resolution, EXIF values etc)
• Zoom high quality photos, videos and GIFs easily with gestures
• Easily force portrait or landscape orientation for easy video viewing
• Quick file share to social media, email or anywhere else
• Print images or set them as wallpaper with a few clicks
• Create home screen shortcuts or widgets to easily access any file or folder
• Show the image location on a map, if available
• Obvious presence of standard operations like rename, copy/move, un/hide, delete
• Rewind videos with horizontal gestures
• Change photo and video brightness, or volume with vertical gestures
• Reorder, lock folders or change cover thumbnails anytime
• Set any image as wallpaper without hassle
… and much much more!
ADVANCED PHOTO EDITOR
Simple Gallery Pro makes it easy to edit your pictures on the fly. Crop, flip, rotate and resize your pictures. If youre feeling a little more creative you can add filters and draw on your pictures!
SUPPORT FOR MANY FILE TYPES
Simple Gallery Pro supports a huge range of different file types including JPEG, PNG, MP4, MKV, RAW, SVG, GIF Panoramic photos, Panoramic videos and many more.
HIGHLY CUSTOMIZABLE FILE MANAGER
From the UI to the function buttons on the bottom toolbar, Simple Gallery Pro is highly customizable and works the way you want it to. No other gallery has this kind of flexibility! Thanks to being open source, were also available in over 30 languages!
RECOVER DELETED PHOTOS & VIDEOS
Accidentally deleted a precious photo or video? Dont worry! Simple Gallery Pro features a handy recycle bin where you can recover deleted photos & videos easily.
PROTECT & HIDE PHOTOS, VIDEOS & FILES
Using pin, pattern or your devices fingerprint scanner you can protect and hide photos, videos & entire albums. You can protect the app itself or place locks on specific functions of the app. For example, you cant delete a file without a fingerprint scan, helping to protect your files from accidental deletion.
Check out the full suite of Simple Tools here:
https://www.simplemobiletools.com
Standalone website of Simple Gallery Pro:
https://www.simplemobiletools.com/gallery
Facebook:
https://www.facebook.com/simplemobiletools
Reddit:
https://www.reddit.com/r/SimpleMobileTools
Don't forget that if you uninstall any paid app within 2 hours, you will automatically be refunded. If you want a refund anytime later, just contact us at hello@simplemobiletools.com and you will get it. That makes it easy to try it out :)
<a href='https://play.google.com/store/apps/details?id=com.simplemobiletools.gallery.pro'><img src='https://simplemobiletools.com/assets/images/google-play.png' alt='Get it on Google Play' height='45' /></a>
<a href='https://f-droid.org/packages/com.simplemobiletools.gallery.pro'><img src='https://simplemobiletools.com/assets/images/f-droid.png' alt='Get it on F-Droid' height='45' /></a>
<div style="display:flex;">
<img alt="App image" src="fastlane/metadata/android/en-US/images/phoneScreenshots/app_1.jpg" width="30%">
<img alt="App image" src="fastlane/metadata/android/en-US/images/phoneScreenshots/app_2.jpg" width="30%">
<img alt="App image" src="fastlane/metadata/android/en-US/images/phoneScreenshots/app_3.jpg" width="30%">
</div>

View file

@ -1,78 +1,143 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
def keystorePropertiesFile = rootProject.file("keystore.properties")
def keystoreProperties = new Properties()
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
compileSdkVersion 25
buildToolsVersion "25.0.2"
compileSdkVersion 29
buildToolsVersion "29.0.3"
defaultConfig {
applicationId "com.simplemobiletools.gallery"
minSdkVersion 16
targetSdkVersion 23
versionCode 109
versionName "2.10.10"
applicationId "com.simplemobiletools.gallery.pro"
minSdkVersion 21
targetSdkVersion 29
versionCode 335
versionName "6.19.0"
setProperty("archivesBaseName", "gallery-$versionCode")
vectorDrawables.useSupportLibrary = true
}
signingConfigs {
release
if (keystorePropertiesFile.exists()) {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
}
}
}
buildTypes {
debug {
// we cannot change the original package name, else PhotoEditorSDK won't work
//applicationIdSuffix ".debug"
}
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
if (keystorePropertiesFile.exists()) {
signingConfig signingConfigs.release
}
}
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
if (is_proprietary) {
main.java.srcDirs += 'src/proprietary/kotlin'
}
}
flavorDimensions "licensing"
productFlavors {
proprietary {}
foss {}
}
lintOptions {
checkReleaseBuilds false
abortOnError false
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
packagingOptions {
exclude 'META-INF/library_release.kotlin_module'
}
}
dependencies {
compile 'com.simplemobiletools:commons:2.20.1'
compile 'com.davemorrissey.labs:subsampling-scale-image-view:3.6.0'
compile 'com.theartofdev.edmodo:android-image-cropper:2.4.0'
compile 'com.bignerdranch.android:recyclerview-multiselect:0.2'
compile 'com.google.code.gson:gson:2.8.0'
compile 'com.github.chrisbanes:PhotoView:1.3.1'
compile 'it.sephiroth.android.exif:library:1.0.1'
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'com.simplemobiletools:commons:5.33.7'
implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0'
implementation 'it.sephiroth.android.exif:library:1.0.1'
implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.19'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'com.google.android.exoplayer:exoplayer-core:2.9.6'
implementation 'com.google.vr:sdk-panowidget:1.180.0'
implementation 'com.google.vr:sdk-videowidget:1.180.0'
implementation 'org.apache.sanselan:sanselan:0.97-incubator'
implementation 'info.androidhive:imagefilters:1.0.7'
implementation 'com.squareup.picasso:picasso:2.71828'
implementation 'com.caverock:androidsvg-aar:1.3'
implementation 'com.github.tibbi:gestureviews:512f929d82'
implementation 'com.github.tibbi:subsampling-scale-image-view:81c021514c'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'com.github.penfeizhou.android.animation:awebp:2.5.1'
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
kapt 'com.github.bumptech.glide:compiler:4.10.0'
kapt 'androidx.room:room-compiler:2.2.6'
implementation 'androidx.room:room-runtime:2.2.6'
annotationProcessor 'androidx.room:room-compiler:2.2.6'
}
buildscript {
ext.kotlin_version = '1.1.2-3'
repositories {
mavenCentral()
// Apply the PESDKPlugin
if (is_proprietary) {
apply plugin: 'ly.img.android.sdk'
imglyConfig {
vesdk {
enabled true
licencePath 'vesdk_license'
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version"
pesdk {
enabled true
licencePath 'pesdk_license'
}
modules {
include 'ui:video-trim'
include 'ui:core'
include 'ui:text'
include 'ui:focus'
include 'ui:brush'
include 'ui:filter'
include 'ui:sticker'
include 'ui:overlay'
include 'ui:transform'
include 'ui:adjustment'
include 'backend:serializer'
include 'backend:sticker-smart'
include 'backend:sticker-animated'
include 'assets:font-basic'
include 'assets:filter-basic'
include 'assets:overlay-basic'
include 'assets:sticker-shapes'
include 'assets:sticker-emoticons'
include 'assets:sticker-animated'
}
}
}
def Properties props = new Properties()
def propFile = new File('signing.properties')
if (propFile.canRead()) {
props.load(new FileInputStream(propFile))
if (props != null && props.containsKey('STORE_FILE') && props.containsKey('KEY_ALIAS') && props.containsKey('PASSWORD')) {
android.signingConfigs.release.storeFile = file(props['STORE_FILE'])
android.signingConfigs.release.storePassword = props['PASSWORD']
android.signingConfigs.release.keyAlias = props['KEY_ALIAS']
android.signingConfigs.release.keyPassword = props['PASSWORD']
} else {
println 'signing.properties found but some entries are missing'
android.buildTypes.release.signingConfig = null
}
} else {
println 'signing.properties not found'
android.buildTypes.release.signingConfig = null
}

View file

@ -1,5 +1,20 @@
-keep class com.simplemobiletools.** { *; }
-dontwarn android.graphics.Canvas
-dontwarn com.simplemobiletools.**
-dontwarn org.apache.**
-renamesourcefileattribute SourceFile
-keepattributes SourceFile, LineNumberTable
# Picasso
-dontwarn javax.annotation.**
-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase
-dontwarn org.codehaus.mojo.animal_sniffer.*
-dontwarn okhttp3.internal.platform.ConscryptPlatform
-keepclassmembers class * implements android.os.Parcelable {
static ** CREATOR;
}
# RenderScript
-keepclasseswithmembernames class * {
native <methods>;
}
-keep class androidx.renderscript.** { *; }

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_launcher_name">Gallery_debug</string>
</resources>

View file

@ -1,33 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.simplemobiletools.gallery">
xmlns:tools="http://schemas.android.com/tools"
package="com.simplemobiletools.gallery.pro"
android:installLocation="auto">
<!-- override the android:maxSdkVersion="28" from PhotoEditorSDK with some higher number -->
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="40"
tools:replace="android:maxSdkVersion" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.SET_WALLPAPER"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission
android:name="android.permission.ACCESS_NETWORK_STATE"
tools:node="remove"/>
<uses-permission
android:name="android.permission.CAMERA"
tools:node="remove"/>
<uses-sdk
tools:overrideLibrary="com.google.vr.widgets.common, com.google.vr.sdk.widgets.pano"/>
<uses-feature
android:name="android.hardware.faketouch"
android:required="false"/>
<application
android:name=".App"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_launcher_name"
android:roundIcon="@mipmap/ic_launcher"
android:requestLegacyExternalStorage="true"
android:supportsRtl="true"
android:theme="@style/AppTheme">
android:theme="@style/AppTheme"
tools:replace="android:label">
<activity
android:name=".activities.SplashActivity"
android:theme="@style/SplashTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
android:theme="@style/SplashTheme"/>
<activity
android:name=".activities.MainActivity"
android:resizeableActivity="true">
<meta-data
android:name="android.app.default_searchable"
android:resource="@xml/searchable"/>
<intent-filter>
<action android:name="android.intent.action.SEARCH"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.PICK"/>
<category android:name="android.intent.category.DEFAULT"/>
@ -53,13 +79,37 @@
<activity
android:name=".activities.MediaActivity"
android:parentActivityName=".activities.MainActivity"/>
android:parentActivityName=".activities.MainActivity">
<meta-data
android:name="android.app.default_searchable"
android:resource="@xml/searchable"/>
<intent-filter>
<action android:name="android.intent.action.SEARCH"/>
</intent-filter>
</activity>
<activity
android:name=".activities.SearchActivity"
android:label="@string/search"
android:parentActivityName=".activities.MainActivity"
android:resizeableActivity="true">
<meta-data
android:name="android.app.default_searchable"
android:resource="@xml/searchable"/>
<intent-filter>
<action android:name="android.intent.action.SEARCH"/>
</intent-filter>
</activity>
<activity
android:name=".activities.ViewPagerActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:parentActivityName=".activities.MediaActivity"
android:theme="@style/FullScreenTheme">
android:parentActivityName=".activities.MediaActivity">
<intent-filter>
<action android:name="com.android.camera.action.REVIEW"/>
@ -82,18 +132,37 @@
android:label="@string/third_party_licences"
android:parentActivityName="com.simplemobiletools.commons.activities.AboutActivity"/>
<activity
android:name=".activities.SettingsActivity"
android:label="@string/settings"
android:parentActivityName=".activities.MainActivity"/>
<activity
android:name="com.simplemobiletools.commons.activities.CustomizationActivity"
android:label="@string/customize_colors"
android:parentActivityName=".activities.SettingsActivity"/>
<activity
android:name="com.simplemobiletools.commons.activities.FAQActivity"
android:label="@string/frequently_asked_questions"
android:parentActivityName="com.simplemobiletools.commons.activities.AboutActivity"/>
<activity
android:name=".activities.SettingsActivity"
android:label="@string/settings"
android:parentActivityName=".activities.MainActivity"/>
<activity
android:name=".activities.PhotoVideoActivity"
android:configChanges="orientation|keyboardHidden|screenSize"/>
<activity
android:name=".activities.VideoPlayerActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:parentActivityName=".activities.MediaActivity"/>
<activity
android:name=".activities.PanoramaPhotoActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:theme="@style/FullScreenTheme"/>
<activity
android:name=".activities.PanoramaVideoActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:theme="@style/FullScreenTheme"/>
@ -107,10 +176,14 @@
android:label="@string/excluded_folders"
android:parentActivityName=".activities.SettingsActivity"/>
<activity
android:name=".activities.HiddenFoldersActivity"
android:label="@string/hidden_folders"
android:parentActivityName=".activities.SettingsActivity"/>
<activity
android:name=".activities.PhotoActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:theme="@style/FullScreenTheme">
android:configChanges="orientation|keyboardHidden|screenSize">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
@ -123,8 +196,7 @@
<activity
android:name=".activities.VideoActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:theme="@style/FullScreenTheme">
android:configChanges="orientation|keyboardHidden|screenSize">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
@ -138,7 +210,10 @@
<activity
android:name=".activities.EditActivity"
android:label="@string/editor">
<intent-filter>
<intent-filter
android:name="foss-editor">
<action android:name="android.intent.action.EDIT"/>
<category android:name="android.intent.category.DEFAULT"/>
@ -177,8 +252,17 @@
</intent-filter>
</activity>
<activity
android:name=".activities.WidgetConfigureActivity"
android:screenOrientation="portrait"
android:theme="@style/MyWidgetConfigTheme">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
</intent-filter>
</activity>
<provider
android:name="android.support.v4.content.FileProvider"
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
@ -188,11 +272,283 @@
</provider>
<receiver
android:name=".receivers.InstallReceiver"
android:name=".receivers.RefreshMediaReceiver"
android:exported="true">
<intent-filter>
<action android:name="com.android.vending.INSTALL_REFERRER"/>
<action android:name="com.simplemobiletools.REFRESH_MEDIA"/>
</intent-filter>
</receiver>
<receiver
android:name=".helpers.MyWidgetProvider"
android:icon="@drawable/img_widget_preview">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_info"/>
</receiver>
<receiver android:name=".receivers.BootCompletedReceiver"
android:exported="true"
android:enabled="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.QUICKBOOT_POWERON"/>
</intent-filter>
</receiver>
<service
android:name=".jobs.NewPhotoFetcher"
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE"/>
<activity-alias
android:name=".activities.SplashActivity.Red"
android:enabled="false"
android:icon="@mipmap/ic_launcher_red"
android:roundIcon="@mipmap/ic_launcher_red"
android:targetActivity=".activities.SplashActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity-alias>
<activity-alias
android:name=".activities.SplashActivity.Pink"
android:enabled="false"
android:icon="@mipmap/ic_launcher_pink"
android:roundIcon="@mipmap/ic_launcher_pink"
android:targetActivity=".activities.SplashActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity-alias>
<activity-alias
android:name=".activities.SplashActivity.Purple"
android:enabled="false"
android:icon="@mipmap/ic_launcher_purple"
android:roundIcon="@mipmap/ic_launcher_purple"
android:targetActivity=".activities.SplashActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity-alias>
<activity-alias
android:name=".activities.SplashActivity.Deep_purple"
android:enabled="false"
android:icon="@mipmap/ic_launcher_deep_purple"
android:roundIcon="@mipmap/ic_launcher_deep_purple"
android:targetActivity=".activities.SplashActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity-alias>
<activity-alias
android:name=".activities.SplashActivity.Indigo"
android:enabled="false"
android:icon="@mipmap/ic_launcher_indigo"
android:roundIcon="@mipmap/ic_launcher_indigo"
android:targetActivity=".activities.SplashActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity-alias>
<activity-alias
android:name=".activities.SplashActivity.Blue"
android:enabled="false"
android:icon="@mipmap/ic_launcher_blue"
android:roundIcon="@mipmap/ic_launcher_blue"
android:targetActivity=".activities.SplashActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity-alias>
<activity-alias
android:name=".activities.SplashActivity.Light_blue"
android:enabled="false"
android:icon="@mipmap/ic_launcher_light_blue"
android:roundIcon="@mipmap/ic_launcher_light_blue"
android:targetActivity=".activities.SplashActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity-alias>
<activity-alias
android:name=".activities.SplashActivity.Cyan"
android:enabled="false"
android:icon="@mipmap/ic_launcher_cyan"
android:roundIcon="@mipmap/ic_launcher_cyan"
android:targetActivity=".activities.SplashActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity-alias>
<activity-alias
android:name=".activities.SplashActivity.Teal"
android:enabled="false"
android:icon="@mipmap/ic_launcher_teal"
android:roundIcon="@mipmap/ic_launcher_teal"
android:targetActivity=".activities.SplashActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity-alias>
<activity-alias
android:name=".activities.SplashActivity.Green"
android:enabled="false"
android:icon="@mipmap/ic_launcher_green"
android:roundIcon="@mipmap/ic_launcher_green"
android:targetActivity=".activities.SplashActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity-alias>
<activity-alias
android:name=".activities.SplashActivity.Light_green"
android:enabled="false"
android:icon="@mipmap/ic_launcher_light_green"
android:roundIcon="@mipmap/ic_launcher_light_green"
android:targetActivity=".activities.SplashActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity-alias>
<activity-alias
android:name=".activities.SplashActivity.Lime"
android:enabled="false"
android:icon="@mipmap/ic_launcher_lime"
android:roundIcon="@mipmap/ic_launcher_lime"
android:targetActivity=".activities.SplashActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity-alias>
<activity-alias
android:name=".activities.SplashActivity.Yellow"
android:enabled="false"
android:icon="@mipmap/ic_launcher_yellow"
android:roundIcon="@mipmap/ic_launcher_yellow"
android:targetActivity=".activities.SplashActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity-alias>
<activity-alias
android:name=".activities.SplashActivity.Amber"
android:enabled="false"
android:icon="@mipmap/ic_launcher_amber"
android:roundIcon="@mipmap/ic_launcher_amber"
android:targetActivity=".activities.SplashActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity-alias>
<activity-alias
android:name=".activities.SplashActivity.Orange"
android:enabled="true"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher"
android:targetActivity=".activities.SplashActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity-alias>
<activity-alias
android:name=".activities.SplashActivity.Deep_orange"
android:enabled="false"
android:icon="@mipmap/ic_launcher_deep_orange"
android:roundIcon="@mipmap/ic_launcher_deep_orange"
android:targetActivity=".activities.SplashActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity-alias>
<activity-alias
android:name=".activities.SplashActivity.Brown"
android:enabled="false"
android:icon="@mipmap/ic_launcher_brown"
android:roundIcon="@mipmap/ic_launcher_brown"
android:targetActivity=".activities.SplashActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity-alias>
<activity-alias
android:name=".activities.SplashActivity.Blue_grey"
android:enabled="false"
android:icon="@mipmap/ic_launcher_blue_grey"
android:roundIcon="@mipmap/ic_launcher_blue_grey"
android:targetActivity=".activities.SplashActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity-alias>
<activity-alias
android:name=".activities.SplashActivity.Grey_black"
android:enabled="false"
android:icon="@mipmap/ic_launcher_grey_black"
android:roundIcon="@mipmap/ic_launcher_grey_black"
android:targetActivity=".activities.SplashActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity-alias>
</application>
</manifest>

View file

@ -1,14 +0,0 @@
package com.simplemobiletools.gallery
import android.app.Application
import com.squareup.leakcanary.LeakCanary
class App : Application() {
override fun onCreate() {
super.onCreate()
if (LeakCanary.isInAnalyzerProcess(this)) {
return
}
LeakCanary.install(this)
}
}

View file

@ -1,199 +0,0 @@
package com.simplemobiletools.gallery.activities
import android.app.Activity
import android.graphics.Bitmap
import android.graphics.Bitmap.CompressFormat
import android.graphics.Point
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import com.simplemobiletools.commons.extensions.getCompressionFormat
import com.simplemobiletools.commons.extensions.getFileOutputStream
import com.simplemobiletools.commons.extensions.scanPath
import com.simplemobiletools.commons.extensions.toast
import com.simplemobiletools.gallery.R
import com.simplemobiletools.gallery.dialogs.ResizeDialog
import com.simplemobiletools.gallery.dialogs.SaveAsDialog
import com.simplemobiletools.gallery.extensions.getRealPathFromURI
import com.theartofdev.edmodo.cropper.CropImageView
import kotlinx.android.synthetic.main.view_crop_image.*
import java.io.*
class EditActivity : SimpleActivity(), CropImageView.OnCropImageCompleteListener {
val TAG = EditActivity::class.java.simpleName
val ASPECT_X = "aspectX"
val ASPECT_Y = "aspectY"
val CROP = "crop"
lateinit var uri: Uri
var resizeWidth = 0
var resizeHeight = 0
var isCropIntent = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.view_crop_image)
if (intent.data == null) {
toast(R.string.invalid_image_path)
finish()
return
}
uri = intent.data
if (uri.scheme != "file" && uri.scheme != "content") {
toast(R.string.unknown_file_location)
finish()
return
}
isCropIntent = intent.extras?.get(CROP) == "true"
crop_image_view.apply {
setOnCropImageCompleteListener(this@EditActivity)
setImageUriAsync(intent.data)
if (isCropIntent && shouldCropSquare())
setFixedAspectRatio(true)
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_editor, menu)
menu.findItem(R.id.resize).isVisible = !isCropIntent
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.save_as -> crop_image_view.getCroppedImageAsync()
R.id.rotate -> crop_image_view.rotateImage(90)
R.id.resize -> resizeImage()
R.id.flip_horizontally -> flipImage(true)
R.id.flip_vertically -> flipImage(false)
else -> return super.onOptionsItemSelected(item)
}
return true
}
private fun resizeImage() {
val point = getAreaSize()
if (point == null) {
toast(R.string.unknown_error_occurred)
return
}
ResizeDialog(this, point) {
resizeWidth = it.x
resizeHeight = it.y
crop_image_view.getCroppedImageAsync()
}
}
private fun shouldCropSquare(): Boolean {
val extras = intent.extras
return if (extras != null && extras.containsKey(ASPECT_X) && extras.containsKey(ASPECT_Y)) {
extras.getInt(ASPECT_X) == extras.getInt(ASPECT_Y)
} else {
false
}
}
private fun getAreaSize(): Point? {
val rect = crop_image_view.cropRect ?: return null
val rotation = crop_image_view.rotatedDegrees
return if (rotation == 0 || rotation == 180) {
Point(rect.width(), rect.height())
} else {
Point(rect.height(), rect.width())
}
}
override fun onCropImageComplete(view: CropImageView, result: CropImageView.CropResult) {
if (result.error == null) {
if (isCropIntent && intent.extras?.containsKey(MediaStore.EXTRA_OUTPUT) == true) {
val targetUri = intent.extras!!.get(MediaStore.EXTRA_OUTPUT) as Uri
var inputStream: InputStream? = null
var outputStream: OutputStream? = null
try {
val stream = ByteArrayOutputStream()
result.bitmap.compress(CompressFormat.JPEG, 100, stream)
inputStream = ByteArrayInputStream(stream.toByteArray())
outputStream = contentResolver.openOutputStream(targetUri)
inputStream.copyTo(outputStream)
} finally {
inputStream?.close()
outputStream?.close()
}
setResult(RESULT_OK)
finish()
} else if (uri.scheme == "file") {
SaveAsDialog(this, uri.path) {
saveBitmapToFile(result.bitmap, it)
}
} else if (uri.scheme == "content") {
val newPath = applicationContext.getRealPathFromURI(uri) ?: ""
if (!newPath.isEmpty()) {
SaveAsDialog(this, newPath) {
saveBitmapToFile(result.bitmap, it)
}
} else {
toast(R.string.image_editing_failed)
finish()
}
} else {
toast(R.string.unknown_file_location)
finish()
}
} else {
toast("${getString(R.string.image_editing_failed)}: ${result.error.message}")
}
}
private fun saveBitmapToFile(bitmap: Bitmap, path: String) {
val file = File(path)
try {
getFileOutputStream(file) {
saveBitmap(file, bitmap, it)
}
} catch (e: Exception) {
Log.e(TAG, "Crop compressing failed $path $e")
toast(R.string.image_editing_failed)
finish()
} catch (e: OutOfMemoryError) {
toast(R.string.out_of_memory_error)
}
}
private fun saveBitmap(file: File, bitmap: Bitmap, out: OutputStream) {
if (resizeWidth > 0 && resizeHeight > 0) {
val resized = Bitmap.createScaledBitmap(bitmap, resizeWidth, resizeHeight, false)
resized.compress(file.getCompressionFormat(), 90, out)
} else {
bitmap.compress(file.getCompressionFormat(), 90, out)
}
setResult(Activity.RESULT_OK, intent)
scanFinalPath(file.absolutePath)
out.close()
}
private fun flipImage(horizontally: Boolean) {
if (horizontally)
crop_image_view.flipImageHorizontally()
else
crop_image_view.flipImageVertically()
}
private fun scanFinalPath(path: String) {
scanPath(path) {
setResult(Activity.RESULT_OK, intent)
toast(R.string.file_saved)
finish()
}
}
}

View file

@ -1,64 +0,0 @@
package com.simplemobiletools.gallery.activities
import android.graphics.PorterDuff
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import com.simplemobiletools.commons.dialogs.FilePickerDialog
import com.simplemobiletools.commons.extensions.beVisibleIf
import com.simplemobiletools.gallery.R
import com.simplemobiletools.gallery.extensions.config
import kotlinx.android.synthetic.main.activity_excluded_folders.*
import kotlinx.android.synthetic.main.item_manage_folder.view.*
class ExcludedFoldersActivity : SimpleActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_excluded_folders)
updateExcludedFolders()
}
private fun updateExcludedFolders() {
excluded_folders_holder.removeAllViews()
val folders = config.excludedFolders
excluded_folders_placeholder.beVisibleIf(folders.isEmpty())
excluded_folders_placeholder.setTextColor(config.textColor)
for (folder in folders) {
layoutInflater.inflate(R.layout.item_manage_folder, null, false).apply {
managed_folder_title.apply {
text = folder
setTextColor(config.textColor)
}
managed_folders_icon.apply {
setColorFilter(config.textColor, PorterDuff.Mode.SRC_IN)
setOnClickListener {
config.removeExcludedFolder(folder)
updateExcludedFolders()
}
}
excluded_folders_holder.addView(this)
}
}
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu_excluded_folders, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.add_folder -> addExcludedFolder()
else -> return super.onOptionsItemSelected(item)
}
return true
}
private fun addExcludedFolder() {
FilePickerDialog(this, pickFile = false, showHidden = config.shouldShowHidden) {
config.addExcludedFolder(it)
updateExcludedFolders()
}
}
}

View file

@ -1,66 +0,0 @@
package com.simplemobiletools.gallery.activities
import android.graphics.PorterDuff
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import com.simplemobiletools.commons.dialogs.FilePickerDialog
import com.simplemobiletools.commons.extensions.beVisibleIf
import com.simplemobiletools.commons.extensions.scanPath
import com.simplemobiletools.gallery.R
import com.simplemobiletools.gallery.extensions.config
import kotlinx.android.synthetic.main.activity_included_folders.*
import kotlinx.android.synthetic.main.item_manage_folder.view.*
class IncludedFoldersActivity : SimpleActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_included_folders)
updateIncludedFolders()
}
private fun updateIncludedFolders() {
included_folders_holder.removeAllViews()
val folders = config.includedFolders
included_folders_placeholder.beVisibleIf(folders.isEmpty())
included_folders_placeholder.setTextColor(config.textColor)
for (folder in folders) {
layoutInflater.inflate(R.layout.item_manage_folder, null, false).apply {
managed_folder_title.apply {
text = folder
setTextColor(config.textColor)
}
managed_folders_icon.apply {
setColorFilter(config.textColor, PorterDuff.Mode.SRC_IN)
setOnClickListener {
config.removeIncludedFolder(folder)
updateIncludedFolders()
}
}
included_folders_holder.addView(this)
}
}
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu_included_folders, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.add_folder -> addIncludedFolder()
else -> return super.onOptionsItemSelected(item)
}
return true
}
private fun addIncludedFolder() {
FilePickerDialog(this, pickFile = false, showHidden = config.shouldShowHidden) {
config.addIncludedFolder(it)
updateIncludedFolders()
scanPath(it) {}
}
}
}

View file

@ -1,441 +0,0 @@
package com.simplemobiletools.gallery.activities
import android.Manifest
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.AsyncTask
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.provider.MediaStore
import android.support.v4.app.ActivityCompat
import android.support.v7.widget.GridLayoutManager
import android.view.Menu
import android.view.MenuItem
import com.google.gson.Gson
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.models.Release
import com.simplemobiletools.gallery.BuildConfig
import com.simplemobiletools.gallery.R
import com.simplemobiletools.gallery.adapters.DirectoryAdapter
import com.simplemobiletools.gallery.asynctasks.GetDirectoriesAsynctask
import com.simplemobiletools.gallery.dialogs.ChangeSortingDialog
import com.simplemobiletools.gallery.extensions.*
import com.simplemobiletools.gallery.helpers.*
import com.simplemobiletools.gallery.models.Directory
import com.simplemobiletools.gallery.views.MyScalableRecyclerView
import kotlinx.android.synthetic.main.activity_main.*
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
import java.io.OutputStream
import java.util.*
class MainActivity : SimpleActivity(), DirectoryAdapter.DirOperationsListener {
private val STORAGE_PERMISSION = 1
private val PICK_MEDIA = 2
private val PICK_WALLPAPER = 3
private val LAST_MEDIA_CHECK_PERIOD = 3000L
lateinit var mDirs: ArrayList<Directory>
private var mIsPickImageIntent = false
private var mIsPickVideoIntent = false
private var mIsGetImageContentIntent = false
private var mIsGetVideoContentIntent = false
private var mIsGetAnyContentIntent = false
private var mIsSetWallpaperIntent = false
private var mIsThirdPartyIntent = false
private var mIsGettingDirs = false
private var mStoredAnimateGifs = true
private var mStoredCropThumbnails = true
private var mLoadedInitialPhotos = false
private var mLastMediaModified = 0
private var mLastMediaHandler = Handler()
private var mCurrAsyncTask: GetDirectoriesAsynctask? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mIsPickImageIntent = isPickImageIntent(intent)
mIsPickVideoIntent = isPickVideoIntent(intent)
mIsGetImageContentIntent = isGetImageContentIntent(intent)
mIsGetVideoContentIntent = isGetVideoContentIntent(intent)
mIsGetAnyContentIntent = isGetAnyContentIntent(intent)
mIsSetWallpaperIntent = isSetWallpaperIntent(intent)
mIsThirdPartyIntent = mIsPickImageIntent || mIsPickVideoIntent || mIsGetImageContentIntent || mIsGetVideoContentIntent ||
mIsGetAnyContentIntent || mIsSetWallpaperIntent
directories_refresh_layout.setOnRefreshListener({ getDirectories() })
mDirs = ArrayList<Directory>()
mStoredAnimateGifs = config.animateGifs
mStoredCropThumbnails = config.cropThumbnails
storeStoragePaths()
checkWhatsNewDialog()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
if (mIsThirdPartyIntent) {
menuInflater.inflate(R.menu.menu_main_intent, menu)
} else {
menuInflater.inflate(R.menu.menu_main, menu)
menu.findItem(R.id.increase_column_count).isVisible = config.dirColumnCnt < 10
menu.findItem(R.id.reduce_column_count).isVisible = config.dirColumnCnt > 1
}
menu.findItem(R.id.temporarily_show_hidden).isVisible = !config.showHiddenMedia
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.sort -> showSortingDialog()
R.id.open_camera -> launchCamera()
R.id.show_all -> showAllMedia()
R.id.temporarily_show_hidden -> temporarilyShowHidden()
R.id.increase_column_count -> increaseColumnCount()
R.id.reduce_column_count -> reduceColumnCount()
R.id.settings -> launchSettings()
R.id.about -> launchAbout()
else -> return super.onOptionsItemSelected(item)
}
return true
}
override fun onResume() {
super.onResume()
if (mStoredAnimateGifs != config.animateGifs) {
mDirs.clear()
}
if (mStoredCropThumbnails != config.cropThumbnails) {
mDirs.clear()
}
tryloadGallery()
invalidateOptionsMenu()
}
override fun onPause() {
super.onPause()
mCurrAsyncTask?.shouldStop = true
storeDirectories()
directories_refresh_layout.isRefreshing = false
mIsGettingDirs = false
mStoredAnimateGifs = config.animateGifs
mStoredCropThumbnails = config.cropThumbnails
MyScalableRecyclerView.mListener = null
mLastMediaHandler.removeCallbacksAndMessages(null)
}
override fun onDestroy() {
super.onDestroy()
config.temporarilyShowHidden = false
}
private fun tryloadGallery() {
if (hasWriteStoragePermission()) {
if (config.showAll)
showAllMedia()
else
getDirectories()
handleZooming()
checkIfColorChanged()
} else {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), STORAGE_PERMISSION)
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == STORAGE_PERMISSION) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
getDirectories()
} else {
toast(R.string.no_storage_permissions)
finish()
}
}
}
private fun getDirectories() {
if (mIsGettingDirs)
return
mIsGettingDirs = true
val dirs = getCachedDirectories()
if (dirs.isNotEmpty() && !mLoadedInitialPhotos) {
gotDirectories(dirs)
}
if (!mLoadedInitialPhotos) {
directories_refresh_layout.isRefreshing = true
}
mLoadedInitialPhotos = true
mCurrAsyncTask = GetDirectoriesAsynctask(applicationContext, mIsPickVideoIntent || mIsGetVideoContentIntent, mIsPickImageIntent || mIsGetImageContentIntent) {
gotDirectories(it)
}
mCurrAsyncTask!!.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)
}
private fun showSortingDialog() {
ChangeSortingDialog(this, true, false) {
getDirectories()
}
}
private fun showAllMedia() {
config.showAll = true
Intent(this, MediaActivity::class.java).apply {
putExtra(DIRECTORY, "/")
startActivity(this)
}
finish()
}
private fun temporarilyShowHidden() {
config.temporarilyShowHidden = true
getDirectories()
}
private fun checkIfColorChanged() {
if (directories_grid.adapter != null && getRecyclerAdapter().foregroundColor != config.primaryColor) {
getRecyclerAdapter().updatePrimaryColor(config.primaryColor)
directories_fastscroller.updateHandleColor()
}
}
override fun tryDeleteFolders(folders: ArrayList<File>) {
for (file in folders) {
deleteFolders(folders) {
runOnUiThread {
refreshItems()
}
}
}
}
private fun getRecyclerAdapter() = (directories_grid.adapter as DirectoryAdapter)
private fun handleZooming() {
val layoutManager = directories_grid.layoutManager as GridLayoutManager
layoutManager.spanCount = config.dirColumnCnt
MyScalableRecyclerView.mListener = object : MyScalableRecyclerView.MyScalableRecyclerViewListener {
override fun zoomIn() {
if (layoutManager.spanCount > 1) {
reduceColumnCount()
getRecyclerAdapter().actMode?.finish()
}
}
override fun zoomOut() {
if (layoutManager.spanCount < 10) {
increaseColumnCount()
getRecyclerAdapter().actMode?.finish()
}
}
override fun selectItem(position: Int) {
getRecyclerAdapter().selectItem(position)
}
override fun selectRange(initialSelection: Int, lastDraggedIndex: Int, minReached: Int, maxReached: Int) {
getRecyclerAdapter().selectRange(initialSelection, lastDraggedIndex, minReached, maxReached)
}
}
}
private fun increaseColumnCount() {
config.dirColumnCnt = ++(directories_grid.layoutManager as GridLayoutManager).spanCount
invalidateOptionsMenu()
}
private fun reduceColumnCount() {
config.dirColumnCnt = --(directories_grid.layoutManager as GridLayoutManager).spanCount
invalidateOptionsMenu()
}
private fun isPickImageIntent(intent: Intent) = isPickIntent(intent) && (hasImageContentData(intent) || isImageType(intent))
private fun isPickVideoIntent(intent: Intent) = isPickIntent(intent) && (hasVideoContentData(intent) || isVideoType(intent))
private fun isPickIntent(intent: Intent) = intent.action == Intent.ACTION_PICK
private fun isGetContentIntent(intent: Intent) = intent.action == Intent.ACTION_GET_CONTENT && intent.type != null
private fun isGetImageContentIntent(intent: Intent) = isGetContentIntent(intent) &&
(intent.type.startsWith("image/") || intent.type == MediaStore.Images.Media.CONTENT_TYPE)
private fun isGetVideoContentIntent(intent: Intent) = isGetContentIntent(intent) &&
(intent.type.startsWith("video/") || intent.type == MediaStore.Video.Media.CONTENT_TYPE)
private fun isGetAnyContentIntent(intent: Intent) = isGetContentIntent(intent) && intent.type == "*/*"
private fun isSetWallpaperIntent(intent: Intent?) = intent?.action == Intent.ACTION_SET_WALLPAPER
private fun hasImageContentData(intent: Intent) = (intent.data == MediaStore.Images.Media.EXTERNAL_CONTENT_URI ||
intent.data == MediaStore.Images.Media.INTERNAL_CONTENT_URI)
private fun hasVideoContentData(intent: Intent) = (intent.data == MediaStore.Video.Media.EXTERNAL_CONTENT_URI ||
intent.data == MediaStore.Video.Media.INTERNAL_CONTENT_URI)
private fun isImageType(intent: Intent) = (intent.type?.startsWith("image/") == true || intent.type == MediaStore.Images.Media.CONTENT_TYPE)
private fun isVideoType(intent: Intent) = (intent.type?.startsWith("video/") == true || intent.type == MediaStore.Video.Media.CONTENT_TYPE)
override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
if (resultCode == Activity.RESULT_OK) {
if (requestCode == PICK_MEDIA && resultData?.data != null) {
Intent().apply {
val path = resultData.data.path
val uri = Uri.fromFile(File(path))
if (mIsGetImageContentIntent || mIsGetVideoContentIntent || mIsGetAnyContentIntent) {
if (intent.extras?.containsKey(MediaStore.EXTRA_OUTPUT) == true) {
var inputStream: InputStream? = null
var outputStream: OutputStream? = null
try {
val output = intent.extras.get(MediaStore.EXTRA_OUTPUT) as Uri
inputStream = FileInputStream(File(path))
outputStream = contentResolver.openOutputStream(output)
inputStream.copyTo(outputStream)
} finally {
inputStream?.close()
outputStream?.close()
}
} else {
val type = File(path).getMimeType("image/jpeg")
setDataAndTypeAndNormalize(uri, type)
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
}
} else if (mIsPickImageIntent || mIsPickVideoIntent) {
data = uri
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
}
setResult(Activity.RESULT_OK, this)
}
finish()
} else if (requestCode == PICK_WALLPAPER) {
setResult(Activity.RESULT_OK)
finish()
}
}
super.onActivityResult(requestCode, resultCode, resultData)
}
private fun itemClicked(path: String) {
Intent(this, MediaActivity::class.java).apply {
putExtra(DIRECTORY, path)
if (mIsSetWallpaperIntent) {
putExtra(SET_WALLPAPER_INTENT, true)
startActivityForResult(this, PICK_WALLPAPER)
} else {
putExtra(GET_IMAGE_INTENT, mIsPickImageIntent || mIsGetImageContentIntent)
putExtra(GET_VIDEO_INTENT, mIsPickVideoIntent || mIsGetVideoContentIntent)
putExtra(GET_ANY_INTENT, mIsGetAnyContentIntent)
startActivityForResult(this, PICK_MEDIA)
}
}
}
private fun gotDirectories(dirs: ArrayList<Directory>) {
mLastMediaModified = getLastMediaModified()
directories_refresh_layout.isRefreshing = false
mIsGettingDirs = false
checkLastMediaChanged()
if (dirs.hashCode() == mDirs.hashCode())
return
mDirs = dirs
setupAdapter()
storeDirectories()
}
private fun storeDirectories() {
if (!config.temporarilyShowHidden) {
val directories = Gson().toJson(mDirs)
config.directories = directories
}
}
private fun setupAdapter() {
val adapter = DirectoryAdapter(this, mDirs, this) {
itemClicked(it.path)
}
val currAdapter = directories_grid.adapter
if (currAdapter != null) {
(currAdapter as DirectoryAdapter).updateDirs(mDirs)
} else {
directories_grid.adapter = adapter
}
directories_fastscroller.setViews(directories_grid, directories_refresh_layout)
}
private fun checkLastMediaChanged() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && isDestroyed)
return
mLastMediaHandler.removeCallbacksAndMessages(null)
mLastMediaHandler.postDelayed({
Thread({
val lastModified = getLastMediaModified()
if (mLastMediaModified != lastModified) {
mLastMediaModified = lastModified
runOnUiThread {
getDirectories()
}
} else {
checkLastMediaChanged()
}
}).start()
}, LAST_MEDIA_CHECK_PERIOD)
}
override fun refreshItems() {
getDirectories()
}
override fun itemLongClicked(position: Int) {
directories_grid.setDragSelectActive(position)
}
private fun checkWhatsNewDialog() {
arrayListOf<Release>().apply {
add(Release(46, R.string.release_46))
add(Release(47, R.string.release_47))
add(Release(49, R.string.release_49))
add(Release(50, R.string.release_50))
add(Release(51, R.string.release_51))
add(Release(52, R.string.release_52))
add(Release(54, R.string.release_54))
add(Release(58, R.string.release_58))
add(Release(62, R.string.release_62))
add(Release(65, R.string.release_65))
add(Release(66, R.string.release_66))
add(Release(69, R.string.release_69))
add(Release(70, R.string.release_70))
add(Release(72, R.string.release_72))
add(Release(74, R.string.release_74))
add(Release(76, R.string.release_76))
add(Release(77, R.string.release_77))
add(Release(83, R.string.release_83))
add(Release(84, R.string.release_84))
add(Release(88, R.string.release_88))
add(Release(89, R.string.release_89))
add(Release(93, R.string.release_93))
add(Release(94, R.string.release_94))
add(Release(97, R.string.release_97))
add(Release(98, R.string.release_98))
add(Release(108, R.string.release_108))
checkWhatsNew(this, BuildConfig.VERSION_CODE)
}
}
}

View file

@ -1,439 +0,0 @@
package com.simplemobiletools.gallery.activities
import android.app.Activity
import android.app.WallpaperManager
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.support.v7.widget.GridLayoutManager
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import com.bumptech.glide.Glide
import com.bumptech.glide.request.animation.GlideAnimation
import com.bumptech.glide.request.target.SimpleTarget
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.simplemobiletools.commons.dialogs.ConfirmationDialog
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.gallery.R
import com.simplemobiletools.gallery.adapters.MediaAdapter
import com.simplemobiletools.gallery.asynctasks.GetMediaAsynctask
import com.simplemobiletools.gallery.dialogs.ChangeSortingDialog
import com.simplemobiletools.gallery.dialogs.ExcludeFolderDialog
import com.simplemobiletools.gallery.extensions.*
import com.simplemobiletools.gallery.helpers.*
import com.simplemobiletools.gallery.models.Medium
import com.simplemobiletools.gallery.views.MyScalableRecyclerView
import kotlinx.android.synthetic.main.activity_media.*
import java.io.File
import java.io.IOException
class MediaActivity : SimpleActivity(), MediaAdapter.MediaOperationsListener {
private val TAG = MediaActivity::class.java.simpleName
private val SAVE_MEDIA_CNT = 40
private val LAST_MEDIA_CHECK_PERIOD = 3000L
private var mPath = ""
private var mIsGetImageIntent = false
private var mIsGetVideoIntent = false
private var mIsGetAnyIntent = false
private var mIsGettingMedia = false
private var mShowAll = false
private var mLoadedInitialPhotos = false
private var mStoredAnimateGifs = true
private var mStoredCropThumbnails = true
private var mLastDrawnHashCode = 0
private var mLastMediaModified = 0
private var mLastMediaHandler = Handler()
companion object {
var mMedia = ArrayList<Medium>()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_media)
intent.apply {
mIsGetImageIntent = getBooleanExtra(GET_IMAGE_INTENT, false)
mIsGetVideoIntent = getBooleanExtra(GET_VIDEO_INTENT, false)
mIsGetAnyIntent = getBooleanExtra(GET_ANY_INTENT, false)
}
media_refresh_layout.setOnRefreshListener({ getMedia() })
mPath = intent.getStringExtra(DIRECTORY)
mStoredAnimateGifs = config.animateGifs
mStoredCropThumbnails = config.cropThumbnails
mShowAll = config.showAll
if (mShowAll)
supportActionBar?.setDisplayHomeAsUpEnabled(false)
}
override fun onResume() {
super.onResume()
if (mShowAll && mStoredAnimateGifs != config.animateGifs) {
mMedia.clear()
}
if (mStoredCropThumbnails != config.cropThumbnails) {
mMedia.clear()
}
tryloadGallery()
invalidateOptionsMenu()
}
override fun onPause() {
super.onPause()
mIsGettingMedia = false
media_refresh_layout.isRefreshing = false
mStoredAnimateGifs = config.animateGifs
mStoredCropThumbnails = config.cropThumbnails
MyScalableRecyclerView.mListener = null
mLastMediaHandler.removeCallbacksAndMessages(null)
}
override fun onDestroy() {
super.onDestroy()
if (config.showAll)
config.temporarilyShowHidden = false
mMedia.clear()
}
private fun tryloadGallery() {
if (hasWriteStoragePermission()) {
val dirName = getHumanizedFilename(mPath)
title = if (mShowAll) resources.getString(R.string.all_folders) else dirName
getMedia()
handleZooming()
checkIfColorChanged()
} else {
finish()
}
}
private fun checkIfColorChanged() {
if (media_grid.adapter != null && getRecyclerAdapter().foregroundColor != config.primaryColor) {
getRecyclerAdapter().updatePrimaryColor(config.primaryColor)
media_fastscroller.updateHandleColor()
}
}
private fun setupAdapter() {
if (isDirEmpty())
return
val adapter = MediaAdapter(this, mMedia, this) {
itemClicked(it.path)
}
val currAdapter = media_grid.adapter
if (currAdapter != null) {
(currAdapter as MediaAdapter).updateMedia(mMedia)
} else {
media_grid.adapter = adapter
}
media_fastscroller.setViews(media_grid, media_refresh_layout)
}
private fun checkLastMediaChanged() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && isDestroyed)
return
mLastMediaHandler.removeCallbacksAndMessages(null)
mLastMediaHandler.postDelayed({
Thread({
val lastModified = getLastMediaModified()
if (mLastMediaModified != lastModified) {
mLastMediaModified = lastModified
runOnUiThread {
getMedia()
}
} else {
checkLastMediaChanged()
}
}).start()
}, LAST_MEDIA_CHECK_PERIOD)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_media, menu)
val isFolderHidden = File(mPath).containsNoMedia()
menu.apply {
findItem(R.id.hide_folder).isVisible = !isFolderHidden && !mShowAll
findItem(R.id.unhide_folder).isVisible = isFolderHidden && !mShowAll
findItem(R.id.folder_view).isVisible = mShowAll
findItem(R.id.open_camera).isVisible = mShowAll
findItem(R.id.about).isVisible = mShowAll
findItem(R.id.temporarily_show_hidden).isVisible = !config.showHiddenMedia
findItem(R.id.increase_column_count).isVisible = config.mediaColumnCnt < 10
findItem(R.id.reduce_column_count).isVisible = config.mediaColumnCnt > 1
}
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.sort -> showSortingDialog()
R.id.toggle_filename -> toggleFilenameVisibility()
R.id.open_camera -> launchCamera()
R.id.folder_view -> switchToFolderView()
R.id.hide_folder -> tryHideFolder()
R.id.unhide_folder -> unhideFolder()
R.id.exclude_folder -> tryExcludeFolder()
R.id.temporarily_show_hidden -> temporarilyShowHidden()
R.id.increase_column_count -> increaseColumnCount()
R.id.reduce_column_count -> reduceColumnCount()
R.id.settings -> launchSettings()
R.id.about -> launchAbout()
else -> return super.onOptionsItemSelected(item)
}
return true
}
private fun toggleFilenameVisibility() {
config.displayFileNames = !config.displayFileNames
if (media_grid.adapter != null)
getRecyclerAdapter().updateDisplayFilenames(config.displayFileNames)
}
private fun showSortingDialog() {
ChangeSortingDialog(this, false, !config.showAll, mPath) {
getMedia()
}
}
private fun switchToFolderView() {
config.showAll = false
startActivity(Intent(this, MainActivity::class.java))
finish()
}
private fun tryHideFolder() {
if (config.wasHideFolderTooltipShown) {
hideFolder()
} else {
ConfirmationDialog(this, getString(R.string.hide_folder_description)) {
config.wasHideFolderTooltipShown = true
hideFolder()
}
}
}
private fun hideFolder() {
addNoMedia(mPath) {
runOnUiThread {
if (!config.shouldShowHidden)
finish()
else
invalidateOptionsMenu()
}
}
}
private fun unhideFolder() {
removeNoMedia(mPath) {
runOnUiThread {
invalidateOptionsMenu()
}
}
}
private fun tryExcludeFolder() {
ExcludeFolderDialog(this, arrayListOf(mPath)) {
finish()
}
}
private fun deleteDirectoryIfEmpty() {
val file = File(mPath)
if (!file.isDownloadsFolder() && file.isDirectory && file.listFiles()?.isEmpty() == true) {
file.delete()
}
}
private fun getMedia() {
if (mIsGettingMedia)
return
mIsGettingMedia = true
val token = object : TypeToken<List<Medium>>() {}.type
val media = Gson().fromJson<ArrayList<Medium>>(config.loadFolderMedia(mPath), token) ?: ArrayList<Medium>(1)
if (media.isNotEmpty() && !mLoadedInitialPhotos) {
gotMedia(media)
} else {
media_refresh_layout.isRefreshing = true
}
mLoadedInitialPhotos = true
GetMediaAsynctask(applicationContext, mPath, mIsGetVideoIntent, mIsGetImageIntent, mShowAll) {
gotMedia(it)
}.execute()
}
private fun isDirEmpty(): Boolean {
return if (mMedia.size <= 0) {
deleteDirectoryIfEmpty()
finish()
true
} else
false
}
private fun temporarilyShowHidden() {
config.temporarilyShowHidden = true
getMedia()
}
private fun getRecyclerAdapter() = (media_grid.adapter as MediaAdapter)
private fun handleZooming() {
val layoutManager = media_grid.layoutManager as GridLayoutManager
layoutManager.spanCount = config.mediaColumnCnt
MyScalableRecyclerView.mListener = object : MyScalableRecyclerView.MyScalableRecyclerViewListener {
override fun zoomIn() {
if (layoutManager.spanCount > 1) {
reduceColumnCount()
getRecyclerAdapter().actMode?.finish()
}
}
override fun zoomOut() {
if (layoutManager.spanCount < 10) {
increaseColumnCount()
getRecyclerAdapter().actMode?.finish()
}
}
override fun selectItem(position: Int) {
getRecyclerAdapter().selectItem(position)
}
override fun selectRange(initialSelection: Int, lastDraggedIndex: Int, minReached: Int, maxReached: Int) {
getRecyclerAdapter().selectRange(initialSelection, lastDraggedIndex, minReached, maxReached)
}
}
}
private fun increaseColumnCount() {
config.mediaColumnCnt = ++(media_grid.layoutManager as GridLayoutManager).spanCount
invalidateOptionsMenu()
}
private fun reduceColumnCount() {
config.mediaColumnCnt = --(media_grid.layoutManager as GridLayoutManager).spanCount
invalidateOptionsMenu()
}
private fun isSetWallpaperIntent() = intent.getBooleanExtra(SET_WALLPAPER_INTENT, false)
override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
if (requestCode == REQUEST_EDIT_IMAGE) {
if (resultCode == Activity.RESULT_OK && resultData != null) {
mMedia.clear()
refreshItems()
}
}
super.onActivityResult(requestCode, resultCode, resultData)
}
private fun itemClicked(path: String) {
if (isSetWallpaperIntent()) {
toast(R.string.setting_wallpaper)
val wantedWidth = wallpaperDesiredMinimumWidth
val wantedHeight = wallpaperDesiredMinimumHeight
val ratio = wantedWidth.toFloat() / wantedHeight
Glide.with(this)
.load(File(path))
.asBitmap()
.override((wantedWidth * ratio).toInt(), wantedHeight)
.fitCenter()
.into(object : SimpleTarget<Bitmap>() {
override fun onResourceReady(bitmap: Bitmap?, glideAnimation: GlideAnimation<in Bitmap>?) {
try {
WallpaperManager.getInstance(applicationContext).setBitmap(bitmap)
setResult(Activity.RESULT_OK)
} catch (e: IOException) {
Log.e(TAG, "item click $e")
}
finish()
}
})
} else if (mIsGetImageIntent || mIsGetVideoIntent || mIsGetAnyIntent) {
Intent().apply {
data = Uri.parse(path)
setResult(Activity.RESULT_OK, this)
}
finish()
} else {
val file = File(path)
val isVideo = file.isVideoFast()
if (isVideo) {
openWith(file, false)
} else {
Intent(this, ViewPagerActivity::class.java).apply {
putExtra(MEDIUM, path)
putExtra(SHOW_ALL, mShowAll)
startActivity(this)
}
}
}
}
private fun gotMedia(media: ArrayList<Medium>) {
mLastMediaModified = getLastMediaModified()
mIsGettingMedia = false
media_refresh_layout.isRefreshing = false
checkLastMediaChanged()
if (mLastDrawnHashCode == 0)
mLastDrawnHashCode = media.hashCode()
if (media.hashCode() == mMedia.hashCode() && media.hashCode() == mLastDrawnHashCode)
return
mLastDrawnHashCode = media.hashCode()
mMedia = media
setupAdapter()
storeFolder()
}
private fun storeFolder() {
if (!config.temporarilyShowHidden) {
val subList = mMedia.subList(0, Math.min(SAVE_MEDIA_CNT, mMedia.size))
val json = Gson().toJson(subList)
config.saveFolderMedia(mPath, json)
}
}
override fun deleteFiles(files: ArrayList<File>) {
val filtered = files.filter { it.isImageVideoGif() } as ArrayList
deleteFiles(filtered) {
if (!it) {
toast(R.string.unknown_error_occurred)
} else if (mMedia.isEmpty()) {
finish()
}
}
}
override fun refreshItems() {
getMedia()
Handler().postDelayed({
getMedia()
}, 1000)
}
override fun itemLongClicked(position: Int) {
media_grid.setDragSelectActive(position)
}
}

View file

@ -1,166 +0,0 @@
package com.simplemobiletools.gallery.activities
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.database.Cursor
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import android.support.v4.app.ActivityCompat
import android.view.Menu
import android.view.MenuItem
import android.view.View
import com.simplemobiletools.commons.extensions.hasWriteStoragePermission
import com.simplemobiletools.commons.extensions.scanPath
import com.simplemobiletools.commons.extensions.toast
import com.simplemobiletools.gallery.R
import com.simplemobiletools.gallery.extensions.*
import com.simplemobiletools.gallery.fragments.PhotoFragment
import com.simplemobiletools.gallery.fragments.VideoFragment
import com.simplemobiletools.gallery.fragments.ViewPagerFragment
import com.simplemobiletools.gallery.helpers.IS_VIEW_INTENT
import com.simplemobiletools.gallery.helpers.MEDIUM
import com.simplemobiletools.gallery.models.Medium
import kotlinx.android.synthetic.main.fragment_holder.*
import java.io.File
open class PhotoVideoActivity : SimpleActivity(), ViewPagerFragment.FragmentListener {
private val STORAGE_PERMISSION = 1
private var mMedium: Medium? = null
private var mIsFullScreen = false
lateinit var mUri: Uri
lateinit var mFragment: ViewPagerFragment
companion object {
var mIsVideo = false
}
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.fragment_holder)
if (hasWriteStoragePermission()) {
checkIntent(savedInstanceState)
} else {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), STORAGE_PERMISSION)
}
}
private fun checkIntent(savedInstanceState: Bundle? = null) {
mUri = intent.data ?: return
if (mUri.scheme == "file") {
scanPath(mUri.path) {}
sendViewPagerIntent(mUri.path)
finish()
return
} else {
val path = applicationContext.getRealPathFromURI(mUri) ?: ""
scanPath(mUri.path) {}
if (path.isNotEmpty()) {
sendViewPagerIntent(path)
finish()
return
}
}
showSystemUI()
val bundle = Bundle()
val file = File(mUri.toString())
mMedium = Medium(file.name, mUri.toString(), mIsVideo, 0, 0, file.length())
bundle.putSerializable(MEDIUM, mMedium)
if (savedInstanceState == null) {
mFragment = if (mIsVideo) VideoFragment() else PhotoFragment()
mFragment.listener = this
mFragment.arguments = bundle
supportFragmentManager.beginTransaction().replace(R.id.fragment_holder, mFragment).commit()
}
if (config.darkBackground)
fragment_holder.background = ColorDrawable(Color.BLACK)
val proj = arrayOf(MediaStore.Images.Media.TITLE)
var cursor: Cursor? = null
try {
cursor = contentResolver.query(mUri, proj, null, null, null)
if (cursor != null && cursor.count != 0) {
val columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.TITLE)
cursor.moveToFirst()
title = cursor.getString(columnIndex)
}
} catch (e: Exception) {
title = mMedium?.name ?: ""
} finally {
cursor?.close()
}
}
override fun onResume() {
super.onResume()
supportActionBar?.setBackgroundDrawable(resources.getDrawable(R.drawable.actionbar_gradient_background))
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == STORAGE_PERMISSION) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
checkIntent()
} else {
toast(R.string.no_storage_permissions)
finish()
}
}
}
private fun sendViewPagerIntent(path: String) {
Intent(this, ViewPagerActivity::class.java).apply {
putExtra(IS_VIEW_INTENT, true)
putExtra(MEDIUM, path)
startActivity(this)
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.photo_video_menu, menu)
menu.findItem(R.id.menu_set_as_wallpaper).isVisible = mMedium?.isImage() == true
menu.findItem(R.id.menu_edit).isVisible = mMedium?.isImage() == true
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_set_as_wallpaper -> trySetAsWallpaper(File(mMedium!!.path))
R.id.menu_open_with -> openWith(File(mMedium!!.path))
R.id.menu_share -> shareUri(mMedium!!, mUri)
R.id.menu_edit -> openEditor(File(mMedium!!.path))
else -> return super.onOptionsItemSelected(item)
}
return true
}
override fun fragmentClicked() {
mIsFullScreen = !mIsFullScreen
if (mIsFullScreen) {
hideSystemUI()
} else {
showSystemUI()
}
}
override fun systemUiVisibilityChanged(visibility: Int) {
if (visibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0) {
mIsFullScreen = false
showSystemUI()
} else {
mIsFullScreen = true
}
}
}

View file

@ -1,156 +0,0 @@
package com.simplemobiletools.gallery.activities
import android.content.Intent
import android.content.res.Resources
import android.os.Bundle
import com.simplemobiletools.commons.dialogs.RadioGroupDialog
import com.simplemobiletools.commons.extensions.updateTextColors
import com.simplemobiletools.commons.models.RadioItem
import com.simplemobiletools.gallery.R
import com.simplemobiletools.gallery.extensions.config
import com.simplemobiletools.gallery.helpers.*
import kotlinx.android.synthetic.main.activity_settings.*
class SettingsActivity : SimpleActivity() {
lateinit var res: Resources
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
res = resources
}
override fun onResume() {
super.onResume()
setupCustomizeColors()
setupManageIncludedFolders()
setupManageExcludedFolders()
setupShowHiddenFolders()
setupAutoplayVideos()
setupLoopVideos()
setupAnimateGifs()
setupMaxBrightness()
setupCropThumbnails()
setupDarkBackground()
setupScreenRotation()
setupShowMedia()
updateTextColors(settings_holder)
}
private fun setupCustomizeColors() {
settings_customize_colors_holder.setOnClickListener {
startCustomizationActivity()
}
}
private fun setupManageIncludedFolders() {
settings_manage_included_folders_holder.setOnClickListener {
startActivity(Intent(this, IncludedFoldersActivity::class.java))
}
}
private fun setupManageExcludedFolders() {
settings_manage_excluded_folders_holder.setOnClickListener {
startActivity(Intent(this, ExcludedFoldersActivity::class.java))
}
}
private fun setupShowHiddenFolders() {
settings_show_hidden_folders.isChecked = config.showHiddenMedia
settings_show_hidden_folders_holder.setOnClickListener {
settings_show_hidden_folders.toggle()
config.showHiddenMedia = settings_show_hidden_folders.isChecked
}
}
private fun setupAutoplayVideos() {
settings_autoplay_videos.isChecked = config.autoplayVideos
settings_autoplay_videos_holder.setOnClickListener {
settings_autoplay_videos.toggle()
config.autoplayVideos = settings_autoplay_videos.isChecked
}
}
private fun setupLoopVideos() {
settings_loop_videos.isChecked = config.loopVideos
settings_loop_videos_holder.setOnClickListener {
settings_loop_videos.toggle()
config.loopVideos = settings_loop_videos.isChecked
}
}
private fun setupAnimateGifs() {
settings_animate_gifs.isChecked = config.animateGifs
settings_animate_gifs_holder.setOnClickListener {
settings_animate_gifs.toggle()
config.animateGifs = settings_animate_gifs.isChecked
}
}
private fun setupMaxBrightness() {
settings_max_brightness.isChecked = config.maxBrightness
settings_max_brightness_holder.setOnClickListener {
settings_max_brightness.toggle()
config.maxBrightness = settings_max_brightness.isChecked
}
}
private fun setupCropThumbnails() {
settings_crop_thumbnails.isChecked = config.cropThumbnails
settings_crop_thumbnails_holder.setOnClickListener {
settings_crop_thumbnails.toggle()
config.cropThumbnails = settings_crop_thumbnails.isChecked
}
}
private fun setupDarkBackground() {
settings_dark_background.isChecked = config.darkBackground
settings_dark_background_holder.setOnClickListener {
settings_dark_background.toggle()
config.darkBackground = settings_dark_background.isChecked
}
}
private fun setupScreenRotation() {
settings_screen_rotation.text = getScreenRotationText()
settings_screen_rotation_holder.setOnClickListener {
val items = arrayListOf(
RadioItem(ROTATE_BY_SYSTEM_SETTING, res.getString(R.string.screen_rotation_system_setting)),
RadioItem(ROTATE_BY_DEVICE_ROTATION, res.getString(R.string.screen_rotation_device_rotation)),
RadioItem(ROTATE_BY_ASPECT_RATIO, res.getString(R.string.screen_rotation_aspect_ratio)))
RadioGroupDialog(this@SettingsActivity, items, config.screenRotation) {
config.screenRotation = it as Int
settings_screen_rotation.text = getScreenRotationText()
}
}
}
private fun getScreenRotationText() = getString(when (config.screenRotation) {
ROTATE_BY_SYSTEM_SETTING -> R.string.screen_rotation_system_setting
ROTATE_BY_DEVICE_ROTATION -> R.string.screen_rotation_device_rotation
else -> R.string.screen_rotation_aspect_ratio
})
private fun setupShowMedia() {
settings_show_media.text = getShowMediaText()
settings_show_media_holder.setOnClickListener {
val items = arrayListOf(
RadioItem(IMAGES_AND_VIDEOS, res.getString(R.string.images_and_videos)),
RadioItem(IMAGES, res.getString(R.string.images)),
RadioItem(VIDEOS, res.getString(R.string.videos)))
RadioGroupDialog(this@SettingsActivity, items, config.showMedia) {
config.showMedia = it as Int
settings_show_media.text = getShowMediaText()
}
}
}
private fun getShowMediaText() = getString(when (config.showMedia) {
IMAGES_AND_VIDEOS -> R.string.images_and_videos
IMAGES -> R.string.images
else -> R.string.videos
})
}

View file

@ -1,22 +0,0 @@
package com.simplemobiletools.gallery.activities
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.toast
import com.simplemobiletools.gallery.R
import com.simplemobiletools.gallery.dialogs.PickDirectoryDialog
import java.io.File
import java.util.*
open class SimpleActivity : BaseSimpleActivity() {
fun tryCopyMoveFilesTo(files: ArrayList<File>, isCopyOperation: Boolean, callback: () -> Unit) {
if (files.isEmpty()) {
toast(R.string.unknown_error_occurred)
return
}
val source = if (files[0].isFile) files[0].parent else files[0].absolutePath
PickDirectoryDialog(this, source) {
copyMoveFilesTo(files, source.trimEnd('/'), it, isCopyOperation, true, callback)
}
}
}

View file

@ -1,13 +0,0 @@
package com.simplemobiletools.gallery.activities
import android.content.Intent
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
class SplashActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startActivity(Intent(this, MainActivity::class.java))
finish()
}
}

View file

@ -1,543 +0,0 @@
package com.simplemobiletools.gallery.activities
import android.app.Activity
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.database.Cursor
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Color
import android.graphics.Matrix
import android.graphics.drawable.ColorDrawable
import android.hardware.SensorManager
import android.media.ExifInterface
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.support.v4.view.ViewPager
import android.util.DisplayMetrics
import android.view.Menu
import android.view.MenuItem
import android.view.OrientationEventListener
import android.view.View
import com.simplemobiletools.commons.dialogs.ConfirmationDialog
import com.simplemobiletools.commons.dialogs.PropertiesDialog
import com.simplemobiletools.commons.dialogs.RenameItemDialog
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.gallery.R
import com.simplemobiletools.gallery.activities.MediaActivity.Companion.mMedia
import com.simplemobiletools.gallery.adapters.MyPagerAdapter
import com.simplemobiletools.gallery.asynctasks.GetMediaAsynctask
import com.simplemobiletools.gallery.dialogs.SaveAsDialog
import com.simplemobiletools.gallery.extensions.*
import com.simplemobiletools.gallery.fragments.PhotoFragment
import com.simplemobiletools.gallery.fragments.ViewPagerFragment
import com.simplemobiletools.gallery.helpers.*
import com.simplemobiletools.gallery.models.Medium
import kotlinx.android.synthetic.main.activity_medium.*
import java.io.File
import java.io.OutputStream
import java.util.*
class ViewPagerActivity : SimpleActivity(), ViewPager.OnPageChangeListener, ViewPagerFragment.FragmentListener {
lateinit var mOrientationEventListener: OrientationEventListener
private var mPath = ""
private var mDirectory = ""
private var mIsFullScreen = false
private var mPos = -1
private var mShowAll = false
private var mRotationDegrees = 0f
private var mLastHandledOrientation = 0
private var mPrevHashcode = 0
companion object {
var screenWidth = 0
var screenHeight = 0
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_medium)
if (!hasWriteStoragePermission()) {
finish()
return
}
measureScreen()
val uri = intent.data
if (uri != null) {
var cursor: Cursor? = null
try {
val proj = arrayOf(MediaStore.Images.Media.DATA)
cursor = contentResolver.query(uri, proj, null, null, null)
if (cursor?.moveToFirst() == true) {
mPath = cursor.getStringValue(MediaStore.Images.Media.DATA)
}
} finally {
cursor?.close()
}
} else {
mPath = intent.getStringExtra(MEDIUM)
mShowAll = config.showAll
}
if (mPath.isEmpty()) {
toast(R.string.unknown_error_occurred)
finish()
return
}
if (intent.extras?.containsKey(IS_VIEW_INTENT) == true) {
config.temporarilyShowHidden = true
}
showSystemUI()
mDirectory = File(mPath).parent
title = mPath.getFilenameFromPath()
if (mMedia.isNotEmpty()) {
gotMedia(mMedia)
}
reloadViewPager()
scanPath(mPath) {}
setupOrientationEventListener()
if (config.darkBackground)
view_pager.background = ColorDrawable(Color.BLACK)
}
override fun onDestroy() {
super.onDestroy()
if (intent.extras?.containsKey(IS_VIEW_INTENT) == true) {
config.temporarilyShowHidden = false
}
}
private fun setupOrientationEventListener() {
mOrientationEventListener = object : OrientationEventListener(this, SensorManager.SENSOR_DELAY_NORMAL) {
override fun onOrientationChanged(orientation: Int) {
val currOrient = if (orientation in 45..134) {
ORIENT_LANDSCAPE_RIGHT
} else if (orientation in 225..314) {
ORIENT_LANDSCAPE_LEFT
} else {
ORIENT_PORTRAIT
}
if (mLastHandledOrientation != currOrient) {
mLastHandledOrientation = currOrient
if (currOrient == ORIENT_LANDSCAPE_LEFT) {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
} else if (currOrient == ORIENT_LANDSCAPE_RIGHT) {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
} else {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
}
}
}
}
override fun onResume() {
super.onResume()
if (!hasWriteStoragePermission()) {
finish()
}
supportActionBar?.setBackgroundDrawable(resources.getDrawable(R.drawable.actionbar_gradient_background))
if (config.maxBrightness) {
val attributes = window.attributes
attributes.screenBrightness = 1f
window.attributes = attributes
}
if (config.screenRotation == ROTATE_BY_DEVICE_ROTATION && mOrientationEventListener.canDetectOrientation()) {
mOrientationEventListener.enable()
} else if (config.screenRotation == ROTATE_BY_SYSTEM_SETTING) {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}
}
override fun onPause() {
super.onPause()
mOrientationEventListener.disable()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_viewpager, menu)
if (getCurrentMedium() == null)
return true
menu.apply {
findItem(R.id.menu_set_as_wallpaper).isVisible = getCurrentMedium()!!.isImage() == true
findItem(R.id.menu_edit).isVisible = getCurrentMedium()!!.isImage() == true
findItem(R.id.menu_rotate).isVisible = getCurrentMedium()!!.isImage() == true
findItem(R.id.menu_save_as).isVisible = mRotationDegrees != 0f
findItem(R.id.menu_hide).isVisible = !getCurrentMedium()!!.name.startsWith('.')
findItem(R.id.menu_unhide).isVisible = getCurrentMedium()!!.name.startsWith('.')
findItem(R.id.menu_rotate).subMenu.apply {
clearHeader()
findItem(R.id.rotate_right).icon = resources.getColoredDrawable(R.drawable.ic_rotate_right, R.color.actionbar_menu_icon)
findItem(R.id.rotate_left).icon = resources.getColoredDrawable(R.drawable.ic_rotate_left, R.color.actionbar_menu_icon)
findItem(R.id.rotate_one_eighty).icon = resources.getColoredDrawable(R.drawable.ic_rotate_one_eighty, R.color.actionbar_menu_icon)
}
}
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (getCurrentMedium() == null)
return true
when (item.itemId) {
R.id.menu_set_as_wallpaper -> trySetAsWallpaper(getCurrentFile())
R.id.menu_copy_to -> copyMoveTo(true)
R.id.menu_move_to -> copyMoveTo(false)
R.id.menu_open_with -> openWith(getCurrentFile())
R.id.menu_hide -> toggleFileVisibility(true)
R.id.menu_unhide -> toggleFileVisibility(false)
R.id.menu_share -> shareMedium(getCurrentMedium()!!)
R.id.menu_delete -> askConfirmDelete()
R.id.menu_rename -> renameFile()
R.id.menu_edit -> openEditor(getCurrentFile())
R.id.menu_properties -> showProperties()
R.id.menu_save_as -> saveImageAs()
R.id.show_on_map -> showOnMap()
R.id.rotate_right -> rotateImage(90f)
R.id.rotate_left -> rotateImage(-90f)
R.id.rotate_one_eighty -> rotateImage(180f)
R.id.settings -> launchSettings()
else -> return super.onOptionsItemSelected(item)
}
return true
}
private fun updatePagerItems() {
val pagerAdapter = MyPagerAdapter(this, supportFragmentManager, mMedia)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1 || !isDestroyed) {
view_pager.apply {
adapter = pagerAdapter
currentItem = mPos
addOnPageChangeListener(this@ViewPagerActivity)
}
}
}
private fun copyMoveTo(isCopyOperation: Boolean) {
val files = ArrayList<File>(1).apply { add(getCurrentFile()) }
tryCopyMoveFilesTo(files, isCopyOperation) {
if (!isCopyOperation) {
reloadViewPager()
}
}
}
private fun toggleFileVisibility(hide: Boolean) {
toggleFileVisibility(getCurrentFile(), hide) {
val newFileName = it.absolutePath.getFilenameFromPath()
title = newFileName
getCurrentMedium()!!.apply {
name = newFileName
path = it.absolutePath
mMedia[mPos] = this
}
invalidateOptionsMenu()
}
}
private fun saveImageAs() {
val currPath = getCurrentPath()
SaveAsDialog(this, currPath) {
Thread({
toast(R.string.saving)
val selectedFile = File(it)
val tmpFile = File(selectedFile.parent, "tmp_${it.getFilenameFromPath()}")
try {
val bitmap = BitmapFactory.decodeFile(currPath)
getFileOutputStream(tmpFile) {
saveFile(tmpFile, bitmap, it)
if (needsStupidWritePermissions(selectedFile.absolutePath)) {
deleteFile(selectedFile) {}
}
renameFile(tmpFile, selectedFile) {
deleteFile(tmpFile) {}
}
}
} catch (e: OutOfMemoryError) {
toast(R.string.out_of_memory_error)
deleteFile(tmpFile) {}
} catch (e: Exception) {
toast(R.string.unknown_error_occurred)
deleteFile(tmpFile) {}
}
}).start()
}
}
private fun saveFile(file: File, bitmap: Bitmap, out: OutputStream) {
val matrix = Matrix()
matrix.postRotate(mRotationDegrees)
val bmp = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
bmp.compress(file.getCompressionFormat(), 90, out)
out.flush()
toast(R.string.file_saved)
out.close()
}
private fun rotateImage(degrees: Float) {
mRotationDegrees = (mRotationDegrees + degrees) % 360
getCurrentFragment()?.rotateImageViewBy(mRotationDegrees)
supportInvalidateOptionsMenu()
}
private fun getCurrentFragment(): PhotoFragment? {
val fragment = (view_pager.adapter as MyPagerAdapter).getCurrentFragment(view_pager.currentItem)
return if (fragment is PhotoFragment)
fragment
else
null
}
private fun showProperties() {
if (getCurrentMedium() != null)
PropertiesDialog(this, getCurrentPath(), false)
}
private fun showOnMap() {
val exif = ExifInterface(getCurrentPath())
val lat = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE)
val lat_ref = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF)
val lon = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE)
val lon_ref = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF)
if (lat == null || lat_ref == null || lon == null || lon_ref == null) {
toast(R.string.unknown_location)
} else {
val geoLat = if (lat_ref == "N") {
convertToDegree(lat)
} else {
0 - convertToDegree(lat)
}
val geoLon = if (lon_ref == "E") {
convertToDegree(lon)
} else {
0 - convertToDegree(lon)
}
val uriBegin = "geo:$geoLat,$geoLon"
val query = "$geoLat, $geoLon"
val encodedQuery = Uri.encode(query)
val uriString = "$uriBegin?q=$encodedQuery&z=16"
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(uriString))
val packageManager = packageManager
if (intent.resolveActivity(packageManager) != null) {
startActivity(intent)
} else {
toast(R.string.no_map_application)
}
}
}
private fun convertToDegree(stringDMS: String): Float {
val dms = stringDMS.split(",".toRegex(), 3).toTypedArray()
val stringD = dms[0].split("/".toRegex(), 2).toTypedArray()
val d0 = stringD[0].toDouble()
val d1 = stringD[1].toDouble()
val floatD = d0 / d1
val stringM = dms[1].split("/".toRegex(), 2).toTypedArray()
val m0 = stringM[0].toDouble()
val m1 = stringM[1].toDouble()
val floatM = m0 / m1
val stringS = dms[2].split("/".toRegex(), 2).toTypedArray()
val s0 = stringS[0].toDouble()
val s1 = stringS[1].toDouble()
val floatS = s0 / s1
return (floatD + floatM / 60 + floatS / 3600).toFloat()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
if (requestCode == REQUEST_EDIT_IMAGE) {
if (resultCode == Activity.RESULT_OK && resultData != null) {
mPos = -1
reloadViewPager()
}
} else if (requestCode == REQUEST_SET_WALLPAPER) {
if (resultCode == Activity.RESULT_OK) {
toast(R.string.wallpaper_set_successfully)
}
}
super.onActivityResult(requestCode, resultCode, resultData)
}
private fun askConfirmDelete() {
ConfirmationDialog(this) {
deleteFileBg(File(mMedia[mPos].path)) {
reloadViewPager()
}
}
}
private fun isDirEmpty(media: ArrayList<Medium>): Boolean {
return if (media.isEmpty()) {
deleteDirectoryIfEmpty()
finish()
true
} else
false
}
private fun renameFile() {
RenameItemDialog(this, getCurrentPath()) {
mMedia[mPos].path = it
updateActionbarTitle()
}
}
override fun onConfigurationChanged(newConfig: Configuration?) {
super.onConfigurationChanged(newConfig)
measureScreen()
}
private fun measureScreen() {
val metrics = DisplayMetrics()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
windowManager.defaultDisplay.getRealMetrics(metrics)
screenWidth = metrics.widthPixels
screenHeight = metrics.heightPixels
} else {
windowManager.defaultDisplay.getMetrics(metrics)
screenWidth = metrics.widthPixels
screenHeight = metrics.heightPixels
}
}
private fun reloadViewPager() {
GetMediaAsynctask(applicationContext, mDirectory, false, false, mShowAll) {
gotMedia(it)
}.execute()
}
private fun gotMedia(media: ArrayList<Medium>) {
if (isDirEmpty(media) || media.hashCode() == mPrevHashcode) {
return
}
mPrevHashcode = media.hashCode()
mMedia = media
if (mPos == -1) {
mPos = getProperPosition()
} else {
mPos = Math.min(mPos, mMedia.size - 1)
}
updateActionbarTitle()
updatePagerItems()
invalidateOptionsMenu()
checkOrientation()
}
private fun getProperPosition(): Int {
mPos = 0
var i = 0
for (medium in mMedia) {
if (medium.path == mPath) {
return i
}
i++
}
return mPos
}
private fun deleteDirectoryIfEmpty() {
val file = File(mDirectory)
if (!file.isDownloadsFolder() && file.isDirectory && file.listFiles()?.isEmpty() == true) {
file.delete()
}
scanPath(mDirectory) {}
}
private fun checkOrientation() {
if (config.screenRotation == ROTATE_BY_ASPECT_RATIO) {
val res = getCurrentFile().getResolution()
if (res.x > res.y) {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
} else if (res.x < res.y) {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
}
}
override fun fragmentClicked() {
mIsFullScreen = !mIsFullScreen
if (mIsFullScreen) {
hideSystemUI()
} else {
showSystemUI()
}
}
override fun systemUiVisibilityChanged(visibility: Int) {
if (visibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0) {
mIsFullScreen = false
showSystemUI()
} else {
mIsFullScreen = true
}
}
private fun updateActionbarTitle() {
runOnUiThread {
if (mPos < mMedia.size) {
title = mMedia[mPos].path.getFilenameFromPath()
}
}
}
private fun getCurrentMedium(): Medium? {
return if (mMedia.isEmpty() || mPos == -1)
null
else
mMedia[Math.min(mPos, mMedia.size - 1)]
}
private fun getCurrentPath() = getCurrentMedium()!!.path
private fun getCurrentFile() = File(getCurrentPath())
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
}
override fun onPageSelected(position: Int) {
if (view_pager.offscreenPageLimit == 1) {
view_pager.offscreenPageLimit = 2
}
mPos = position
updateActionbarTitle()
mRotationDegrees = 0f
supportInvalidateOptionsMenu()
}
override fun onPageScrollStateChanged(state: Int) {
if (state == ViewPager.SCROLL_STATE_IDLE) {
checkOrientation()
}
}
}

View file

@ -1,479 +0,0 @@
package com.simplemobiletools.gallery.adapters
import android.os.Build
import android.support.v7.view.ActionMode
import android.support.v7.widget.RecyclerView
import android.util.SparseArray
import android.view.*
import android.widget.FrameLayout
import com.bignerdranch.android.multiselector.ModalMultiSelectorCallback
import com.bignerdranch.android.multiselector.MultiSelector
import com.bignerdranch.android.multiselector.SwappingHolder
import com.bumptech.glide.Glide
import com.google.gson.Gson
import com.simplemobiletools.commons.dialogs.ConfirmationDialog
import com.simplemobiletools.commons.dialogs.PropertiesDialog
import com.simplemobiletools.commons.dialogs.RenameItemDialog
import com.simplemobiletools.commons.extensions.isAStorageRootFolder
import com.simplemobiletools.commons.extensions.isImageVideoGif
import com.simplemobiletools.commons.extensions.needsStupidWritePermissions
import com.simplemobiletools.commons.extensions.toast
import com.simplemobiletools.gallery.R
import com.simplemobiletools.gallery.activities.SimpleActivity
import com.simplemobiletools.gallery.dialogs.ExcludeFolderDialog
import com.simplemobiletools.gallery.dialogs.PickMediumDialog
import com.simplemobiletools.gallery.extensions.*
import com.simplemobiletools.gallery.models.AlbumCover
import com.simplemobiletools.gallery.models.Directory
import kotlinx.android.synthetic.main.directory_item.view.*
import kotlinx.android.synthetic.main.directory_tmb.view.*
import java.io.File
import java.util.*
class DirectoryAdapter(val activity: SimpleActivity, var dirs: MutableList<Directory>, val listener: DirOperationsListener?, val itemClick: (Directory) -> Unit) :
RecyclerView.Adapter<DirectoryAdapter.ViewHolder>() {
val multiSelector = MultiSelector()
val config = activity.config
var actMode: ActionMode? = null
var itemViews = SparseArray<View>()
val selectedPositions = HashSet<Int>()
var foregroundColor = 0
var pinnedFolders = config.pinnedFolders
fun toggleItemSelection(select: Boolean, pos: Int) {
if (itemViews[pos] != null)
getProperView(itemViews[pos]!!).isSelected = select
if (select)
selectedPositions.add(pos)
else
selectedPositions.remove(pos)
if (selectedPositions.isEmpty()) {
actMode?.finish()
return
}
updateTitle(selectedPositions.size)
actMode?.invalidate()
}
fun getProperView(itemView: View): View {
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
itemView.dir_frame
else
itemView.dir_thumbnail
}
fun updateTitle(cnt: Int) {
actMode?.title = "$cnt / ${dirs.size}"
}
fun updatePrimaryColor(color: Int) {
foregroundColor = color
(0..itemViews.size() - 1).mapNotNull { itemViews[it] }
.forEach { setupItemViewForeground(it) }
}
private fun setupItemViewForeground(itemView: View) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
(getProperView(itemView) as FrameLayout).foreground = foregroundColor.createSelector()
else
getProperView(itemView).foreground = foregroundColor.createSelector()
}
val adapterListener = object : MyAdapterListener {
override fun toggleItemSelectionAdapter(select: Boolean, position: Int) {
toggleItemSelection(select, position)
}
override fun setupItemForeground(itemView: View) {
setupItemViewForeground(itemView)
}
override fun getSelectedPositions(): HashSet<Int> = selectedPositions
}
init {
foregroundColor = config.primaryColor
}
val multiSelectorMode = object : ModalMultiSelectorCallback(multiSelector) {
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
when (item.itemId) {
R.id.cab_properties -> showProperties()
R.id.cab_rename -> renameDir()
R.id.cab_pin -> pinFolders(true)
R.id.cab_unpin -> pinFolders(false)
R.id.cab_hide -> toggleFoldersVisibility(true)
R.id.cab_unhide -> toggleFoldersVisibility(false)
R.id.cab_exclude -> tryExcludeFolder()
R.id.cab_copy_to -> copyMoveTo(true)
R.id.cab_move_to -> copyMoveTo(false)
R.id.cab_select_all -> selectAll()
R.id.cab_delete -> askConfirmDelete()
R.id.cab_select_photo -> changeAlbumCover(false)
R.id.cab_use_default -> changeAlbumCover(true)
else -> return false
}
return true
}
override fun onCreateActionMode(actionMode: ActionMode?, menu: Menu?): Boolean {
super.onCreateActionMode(actionMode, menu)
actMode = actionMode
activity.menuInflater.inflate(R.menu.cab_directories, menu)
return true
}
override fun onPrepareActionMode(actionMode: ActionMode?, menu: Menu): Boolean {
menu.findItem(R.id.cab_rename).isVisible = selectedPositions.size <= 1
menu.findItem(R.id.cab_change_cover_image).isVisible = selectedPositions.size <= 1
checkHideBtnVisibility(menu)
checkPinBtnVisibility(menu)
return true
}
override fun onDestroyActionMode(actionMode: ActionMode?) {
super.onDestroyActionMode(actionMode)
selectedPositions.forEach {
if (itemViews[it] != null)
getProperView(itemViews[it]!!).isSelected = false
}
selectedPositions.clear()
actMode = null
}
fun checkHideBtnVisibility(menu: Menu) {
var hiddenCnt = 0
var unhiddenCnt = 0
selectedPositions.map { dirs.getOrNull(it)?.path }.filterNotNull().forEach {
if (File(it).containsNoMedia())
hiddenCnt++
else
unhiddenCnt++
}
menu.findItem(R.id.cab_hide).isVisible = unhiddenCnt > 0
menu.findItem(R.id.cab_unhide).isVisible = hiddenCnt > 0
}
fun checkPinBtnVisibility(menu: Menu) {
val pinnedFolders = config.pinnedFolders
var pinnedCnt = 0
var unpinnedCnt = 0
selectedPositions.map { dirs.getOrNull(it)?.path }.filterNotNull().forEach {
if (pinnedFolders.contains(it))
pinnedCnt++
else
unpinnedCnt++
}
menu.findItem(R.id.cab_pin).isVisible = unpinnedCnt > 0
menu.findItem(R.id.cab_unpin).isVisible = pinnedCnt > 0
}
}
private fun showProperties() {
if (selectedPositions.size <= 1) {
PropertiesDialog(activity, dirs[selectedPositions.first()].path, config.shouldShowHidden)
} else {
val paths = ArrayList<String>()
selectedPositions.forEach { paths.add(dirs[it].path) }
PropertiesDialog(activity, paths, config.shouldShowHidden)
}
}
private fun renameDir() {
val path = dirs[selectedPositions.first()].path
val dir = File(path)
if (activity.isAStorageRootFolder(dir.absolutePath)) {
activity.toast(R.string.rename_folder_root)
return
}
RenameItemDialog(activity, dir.absolutePath) {
activity.runOnUiThread {
listener?.refreshItems()
actMode?.finish()
}
}
}
private fun toggleFoldersVisibility(hide: Boolean) {
getSelectedPaths().forEach {
if (hide) {
if (config.wasHideFolderTooltipShown) {
hideFolder(it)
} else {
config.wasHideFolderTooltipShown = true
ConfirmationDialog(activity, activity.getString(R.string.hide_folder_description)) {
hideFolder(it)
}
}
} else {
activity.removeNoMedia(it) {
noMediaHandled()
}
}
}
}
private fun hideFolder(path: String) {
activity.addNoMedia(path) {
noMediaHandled()
}
}
private fun tryExcludeFolder() {
ExcludeFolderDialog(activity, getSelectedPaths().toList()) {
listener?.refreshItems()
actMode?.finish()
}
}
private fun noMediaHandled() {
activity.runOnUiThread {
listener?.refreshItems()
actMode?.finish()
}
}
private fun pinFolders(pin: Boolean) {
if (pin)
config.addPinnedFolders(getSelectedPaths())
else
config.removePinnedFolders(getSelectedPaths())
pinnedFolders = config.pinnedFolders
listener?.refreshItems()
notifyDataSetChanged()
actMode?.finish()
}
private fun copyMoveTo(isCopyOperation: Boolean) {
if (selectedPositions.isEmpty())
return
val files = ArrayList<File>()
selectedPositions.forEach {
val dir = File(dirs[it].path)
files.addAll(dir.listFiles().filter { it.isFile && it.isImageVideoGif() })
}
activity.tryCopyMoveFilesTo(files, isCopyOperation) {
listener?.refreshItems()
actMode?.finish()
}
}
fun selectAll() {
val cnt = dirs.size
for (i in 0..cnt - 1) {
selectedPositions.add(i)
notifyItemChanged(i)
}
updateTitle(cnt)
actMode?.invalidate()
}
private fun askConfirmDelete() {
ConfirmationDialog(activity) {
deleteFiles()
actMode?.finish()
}
}
private fun deleteFiles() {
val folders = ArrayList<File>(selectedPositions.size)
val removeFolders = ArrayList<Directory>(selectedPositions.size)
var needPermissionForPath = ""
selectedPositions.forEach {
val path = dirs[it].path
if (activity.needsStupidWritePermissions(path) && config.treeUri.isEmpty()) {
needPermissionForPath = path
}
}
activity.handleSAFDialog(File(needPermissionForPath)) {
selectedPositions.sortedDescending().forEach {
val directory = dirs[it]
folders.add(File(directory.path))
removeFolders.add(directory)
notifyItemRemoved(it)
itemViews.put(it, null)
}
dirs.removeAll(removeFolders)
selectedPositions.clear()
listener?.tryDeleteFolders(folders)
val newItems = SparseArray<View>()
var curIndex = 0
for (i in 0..itemViews.size() - 1) {
if (itemViews[i] != null) {
newItems.put(curIndex, itemViews[i])
curIndex++
}
}
itemViews = newItems
}
}
private fun changeAlbumCover(useDefault: Boolean) {
if (selectedPositions.size != 1)
return
val path = dirs[selectedPositions.first()].path
var albumCovers = config.parseAlbumCovers()
if (useDefault) {
albumCovers = albumCovers.filterNot { it.path == path } as ArrayList
storeCovers(albumCovers)
} else {
PickMediumDialog(activity, path) {
albumCovers = albumCovers.filterNot { it.path == path } as ArrayList
albumCovers.add(AlbumCover(path, it))
storeCovers(albumCovers)
}
}
}
private fun storeCovers(albumCovers: ArrayList<AlbumCover>) {
activity.config.albumCovers = Gson().toJson(albumCovers)
actMode?.finish()
listener?.refreshItems()
}
private fun getSelectedPaths(): HashSet<String> {
val paths = HashSet<String>(selectedPositions.size)
selectedPositions.forEach { paths.add(dirs[it].path) }
return paths
}
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent?.context).inflate(R.layout.directory_item, parent, false)
return ViewHolder(view, adapterListener, itemClick)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val dir = dirs[position]
itemViews.put(position, holder.bindView(activity, multiSelectorMode, multiSelector, dir, pinnedFolders.contains(dir.path), listener))
toggleItemSelection(selectedPositions.contains(position), position)
holder.itemView.tag = holder
}
override fun onViewRecycled(holder: ViewHolder?) {
super.onViewRecycled(holder)
holder?.stopLoad()
}
override fun getItemCount() = dirs.size
fun updateDirs(newDirs: ArrayList<Directory>) {
dirs = newDirs
notifyDataSetChanged()
}
fun selectItem(pos: Int) {
toggleItemSelection(true, pos)
}
fun selectRange(from: Int, to: Int, min: Int, max: Int) {
if (from == to) {
(min..max).filter { it != from }
.forEach { toggleItemSelection(false, it) }
return
}
if (to < from) {
for (i in to..from)
toggleItemSelection(true, i)
if (min > -1 && min < to) {
(min..to - 1).filter { it != from }
.forEach { toggleItemSelection(false, it) }
}
if (max > -1) {
for (i in from + 1..max)
toggleItemSelection(false, i)
}
} else {
for (i in from..to)
toggleItemSelection(true, i)
if (max > -1 && max > to) {
(to + 1..max).filter { it != from }
.forEach { toggleItemSelection(false, it) }
}
if (min > -1) {
for (i in min..from - 1)
toggleItemSelection(false, i)
}
}
}
class ViewHolder(val view: View, val adapter: MyAdapterListener, val itemClick: (Directory) -> (Unit)) : SwappingHolder(view, MultiSelector()) {
fun bindView(activity: SimpleActivity, multiSelectorCallback: ModalMultiSelectorCallback, multiSelector: MultiSelector, directory: Directory,
isPinned: Boolean, listener: DirOperationsListener?): View {
itemView.apply {
dir_name.text = directory.name
photo_cnt.text = directory.mediaCnt.toString()
dir_pin.visibility = if (isPinned) View.VISIBLE else View.GONE
activity.loadImage(directory.tmb, dir_thumbnail)
setOnClickListener { viewClicked(multiSelector, directory) }
setOnLongClickListener {
if (listener != null) {
if (!multiSelector.isSelectable) {
activity.startSupportActionMode(multiSelectorCallback)
adapter.toggleItemSelectionAdapter(true, layoutPosition)
}
listener.itemLongClicked(layoutPosition)
}
true
}
adapter.setupItemForeground(this)
}
return itemView
}
fun viewClicked(multiSelector: MultiSelector, directory: Directory) {
if (multiSelector.isSelectable) {
val isSelected = adapter.getSelectedPositions().contains(layoutPosition)
adapter.toggleItemSelectionAdapter(!isSelected, layoutPosition)
} else {
itemClick(directory)
}
}
fun stopLoad() {
Glide.clear(view.dir_thumbnail)
}
}
interface MyAdapterListener {
fun toggleItemSelectionAdapter(select: Boolean, position: Int)
fun setupItemForeground(itemView: View)
fun getSelectedPositions(): HashSet<Int>
}
interface DirOperationsListener {
fun refreshItems()
fun tryDeleteFolders(folders: ArrayList<File>)
fun itemLongClicked(position: Int)
}
}

View file

@ -1,394 +0,0 @@
package com.simplemobiletools.gallery.adapters
import android.os.Build
import android.support.v7.view.ActionMode
import android.support.v7.widget.RecyclerView
import android.util.SparseArray
import android.view.*
import android.widget.FrameLayout
import com.bignerdranch.android.multiselector.ModalMultiSelectorCallback
import com.bignerdranch.android.multiselector.MultiSelector
import com.bignerdranch.android.multiselector.SwappingHolder
import com.bumptech.glide.Glide
import com.simplemobiletools.commons.dialogs.ConfirmationDialog
import com.simplemobiletools.commons.dialogs.PropertiesDialog
import com.simplemobiletools.commons.dialogs.RenameItemDialog
import com.simplemobiletools.commons.extensions.beVisibleIf
import com.simplemobiletools.gallery.R
import com.simplemobiletools.gallery.activities.SimpleActivity
import com.simplemobiletools.gallery.extensions.*
import com.simplemobiletools.gallery.models.Medium
import kotlinx.android.synthetic.main.photo_video_item.view.*
import kotlinx.android.synthetic.main.photo_video_tmb.view.*
import java.io.File
import java.util.*
class MediaAdapter(val activity: SimpleActivity, var media: MutableList<Medium>, val listener: MediaOperationsListener?, val itemClick: (Medium) -> Unit) :
RecyclerView.Adapter<MediaAdapter.ViewHolder>() {
val multiSelector = MultiSelector()
val config = activity.config
var actMode: ActionMode? = null
var itemViews = SparseArray<View>()
val selectedPositions = HashSet<Int>()
var foregroundColor = 0
var displayFilenames = config.displayFileNames
fun toggleItemSelection(select: Boolean, pos: Int) {
if (itemViews[pos] != null)
getProperView(itemViews[pos]!!).isSelected = select
if (select)
selectedPositions.add(pos)
else
selectedPositions.remove(pos)
if (selectedPositions.isEmpty()) {
actMode?.finish()
return
}
updateTitle(selectedPositions.size)
actMode?.invalidate()
}
fun getProperView(itemView: View): View {
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
itemView.medium_thumbnail_holder
else
itemView.medium_thumbnail
}
fun updateTitle(cnt: Int) {
actMode?.title = "$cnt / ${media.size}"
}
fun updatePrimaryColor(color: Int) {
foregroundColor = color
(0..itemViews.size() - 1).mapNotNull { itemViews[it] }
.forEach { setupItemViewForeground(it) }
}
private fun setupItemViewForeground(itemView: View) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
(getProperView(itemView) as FrameLayout).foreground = foregroundColor.createSelector()
else
getProperView(itemView).foreground = foregroundColor.createSelector()
}
val adapterListener = object : MyAdapterListener {
override fun toggleItemSelectionAdapter(select: Boolean, position: Int) {
toggleItemSelection(select, position)
}
override fun setupItemForeground(itemView: View) {
setupItemViewForeground(itemView)
}
override fun getSelectedPositions(): HashSet<Int> = selectedPositions
}
init {
foregroundColor = config.primaryColor
}
val multiSelectorMode = object : ModalMultiSelectorCallback(multiSelector) {
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
when (item.itemId) {
R.id.cab_properties -> showProperties()
R.id.cab_rename -> renameFile()
R.id.cab_edit -> editFile()
R.id.cab_hide -> toggleFileVisibility(true)
R.id.cab_unhide -> toggleFileVisibility(false)
R.id.cab_share -> shareMedia()
R.id.cab_copy_to -> copyMoveTo(true)
R.id.cab_move_to -> copyMoveTo(false)
R.id.cab_select_all -> selectAll()
R.id.cab_delete -> askConfirmDelete()
else -> return false
}
return true
}
override fun onCreateActionMode(actionMode: ActionMode?, menu: Menu?): Boolean {
super.onCreateActionMode(actionMode, menu)
actMode = actionMode
activity.menuInflater.inflate(R.menu.cab_media, menu)
return true
}
override fun onPrepareActionMode(actionMode: ActionMode?, menu: Menu): Boolean {
menu.findItem(R.id.cab_rename).isVisible = selectedPositions.size <= 1
menu.findItem(R.id.cab_edit).isVisible = selectedPositions.size == 1 && media[selectedPositions.first()].isImage()
checkHideBtnVisibility(menu)
return true
}
override fun onDestroyActionMode(actionMode: ActionMode?) {
super.onDestroyActionMode(actionMode)
selectedPositions.forEach {
if (itemViews[it] != null)
getProperView(itemViews[it]!!).isSelected = false
}
selectedPositions.clear()
actMode = null
}
fun checkHideBtnVisibility(menu: Menu) {
var hiddenCnt = 0
var unhiddenCnt = 0
selectedPositions.map { media.getOrNull(it) }.filterNotNull().forEach {
if (it.name.startsWith('.'))
hiddenCnt++
else
unhiddenCnt++
}
menu.findItem(R.id.cab_hide).isVisible = unhiddenCnt > 0
menu.findItem(R.id.cab_unhide).isVisible = hiddenCnt > 0
}
}
private fun showProperties() {
if (selectedPositions.size <= 1) {
PropertiesDialog(activity, media[selectedPositions.first()].path, config.shouldShowHidden)
} else {
val paths = ArrayList<String>()
selectedPositions.forEach { paths.add(media[it].path) }
PropertiesDialog(activity, paths, config.shouldShowHidden)
}
}
private fun renameFile() {
RenameItemDialog(activity, getCurrentFile().absolutePath) {
activity.runOnUiThread {
listener?.refreshItems()
actMode?.finish()
}
}
}
private fun editFile() {
activity.openEditor(getCurrentFile())
actMode?.finish()
}
private fun toggleFileVisibility(hide: Boolean) {
Thread({
getSelectedMedia().forEach {
val oldFile = File(it.path)
activity.toggleFileVisibility(oldFile, hide) {}
}
activity.runOnUiThread {
listener?.refreshItems()
actMode?.finish()
}
}).start()
}
private fun shareMedia() {
if (selectedPositions.size <= 1) {
activity.shareMedium(getSelectedMedia()[0])
} else {
activity.shareMedia(getSelectedMedia())
}
}
private fun copyMoveTo(isCopyOperation: Boolean) {
val files = ArrayList<File>()
selectedPositions.forEach { files.add(File(media[it].path)) }
activity.tryCopyMoveFilesTo(files, isCopyOperation) {
if (!isCopyOperation) {
listener?.refreshItems()
}
actMode?.finish()
}
}
fun selectAll() {
val cnt = media.size
for (i in 0..cnt - 1) {
selectedPositions.add(i)
multiSelector.setSelected(i, 0, true)
notifyItemChanged(i)
}
updateTitle(cnt)
actMode?.invalidate()
}
private fun askConfirmDelete() {
ConfirmationDialog(activity) {
deleteFiles()
actMode?.finish()
}
}
private fun getCurrentFile() = File(media[selectedPositions.first()].path)
private fun deleteFiles() {
if (selectedPositions.isEmpty())
return
val files = ArrayList<File>(selectedPositions.size)
val removeMedia = ArrayList<Medium>(selectedPositions.size)
activity.handleSAFDialog(File(media[selectedPositions.first()].path)) {
selectedPositions.sortedDescending().forEach {
val medium = media[it]
files.add(File(medium.path))
removeMedia.add(medium)
notifyItemRemoved(it)
itemViews.put(it, null)
}
media.removeAll(removeMedia)
selectedPositions.clear()
listener?.deleteFiles(files)
val newItems = SparseArray<View>()
var curIndex = 0
for (i in 0..itemViews.size() - 1) {
if (itemViews[i] != null) {
newItems.put(curIndex, itemViews[i])
curIndex++
}
}
itemViews = newItems
}
}
private fun getSelectedMedia(): List<Medium> {
val selectedMedia = ArrayList<Medium>(selectedPositions.size)
selectedPositions.forEach { selectedMedia.add(media[it]) }
return selectedMedia
}
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent?.context).inflate(R.layout.photo_video_item, parent, false)
return ViewHolder(view, adapterListener, itemClick)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
itemViews.put(position, holder.bindView(activity, multiSelectorMode, multiSelector, media[position], listener, displayFilenames))
toggleItemSelection(selectedPositions.contains(position), position)
holder.itemView.tag = holder
}
override fun onViewRecycled(holder: ViewHolder?) {
super.onViewRecycled(holder)
holder?.stopLoad()
}
override fun getItemCount() = media.size
fun updateMedia(newMedia: ArrayList<Medium>) {
media = newMedia
notifyDataSetChanged()
}
fun updateDisplayFilenames(display: Boolean) {
displayFilenames = display
notifyDataSetChanged()
}
fun selectItem(pos: Int) {
toggleItemSelection(true, pos)
}
fun selectRange(from: Int, to: Int, min: Int, max: Int) {
if (from == to) {
(min..max).filter { it != from }
.forEach { toggleItemSelection(false, it) }
return
}
if (to < from) {
for (i in to..from)
toggleItemSelection(true, i)
if (min > -1 && min < to) {
(min..to - 1).filter { it != from }
.forEach { toggleItemSelection(false, it) }
}
if (max > -1) {
for (i in from + 1..max)
toggleItemSelection(false, i)
}
} else {
for (i in from..to)
toggleItemSelection(true, i)
if (max > -1 && max > to) {
(to + 1..max).filter { it != from }
.forEach { toggleItemSelection(false, it) }
}
if (min > -1) {
for (i in min..from - 1)
toggleItemSelection(false, i)
}
}
}
class ViewHolder(val view: View, val adapter: MyAdapterListener, val itemClick: (Medium) -> (Unit)) : SwappingHolder(view, MultiSelector()) {
fun bindView(activity: SimpleActivity, multiSelectorCallback: ModalMultiSelectorCallback, multiSelector: MultiSelector, medium: Medium,
listener: MediaOperationsListener?, displayFilenames: Boolean): View {
itemView.apply {
play_outline.visibility = if (medium.video) View.VISIBLE else View.GONE
photo_name.beVisibleIf(displayFilenames)
photo_name.text = medium.name
activity.loadImage(medium.path, medium_thumbnail)
setOnClickListener { viewClicked(multiSelector, medium) }
setOnLongClickListener {
if (listener != null) {
if (!multiSelector.isSelectable) {
activity.startSupportActionMode(multiSelectorCallback)
adapter.toggleItemSelectionAdapter(true, layoutPosition)
}
listener.itemLongClicked(layoutPosition)
}
true
}
adapter.setupItemForeground(this)
}
return itemView
}
fun viewClicked(multiSelector: MultiSelector, medium: Medium) {
if (multiSelector.isSelectable) {
val isSelected = adapter.getSelectedPositions().contains(layoutPosition)
adapter.toggleItemSelectionAdapter(!isSelected, layoutPosition)
} else {
itemClick(medium)
}
}
fun stopLoad() {
Glide.clear(view.medium_thumbnail)
}
}
interface MyAdapterListener {
fun toggleItemSelectionAdapter(select: Boolean, position: Int)
fun setupItemForeground(itemView: View)
fun getSelectedPositions(): HashSet<Int>
}
interface MediaOperationsListener {
fun refreshItems()
fun deleteFiles(files: ArrayList<File>)
fun itemLongClicked(position: Int)
}
}

View file

@ -1,52 +0,0 @@
package com.simplemobiletools.gallery.adapters
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v4.app.FragmentManager
import android.support.v4.app.FragmentStatePagerAdapter
import android.support.v4.view.PagerAdapter
import android.util.SparseArray
import android.view.ViewGroup
import com.simplemobiletools.gallery.activities.ViewPagerActivity
import com.simplemobiletools.gallery.fragments.PhotoFragment
import com.simplemobiletools.gallery.fragments.VideoFragment
import com.simplemobiletools.gallery.fragments.ViewPagerFragment
import com.simplemobiletools.gallery.helpers.MEDIUM
import com.simplemobiletools.gallery.models.Medium
class MyPagerAdapter(val activity: ViewPagerActivity, fm: FragmentManager, val media: MutableList<Medium>) : FragmentStatePagerAdapter(fm) {
private val mFragments = SparseArray<ViewPagerFragment>()
override fun getCount() = media.size
override fun getItem(position: Int): Fragment {
val medium = media[position]
val bundle = Bundle()
bundle.putSerializable(MEDIUM, medium)
val fragment: ViewPagerFragment
if (medium.video) {
fragment = VideoFragment()
} else {
fragment = PhotoFragment()
}
fragment.arguments = bundle
fragment.listener = activity
return fragment
}
override fun getItemPosition(item: Any?) = PagerAdapter.POSITION_NONE
override fun instantiateItem(container: ViewGroup?, position: Int): Any {
val fragment = super.instantiateItem(container, position) as ViewPagerFragment
mFragments.put(position, fragment)
return fragment
}
override fun destroyItem(container: ViewGroup?, position: Int, `object`: Any?) {
mFragments.remove(position)
super.destroyItem(container, position, `object`)
}
fun getCurrentFragment(position: Int) = mFragments.get(position)
}

View file

@ -1,124 +0,0 @@
package com.simplemobiletools.gallery.asynctasks
import android.content.Context
import android.os.AsyncTask
import com.simplemobiletools.commons.extensions.getFilenameFromPath
import com.simplemobiletools.commons.extensions.hasWriteStoragePermission
import com.simplemobiletools.commons.extensions.internalStoragePath
import com.simplemobiletools.commons.extensions.sdCardPath
import com.simplemobiletools.gallery.R
import com.simplemobiletools.gallery.extensions.config
import com.simplemobiletools.gallery.extensions.containsNoMedia
import com.simplemobiletools.gallery.extensions.getFilesFrom
import com.simplemobiletools.gallery.models.Directory
import com.simplemobiletools.gallery.models.Medium
import java.io.File
import java.util.*
class GetDirectoriesAsynctask(val context: Context, val isPickVideo: Boolean, val isPickImage: Boolean,
val callback: (dirs: ArrayList<Directory>) -> Unit) : AsyncTask<Void, Void, ArrayList<Directory>>() {
var config = context.config
var shouldStop = false
val showHidden = config.shouldShowHidden
override fun doInBackground(vararg params: Void): ArrayList<Directory> {
if (!context.hasWriteStoragePermission())
return ArrayList()
val media = context.getFilesFrom("", isPickImage, isPickVideo)
val excludedPaths = config.excludedFolders
val directories = groupDirectories(media)
val dirs = ArrayList(directories.values.filter { File(it.path).exists() }).filter { shouldFolderBeVisible(it.path, excludedPaths) } as ArrayList<Directory>
Directory.sorting = config.directorySorting
dirs.sort()
return movePinnedToFront(dirs)
}
private fun groupDirectories(media: ArrayList<Medium>): Map<String, Directory> {
val albumCovers = config.parseAlbumCovers()
val hidden = context.resources.getString(R.string.hidden)
val directories = LinkedHashMap<String, Directory>()
for ((name, path, isVideo, dateModified, dateTaken, size) in media) {
if (shouldStop)
cancel(true)
val parentDir = File(path).parent ?: continue
if (directories.containsKey(parentDir)) {
val directory = directories[parentDir]!!
val newImageCnt = directory.mediaCnt + 1
directory.mediaCnt = newImageCnt
directory.addSize(size)
} else {
var dirName = parentDir.getFilenameFromPath()
if (parentDir == context.internalStoragePath) {
dirName = context.getString(R.string.internal)
} else if (parentDir == context.sdCardPath) {
dirName = context.getString(R.string.sd_card)
}
if (File(parentDir).containsNoMedia()) {
dirName += " $hidden"
if (!showHidden)
continue
}
var thumbnail = path
albumCovers.forEach {
if (it.path == parentDir && File(it.tmb).exists()) {
thumbnail = it.tmb
}
}
val directory = Directory(parentDir, thumbnail, dirName, 1, dateModified, dateTaken, size)
directories.put(parentDir, directory)
}
}
return directories
}
private fun shouldFolderBeVisible(path: String, excludedPaths: MutableSet<String>): Boolean {
val file = File(path)
return if (isThisOrParentExcluded(path, excludedPaths))
false
else if (!config.shouldShowHidden && file.isDirectory && file.canonicalFile == file.absoluteFile) {
var containsNoMediaOrDot = file.containsNoMedia() || path.contains("/.")
if (!containsNoMediaOrDot) {
containsNoMediaOrDot = checkParentHasNoMedia(file.parentFile)
}
!containsNoMediaOrDot
} else {
true
}
}
private fun checkParentHasNoMedia(file: File): Boolean {
var curFile = file
while (true) {
if (curFile.containsNoMedia()) {
return true
}
curFile = curFile.parentFile
if (curFile.absolutePath == "/")
break
}
return false
}
private fun isThisOrParentExcluded(path: String, excludedPaths: MutableSet<String>) = excludedPaths.any { path.startsWith(it) }
private fun movePinnedToFront(dirs: ArrayList<Directory>): ArrayList<Directory> {
val foundFolders = ArrayList<Directory>()
val pinnedFolders = config.pinnedFolders
dirs.forEach { if (pinnedFolders.contains(it.path)) foundFolders.add(it) }
dirs.removeAll(foundFolders)
dirs.addAll(0, foundFolders)
return dirs
}
override fun onPostExecute(dirs: ArrayList<Directory>) {
super.onPostExecute(dirs)
callback.invoke(dirs)
}
}

View file

@ -1,22 +0,0 @@
package com.simplemobiletools.gallery.asynctasks
import android.content.Context
import android.os.AsyncTask
import com.simplemobiletools.gallery.extensions.getFilesFrom
import com.simplemobiletools.gallery.models.Medium
import java.util.*
class GetMediaAsynctask(val context: Context, val mPath: String, val isPickVideo: Boolean = false, val isPickImage: Boolean = false,
val showAll: Boolean, val callback: (media: ArrayList<Medium>) -> Unit) :
AsyncTask<Void, Void, ArrayList<Medium>>() {
override fun doInBackground(vararg params: Void): ArrayList<Medium> {
val path = if (showAll) "" else mPath
return context.getFilesFrom(path, isPickImage, isPickVideo)
}
override fun onPostExecute(media: ArrayList<Medium>) {
super.onPostExecute(media)
callback.invoke(media)
}
}

View file

@ -1,90 +0,0 @@
package com.simplemobiletools.gallery.dialogs
import android.content.DialogInterface
import android.support.v7.app.AlertDialog
import android.view.LayoutInflater
import android.view.View
import com.simplemobiletools.commons.extensions.beVisibleIf
import com.simplemobiletools.commons.extensions.setupDialogStuff
import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.gallery.R
import com.simplemobiletools.gallery.activities.SimpleActivity
import com.simplemobiletools.gallery.extensions.config
import com.simplemobiletools.gallery.helpers.Config
import kotlinx.android.synthetic.main.dialog_change_sorting.view.*
class ChangeSortingDialog(val activity: SimpleActivity, val isDirectorySorting: Boolean, showFolderCheckbox: Boolean,
val path: String = "", val callback: () -> Unit) :
DialogInterface.OnClickListener {
private var currSorting = 0
private var config: Config = activity.config
private var view: View
init {
view = LayoutInflater.from(activity).inflate(R.layout.dialog_change_sorting, null).apply {
use_for_this_folder_divider.beVisibleIf(showFolderCheckbox)
sorting_dialog_use_for_this_folder.beVisibleIf(showFolderCheckbox)
sorting_dialog_use_for_this_folder.isChecked = config.hasCustomSorting(path)
}
AlertDialog.Builder(activity)
.setPositiveButton(R.string.ok, this)
.setNegativeButton(R.string.cancel, null)
.create().apply {
activity.setupDialogStuff(view, this, R.string.sort_by)
}
currSorting = if (isDirectorySorting) config.directorySorting else config.getFileSorting(path)
setupSortRadio()
setupOrderRadio()
}
private fun setupSortRadio() {
val sortingRadio = view.sorting_dialog_radio_sorting
var sortBtn = sortingRadio.sorting_dialog_radio_name
if (currSorting and SORT_BY_SIZE != 0) {
sortBtn = sortingRadio.sorting_dialog_radio_size
} else if (currSorting and SORT_BY_DATE_MODIFIED != 0) {
sortBtn = sortingRadio.sorting_dialog_radio_last_modified
} else if (currSorting and SORT_BY_DATE_TAKEN != 0)
sortBtn = sortingRadio.sorting_dialog_radio_date_taken
sortBtn.isChecked = true
}
private fun setupOrderRadio() {
val orderRadio = view.sorting_dialog_radio_order
var orderBtn = orderRadio.sorting_dialog_radio_ascending
if (currSorting and SORT_DESCENDING != 0) {
orderBtn = orderRadio.sorting_dialog_radio_descending
}
orderBtn.isChecked = true
}
override fun onClick(dialog: DialogInterface, which: Int) {
val sortingRadio = view.sorting_dialog_radio_sorting
var sorting = when (sortingRadio.checkedRadioButtonId) {
R.id.sorting_dialog_radio_name -> SORT_BY_NAME
R.id.sorting_dialog_radio_size -> SORT_BY_SIZE
R.id.sorting_dialog_radio_last_modified -> SORT_BY_DATE_MODIFIED
else -> SORT_BY_DATE_TAKEN
}
if (view.sorting_dialog_radio_order.checkedRadioButtonId == R.id.sorting_dialog_radio_descending) {
sorting = sorting or SORT_DESCENDING
}
if (isDirectorySorting) {
config.directorySorting = sorting
} else {
if (view.sorting_dialog_use_for_this_folder.isChecked) {
config.saveFileSorting(path, sorting)
} else {
config.removeFileSorting(path)
config.fileSorting = sorting
}
}
callback.invoke()
}
}

View file

@ -1,68 +0,0 @@
package com.simplemobiletools.gallery.dialogs
import android.support.v7.app.AlertDialog
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import com.simplemobiletools.commons.dialogs.FilePickerDialog
import com.simplemobiletools.commons.extensions.setupDialogStuff
import com.simplemobiletools.commons.extensions.toast
import com.simplemobiletools.gallery.R
import com.simplemobiletools.gallery.activities.SimpleActivity
import com.simplemobiletools.gallery.adapters.DirectoryAdapter
import com.simplemobiletools.gallery.asynctasks.GetDirectoriesAsynctask
import com.simplemobiletools.gallery.extensions.config
import com.simplemobiletools.gallery.extensions.getCachedDirectories
import com.simplemobiletools.gallery.models.Directory
import kotlinx.android.synthetic.main.dialog_directory_picker.view.*
class PickDirectoryDialog(val activity: SimpleActivity, val sourcePath: String, val callback: (path: String) -> Unit) {
var dialog: AlertDialog
var directoriesGrid: RecyclerView
var shownDirectories: ArrayList<Directory> = ArrayList()
init {
val view = LayoutInflater.from(activity).inflate(R.layout.dialog_directory_picker, null)
directoriesGrid = view.directories_grid
dialog = AlertDialog.Builder(activity)
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.cancel, null)
.setNeutralButton(R.string.other_folder, { dialogInterface, i -> showOtherFolder() })
.create().apply {
activity.setupDialogStuff(view, this, R.string.select_destination)
val dirs = activity.getCachedDirectories()
if (dirs.isNotEmpty()) {
gotDirectories(dirs)
}
GetDirectoriesAsynctask(activity, false, false) {
gotDirectories(it)
}.execute()
}
}
fun showOtherFolder() {
val showHidden = activity.config.shouldShowHidden
FilePickerDialog(activity, sourcePath, false, showHidden, true) {
callback.invoke(it)
}
}
private fun gotDirectories(directories: ArrayList<Directory>) {
if (directories.hashCode() == shownDirectories.hashCode())
return
shownDirectories = directories
val adapter = DirectoryAdapter(activity, directories, null) {
if (it.path.trimEnd('/') == sourcePath) {
activity.toast(R.string.source_and_destination_same)
return@DirectoryAdapter
} else {
callback.invoke(it.path)
dialog.dismiss()
}
}
directoriesGrid.adapter = adapter
}
}

View file

@ -1,56 +0,0 @@
package com.simplemobiletools.gallery.dialogs
import android.support.v7.app.AlertDialog
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.simplemobiletools.commons.extensions.setupDialogStuff
import com.simplemobiletools.gallery.R
import com.simplemobiletools.gallery.activities.SimpleActivity
import com.simplemobiletools.gallery.adapters.MediaAdapter
import com.simplemobiletools.gallery.asynctasks.GetMediaAsynctask
import com.simplemobiletools.gallery.extensions.config
import com.simplemobiletools.gallery.models.Medium
import kotlinx.android.synthetic.main.dialog_medium_picker.view.*
class PickMediumDialog(val activity: SimpleActivity, val path: String, val callback: (path: String) -> Unit) {
var dialog: AlertDialog
var mediaGrid: RecyclerView
var shownMedia: ArrayList<Medium> = ArrayList()
init {
val view = LayoutInflater.from(activity).inflate(R.layout.dialog_medium_picker, null)
mediaGrid = view.media_grid
dialog = AlertDialog.Builder(activity)
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.cancel, null)
.create().apply {
activity.setupDialogStuff(view, this, R.string.select_photo)
val token = object : TypeToken<List<Medium>>() {}.type
val media = Gson().fromJson<ArrayList<Medium>>(activity.config.loadFolderMedia(path), token) ?: ArrayList<Medium>(1)
if (media.isNotEmpty()) {
gotMedia(media)
}
GetMediaAsynctask(activity, path, false, true, false) {
gotMedia(it)
}.execute()
}
}
private fun gotMedia(media: ArrayList<Medium>) {
if (media.hashCode() == shownMedia.hashCode())
return
shownMedia = media
val adapter = MediaAdapter(activity, media, null) {
callback(it.path)
dialog.dismiss()
}
mediaGrid.adapter = adapter
}
}

View file

@ -1,97 +0,0 @@
package com.simplemobiletools.gallery.dialogs
import android.graphics.Point
import android.support.v7.app.AlertDialog
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.WindowManager
import android.widget.EditText
import com.simplemobiletools.commons.extensions.setupDialogStuff
import com.simplemobiletools.commons.extensions.toast
import com.simplemobiletools.commons.extensions.value
import com.simplemobiletools.gallery.R
import com.simplemobiletools.gallery.activities.SimpleActivity
import kotlinx.android.synthetic.main.resize_image.view.*
class ResizeDialog(val activity: SimpleActivity, val size: Point, val callback: (newSize: Point) -> Unit) {
init {
val view = LayoutInflater.from(activity).inflate(R.layout.resize_image, null)
val widthView = view.image_width
val heightView = view.image_height
widthView.setText(size.x.toString())
heightView.setText(size.y.toString())
val ratio = size.x / size.y.toFloat()
widthView.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
if (widthView.hasFocus()) {
var width = getViewValue(widthView)
if (width > size.x) {
widthView.setText(size.x.toString())
width = size.x
}
if (view.keep_aspect_ratio.isChecked) {
heightView.setText((width / ratio).toInt().toString())
}
}
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
}
})
heightView.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
if (heightView.hasFocus()) {
var height = getViewValue(heightView)
if (height > size.y) {
heightView.setText(size.y.toString())
height = size.y
}
if (view.keep_aspect_ratio.isChecked) {
widthView.setText((height * ratio).toInt().toString())
}
}
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
}
})
AlertDialog.Builder(activity)
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.cancel, null)
.create().apply {
window!!.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
activity.setupDialogStuff(view, this, R.string.resize_and_save)
getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener({
val width = getViewValue(widthView)
val height = getViewValue(heightView)
if (width <= 0 || height <= 0) {
activity.toast(R.string.invalid_values)
return@setOnClickListener
}
val newSize = Point(getViewValue(widthView), getViewValue(heightView))
callback.invoke(newSize)
dismiss()
})
}
}
fun getViewValue(view: EditText): Int {
val textValue = view.value
return if (textValue.isEmpty()) 0 else textValue.toInt()
}
}

View file

@ -1,79 +0,0 @@
package com.simplemobiletools.gallery.dialogs
import android.support.v7.app.AlertDialog
import android.view.LayoutInflater
import android.view.WindowManager
import com.simplemobiletools.commons.dialogs.ConfirmationDialog
import com.simplemobiletools.commons.dialogs.FilePickerDialog
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.gallery.R
import com.simplemobiletools.gallery.activities.SimpleActivity
import kotlinx.android.synthetic.main.dialog_save_as.view.*
import java.io.File
class SaveAsDialog(val activity: SimpleActivity, val path: String, val callback: (savePath: String) -> Unit) {
init {
var realPath = File(path).parent.trimEnd('/')
val view = LayoutInflater.from(activity).inflate(R.layout.dialog_save_as, null).apply {
save_as_path.text = activity.humanizePath(realPath)
val fullName = path.getFilenameFromPath()
val dotAt = fullName.lastIndexOf(".")
var name = fullName
if (dotAt > 0) {
name = fullName.substring(0, dotAt)
val extension = fullName.substring(dotAt + 1)
save_as_extension.setText(extension)
}
save_as_name.setText(name)
save_as_path.setOnClickListener {
FilePickerDialog(activity, realPath, false, false, true) {
save_as_path.text = activity.humanizePath(it)
realPath = it
}
}
}
AlertDialog.Builder(activity)
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.cancel, null)
.create().apply {
window!!.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
activity.setupDialogStuff(view, this, R.string.save_as)
getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener({
val filename = view.save_as_name.value
val extension = view.save_as_extension.value
if (filename.isEmpty()) {
activity.toast(R.string.filename_cannot_be_empty)
return@setOnClickListener
}
if (extension.isEmpty()) {
activity.toast(R.string.extension_cannot_be_empty)
return@setOnClickListener
}
val newFile = File(realPath, "$filename.$extension")
if (!newFile.name.isAValidFilename()) {
activity.toast(R.string.filename_invalid_characters)
return@setOnClickListener
}
if (newFile.exists()) {
val title = String.format(activity.getString(R.string.file_already_exists_overwrite), newFile.name)
ConfirmationDialog(activity, title) {
callback.invoke(newFile.absolutePath)
dismiss()
}
} else {
callback.invoke(newFile.absolutePath)
dismiss()
}
})
}
}
}

View file

@ -1,24 +0,0 @@
package com.simplemobiletools.gallery.extensions
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.RippleDrawable
import android.graphics.drawable.StateListDrawable
import android.os.Build
import com.simplemobiletools.commons.extensions.adjustAlpha
fun Int.createSelector(): StateListDrawable {
val statelist = StateListDrawable()
val selectedDrawable = ColorDrawable(adjustAlpha(0.5f))
statelist.addState(intArrayOf(android.R.attr.state_selected), selectedDrawable)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
val pressedDrawable = ColorDrawable(adjustAlpha(0.2f))
statelist.addState(intArrayOf(android.R.attr.state_pressed), pressedDrawable)
} else {
val pressedDrawable = RippleDrawable(ColorStateList.valueOf(adjustAlpha(0.2f)), null, ColorDrawable(Color.WHITE))
statelist.addState(intArrayOf(), pressedDrawable)
}
return statelist
}

View file

@ -1,328 +0,0 @@
package com.simplemobiletools.gallery.extensions
import android.app.Activity
import android.content.Intent
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import android.support.v7.app.AppCompatActivity
import android.util.DisplayMetrics
import android.view.KeyCharacterMap
import android.view.KeyEvent
import android.view.View
import android.view.ViewConfiguration
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.signature.StringSignature
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.gallery.BuildConfig
import com.simplemobiletools.gallery.R
import com.simplemobiletools.gallery.activities.SimpleActivity
import com.simplemobiletools.gallery.helpers.NOMEDIA
import com.simplemobiletools.gallery.helpers.REQUEST_EDIT_IMAGE
import com.simplemobiletools.gallery.helpers.REQUEST_SET_WALLPAPER
import com.simplemobiletools.gallery.models.Directory
import com.simplemobiletools.gallery.models.Medium
import com.simplemobiletools.gallery.views.MySquareImageView
import java.io.File
import java.util.*
fun Activity.shareUri(medium: Medium, uri: Uri) {
val shareTitle = resources.getString(R.string.share_via)
Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_STREAM, uri)
type = medium.getMimeType()
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
startActivity(Intent.createChooser(this, shareTitle))
}
}
fun Activity.shareMedium(medium: Medium) {
val shareTitle = resources.getString(R.string.share_via)
val file = File(medium.path)
val uri = Uri.fromFile(file)
Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_STREAM, uri)
type = medium.getMimeType()
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
startActivity(Intent.createChooser(this, shareTitle))
}
}
fun Activity.shareMedia(media: List<Medium>) {
val shareTitle = resources.getString(R.string.share_via)
val uris = ArrayList<Uri>(media.size)
Intent().apply {
action = Intent.ACTION_SEND_MULTIPLE
type = "image/* video/*"
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
media.map { File(it.path) }
.mapTo(uris) { Uri.fromFile(it) }
putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris)
startActivity(Intent.createChooser(this, shareTitle))
}
}
fun Activity.trySetAsWallpaper(file: File) {
try {
var uri = Uri.fromFile(file)
if (!setAsWallpaper(uri, file)) {
uri = getFileContentUri(file)
setAsWallpaper(uri, file, false)
}
} catch (e: Exception) {
toast(R.string.unknown_error_occurred)
}
}
fun Activity.setAsWallpaper(uri: Uri, file: File, showToast: Boolean = true): Boolean {
var success = false
Intent().apply {
action = Intent.ACTION_ATTACH_DATA
setDataAndType(uri, file.getMimeType("image/*"))
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
val chooser = Intent.createChooser(this, getString(R.string.set_as_wallpaper_with))
if (resolveActivity(packageManager) != null) {
startActivityForResult(chooser, REQUEST_SET_WALLPAPER)
success = true
} else {
if (showToast) {
toast(R.string.no_wallpaper_setter_found)
}
success = false
}
}
return success
}
fun Activity.getFileContentUri(file: File): Uri? {
val uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
val projection = arrayOf(MediaStore.Images.Media._ID)
val selection = "${MediaStore.Images.Media.DATA} = ?"
val selectionArgs = arrayOf(file.absolutePath)
var cursor: Cursor? = null
try {
cursor = contentResolver.query(uri, projection, selection, selectionArgs, null)
if (cursor?.moveToFirst() == true) {
val id = cursor.getIntValue(MediaStore.Images.Media._ID)
return Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "$id")
}
} finally {
cursor?.close()
}
return null
}
fun Activity.openWith(file: File, forceChooser: Boolean = true) {
val uri = Uri.fromFile(file)
Intent().apply {
action = Intent.ACTION_VIEW
setDataAndType(uri, file.getMimeType())
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
val chooser = Intent.createChooser(this, getString(R.string.open_with))
if (resolveActivity(packageManager) != null) {
startActivity(if (forceChooser) chooser else this)
} else {
toast(R.string.no_app_found)
}
}
}
fun Activity.openEditor(file: File) {
val uri = Uri.fromFile(file)
Intent().apply {
action = Intent.ACTION_EDIT
setDataAndType(uri, "image/*")
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
if (resolveActivity(packageManager) != null) {
startActivityForResult(this, REQUEST_EDIT_IMAGE)
} else {
toast(R.string.no_editor_found)
}
}
}
fun Activity.hasNavBar(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
val display = windowManager.defaultDisplay
val realDisplayMetrics = DisplayMetrics()
display.getRealMetrics(realDisplayMetrics)
val realHeight = realDisplayMetrics.heightPixels
val realWidth = realDisplayMetrics.widthPixels
val displayMetrics = DisplayMetrics()
display.getMetrics(displayMetrics)
val displayHeight = displayMetrics.heightPixels
val displayWidth = displayMetrics.widthPixels
realWidth - displayWidth > 0 || realHeight - displayHeight > 0
} else {
val hasMenuKey = ViewConfiguration.get(applicationContext).hasPermanentMenuKey()
val hasBackKey = KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_BACK)
!hasMenuKey && !hasBackKey
}
}
fun Activity.launchCamera() {
val intent = Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA)
if (intent.resolveActivity(packageManager) != null) {
startActivity(intent)
} else {
toast(R.string.no_camera_app_found)
}
}
fun SimpleActivity.launchAbout() {
startAboutActivity(R.string.app_name, LICENSE_KOTLIN or LICENSE_GLIDE or LICENSE_CROPPER or LICENSE_MULTISELECT or LICENSE_RTL
or LICENSE_PHOTOVIEW or LICENSE_SUBSAMPLING, BuildConfig.VERSION_NAME)
}
fun AppCompatActivity.showSystemUI() {
supportActionBar?.show()
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
}
fun AppCompatActivity.hideSystemUI() {
supportActionBar?.hide()
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
View.SYSTEM_UI_FLAG_LOW_PROFILE or
View.SYSTEM_UI_FLAG_FULLSCREEN or
View.SYSTEM_UI_FLAG_IMMERSIVE
}
fun SimpleActivity.addNoMedia(path: String, callback: () -> Unit) {
val file = File(path, NOMEDIA)
if (file.exists())
return
if (needsStupidWritePermissions(path)) {
handleSAFDialog(file) {
try {
getFileDocument(path)?.createFile("", NOMEDIA)
} catch (e: Exception) {
toast(R.string.unknown_error_occurred)
}
}
} else {
try {
file.createNewFile()
} catch (e: Exception) {
toast(R.string.unknown_error_occurred)
}
}
scanFile(file) {
callback()
}
}
fun SimpleActivity.removeNoMedia(path: String, callback: () -> Unit) {
val file = File(path, NOMEDIA)
deleteFile(file) {
callback()
}
}
fun SimpleActivity.toggleFileVisibility(oldFile: File, hide: Boolean, callback: (newFile: File) -> Unit) {
val path = oldFile.parent
var filename = oldFile.name
if (hide) {
filename = ".${filename.trimStart('.')}"
} else {
filename = filename.substring(1, filename.length)
}
val newFile = File(path, filename)
renameFile(oldFile, newFile) {
newFile.setLastModified(System.currentTimeMillis())
callback(newFile)
}
}
fun Activity.getFileSignature(path: String) = StringSignature(File(path).lastModified().toString())
fun Activity.loadImage(path: String, target: MySquareImageView) {
if (path.isImageFast() || path.isVideoFast()) {
if (path.isPng()) {
loadPng(path, target)
} else {
loadJpg(path, target)
}
} else if (path.isGif()) {
if (config.animateGifs) {
loadAnimatedGif(path, target)
} else {
loadStaticGif(path, target)
}
}
}
fun Activity.loadPng(path: String, target: MySquareImageView) {
val builder = Glide.with(applicationContext)
.load(path)
.asBitmap()
.signature(getFileSignature(path))
.diskCacheStrategy(DiskCacheStrategy.RESULT)
.format(DecodeFormat.PREFER_ARGB_8888)
if (config.cropThumbnails) builder.centerCrop() else builder.fitCenter()
builder.into(target)
}
fun Activity.loadJpg(path: String, target: MySquareImageView) {
val builder = Glide.with(applicationContext)
.load(path)
.signature(getFileSignature(path))
.diskCacheStrategy(DiskCacheStrategy.RESULT)
.crossFade()
if (config.cropThumbnails) builder.centerCrop() else builder.fitCenter()
builder.into(target)
}
fun Activity.loadAnimatedGif(path: String, target: MySquareImageView) {
val builder = Glide.with(applicationContext)
.load(path)
.asGif()
.signature(getFileSignature(path))
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
.crossFade()
if (config.cropThumbnails) builder.centerCrop() else builder.fitCenter()
builder.into(target)
}
fun Activity.loadStaticGif(path: String, target: MySquareImageView) {
val builder = Glide.with(applicationContext)
.load(path)
.asBitmap()
.signature(getFileSignature(path))
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
if (config.cropThumbnails) builder.centerCrop() else builder.fitCenter()
builder.into(target)
}
fun Activity.getCachedDirectories(): ArrayList<Directory> {
val token = object : TypeToken<List<Directory>>() {}.type
return Gson().fromJson<ArrayList<Directory>>(config.directories, token) ?: ArrayList<Directory>(1)
}

View file

@ -1,236 +0,0 @@
package com.simplemobiletools.gallery.extensions
import android.content.Context
import android.content.Intent
import android.database.Cursor
import android.net.Uri
import android.provider.MediaStore
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.SORT_BY_DATE_MODIFIED
import com.simplemobiletools.commons.helpers.SORT_BY_NAME
import com.simplemobiletools.commons.helpers.SORT_BY_SIZE
import com.simplemobiletools.commons.helpers.SORT_DESCENDING
import com.simplemobiletools.gallery.activities.SettingsActivity
import com.simplemobiletools.gallery.helpers.Config
import com.simplemobiletools.gallery.helpers.IMAGES
import com.simplemobiletools.gallery.helpers.NOMEDIA
import com.simplemobiletools.gallery.helpers.VIDEOS
import com.simplemobiletools.gallery.models.Medium
import java.io.File
import java.util.*
fun Context.getRealPathFromURI(uri: Uri): String? {
var cursor: Cursor? = null
try {
val projection = arrayOf(MediaStore.Images.Media.DATA)
cursor = contentResolver.query(uri, projection, null, null, null)
if (cursor?.moveToFirst() == true) {
val index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
return cursor.getString(index)
}
} catch (e: Exception) {
} finally {
cursor?.close()
}
return null
}
fun Context.getHumanizedFilename(path: String): String {
val humanized = humanizePath(path)
return humanized.substring(humanized.lastIndexOf("/") + 1)
}
fun Context.launchSettings() {
startActivity(Intent(this, SettingsActivity::class.java))
}
val Context.config: Config get() = Config.newInstance(this)
fun Context.getFilesFrom(curPath: String, isPickImage: Boolean, isPickVideo: Boolean): ArrayList<Medium> {
val projection = arrayOf(MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.DATE_TAKEN,
MediaStore.Images.Media.DATE_MODIFIED,
MediaStore.Images.Media.DATA,
MediaStore.Images.Media.SIZE)
val uri = MediaStore.Files.getContentUri("external")
val selection = if (curPath.isEmpty()) null else "(${MediaStore.Images.Media.DATA} LIKE ? AND ${MediaStore.Images.Media.DATA} NOT LIKE ?)"
val selectionArgs = if (curPath.isEmpty()) null else arrayOf("$curPath/%", "$curPath/%/%")
try {
val cur = contentResolver.query(uri, projection, selection, selectionArgs, getSortingForFolder(curPath))
return parseCursor(this, cur, isPickImage, isPickVideo, curPath)
} catch (e: Exception) {
return ArrayList()
}
}
private fun parseCursor(context: Context, cur: Cursor, isPickImage: Boolean, isPickVideo: Boolean, curPath: String): ArrayList<Medium> {
val curMedia = ArrayList<Medium>()
val config = context.config
val showMedia = config.showMedia
val showHidden = config.shouldShowHidden
val excludedFolders = config.excludedFolders
val noMediaFolders = context.getNoMediaFolders()
cur.use { cur ->
if (cur.moveToFirst()) {
do {
try {
val path = cur.getStringValue(MediaStore.Images.Media.DATA)
var size = cur.getLongValue(MediaStore.Images.Media.SIZE)
if (size == 0L) {
size = File(path).length()
}
if (size <= 0L) {
continue
}
var filename = cur.getStringValue(MediaStore.Images.Media.DISPLAY_NAME) ?: ""
if (filename.isEmpty())
filename = path.getFilenameFromPath()
val isImage = filename.isImageFast() || filename.isGif()
val isVideo = if (isImage) false else filename.isVideoFast()
if (!isImage && !isVideo)
continue
if (isVideo && (isPickImage || showMedia == IMAGES))
continue
if (isImage && (isPickVideo || showMedia == VIDEOS))
continue
if (!showHidden && filename.startsWith('.'))
continue
var isExcluded = false
excludedFolders.forEach {
if (path.startsWith(it)) {
isExcluded = true
}
}
if (!isExcluded && !showHidden) {
noMediaFolders.forEach {
if (path.startsWith(it)) {
isExcluded = true
}
}
}
if (!isExcluded && !showHidden && path.contains("/.")) {
isExcluded = true
}
if (!isExcluded) {
val dateTaken = cur.getLongValue(MediaStore.Images.Media.DATE_TAKEN)
val dateModified = cur.getIntValue(MediaStore.Images.Media.DATE_MODIFIED) * 1000L
val medium = Medium(filename, path, isVideo, dateModified, dateTaken, size)
curMedia.add(medium)
}
} catch (e: Exception) {
continue
}
} while (cur.moveToNext())
}
}
config.includedFolders.filter { it.isEmpty() || it == curPath }.mapNotNull { File(it).listFiles() }.forEach {
for (file in it) {
val size = file.length()
if (size <= 0L) {
continue
}
val filename = file.name
val isImage = filename.isImageFast() || filename.isGif()
val isVideo = if (isImage) false else filename.isVideoFast()
if (!isImage && !isVideo)
continue
if (isVideo && (isPickImage || showMedia == IMAGES))
continue
if (isImage && (isPickVideo || showMedia == VIDEOS))
continue
val dateTaken = file.lastModified()
val dateModified = file.lastModified()
val medium = Medium(filename, file.absolutePath, isVideo, dateModified, dateTaken, size)
val isAlreadyAdded = curMedia.any { it.path == file.absolutePath }
if (!isAlreadyAdded)
curMedia.add(medium)
}
}
Medium.sorting = config.getFileSorting(curPath)
curMedia.sort()
return curMedia
}
fun Context.getSortingForFolder(path: String): String {
val sorting = config.getFileSorting(path)
val sortValue = if (sorting and SORT_BY_NAME > 0)
MediaStore.Images.Media.DISPLAY_NAME
else if (sorting and SORT_BY_SIZE > 0)
MediaStore.Images.Media.SIZE
else if (sorting and SORT_BY_DATE_MODIFIED > 0)
MediaStore.Images.Media.DATE_MODIFIED
else
MediaStore.Images.Media.DATE_TAKEN
return if (sorting and SORT_DESCENDING > 0)
"$sortValue DESC"
else
"$sortValue ASC"
}
fun Context.getNoMediaFolders(): ArrayList<String> {
val folders = ArrayList<String>()
val noMediaCondition = "${MediaStore.Files.FileColumns.MEDIA_TYPE} = ${MediaStore.Files.FileColumns.MEDIA_TYPE_NONE}"
val uri = MediaStore.Files.getContentUri("external")
val columns = arrayOf(MediaStore.Files.FileColumns.DATA)
val where = "$noMediaCondition AND ${MediaStore.Files.FileColumns.TITLE} LIKE ?"
val args = arrayOf("%$NOMEDIA%")
var cursor: Cursor? = null
try {
cursor = contentResolver.query(uri, columns, where, args, null)
if (cursor?.moveToFirst() == true) {
do {
val path = cursor.getString(cursor.getColumnIndex(MediaStore.Files.FileColumns.DATA)) ?: continue
val noMediaFile = File(path)
if (noMediaFile.exists())
folders.add(noMediaFile.parent)
} while (cursor.moveToNext())
}
} finally {
cursor?.close()
}
return folders
}
fun Context.getLastMediaModified(): Int {
val max = "max"
val uri = MediaStore.Files.getContentUri("external")
val projection = arrayOf(MediaStore.Images.Media._ID, "MAX(${MediaStore.Images.Media.DATE_MODIFIED}) AS $max")
var cursor: Cursor? = null
try {
cursor = contentResolver.query(uri, projection, null, null, null)
if (cursor?.moveToFirst() == true) {
return cursor.getIntValue(max)
}
} finally {
cursor?.close()
}
return 0
}

View file

@ -1,9 +0,0 @@
package com.simplemobiletools.gallery.extensions
import android.os.Environment
import com.simplemobiletools.gallery.helpers.NOMEDIA
import java.io.File
fun File.containsNoMedia() = isDirectory && File(this, NOMEDIA).exists()
fun File.isDownloadsFolder() = absolutePath == Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString()

View file

@ -1,268 +0,0 @@
package com.simplemobiletools.gallery.fragments
import android.content.res.Configuration
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Color
import android.graphics.Matrix
import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.bumptech.glide.Glide
import com.bumptech.glide.Priority
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.simplemobiletools.commons.extensions.beGone
import com.simplemobiletools.commons.extensions.beVisible
import com.simplemobiletools.commons.extensions.toast
import com.simplemobiletools.gallery.R
import com.simplemobiletools.gallery.activities.ViewPagerActivity
import com.simplemobiletools.gallery.extensions.config
import com.simplemobiletools.gallery.extensions.getFileSignature
import com.simplemobiletools.gallery.extensions.getRealPathFromURI
import com.simplemobiletools.gallery.helpers.GlideRotateTransformation
import com.simplemobiletools.gallery.helpers.MEDIUM
import com.simplemobiletools.gallery.models.Medium
import it.sephiroth.android.library.exif2.ExifInterface
import kotlinx.android.synthetic.main.pager_photo_item.view.*
import uk.co.senab.photoview.PhotoViewAttacher
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
class PhotoFragment : ViewPagerFragment() {
lateinit var medium: Medium
lateinit var view: ViewGroup
private var isFragmentVisible = false
private var wasInit = false
private var RATIO_THRESHOLD = 0.1
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
view = inflater.inflate(R.layout.pager_photo_item, container, false) as ViewGroup
medium = arguments.getSerializable(MEDIUM) as Medium
if (medium.path.startsWith("content://")) {
val originalPath = medium.path
medium.path = context.getRealPathFromURI(Uri.parse(medium.path)) ?: ""
if (medium.path.isEmpty()) {
var out: FileOutputStream? = null
try {
var inputStream = context.contentResolver.openInputStream(Uri.parse(originalPath))
val exif = ExifInterface()
exif.readExif(inputStream, ExifInterface.Options.OPTION_ALL)
val tag = exif.getTag(ExifInterface.TAG_ORIENTATION)
val orientation = tag?.getValueAsInt(-1) ?: -1
inputStream = context.contentResolver.openInputStream(Uri.parse(originalPath))
val original = BitmapFactory.decodeStream(inputStream)
val rotated = rotateViaMatrix(original, orientation)
exif.setTagValue(ExifInterface.TAG_ORIENTATION, 1)
exif.removeCompressedThumbnail()
val file = File(context.externalCacheDir, Uri.parse(originalPath).lastPathSegment)
out = FileOutputStream(file)
rotated.compress(Bitmap.CompressFormat.JPEG, 100, out)
medium.path = file.absolutePath
} catch (e: Exception) {
activity.toast(R.string.unknown_error_occurred)
return view
} finally {
try {
out?.close()
} catch (e: IOException) {
}
}
}
}
view.subsampling_view.setOnClickListener({ photoClicked() })
view.photo_view.apply {
maximumScale = 8f
mediumScale = 3f
setOnPhotoTapListener(object : PhotoViewAttacher.OnPhotoTapListener {
override fun onPhotoTap(view: View?, x: Float, y: Float) {
photoClicked()
}
override fun onOutsidePhotoTap() {
photoClicked()
}
})
}
loadImage()
activity.window.decorView.setOnSystemUiVisibilityChangeListener { visibility ->
listener?.systemUiVisibilityChanged(visibility)
}
wasInit = true
return view
}
override fun setMenuVisibility(menuVisible: Boolean) {
super.setMenuVisibility(menuVisible)
isFragmentVisible = menuVisible
if (wasInit) {
if (menuVisible) {
addZoomableView()
} else {
view.subsampling_view.apply {
recycle()
beGone()
background = ColorDrawable(Color.TRANSPARENT)
}
}
}
}
private fun degreesForRotation(orientation: Int): Int {
return when (orientation) {
8 -> 270
3 -> 180
6 -> 90
else -> 0
}
}
private fun rotateViaMatrix(original: Bitmap, orientation: Int): Bitmap {
val degrees = degreesForRotation(orientation).toFloat()
return if (degrees == 0f) {
original
} else {
val matrix = Matrix()
matrix.setRotate(degrees)
Bitmap.createBitmap(original, 0, 0, original.width, original.height, matrix, true)
}
}
private fun loadImage() {
if (medium.isGif()) {
Glide.with(this)
.load(medium.path)
.asGif()
.crossFade()
.priority(if (isFragmentVisible) Priority.IMMEDIATE else Priority.LOW)
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
.into(view.photo_view)
} else {
loadBitmap()
}
}
private fun loadBitmap(degrees: Float = 0f) {
if (degrees == 0f) {
val targetWidth = if (ViewPagerActivity.screenWidth == 0) Target.SIZE_ORIGINAL else ViewPagerActivity.screenWidth
val targetHeight = if (ViewPagerActivity.screenHeight == 0) Target.SIZE_ORIGINAL else ViewPagerActivity.screenHeight
Glide.with(this)
.load(medium.path)
.asBitmap()
.signature(activity.getFileSignature(medium.path))
.format(if (medium.isPng()) DecodeFormat.PREFER_ARGB_8888 else DecodeFormat.PREFER_RGB_565)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.override(targetWidth, targetHeight)
.listener(object : RequestListener<String, Bitmap> {
override fun onException(e: Exception?, model: String?, target: Target<Bitmap>?, isFirstResource: Boolean): Boolean {
return false
}
override fun onResourceReady(resource: Bitmap, model: String?, target: Target<Bitmap>?, isFromMemoryCache: Boolean, isFirstResource: Boolean): Boolean {
if (isFragmentVisible)
addZoomableView()
return false
}
}).into(view.photo_view)
} else {
Glide.with(this)
.load(medium.path)
.asBitmap()
.transform(GlideRotateTransformation(context, degrees))
.thumbnail(0.2f)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.into(view.photo_view)
}
}
private fun addZoomableView() {
if ((medium.isImage()) && isFragmentVisible && view.subsampling_view.visibility == View.GONE) {
view.subsampling_view.apply {
maxScale = 10f
beVisible()
setImage(ImageSource.uri(medium.path))
orientation = SubsamplingScaleImageView.ORIENTATION_USE_EXIF
setOnImageEventListener(object : SubsamplingScaleImageView.OnImageEventListener {
override fun onImageLoaded() {
}
override fun onReady() {
background = ColorDrawable(if (context.config.darkBackground) Color.BLACK else context.config.backgroundColor)
setDoubleTapZoomScale(getDoubleTapZoomScale())
}
override fun onTileLoadError(e: Exception?) {
}
override fun onPreviewReleased() {
}
override fun onImageLoadError(e: Exception?) {
background = ColorDrawable(Color.TRANSPARENT)
beGone()
}
override fun onPreviewLoadError(e: Exception?) {
background = ColorDrawable(Color.TRANSPARENT)
beGone()
}
})
}
}
}
private fun getDoubleTapZoomScale(): Float {
val displayAspectRatio = ViewPagerActivity.screenHeight / (ViewPagerActivity.screenWidth).toFloat()
val bitmapOptions = BitmapFactory.Options()
bitmapOptions.inJustDecodeBounds = true
BitmapFactory.decodeFile(medium.path, bitmapOptions)
val width = bitmapOptions.outWidth
val height = bitmapOptions.outHeight
val bitmapAspectRatio = height / (width).toFloat()
return if (Math.abs(displayAspectRatio - bitmapAspectRatio) < RATIO_THRESHOLD) {
2f
} else if (bitmapAspectRatio > 1f) {
width / (height).toFloat()
} else {
bitmapAspectRatio
}
}
override fun onDestroyView() {
super.onDestroyView()
Glide.clear(view.photo_view)
}
fun rotateImageViewBy(degrees: Float) {
view.subsampling_view.beGone()
loadBitmap(degrees)
}
override fun onConfigurationChanged(newConfig: Configuration?) {
super.onConfigurationChanged(newConfig)
loadImage()
}
private fun photoClicked() {
listener?.fragmentClicked()
}
}

View file

@ -1,364 +0,0 @@
package com.simplemobiletools.gallery.fragments
import android.content.res.Configuration
import android.media.AudioManager
import android.media.MediaPlayer
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.util.DisplayMetrics
import android.util.Log
import android.view.*
import android.view.animation.AnimationUtils
import android.widget.SeekBar
import android.widget.TextView
import com.simplemobiletools.commons.extensions.getFormattedDuration
import com.simplemobiletools.commons.extensions.toast
import com.simplemobiletools.commons.extensions.updateTextColors
import com.simplemobiletools.gallery.R
import com.simplemobiletools.gallery.extensions.config
import com.simplemobiletools.gallery.extensions.getNavBarHeight
import com.simplemobiletools.gallery.extensions.hasNavBar
import com.simplemobiletools.gallery.helpers.MEDIUM
import com.simplemobiletools.gallery.models.Medium
import kotlinx.android.synthetic.main.pager_video_item.view.*
import java.io.IOException
class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSeekBarChangeListener {
private var mMediaPlayer: MediaPlayer? = null
private var mSurfaceView: SurfaceView? = null
private var mSurfaceHolder: SurfaceHolder? = null
private var mCurrTimeView: TextView? = null
private var mTimerHandler: Handler? = null
private var mSeekBar: SeekBar? = null
private var mTimeHolder: View? = null
private var mIsPlaying = false
private var mIsDragged = false
private var mIsFullscreen = false
private var mIsFragmentVisible = false
private var mCurrTime = 0
private var mDuration = 0
lateinit var mView: View
lateinit var medium: Medium
companion object {
private val TAG = VideoFragment::class.java.simpleName
private val PROGRESS = "progress"
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
mView = inflater.inflate(R.layout.pager_video_item, container, false)
setupPlayer()
medium = arguments.getSerializable(MEDIUM) as Medium
if (savedInstanceState != null) {
mCurrTime = savedInstanceState.getInt(PROGRESS)
}
mIsFullscreen = activity.window.decorView.systemUiVisibility and View.SYSTEM_UI_FLAG_FULLSCREEN == View.SYSTEM_UI_FLAG_FULLSCREEN
activity.window.decorView.setOnSystemUiVisibilityChangeListener { visibility ->
val fullscreen = visibility and View.SYSTEM_UI_FLAG_FULLSCREEN != 0
mIsFullscreen = fullscreen
checkFullscreen()
listener?.systemUiVisibilityChanged(visibility)
}
return mView
}
override fun onResume() {
super.onResume()
activity.updateTextColors(mView.video_holder)
}
private fun setupPlayer() {
if (activity == null)
return
mView.video_play_outline.setOnClickListener { togglePlayPause() }
mSurfaceView = mView.video_surface
mSurfaceHolder = mSurfaceView!!.holder
mSurfaceHolder!!.addCallback(this)
mSurfaceView!!.setOnClickListener({ toggleFullscreen() })
mView.video_holder.setOnClickListener { toggleFullscreen() }
initTimeHolder()
}
override fun setMenuVisibility(menuVisible: Boolean) {
super.setMenuVisibility(menuVisible)
mIsFragmentVisible = menuVisible
if (menuVisible) {
if (context?.config?.autoplayVideos == true) {
playVideo()
}
} else if (mIsPlaying) {
pauseVideo()
}
}
override fun onConfigurationChanged(newConfig: Configuration?) {
super.onConfigurationChanged(newConfig)
setVideoSize()
initTimeHolder()
}
private fun toggleFullscreen() {
mIsFullscreen = !mIsFullscreen
checkFullscreen()
listener?.fragmentClicked()
}
private fun initTimeHolder() {
mTimeHolder = mView.video_time_holder
val res = resources
val height = res.getNavBarHeight()
val left = mTimeHolder!!.paddingLeft
val top = mTimeHolder!!.paddingTop
var right = res.getDimension(R.dimen.timer_padding).toInt()
var bottom = 0
if (activity.hasNavBar()) {
if (res.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
bottom += height
} else {
right += height
}
mTimeHolder!!.setPadding(left, top, right, bottom)
}
mCurrTimeView = mView.video_curr_time
mSeekBar = mView.video_seekbar
mSeekBar!!.setOnSeekBarChangeListener(this)
if (mIsFullscreen)
mTimeHolder!!.visibility = View.INVISIBLE
}
private fun setupTimeHolder() {
mSeekBar!!.max = mDuration
mView.video_duration.text = mDuration.getFormattedDuration()
mTimerHandler = Handler()
setupTimer()
}
private fun setupTimer() {
activity.runOnUiThread(object : Runnable {
override fun run() {
if (mMediaPlayer != null && !mIsDragged && mIsPlaying) {
mCurrTime = mMediaPlayer!!.currentPosition / 1000
mSeekBar!!.progress = mCurrTime
mCurrTimeView!!.text = mCurrTime.getFormattedDuration()
}
mTimerHandler!!.postDelayed(this, 1000)
}
})
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt(PROGRESS, mCurrTime)
}
private fun checkFullscreen() {
if (activity == null)
return
var anim = android.R.anim.fade_in
if (mIsFullscreen) {
anim = android.R.anim.fade_out
mSeekBar!!.setOnSeekBarChangeListener(null)
} else {
mSeekBar!!.setOnSeekBarChangeListener(this)
}
AnimationUtils.loadAnimation(activity, anim).apply {
duration = 150
fillAfter = true
mTimeHolder!!.startAnimation(this)
}
}
private fun togglePlayPause() {
if (activity == null || !isAdded)
return
mIsPlaying = !mIsPlaying
if (mIsPlaying) {
playVideo()
} else {
pauseVideo()
}
}
private fun playVideo() {
mIsPlaying = true
mMediaPlayer?.start()
mView.video_play_outline.setImageDrawable(null)
activity.window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
private fun pauseVideo() {
mIsPlaying = false
mMediaPlayer?.pause()
mView.video_play_outline.setImageDrawable(resources.getDrawable(R.drawable.img_play_outline_big))
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
private fun initMediaPlayer() {
if (mMediaPlayer != null)
return
try {
mMediaPlayer = MediaPlayer().apply {
setDataSource(context, Uri.parse(medium.path))
setDisplay(mSurfaceHolder)
setOnCompletionListener { videoCompleted() }
setOnVideoSizeChangedListener({ mediaPlayer, width, height -> setVideoSize() })
setOnPreparedListener { videoPrepared(it) }
setAudioStreamType(AudioManager.STREAM_MUSIC)
prepareAsync()
}
} catch (e: IOException) {
Log.e(TAG, "init media player failed $e")
}
}
private fun setProgress(seconds: Int) {
mMediaPlayer!!.seekTo(seconds * 1000)
mSeekBar!!.progress = seconds
mCurrTimeView!!.text = seconds.getFormattedDuration()
}
private fun addPreviewImage() {
mMediaPlayer!!.start()
mMediaPlayer!!.pause()
}
override fun onPause() {
super.onPause()
pauseVideo()
mIsFragmentVisible = false
}
override fun onDestroy() {
super.onDestroy()
if (activity?.isChangingConfigurations == false) {
cleanup()
}
}
private fun cleanup() {
pauseVideo()
mCurrTimeView?.text = 0.getFormattedDuration()
mMediaPlayer?.release()
mMediaPlayer = null
mSeekBar?.progress = 0
mTimerHandler?.removeCallbacksAndMessages(null)
}
private fun videoPrepared(mediaPlayer: MediaPlayer) {
mDuration = mediaPlayer.duration / 1000
addPreviewImage()
setupTimeHolder()
setProgress(mCurrTime)
if (mIsFragmentVisible && context.config.autoplayVideos)
playVideo()
}
private fun videoCompleted() {
if (context.config.loopVideos) {
playVideo()
} else {
mSeekBar!!.progress = mSeekBar!!.max
mCurrTimeView!!.text = mDuration.getFormattedDuration()
pauseVideo()
}
}
override fun surfaceCreated(holder: SurfaceHolder) {
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
if (width != 0 && height != 0 && mSurfaceView != null)
setVideoSize()
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
mMediaPlayer?.release()
mMediaPlayer = null
}
private fun setVideoSize() {
if (mSurfaceHolder == null)
mSurfaceHolder = mSurfaceView!!.holder
if (activity == null || mSurfaceHolder == null || !mSurfaceHolder!!.surface.isValid)
return
initMediaPlayer()
if (mMediaPlayer == null) {
activity.toast(R.string.unknown_error_occurred)
return
}
val videoProportion = mMediaPlayer!!.videoWidth.toFloat() / mMediaPlayer!!.videoHeight.toFloat()
val display = activity.windowManager.defaultDisplay
val screenWidth: Int
val screenHeight: Int
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
val realMetrics = DisplayMetrics()
display.getRealMetrics(realMetrics)
screenWidth = realMetrics.widthPixels
screenHeight = realMetrics.heightPixels
} else {
screenWidth = display.width
screenHeight = display.height
}
val screenProportion = screenWidth.toFloat() / screenHeight.toFloat()
mSurfaceView!!.layoutParams.apply {
if (videoProportion > screenProportion) {
width = screenWidth
height = (screenWidth.toFloat() / videoProportion).toInt()
} else {
width = (videoProportion * screenHeight.toFloat()).toInt()
height = screenHeight
}
mSurfaceView!!.layoutParams = this
}
}
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
if (mMediaPlayer != null && fromUser) {
setProgress(progress)
}
}
override fun onStartTrackingTouch(seekBar: SeekBar) {
initMediaPlayer()
mMediaPlayer!!.pause()
mIsDragged = true
}
override fun onStopTrackingTouch(seekBar: SeekBar) {
if (!mIsPlaying) {
togglePlayPause()
} else {
mMediaPlayer!!.start()
}
mIsDragged = false
}
}

View file

@ -1,13 +0,0 @@
package com.simplemobiletools.gallery.fragments
import android.support.v4.app.Fragment
abstract class ViewPagerFragment : Fragment() {
var listener: FragmentListener? = null
interface FragmentListener {
fun fragmentClicked()
fun systemUiVisibilityChanged(visibility: Int)
}
}

View file

@ -1,182 +0,0 @@
package com.simplemobiletools.gallery.helpers
import android.content.Context
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.simplemobiletools.commons.helpers.BaseConfig
import com.simplemobiletools.commons.helpers.SORT_BY_DATE_MODIFIED
import com.simplemobiletools.commons.helpers.SORT_DESCENDING
import com.simplemobiletools.gallery.R
import com.simplemobiletools.gallery.models.AlbumCover
import java.util.*
class Config(context: Context) : BaseConfig(context) {
companion object {
fun newInstance(context: Context) = Config(context)
}
var fileSorting: Int
get() = prefs.getInt(SORT_ORDER, SORT_BY_DATE_MODIFIED or SORT_DESCENDING)
set(order) = prefs.edit().putInt(SORT_ORDER, order).apply()
var directorySorting: Int
get() = prefs.getInt(DIRECTORY_SORT_ORDER, SORT_BY_DATE_MODIFIED or SORT_DESCENDING)
set(order) = prefs.edit().putInt(DIRECTORY_SORT_ORDER, order).apply()
fun saveFileSorting(path: String, value: Int) {
if (path.isEmpty()) {
fileSorting = value
} else {
prefs.edit().putInt(SORT_FOLDER_PREFIX + path, value).apply()
}
}
fun getFileSorting(path: String) = prefs.getInt(SORT_FOLDER_PREFIX + path, fileSorting)
fun removeFileSorting(path: String) {
prefs.edit().remove(SORT_FOLDER_PREFIX + path).apply()
}
fun hasCustomSorting(path: String) = prefs.contains(SORT_FOLDER_PREFIX + path)
var wasHideFolderTooltipShown: Boolean
get() = prefs.getBoolean(HIDE_FOLDER_TOOLTIP_SHOWN, false)
set(wasShown) = prefs.edit().putBoolean(HIDE_FOLDER_TOOLTIP_SHOWN, wasShown).apply()
var shouldShowHidden = showHiddenMedia || temporarilyShowHidden
var showHiddenMedia: Boolean
get() = prefs.getBoolean(SHOW_HIDDEN_MEDIA, false)
set(showHiddenFolders) = prefs.edit().putBoolean(SHOW_HIDDEN_MEDIA, showHiddenFolders).apply()
var temporarilyShowHidden: Boolean
get() = prefs.getBoolean(TEMPORARILY_SHOW_HIDDEN, false)
set(temporarilyShowHidden) = prefs.edit().putBoolean(TEMPORARILY_SHOW_HIDDEN, temporarilyShowHidden).apply()
var pinnedFolders: Set<String>
get() = prefs.getStringSet(PINNED_FOLDERS, HashSet<String>())
set(pinnedFolders) = prefs.edit().putStringSet(PINNED_FOLDERS, pinnedFolders).apply()
var showAll: Boolean
get() = prefs.getBoolean(SHOW_ALL, false)
set(showAll) = prefs.edit().putBoolean(SHOW_ALL, showAll).apply()
fun addPinnedFolders(paths: Set<String>) {
val currPinnedFolders = HashSet<String>(pinnedFolders)
currPinnedFolders.addAll(paths)
pinnedFolders = currPinnedFolders
}
fun removePinnedFolders(paths: Set<String>) {
val currPinnedFolders = HashSet<String>(pinnedFolders)
currPinnedFolders.removeAll(paths)
pinnedFolders = currPinnedFolders
}
fun addExcludedFolder(path: String) {
addExcludedFolders(HashSet<String>(Arrays.asList(path)))
}
fun addExcludedFolders(paths: Set<String>) {
val currExcludedFolders = HashSet<String>(excludedFolders)
currExcludedFolders.addAll(paths)
excludedFolders = currExcludedFolders
}
fun removeExcludedFolder(path: String) {
val currExcludedFolders = HashSet<String>(excludedFolders)
currExcludedFolders.remove(path)
excludedFolders = currExcludedFolders
}
var excludedFolders: MutableSet<String>
get() = prefs.getStringSet(EXCLUDED_FOLDERS, getDataFolder())
set(excludedFolders) = prefs.edit().remove(EXCLUDED_FOLDERS).putStringSet(EXCLUDED_FOLDERS, excludedFolders).apply()
private fun getDataFolder(): Set<String> {
val folders = HashSet<String>()
val dataFolder = context.externalCacheDir?.parentFile?.parent?.trimEnd('/') ?: ""
if (dataFolder.endsWith("data"))
folders.add(dataFolder)
return folders
}
fun addIncludedFolder(path: String) {
val currIncludedFolders = HashSet<String>(includedFolders)
currIncludedFolders.add(path)
includedFolders = currIncludedFolders
}
fun removeIncludedFolder(path: String) {
val currIncludedFolders = HashSet<String>(includedFolders)
currIncludedFolders.remove(path)
includedFolders = currIncludedFolders
}
var includedFolders: MutableSet<String>
get() = prefs.getStringSet(INCLUDED_FOLDERS, HashSet<String>())
set(includedFolders) = prefs.edit().remove(INCLUDED_FOLDERS).putStringSet(INCLUDED_FOLDERS, includedFolders).apply()
fun saveFolderMedia(path: String, json: String) {
prefs.edit().putString(SAVE_FOLDER_PREFIX + path, json).apply()
}
fun loadFolderMedia(path: String) = prefs.getString(SAVE_FOLDER_PREFIX + path, "")
var autoplayVideos: Boolean
get() = prefs.getBoolean(AUTOPLAY_VIDEOS, false)
set(autoplay) = prefs.edit().putBoolean(AUTOPLAY_VIDEOS, autoplay).apply()
var animateGifs: Boolean
get() = prefs.getBoolean(ANIMATE_GIFS, false)
set(animateGifs) = prefs.edit().putBoolean(ANIMATE_GIFS, animateGifs).apply()
var maxBrightness: Boolean
get() = prefs.getBoolean(MAX_BRIGHTNESS, false)
set(maxBrightness) = prefs.edit().putBoolean(MAX_BRIGHTNESS, maxBrightness).apply()
var cropThumbnails: Boolean
get() = prefs.getBoolean(CROP_THUMBNAILS, true)
set(cropThumbnails) = prefs.edit().putBoolean(CROP_THUMBNAILS, cropThumbnails).apply()
var screenRotation: Int
get() = prefs.getInt(SCREEN_ROTATION, ROTATE_BY_SYSTEM_SETTING)
set(screenRotation) = prefs.edit().putInt(SCREEN_ROTATION, screenRotation).apply()
var loopVideos: Boolean
get() = prefs.getBoolean(LOOP_VIDEOS, false)
set(loop) = prefs.edit().putBoolean(LOOP_VIDEOS, loop).apply()
var displayFileNames: Boolean
get() = prefs.getBoolean(DISPLAY_FILE_NAMES, false)
set(display) = prefs.edit().putBoolean(DISPLAY_FILE_NAMES, display).apply()
var darkBackground: Boolean
get() = prefs.getBoolean(DARK_BACKGROUND, false)
set(darkBackground) = prefs.edit().putBoolean(DARK_BACKGROUND, darkBackground).apply()
var showMedia: Int
get() = prefs.getInt(SHOW_MEDIA, IMAGES_AND_VIDEOS)
set(showMedia) = prefs.edit().putInt(SHOW_MEDIA, showMedia).apply()
var dirColumnCnt: Int
get() = prefs.getInt(DIR_COLUMN_CNT, context.resources.getInteger(R.integer.directory_columns))
set(dirColumnCnt) = prefs.edit().putInt(DIR_COLUMN_CNT, dirColumnCnt).apply()
var mediaColumnCnt: Int
get() = prefs.getInt(MEDIA_COLUMN_CNT, context.resources.getInteger(R.integer.media_columns))
set(mediaColumnCnt) = prefs.edit().putInt(MEDIA_COLUMN_CNT, mediaColumnCnt).apply()
var directories: String
get() = prefs.getString(DIRECTORIES, "")
set(directories) = prefs.edit().putString(DIRECTORIES, directories).apply()
var albumCovers: String
get() = prefs.getString(ALBUM_COVERS, "")
set(albumCovers) = prefs.edit().putString(ALBUM_COVERS, albumCovers).apply()
fun parseAlbumCovers(): ArrayList<AlbumCover> {
val listType = object : TypeToken<List<AlbumCover>>() {}.type
return Gson().fromJson<ArrayList<AlbumCover>>(albumCovers, listType) ?: ArrayList(1)
}
}

View file

@ -1,55 +0,0 @@
package com.simplemobiletools.gallery.helpers
// shared preferences
val SORT_ORDER = "sort_order"
val DIRECTORY_SORT_ORDER = "directory_sort_order"
val SORT_FOLDER_PREFIX = "sort_folder_"
val SHOW_HIDDEN_MEDIA = "show_hidden_media"
val TEMPORARILY_SHOW_HIDDEN = "temporarily_show_hidden"
val AUTOPLAY_VIDEOS = "autoplay_videos"
val LOOP_VIDEOS = "loop_videos"
val ANIMATE_GIFS = "animate_gifs"
val MAX_BRIGHTNESS = "max_brightness"
val CROP_THUMBNAILS = "crop_thumbnails"
val SCREEN_ROTATION = "screen_rotation"
val DISPLAY_FILE_NAMES = "display_file_names"
val DARK_BACKGROUND = "dark_background"
val PINNED_FOLDERS = "pinned_folders"
val DIR_COLUMN_CNT = "dir_column_cnt"
val MEDIA_COLUMN_CNT = "media_column_cnt"
val SHOW_ALL = "show_all" // display images and videos from all folders together
val SHOW_MEDIA = "show_media"
val SAVE_FOLDER_PREFIX = "folder2_"
val HIDE_FOLDER_TOOLTIP_SHOWN = "hide_folder_tooltip_shown"
val EXCLUDED_FOLDERS = "excluded_folders"
val INCLUDED_FOLDERS = "included_folders"
val ALBUM_COVERS = "album_covers"
val NOMEDIA = ".nomedia"
val DIRECTORY = "directory"
val MEDIUM = "medium"
val GET_IMAGE_INTENT = "get_image_intent"
val GET_VIDEO_INTENT = "get_video_intent"
val GET_ANY_INTENT = "get_any_intent"
val SET_WALLPAPER_INTENT = "set_wallpaper_intent"
val DIRECTORIES = "directories2"
val IS_VIEW_INTENT = "is_view_intent"
val REQUEST_EDIT_IMAGE = 1
val REQUEST_SET_WALLPAPER = 2
// show media
val IMAGES_AND_VIDEOS = 0
val IMAGES = 1
val VIDEOS = 2
// rotations
val ROTATE_BY_SYSTEM_SETTING = 0
val ROTATE_BY_DEVICE_ROTATION = 1
val ROTATE_BY_ASPECT_RATIO = 2
val ORIENT_PORTRAIT = 0
val ORIENT_LANDSCAPE_LEFT = 1
val ORIENT_LANDSCAPE_RIGHT = 2
val ORIENT_UPSIDE_DOWN = 3

View file

@ -1,21 +0,0 @@
package com.simplemobiletools.gallery.helpers
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Matrix
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
class GlideRotateTransformation(context: Context, val rotateRotationAngle: Float) : BitmapTransformation(context) {
override fun transform(pool: BitmapPool, bitmap: Bitmap, outWidth: Int, outHeight: Int): Bitmap {
if (rotateRotationAngle % 360 == 0f)
return bitmap
val matrix = Matrix()
matrix.postRotate(rotateRotationAngle)
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
}
override fun getId() = "GlideRotateTransformation $rotateRotationAngle"
}

View file

@ -1,52 +0,0 @@
package com.simplemobiletools.gallery.models
import com.simplemobiletools.commons.helpers.SORT_BY_DATE_MODIFIED
import com.simplemobiletools.commons.helpers.SORT_BY_NAME
import com.simplemobiletools.commons.helpers.SORT_BY_SIZE
import com.simplemobiletools.commons.helpers.SORT_DESCENDING
import java.io.Serializable
data class Directory(val path: String, val tmb: String, val name: String, var mediaCnt: Int, val modified: Long, val taken: Long,
var size: Long) : Serializable, Comparable<Directory> {
companion object {
private val serialVersionUID = -6553345863555455L
var sorting: Int = 0
}
fun addSize(bytes: Long) {
size += bytes
}
override fun compareTo(other: Directory): Int {
var result: Int
if (sorting and SORT_BY_NAME != 0) {
result = name.toLowerCase().compareTo(other.name.toLowerCase())
} else if (sorting and SORT_BY_SIZE != 0) {
result = if (size == other.size)
0
else if (size > other.size)
1
else
-1
} else if (sorting and SORT_BY_DATE_MODIFIED != 0) {
result = if (modified == other.modified)
0
else if (modified > other.modified)
1
else
-1
} else {
result = if (taken == other.taken)
0
else if (taken > other.taken)
1
else
-1
}
if (sorting and SORT_DESCENDING != 0) {
result *= -1
}
return result
}
}

View file

@ -1,59 +0,0 @@
package com.simplemobiletools.gallery.models
import com.simplemobiletools.commons.extensions.getMimeType
import com.simplemobiletools.commons.extensions.isGif
import com.simplemobiletools.commons.extensions.isPng
import com.simplemobiletools.commons.helpers.SORT_BY_DATE_MODIFIED
import com.simplemobiletools.commons.helpers.SORT_BY_NAME
import com.simplemobiletools.commons.helpers.SORT_BY_SIZE
import com.simplemobiletools.commons.helpers.SORT_DESCENDING
import java.io.File
import java.io.Serializable
data class Medium(var name: String, var path: String, val video: Boolean, val modified: Long, val taken: Long, val size: Long) : Serializable, Comparable<Medium> {
companion object {
private val serialVersionUID = -6553149366975455L
var sorting: Int = 0
}
fun isPng() = path.isPng()
fun isGif() = path.isGif()
fun isImage() = !isGif() && !video
fun getMimeType() = File(path).getMimeType()
override fun compareTo(other: Medium): Int {
var res: Int
if (sorting and SORT_BY_NAME != 0) {
res = name.toLowerCase().compareTo(other.name.toLowerCase())
} else if (sorting and SORT_BY_SIZE != 0) {
res = if (size == other.size)
0
else if (size > other.size)
1
else
-1
} else if (sorting and SORT_BY_DATE_MODIFIED != 0) {
res = if (modified == other.modified)
0
else if (modified > other.modified)
1
else
-1
} else {
res = if (taken == other.taken)
0
else if (taken > other.taken)
1
else
-1
}
if (sorting and SORT_DESCENDING != 0) {
res *= -1
}
return res
}
}

View file

@ -0,0 +1,13 @@
package com.simplemobiletools.gallery.pro
import android.app.Application
import com.github.ajalt.reprint.core.Reprint
import com.simplemobiletools.commons.extensions.checkUseEnglish
class App : Application() {
override fun onCreate() {
super.onCreate()
checkUseEnglish()
Reprint.initialize(this)
}
}

View file

@ -0,0 +1,897 @@
package com.simplemobiletools.gallery.pro.activities
import android.annotation.TargetApi
import android.app.Activity
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Bitmap.CompressFormat
import android.graphics.Color
import android.graphics.Point
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.provider.MediaStore
import android.view.Menu
import android.view.MenuItem
import android.widget.RelativeLayout
import androidx.exifinterface.media.ExifInterface
import androidx.recyclerview.widget.LinearLayoutManager
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.Target
import com.simplemobiletools.commons.dialogs.ColorPickerDialog
import com.simplemobiletools.commons.dialogs.ConfirmationDialog
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.REAL_FILE_PATH
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.commons.helpers.isNougatPlus
import com.simplemobiletools.commons.models.FileDirItem
import com.simplemobiletools.gallery.pro.BuildConfig
import com.simplemobiletools.gallery.pro.R
import com.simplemobiletools.gallery.pro.adapters.FiltersAdapter
import com.simplemobiletools.gallery.pro.dialogs.OtherAspectRatioDialog
import com.simplemobiletools.gallery.pro.dialogs.ResizeDialog
import com.simplemobiletools.gallery.pro.dialogs.SaveAsDialog
import com.simplemobiletools.gallery.pro.extensions.config
import com.simplemobiletools.gallery.pro.extensions.copyNonDimensionAttributesTo
import com.simplemobiletools.gallery.pro.extensions.fixDateTaken
import com.simplemobiletools.gallery.pro.extensions.openEditor
import com.simplemobiletools.gallery.pro.helpers.*
import com.simplemobiletools.gallery.pro.models.FilterItem
import com.theartofdev.edmodo.cropper.CropImageView
import com.zomato.photofilters.FilterPack
import com.zomato.photofilters.imageprocessors.Filter
import kotlinx.android.synthetic.main.activity_edit.*
import kotlinx.android.synthetic.main.bottom_actions_aspect_ratio.*
import kotlinx.android.synthetic.main.bottom_editor_actions_filter.*
import kotlinx.android.synthetic.main.bottom_editor_crop_rotate_actions.*
import kotlinx.android.synthetic.main.bottom_editor_draw_actions.*
import kotlinx.android.synthetic.main.bottom_editor_primary_actions.*
import java.io.*
class EditActivity : SimpleActivity(), CropImageView.OnCropImageCompleteListener {
companion object {
init {
System.loadLibrary("NativeImageProcessor")
}
}
private val TEMP_FOLDER_NAME = "images"
private val ASPECT_X = "aspectX"
private val ASPECT_Y = "aspectY"
private val CROP = "crop"
// constants for bottom primary action groups
private val PRIMARY_ACTION_NONE = 0
private val PRIMARY_ACTION_FILTER = 1
private val PRIMARY_ACTION_CROP_ROTATE = 2
private val PRIMARY_ACTION_DRAW = 3
private val CROP_ROTATE_NONE = 0
private val CROP_ROTATE_ASPECT_RATIO = 1
private lateinit var saveUri: Uri
private var uri: Uri? = null
private var resizeWidth = 0
private var resizeHeight = 0
private var drawColor = 0
private var lastOtherAspectRatio: Pair<Float, Float>? = null
private var currPrimaryAction = PRIMARY_ACTION_NONE
private var currCropRotateAction = CROP_ROTATE_ASPECT_RATIO
private var currAspectRatio = ASPECT_RATIO_FREE
private var isCropIntent = false
private var isEditingWithThirdParty = false
private var isSharingBitmap = false
private var wasDrawCanvasPositioned = false
private var oldExif: ExifInterface? = null
private var filterInitialBitmap: Bitmap? = null
private var originalUri: Uri? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_edit)
if (checkAppSideloading()) {
return
}
initEditActivity()
}
override fun onResume() {
super.onResume()
isEditingWithThirdParty = false
bottom_draw_width.setColors(config.textColor, getAdjustedPrimaryColor(), config.backgroundColor)
}
override fun onStop() {
super.onStop()
if (isEditingWithThirdParty) {
finish()
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_editor, menu)
updateMenuItemColors(menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.save_as -> saveImage()
R.id.edit -> editWith()
R.id.share -> shareImage()
else -> return super.onOptionsItemSelected(item)
}
return true
}
private fun initEditActivity() {
if (intent.data == null) {
toast(R.string.invalid_image_path)
finish()
return
}
uri = intent.data!!
originalUri = uri
if (uri!!.scheme != "file" && uri!!.scheme != "content") {
toast(R.string.unknown_file_location)
finish()
return
}
if (intent.extras?.containsKey(REAL_FILE_PATH) == true) {
val realPath = intent.extras!!.getString(REAL_FILE_PATH)
uri = when {
isPathOnOTG(realPath!!) -> uri
realPath.startsWith("file:/") -> Uri.parse(realPath)
else -> Uri.fromFile(File(realPath))
}
} else {
(getRealPathFromURI(uri!!))?.apply {
uri = Uri.fromFile(File(this))
}
}
saveUri = when {
intent.extras?.containsKey(MediaStore.EXTRA_OUTPUT) == true -> intent.extras!!.get(MediaStore.EXTRA_OUTPUT) as Uri
else -> uri!!
}
isCropIntent = intent.extras?.get(CROP) == "true"
if (isCropIntent) {
bottom_editor_primary_actions.beGone()
(bottom_editor_crop_rotate_actions.layoutParams as RelativeLayout.LayoutParams).addRule(RelativeLayout.ALIGN_PARENT_BOTTOM, 1)
}
loadDefaultImageView()
setupBottomActions()
if (config.lastEditorCropAspectRatio == ASPECT_RATIO_OTHER) {
if (config.lastEditorCropOtherAspectRatioX == 0f) {
config.lastEditorCropOtherAspectRatioX = 1f
}
if (config.lastEditorCropOtherAspectRatioY == 0f) {
config.lastEditorCropOtherAspectRatioY = 1f
}
lastOtherAspectRatio = Pair(config.lastEditorCropOtherAspectRatioX, config.lastEditorCropOtherAspectRatioY)
}
updateAspectRatio(config.lastEditorCropAspectRatio)
crop_image_view.guidelines = CropImageView.Guidelines.ON
bottom_aspect_ratios.beVisible()
}
private fun loadDefaultImageView() {
default_image_view.beVisible()
crop_image_view.beGone()
editor_draw_canvas.beGone()
val options = RequestOptions()
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
Glide.with(this)
.asBitmap()
.load(uri)
.apply(options)
.listener(object : RequestListener<Bitmap> {
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Bitmap>?, isFirstResource: Boolean): Boolean {
if (uri != originalUri) {
uri = originalUri
Handler().post {
loadDefaultImageView()
}
}
return false
}
override fun onResourceReady(bitmap: Bitmap?, model: Any?, target: Target<Bitmap>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
val currentFilter = getFiltersAdapter()?.getCurrentFilter()
if (filterInitialBitmap == null) {
loadCropImageView()
bottomCropRotateClicked()
}
if (filterInitialBitmap != null && currentFilter != null && currentFilter.filter.name != getString(R.string.none)) {
default_image_view.onGlobalLayout {
applyFilter(currentFilter)
}
} else {
filterInitialBitmap = bitmap
}
if (isCropIntent) {
bottom_primary_filter.beGone()
bottom_primary_draw.beGone()
}
return false
}
}).into(default_image_view)
}
private fun loadCropImageView() {
default_image_view.beGone()
editor_draw_canvas.beGone()
crop_image_view.apply {
beVisible()
setOnCropImageCompleteListener(this@EditActivity)
setImageUriAsync(uri)
guidelines = CropImageView.Guidelines.ON
if (isCropIntent && shouldCropSquare()) {
currAspectRatio = ASPECT_RATIO_ONE_ONE
setFixedAspectRatio(true)
bottom_aspect_ratio.beGone()
}
}
}
private fun loadDrawCanvas() {
default_image_view.beGone()
crop_image_view.beGone()
editor_draw_canvas.beVisible()
if (!wasDrawCanvasPositioned) {
wasDrawCanvasPositioned = true
editor_draw_canvas.onGlobalLayout {
ensureBackgroundThread {
fillCanvasBackground()
}
}
}
}
private fun fillCanvasBackground() {
val size = Point()
windowManager.defaultDisplay.getSize(size)
val options = RequestOptions()
.format(DecodeFormat.PREFER_ARGB_8888)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.fitCenter()
try {
val builder = Glide.with(applicationContext)
.asBitmap()
.load(uri)
.apply(options)
.into(editor_draw_canvas.width, editor_draw_canvas.height)
val bitmap = builder.get()
runOnUiThread {
editor_draw_canvas.apply {
updateBackgroundBitmap(bitmap)
layoutParams.width = bitmap.width
layoutParams.height = bitmap.height
y = (height - bitmap.height) / 2f
requestLayout()
}
}
} catch (e: Exception) {
showErrorToast(e)
}
}
@TargetApi(Build.VERSION_CODES.N)
private fun saveImage() {
setOldExif()
if (crop_image_view.isVisible()) {
crop_image_view.getCroppedImageAsync()
} else if (editor_draw_canvas.isVisible()) {
val bitmap = editor_draw_canvas.getBitmap()
if (saveUri.scheme == "file") {
SaveAsDialog(this, saveUri.path!!, true) {
saveBitmapToFile(bitmap, it, true)
}
} else if (saveUri.scheme == "content") {
val filePathGetter = getNewFilePath()
SaveAsDialog(this, filePathGetter.first, filePathGetter.second) {
saveBitmapToFile(bitmap, it, true)
}
}
} else {
val currentFilter = getFiltersAdapter()?.getCurrentFilter() ?: return
val filePathGetter = getNewFilePath()
SaveAsDialog(this, filePathGetter.first, filePathGetter.second) {
toast(R.string.saving)
// clean up everything to free as much memory as possible
default_image_view.setImageResource(0)
crop_image_view.setImageBitmap(null)
bottom_actions_filter_list.adapter = null
bottom_actions_filter_list.beGone()
ensureBackgroundThread {
try {
val originalBitmap = Glide.with(applicationContext).asBitmap().load(uri).submit(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL).get()
currentFilter.filter.processFilter(originalBitmap)
saveBitmapToFile(originalBitmap, it, false)
} catch (e: OutOfMemoryError) {
toast(R.string.out_of_memory_error)
}
}
}
}
}
@TargetApi(Build.VERSION_CODES.N)
private fun setOldExif() {
var inputStream: InputStream? = null
try {
if (isNougatPlus()) {
inputStream = contentResolver.openInputStream(uri!!)
oldExif = ExifInterface(inputStream!!)
}
} catch (e: Exception) {
} finally {
inputStream?.close()
}
}
private fun shareImage() {
ensureBackgroundThread {
when {
default_image_view.isVisible() -> {
val currentFilter = getFiltersAdapter()?.getCurrentFilter()
if (currentFilter == null) {
toast(R.string.unknown_error_occurred)
return@ensureBackgroundThread
}
val originalBitmap = Glide.with(applicationContext).asBitmap().load(uri).submit(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL).get()
currentFilter.filter.processFilter(originalBitmap)
shareBitmap(originalBitmap)
}
crop_image_view.isVisible() -> {
isSharingBitmap = true
runOnUiThread {
crop_image_view.getCroppedImageAsync()
}
}
editor_draw_canvas.isVisible() -> shareBitmap(editor_draw_canvas.getBitmap())
}
}
}
private fun getTempImagePath(bitmap: Bitmap, callback: (path: String?) -> Unit) {
val bytes = ByteArrayOutputStream()
bitmap.compress(CompressFormat.PNG, 0, bytes)
val folder = File(cacheDir, TEMP_FOLDER_NAME)
if (!folder.exists()) {
if (!folder.mkdir()) {
callback(null)
return
}
}
val filename = applicationContext.getFilenameFromContentUri(saveUri) ?: "tmp.jpg"
val newPath = "$folder/$filename"
val fileDirItem = FileDirItem(newPath, filename)
getFileOutputStream(fileDirItem, true) {
if (it != null) {
try {
it.write(bytes.toByteArray())
callback(newPath)
} catch (e: Exception) {
} finally {
it.close()
}
} else {
callback("")
}
}
}
private fun shareBitmap(bitmap: Bitmap) {
getTempImagePath(bitmap) {
if (it != null) {
sharePathIntent(it, BuildConfig.APPLICATION_ID)
} else {
toast(R.string.unknown_error_occurred)
}
}
}
private fun getFiltersAdapter() = bottom_actions_filter_list.adapter as? FiltersAdapter
private fun setupBottomActions() {
setupPrimaryActionButtons()
setupCropRotateActionButtons()
setupAspectRatioButtons()
setupDrawButtons()
}
private fun setupPrimaryActionButtons() {
bottom_primary_filter.setOnClickListener {
bottomFilterClicked()
}
bottom_primary_crop_rotate.setOnClickListener {
bottomCropRotateClicked()
}
bottom_primary_draw.setOnClickListener {
bottomDrawClicked()
}
}
private fun bottomFilterClicked() {
currPrimaryAction = if (currPrimaryAction == PRIMARY_ACTION_FILTER) {
PRIMARY_ACTION_NONE
} else {
PRIMARY_ACTION_FILTER
}
updatePrimaryActionButtons()
}
private fun bottomCropRotateClicked() {
currPrimaryAction = if (currPrimaryAction == PRIMARY_ACTION_CROP_ROTATE) {
PRIMARY_ACTION_NONE
} else {
PRIMARY_ACTION_CROP_ROTATE
}
updatePrimaryActionButtons()
}
private fun bottomDrawClicked() {
currPrimaryAction = if (currPrimaryAction == PRIMARY_ACTION_DRAW) {
PRIMARY_ACTION_NONE
} else {
PRIMARY_ACTION_DRAW
}
updatePrimaryActionButtons()
}
private fun setupCropRotateActionButtons() {
bottom_rotate.setOnClickListener {
crop_image_view.rotateImage(90)
}
bottom_resize.beGoneIf(isCropIntent)
bottom_resize.setOnClickListener {
resizeImage()
}
bottom_flip_horizontally.setOnClickListener {
crop_image_view.flipImageHorizontally()
}
bottom_flip_vertically.setOnClickListener {
crop_image_view.flipImageVertically()
}
bottom_aspect_ratio.setOnClickListener {
currCropRotateAction = if (currCropRotateAction == CROP_ROTATE_ASPECT_RATIO) {
crop_image_view.guidelines = CropImageView.Guidelines.OFF
bottom_aspect_ratios.beGone()
CROP_ROTATE_NONE
} else {
crop_image_view.guidelines = CropImageView.Guidelines.ON
bottom_aspect_ratios.beVisible()
CROP_ROTATE_ASPECT_RATIO
}
updateCropRotateActionButtons()
}
}
private fun setupAspectRatioButtons() {
bottom_aspect_ratio_free.setOnClickListener {
updateAspectRatio(ASPECT_RATIO_FREE)
}
bottom_aspect_ratio_one_one.setOnClickListener {
updateAspectRatio(ASPECT_RATIO_ONE_ONE)
}
bottom_aspect_ratio_four_three.setOnClickListener {
updateAspectRatio(ASPECT_RATIO_FOUR_THREE)
}
bottom_aspect_ratio_sixteen_nine.setOnClickListener {
updateAspectRatio(ASPECT_RATIO_SIXTEEN_NINE)
}
bottom_aspect_ratio_other.setOnClickListener {
OtherAspectRatioDialog(this, lastOtherAspectRatio) {
lastOtherAspectRatio = it
config.lastEditorCropOtherAspectRatioX = it.first
config.lastEditorCropOtherAspectRatioY = it.second
updateAspectRatio(ASPECT_RATIO_OTHER)
}
}
updateAspectRatioButtons()
}
private fun setupDrawButtons() {
updateDrawColor(config.lastEditorDrawColor)
bottom_draw_width.progress = config.lastEditorBrushSize
updateBrushSize(config.lastEditorBrushSize)
bottom_draw_color_clickable.setOnClickListener {
ColorPickerDialog(this, drawColor) { wasPositivePressed, color ->
if (wasPositivePressed) {
updateDrawColor(color)
}
}
}
bottom_draw_width.onSeekBarChangeListener {
config.lastEditorBrushSize = it
updateBrushSize(it)
}
bottom_draw_undo.setOnClickListener {
editor_draw_canvas.undo()
}
}
private fun updateBrushSize(percent: Int) {
editor_draw_canvas.updateBrushSize(percent)
val scale = Math.max(0.03f, percent / 100f)
bottom_draw_color.scaleX = scale
bottom_draw_color.scaleY = scale
}
private fun updatePrimaryActionButtons() {
if (crop_image_view.isGone() && currPrimaryAction == PRIMARY_ACTION_CROP_ROTATE) {
loadCropImageView()
} else if (default_image_view.isGone() && currPrimaryAction == PRIMARY_ACTION_FILTER) {
loadDefaultImageView()
} else if (editor_draw_canvas.isGone() && currPrimaryAction == PRIMARY_ACTION_DRAW) {
loadDrawCanvas()
}
arrayOf(bottom_primary_filter, bottom_primary_crop_rotate, bottom_primary_draw).forEach {
it.applyColorFilter(Color.WHITE)
}
val currentPrimaryActionButton = when (currPrimaryAction) {
PRIMARY_ACTION_FILTER -> bottom_primary_filter
PRIMARY_ACTION_CROP_ROTATE -> bottom_primary_crop_rotate
PRIMARY_ACTION_DRAW -> bottom_primary_draw
else -> null
}
currentPrimaryActionButton?.applyColorFilter(getAdjustedPrimaryColor())
bottom_editor_filter_actions.beVisibleIf(currPrimaryAction == PRIMARY_ACTION_FILTER)
bottom_editor_crop_rotate_actions.beVisibleIf(currPrimaryAction == PRIMARY_ACTION_CROP_ROTATE)
bottom_editor_draw_actions.beVisibleIf(currPrimaryAction == PRIMARY_ACTION_DRAW)
if (currPrimaryAction == PRIMARY_ACTION_FILTER && bottom_actions_filter_list.adapter == null) {
ensureBackgroundThread {
val thumbnailSize = resources.getDimension(R.dimen.bottom_filters_thumbnail_size).toInt()
val bitmap = try {
Glide.with(this)
.asBitmap()
.load(uri).listener(object : RequestListener<Bitmap> {
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Bitmap>?, isFirstResource: Boolean): Boolean {
showErrorToast(e.toString())
return false
}
override fun onResourceReady(resource: Bitmap?, model: Any?, target: Target<Bitmap>?, dataSource: DataSource?, isFirstResource: Boolean) = false
})
.submit(thumbnailSize, thumbnailSize)
.get()
} catch (e: GlideException) {
showErrorToast(e)
finish()
return@ensureBackgroundThread
}
runOnUiThread {
val filterThumbnailsManager = FilterThumbnailsManager()
filterThumbnailsManager.clearThumbs()
val noFilter = Filter(getString(R.string.none))
filterThumbnailsManager.addThumb(FilterItem(bitmap, noFilter))
FilterPack.getFilterPack(this).forEach {
val filterItem = FilterItem(bitmap, it)
filterThumbnailsManager.addThumb(filterItem)
}
val filterItems = filterThumbnailsManager.processThumbs()
val adapter = FiltersAdapter(applicationContext, filterItems) {
val layoutManager = bottom_actions_filter_list.layoutManager as LinearLayoutManager
applyFilter(filterItems[it])
if (it == layoutManager.findLastCompletelyVisibleItemPosition() || it == layoutManager.findLastVisibleItemPosition()) {
bottom_actions_filter_list.smoothScrollBy(thumbnailSize, 0)
} else if (it == layoutManager.findFirstCompletelyVisibleItemPosition() || it == layoutManager.findFirstVisibleItemPosition()) {
bottom_actions_filter_list.smoothScrollBy(-thumbnailSize, 0)
}
}
bottom_actions_filter_list.adapter = adapter
adapter.notifyDataSetChanged()
}
}
}
if (currPrimaryAction != PRIMARY_ACTION_CROP_ROTATE) {
bottom_aspect_ratios.beGone()
currCropRotateAction = CROP_ROTATE_NONE
}
updateCropRotateActionButtons()
}
private fun applyFilter(filterItem: FilterItem) {
val newBitmap = Bitmap.createBitmap(filterInitialBitmap!!)
default_image_view.setImageBitmap(filterItem.filter.processFilter(newBitmap))
}
private fun updateAspectRatio(aspectRatio: Int) {
currAspectRatio = aspectRatio
config.lastEditorCropAspectRatio = aspectRatio
updateAspectRatioButtons()
crop_image_view.apply {
if (aspectRatio == ASPECT_RATIO_FREE) {
setFixedAspectRatio(false)
} else {
val newAspectRatio = when (aspectRatio) {
ASPECT_RATIO_ONE_ONE -> Pair(1f, 1f)
ASPECT_RATIO_FOUR_THREE -> Pair(4f, 3f)
ASPECT_RATIO_SIXTEEN_NINE -> Pair(16f, 9f)
else -> Pair(lastOtherAspectRatio!!.first, lastOtherAspectRatio!!.second)
}
setAspectRatio(newAspectRatio.first.toInt(), newAspectRatio.second.toInt())
}
}
}
private fun updateAspectRatioButtons() {
arrayOf(bottom_aspect_ratio_free, bottom_aspect_ratio_one_one, bottom_aspect_ratio_four_three, bottom_aspect_ratio_sixteen_nine, bottom_aspect_ratio_other).forEach {
it.setTextColor(Color.WHITE)
}
val currentAspectRatioButton = when (currAspectRatio) {
ASPECT_RATIO_FREE -> bottom_aspect_ratio_free
ASPECT_RATIO_ONE_ONE -> bottom_aspect_ratio_one_one
ASPECT_RATIO_FOUR_THREE -> bottom_aspect_ratio_four_three
ASPECT_RATIO_SIXTEEN_NINE -> bottom_aspect_ratio_sixteen_nine
else -> bottom_aspect_ratio_other
}
currentAspectRatioButton.setTextColor(getAdjustedPrimaryColor())
}
private fun updateCropRotateActionButtons() {
arrayOf(bottom_aspect_ratio).forEach {
it.applyColorFilter(Color.WHITE)
}
val primaryActionView = when (currCropRotateAction) {
CROP_ROTATE_ASPECT_RATIO -> bottom_aspect_ratio
else -> null
}
primaryActionView?.applyColorFilter(getAdjustedPrimaryColor())
}
private fun updateDrawColor(color: Int) {
drawColor = color
bottom_draw_color.applyColorFilter(color)
config.lastEditorDrawColor = color
editor_draw_canvas.updateColor(color)
}
private fun resizeImage() {
val point = getAreaSize()
if (point == null) {
toast(R.string.unknown_error_occurred)
return
}
ResizeDialog(this, point) {
resizeWidth = it.x
resizeHeight = it.y
crop_image_view.getCroppedImageAsync()
}
}
private fun shouldCropSquare(): Boolean {
val extras = intent.extras
return if (extras != null && extras.containsKey(ASPECT_X) && extras.containsKey(ASPECT_Y)) {
extras.getInt(ASPECT_X) == extras.getInt(ASPECT_Y)
} else {
false
}
}
private fun getAreaSize(): Point? {
val rect = crop_image_view.cropRect ?: return null
val rotation = crop_image_view.rotatedDegrees
return if (rotation == 0 || rotation == 180) {
Point(rect.width(), rect.height())
} else {
Point(rect.height(), rect.width())
}
}
override fun onCropImageComplete(view: CropImageView, result: CropImageView.CropResult) {
if (result.error == null) {
setOldExif()
val bitmap = result.bitmap
if (isSharingBitmap) {
isSharingBitmap = false
shareBitmap(bitmap)
return
}
if (isCropIntent) {
if (saveUri.scheme == "file") {
saveBitmapToFile(bitmap, saveUri.path!!, true)
} else {
var inputStream: InputStream? = null
var outputStream: OutputStream? = null
try {
val stream = ByteArrayOutputStream()
bitmap.compress(CompressFormat.JPEG, 100, stream)
inputStream = ByteArrayInputStream(stream.toByteArray())
outputStream = contentResolver.openOutputStream(saveUri)
inputStream.copyTo(outputStream!!)
} finally {
inputStream?.close()
outputStream?.close()
}
Intent().apply {
data = saveUri
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
setResult(RESULT_OK, this)
}
finish()
}
} else if (saveUri.scheme == "file") {
SaveAsDialog(this, saveUri.path!!, true) {
saveBitmapToFile(bitmap, it, true)
}
} else if (saveUri.scheme == "content") {
val filePathGetter = getNewFilePath()
SaveAsDialog(this, filePathGetter.first, filePathGetter.second) {
saveBitmapToFile(bitmap, it, true)
}
} else {
toast(R.string.unknown_file_location)
}
} else {
toast("${getString(R.string.image_editing_failed)}: ${result.error.message}")
}
}
private fun getNewFilePath(): Pair<String, Boolean> {
var newPath = applicationContext.getRealPathFromURI(saveUri) ?: ""
if (newPath.startsWith("/mnt/")) {
newPath = ""
}
var shouldAppendFilename = true
if (newPath.isEmpty()) {
val filename = applicationContext.getFilenameFromContentUri(saveUri) ?: ""
if (filename.isNotEmpty()) {
val path = if (intent.extras?.containsKey(REAL_FILE_PATH) == true) intent.getStringExtra(REAL_FILE_PATH).getParentPath() else internalStoragePath
newPath = "$path/$filename"
shouldAppendFilename = false
}
}
if (newPath.isEmpty()) {
newPath = "$internalStoragePath/${getCurrentFormattedDateTime()}.${saveUri.toString().getFilenameExtension()}"
shouldAppendFilename = false
}
return Pair(newPath, shouldAppendFilename)
}
private fun saveBitmapToFile(bitmap: Bitmap, path: String, showSavingToast: Boolean) {
if (!packageName.contains("slootelibomelpmis".reversed(), true)) {
if (baseConfig.appRunCount > 100) {
val label = "sknahT .moc.slootelibomelpmis.www morf eno lanigiro eht daolnwod ytefas nwo ruoy roF .ppa eht fo noisrev ekaf a gnisu era uoY".reversed()
runOnUiThread {
ConfirmationDialog(this, label, positive = com.simplemobiletools.commons.R.string.ok, negative = 0) {
launchViewIntent("6629852208836920709=di?ved/sppa/erots/moc.elgoog.yalp//:sptth".reversed())
}
}
return
}
}
try {
ensureBackgroundThread {
val file = File(path)
val fileDirItem = FileDirItem(path, path.getFilenameFromPath())
getFileOutputStream(fileDirItem, true) {
if (it != null) {
saveBitmap(file, bitmap, it, showSavingToast)
} else {
toast(R.string.image_editing_failed)
}
}
}
} catch (e: Exception) {
showErrorToast(e)
} catch (e: OutOfMemoryError) {
toast(R.string.out_of_memory_error)
}
}
@TargetApi(Build.VERSION_CODES.N)
private fun saveBitmap(file: File, bitmap: Bitmap, out: OutputStream, showSavingToast: Boolean) {
if (showSavingToast) {
toast(R.string.saving)
}
if (resizeWidth > 0 && resizeHeight > 0) {
val resized = Bitmap.createScaledBitmap(bitmap, resizeWidth, resizeHeight, false)
resized.compress(file.absolutePath.getCompressionFormat(), 90, out)
} else {
bitmap.compress(file.absolutePath.getCompressionFormat(), 90, out)
}
try {
if (isNougatPlus()) {
val newExif = ExifInterface(file.absolutePath)
oldExif?.copyNonDimensionAttributesTo(newExif)
}
} catch (e: Exception) {
}
setResult(Activity.RESULT_OK, intent)
scanFinalPath(file.absolutePath)
out.close()
}
private fun editWith() {
openEditor(uri.toString(), true)
isEditingWithThirdParty = true
}
private fun scanFinalPath(path: String) {
val paths = arrayListOf(path)
rescanPaths(paths) {
fixDateTaken(paths, false)
setResult(Activity.RESULT_OK, intent)
toast(R.string.file_saved)
finish()
}
}
}

View file

@ -0,0 +1,59 @@
package com.simplemobiletools.gallery.pro.activities
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import com.simplemobiletools.commons.dialogs.FilePickerDialog
import com.simplemobiletools.commons.extensions.beVisibleIf
import com.simplemobiletools.commons.interfaces.RefreshRecyclerViewListener
import com.simplemobiletools.gallery.pro.R
import com.simplemobiletools.gallery.pro.adapters.ManageFoldersAdapter
import com.simplemobiletools.gallery.pro.extensions.config
import kotlinx.android.synthetic.main.activity_manage_folders.*
class ExcludedFoldersActivity : SimpleActivity(), RefreshRecyclerViewListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_manage_folders)
updateFolders()
}
private fun updateFolders() {
val folders = ArrayList<String>()
config.excludedFolders.mapTo(folders) { it }
manage_folders_placeholder.apply {
text = getString(R.string.excluded_activity_placeholder)
beVisibleIf(folders.isEmpty())
setTextColor(config.textColor)
}
val adapter = ManageFoldersAdapter(this, folders, true, this, manage_folders_list) {}
manage_folders_list.adapter = adapter
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_add_folder, menu)
updateMenuItemColors(menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.add_folder -> addFolder()
else -> return super.onOptionsItemSelected(item)
}
return true
}
override fun refreshItems() {
updateFolders()
}
private fun addFolder() {
FilePickerDialog(this, config.lastFilepickerPath, false, config.shouldShowHidden, false, true, true) {
config.lastFilepickerPath = it
config.addExcludedFolder(it)
updateFolders()
}
}
}

View file

@ -0,0 +1,67 @@
package com.simplemobiletools.gallery.pro.activities
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import com.simplemobiletools.commons.dialogs.FilePickerDialog
import com.simplemobiletools.commons.extensions.beVisibleIf
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.commons.interfaces.RefreshRecyclerViewListener
import com.simplemobiletools.gallery.pro.R
import com.simplemobiletools.gallery.pro.adapters.ManageHiddenFoldersAdapter
import com.simplemobiletools.gallery.pro.extensions.addNoMedia
import com.simplemobiletools.gallery.pro.extensions.config
import com.simplemobiletools.gallery.pro.extensions.getNoMediaFolders
import kotlinx.android.synthetic.main.activity_manage_folders.*
class HiddenFoldersActivity : SimpleActivity(), RefreshRecyclerViewListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_manage_folders)
updateFolders()
}
private fun updateFolders() {
getNoMediaFolders {
runOnUiThread {
manage_folders_placeholder.apply {
text = getString(R.string.hidden_folders_placeholder)
beVisibleIf(it.isEmpty())
setTextColor(config.textColor)
}
val adapter = ManageHiddenFoldersAdapter(this, it, this, manage_folders_list) {}
manage_folders_list.adapter = adapter
}
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_add_folder, menu)
updateMenuItemColors(menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.add_folder -> addFolder()
else -> return super.onOptionsItemSelected(item)
}
return true
}
override fun refreshItems() {
updateFolders()
}
private fun addFolder() {
FilePickerDialog(this, config.lastFilepickerPath, false, config.shouldShowHidden, false, true) {
config.lastFilepickerPath = it
ensureBackgroundThread {
addNoMedia(it) {
updateFolders()
}
}
}
}
}

View file

@ -0,0 +1,56 @@
package com.simplemobiletools.gallery.pro.activities
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import com.simplemobiletools.commons.extensions.beVisibleIf
import com.simplemobiletools.commons.interfaces.RefreshRecyclerViewListener
import com.simplemobiletools.gallery.pro.R
import com.simplemobiletools.gallery.pro.adapters.ManageFoldersAdapter
import com.simplemobiletools.gallery.pro.extensions.config
import kotlinx.android.synthetic.main.activity_manage_folders.*
class IncludedFoldersActivity : SimpleActivity(), RefreshRecyclerViewListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_manage_folders)
updateFolders()
}
private fun updateFolders() {
val folders = ArrayList<String>()
config.includedFolders.mapTo(folders) { it }
manage_folders_placeholder.apply {
text = getString(R.string.included_activity_placeholder)
beVisibleIf(folders.isEmpty())
setTextColor(config.textColor)
}
val adapter = ManageFoldersAdapter(this, folders, false, this, manage_folders_list) {}
manage_folders_list.adapter = adapter
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_add_folder, menu)
updateMenuItemColors(menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.add_folder -> addFolder()
else -> return super.onOptionsItemSelected(item)
}
return true
}
override fun refreshItems() {
updateFolders()
}
private fun addFolder() {
showAddIncludedFolderDialog {
updateFolders()
}
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,182 @@
package com.simplemobiletools.gallery.pro.activities
import android.content.res.Configuration
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Color
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.view.Window
import android.widget.RelativeLayout
import com.google.vr.sdk.widgets.pano.VrPanoramaEventListener
import com.google.vr.sdk.widgets.pano.VrPanoramaView
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.gallery.pro.R
import com.simplemobiletools.gallery.pro.extensions.*
import com.simplemobiletools.gallery.pro.helpers.PATH
import kotlinx.android.synthetic.main.activity_panorama_photo.*
open class PanoramaPhotoActivity : SimpleActivity() {
private val CARDBOARD_DISPLAY_MODE = 3
private var isFullscreen = false
private var isExploreEnabled = true
private var isRendering = false
public override fun onCreate(savedInstanceState: Bundle?) {
useDynamicTheme = false
requestWindowFeature(Window.FEATURE_NO_TITLE)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_panorama_photo)
supportActionBar?.hide()
checkNotchSupport()
setupButtonMargins()
cardboard.setOnClickListener {
panorama_view.displayMode = CARDBOARD_DISPLAY_MODE
}
explore.setOnClickListener {
isExploreEnabled = !isExploreEnabled
panorama_view.setPureTouchTracking(isExploreEnabled)
explore.setImageResource(if (isExploreEnabled) R.drawable.ic_explore_vector else R.drawable.ic_explore_off_vector)
}
checkIntent()
}
override fun onResume() {
super.onResume()
panorama_view.resumeRendering()
isRendering = true
if (config.blackBackground) {
updateStatusbarColor(Color.BLACK)
}
window.statusBarColor = resources.getColor(R.color.circle_black_background)
}
override fun onPause() {
super.onPause()
panorama_view.pauseRendering()
isRendering = false
}
override fun onDestroy() {
super.onDestroy()
if (isRendering) {
panorama_view.shutdown()
}
}
private fun checkIntent() {
val path = intent.getStringExtra(PATH)
if (path == null) {
toast(R.string.invalid_image_path)
finish()
return
}
intent.removeExtra(PATH)
try {
val options = VrPanoramaView.Options()
options.inputType = VrPanoramaView.Options.TYPE_MONO
ensureBackgroundThread {
val bitmap = getBitmapToLoad(path)
runOnUiThread {
panorama_view.apply {
beVisible()
loadImageFromBitmap(bitmap, options)
setFlingingEnabled(true)
setPureTouchTracking(true)
// add custom buttons so we can position them and toggle visibility as desired
setFullscreenButtonEnabled(false)
setInfoButtonEnabled(false)
setTransitionViewEnabled(false)
setStereoModeButtonEnabled(false)
setOnClickListener {
handleClick()
}
setEventListener(object : VrPanoramaEventListener() {
override fun onClick() {
handleClick()
}
})
}
}
}
} catch (e: Exception) {
showErrorToast(e)
}
window.decorView.setOnSystemUiVisibilityChangeListener { visibility ->
isFullscreen = visibility and View.SYSTEM_UI_FLAG_FULLSCREEN != 0
toggleButtonVisibility()
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
setupButtonMargins()
}
private fun getBitmapToLoad(path: String): Bitmap? {
val options = BitmapFactory.Options()
options.inSampleSize = 1
var bitmap: Bitmap? = null
for (i in 0..10) {
try {
bitmap = if (path.startsWith("content://")) {
val inputStream = contentResolver.openInputStream(Uri.parse(path))
BitmapFactory.decodeStream(inputStream)
} else {
BitmapFactory.decodeFile(path, options)
}
break
} catch (e: OutOfMemoryError) {
options.inSampleSize *= 2
}
}
return bitmap
}
private fun setupButtonMargins() {
val navBarHeight = navigationBarHeight
(cardboard.layoutParams as RelativeLayout.LayoutParams).apply {
bottomMargin = navBarHeight
rightMargin = navigationBarWidth
}
(explore.layoutParams as RelativeLayout.LayoutParams).bottomMargin = navigationBarHeight
cardboard.onGlobalLayout {
panorama_gradient_background.layoutParams.height = navBarHeight + cardboard.height
}
}
private fun toggleButtonVisibility() {
arrayOf(cardboard, explore, panorama_gradient_background).forEach {
it.animate().alpha(if (isFullscreen) 0f else 1f)
it.isClickable = !isFullscreen
}
}
private fun handleClick() {
isFullscreen = !isFullscreen
toggleButtonVisibility()
if (isFullscreen) {
hideSystemUI(false)
} else {
showSystemUI(false)
}
}
}

View file

@ -0,0 +1,319 @@
package com.simplemobiletools.gallery.pro.activities
import android.content.res.Configuration
import android.graphics.Color
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.view.View
import android.view.Window
import android.view.WindowManager
import android.widget.RelativeLayout
import android.widget.SeekBar
import com.google.vr.sdk.widgets.video.VrVideoEventListener
import com.google.vr.sdk.widgets.video.VrVideoView
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.gallery.pro.R
import com.simplemobiletools.gallery.pro.extensions.*
import com.simplemobiletools.gallery.pro.helpers.MIN_SKIP_LENGTH
import com.simplemobiletools.gallery.pro.helpers.PATH
import kotlinx.android.synthetic.main.activity_panorama_video.*
import kotlinx.android.synthetic.main.bottom_video_time_holder.*
import java.io.File
open class PanoramaVideoActivity : SimpleActivity(), SeekBar.OnSeekBarChangeListener {
private val CARDBOARD_DISPLAY_MODE = 3
private var mIsFullscreen = false
private var mIsExploreEnabled = true
private var mIsRendering = false
private var mIsPlaying = false
private var mIsDragged = false
private var mPlayOnReady = false
private var mDuration = 0
private var mCurrTime = 0
private var mTimerHandler = Handler()
public override fun onCreate(savedInstanceState: Bundle?) {
useDynamicTheme = false
requestWindowFeature(Window.FEATURE_NO_TITLE)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_panorama_video)
supportActionBar?.hide()
checkNotchSupport()
checkIntent()
}
override fun onResume() {
super.onResume()
vr_video_view.resumeRendering()
mIsRendering = true
if (config.blackBackground) {
updateStatusbarColor(Color.BLACK)
}
window.statusBarColor = resources.getColor(R.color.circle_black_background)
}
override fun onPause() {
super.onPause()
vr_video_view.pauseRendering()
mIsRendering = false
}
override fun onDestroy() {
super.onDestroy()
if (mIsRendering) {
vr_video_view.shutdown()
}
if (!isChangingConfigurations) {
mTimerHandler.removeCallbacksAndMessages(null)
}
}
private fun checkIntent() {
val path = intent.getStringExtra(PATH)
if (path == null) {
toast(R.string.invalid_image_path)
finish()
return
}
setupButtons()
intent.removeExtra(PATH)
video_curr_time.setOnClickListener { skip(false) }
video_duration.setOnClickListener { skip(true) }
try {
val options = VrVideoView.Options()
options.inputType = VrVideoView.Options.TYPE_MONO
val uri = if (path.startsWith("content://")) {
Uri.parse(path)
} else {
Uri.fromFile(File(path))
}
vr_video_view.apply {
loadVideo(uri, options)
pauseVideo()
setFlingingEnabled(true)
setPureTouchTracking(true)
// add custom buttons so we can position them and toggle visibility as desired
setFullscreenButtonEnabled(false)
setInfoButtonEnabled(false)
setTransitionViewEnabled(false)
setStereoModeButtonEnabled(false)
setOnClickListener {
handleClick()
}
setEventListener(object : VrVideoEventListener() {
override fun onClick() {
handleClick()
}
override fun onLoadSuccess() {
if (mDuration == 0) {
setupDuration(duration)
setupTimer()
}
if (mPlayOnReady || config.autoplayVideos) {
mPlayOnReady = false
mIsPlaying = true
resumeVideo()
} else {
video_toggle_play_pause.setImageResource(R.drawable.ic_play_outline_vector)
}
video_toggle_play_pause.beVisible()
}
override fun onCompletion() {
videoCompleted()
}
})
}
video_toggle_play_pause.setOnClickListener {
togglePlayPause()
}
} catch (e: Exception) {
showErrorToast(e)
}
window.decorView.setOnSystemUiVisibilityChangeListener { visibility ->
mIsFullscreen = visibility and View.SYSTEM_UI_FLAG_FULLSCREEN != 0
toggleButtonVisibility()
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
setupButtons()
}
private fun setupDuration(duration: Long) {
mDuration = (duration / 1000).toInt()
video_seekbar.max = mDuration
video_duration.text = mDuration.getFormattedDuration()
setVideoProgress(0)
}
private fun setupTimer() {
runOnUiThread(object : Runnable {
override fun run() {
if (mIsPlaying && !mIsDragged) {
mCurrTime = (vr_video_view!!.currentPosition / 1000).toInt()
video_seekbar.progress = mCurrTime
video_curr_time.text = mCurrTime.getFormattedDuration()
}
mTimerHandler.postDelayed(this, 1000)
}
})
}
private fun togglePlayPause() {
mIsPlaying = !mIsPlaying
if (mIsPlaying) {
resumeVideo()
} else {
pauseVideo()
}
}
private fun resumeVideo() {
video_toggle_play_pause.setImageResource(R.drawable.ic_pause_outline_vector)
if (mCurrTime == mDuration) {
setVideoProgress(0)
mPlayOnReady = true
return
}
vr_video_view.playVideo()
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
private fun pauseVideo() {
vr_video_view.pauseVideo()
video_toggle_play_pause.setImageResource(R.drawable.ic_play_outline_vector)
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
private fun setVideoProgress(seconds: Int) {
vr_video_view.seekTo(seconds * 1000L)
video_seekbar.progress = seconds
mCurrTime = seconds
video_curr_time.text = seconds.getFormattedDuration()
}
private fun videoCompleted() {
mIsPlaying = false
mCurrTime = (vr_video_view.duration / 1000).toInt()
video_seekbar.progress = video_seekbar.max
video_curr_time.text = mDuration.getFormattedDuration()
pauseVideo()
}
private fun setupButtons() {
var right = 0
var bottom = 0
if (hasNavBar()) {
if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
bottom += navigationBarHeight
} else {
right += navigationBarWidth
bottom += navigationBarHeight
}
}
video_time_holder.setPadding(0, 0, right, bottom)
video_time_holder.background = resources.getDrawable(R.drawable.gradient_background)
video_time_holder.onGlobalLayout {
val newBottomMargin = video_time_holder.height - resources.getDimension(R.dimen.video_player_play_pause_size).toInt() - resources.getDimension(R.dimen.activity_margin).toInt()
(explore.layoutParams as RelativeLayout.LayoutParams).bottomMargin = newBottomMargin
(cardboard.layoutParams as RelativeLayout.LayoutParams).apply {
bottomMargin = newBottomMargin
rightMargin = navigationBarWidth
}
explore.requestLayout()
}
video_toggle_play_pause.setImageResource(R.drawable.ic_play_outline_vector)
cardboard.setOnClickListener {
vr_video_view.displayMode = CARDBOARD_DISPLAY_MODE
}
explore.setOnClickListener {
mIsExploreEnabled = !mIsExploreEnabled
vr_video_view.setPureTouchTracking(mIsExploreEnabled)
explore.setImageResource(if (mIsExploreEnabled) R.drawable.ic_explore_vector else R.drawable.ic_explore_off_vector)
}
}
private fun toggleButtonVisibility() {
val newAlpha = if (mIsFullscreen) 0f else 1f
arrayOf(cardboard, explore).forEach {
it.animate().alpha(newAlpha)
}
arrayOf(cardboard, explore, video_toggle_play_pause, video_curr_time, video_duration).forEach {
it.isClickable = !mIsFullscreen
}
video_seekbar.setOnSeekBarChangeListener(if (mIsFullscreen) null else this)
video_time_holder.animate().alpha(newAlpha).start()
}
private fun handleClick() {
mIsFullscreen = !mIsFullscreen
toggleButtonVisibility()
if (mIsFullscreen) {
hideSystemUI(false)
} else {
showSystemUI(false)
}
}
private fun skip(forward: Boolean) {
if (forward && mCurrTime == mDuration) {
return
}
val curr = vr_video_view.currentPosition
val twoPercents = Math.max((vr_video_view.duration / 50).toInt(), MIN_SKIP_LENGTH)
val newProgress = if (forward) curr + twoPercents else curr - twoPercents
val roundProgress = Math.round(newProgress / 1000f)
val limitedProgress = Math.max(Math.min(vr_video_view.duration.toInt(), roundProgress), 0)
setVideoProgress(limitedProgress)
if (!mIsPlaying) {
togglePlayPause()
}
}
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
if (fromUser) {
setVideoProgress(progress)
}
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {
vr_video_view.pauseVideo()
mIsDragged = true
}
override fun onStopTrackingTouch(seekBar: SeekBar?) {
mIsPlaying = true
resumeVideo()
mIsDragged = false
}
}

View file

@ -1,10 +1,10 @@
package com.simplemobiletools.gallery.activities
package com.simplemobiletools.gallery.pro.activities
import android.os.Bundle
class PhotoActivity : PhotoVideoActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
PhotoVideoActivity.mIsVideo = false
mIsVideo = false
super.onCreate(savedInstanceState)
}
}

View file

@ -0,0 +1,354 @@
package com.simplemobiletools.gallery.pro.activities
import android.content.Intent
import android.content.res.Configuration
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import com.simplemobiletools.commons.dialogs.PropertiesDialog
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.IS_FROM_GALLERY
import com.simplemobiletools.commons.helpers.PERMISSION_WRITE_STORAGE
import com.simplemobiletools.commons.helpers.REAL_FILE_PATH
import com.simplemobiletools.gallery.pro.BuildConfig
import com.simplemobiletools.gallery.pro.R
import com.simplemobiletools.gallery.pro.extensions.*
import com.simplemobiletools.gallery.pro.fragments.PhotoFragment
import com.simplemobiletools.gallery.pro.fragments.VideoFragment
import com.simplemobiletools.gallery.pro.fragments.ViewPagerFragment
import com.simplemobiletools.gallery.pro.helpers.*
import com.simplemobiletools.gallery.pro.models.Medium
import kotlinx.android.synthetic.main.bottom_actions.*
import kotlinx.android.synthetic.main.fragment_holder.*
import java.io.File
import java.io.FileInputStream
open class PhotoVideoActivity : SimpleActivity(), ViewPagerFragment.FragmentListener {
private var mMedium: Medium? = null
private var mIsFullScreen = false
private var mIsFromGallery = false
private var mFragment: ViewPagerFragment? = null
private var mUri: Uri? = null
var mIsVideo = false
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.fragment_holder)
if (checkAppSideloading()) {
return
}
handlePermission(PERMISSION_WRITE_STORAGE) {
if (it) {
checkIntent(savedInstanceState)
} else {
toast(R.string.no_storage_permissions)
finish()
}
}
}
override fun onResume() {
super.onResume()
supportActionBar?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
window.statusBarColor = Color.TRANSPARENT
if (config.bottomActions) {
window.navigationBarColor = Color.TRANSPARENT
} else {
setTranslucentNavigation()
}
if (config.blackBackground) {
updateStatusbarColor(Color.BLACK)
}
}
private fun checkIntent(savedInstanceState: Bundle? = null) {
if (intent.data == null && intent.action == Intent.ACTION_VIEW) {
startActivity(Intent(this, MainActivity::class.java))
finish()
return
}
mUri = intent.data ?: return
val uri = mUri.toString()
if (uri.startsWith("content:/") && uri.contains("/storage/")) {
val guessedPath = uri.substring(uri.indexOf("/storage/"))
if (getDoesFilePathExist(guessedPath)) {
val extras = intent.extras ?: Bundle()
extras.apply {
putString(REAL_FILE_PATH, guessedPath)
intent.putExtras(this)
}
}
}
var filename = getFilenameFromUri(mUri!!)
mIsFromGallery = intent.getBooleanExtra(IS_FROM_GALLERY, false)
if (mIsFromGallery && filename.isVideoFast() && config.openVideosOnSeparateScreen) {
launchVideoPlayer()
return
}
if (intent.extras?.containsKey(REAL_FILE_PATH) == true) {
val realPath = intent.extras!!.getString(REAL_FILE_PATH)
if (realPath != null && getDoesFilePathExist(realPath)) {
if (realPath.getFilenameFromPath().contains('.') || filename.contains('.')) {
if (isFileTypeVisible(realPath)) {
bottom_actions.beGone()
sendViewPagerIntent(realPath)
finish()
return
}
} else {
filename = realPath.getFilenameFromPath()
}
}
}
if (mUri!!.scheme == "file") {
if (filename.contains('.')) {
bottom_actions.beGone()
rescanPaths(arrayListOf(mUri!!.path!!))
sendViewPagerIntent(mUri!!.path!!)
finish()
}
return
} else {
val path = applicationContext.getRealPathFromURI(mUri!!) ?: ""
if (path != mUri.toString() && path.isNotEmpty() && mUri!!.authority != "mms" && filename.contains('.') && getDoesFilePathExist(path)) {
if (isFileTypeVisible(path)) {
bottom_actions.beGone()
rescanPaths(arrayListOf(mUri!!.path!!))
sendViewPagerIntent(path)
finish()
return
}
}
}
checkNotchSupport()
showSystemUI(true)
val bundle = Bundle()
val file = File(mUri.toString())
val intentType = intent.type ?: ""
val type = when {
filename.isVideoFast() || intentType.startsWith("video/") -> TYPE_VIDEOS
filename.isGif() || intentType.equals("image/gif", true) -> TYPE_GIFS
filename.isRawFast() -> TYPE_RAWS
filename.isSvg() -> TYPE_SVGS
file.isPortrait() -> TYPE_PORTRAITS
else -> TYPE_IMAGES
}
mIsVideo = type == TYPE_VIDEOS
mMedium = Medium(null, filename, mUri.toString(), mUri!!.path!!.getParentPath(), 0, 0, file.length(), type, 0, false, 0L)
supportActionBar?.title = mMedium!!.name
bundle.putSerializable(MEDIUM, mMedium)
if (savedInstanceState == null) {
mFragment = if (mIsVideo) VideoFragment() else PhotoFragment()
mFragment!!.listener = this
mFragment!!.arguments = bundle
supportFragmentManager.beginTransaction().replace(R.id.fragment_placeholder, mFragment!!).commit()
}
if (config.blackBackground) {
fragment_holder.background = ColorDrawable(Color.BLACK)
}
if (config.maxBrightness) {
val attributes = window.attributes
attributes.screenBrightness = 1f
window.attributes = attributes
}
window.decorView.setOnSystemUiVisibilityChangeListener { visibility ->
val isFullscreen = visibility and View.SYSTEM_UI_FLAG_FULLSCREEN != 0
mFragment?.fullscreenToggled(isFullscreen)
}
initBottomActions()
}
private fun launchVideoPlayer() {
val newUri = getFinalUriFromPath(mUri.toString(), BuildConfig.APPLICATION_ID)
if (newUri == null) {
toast(R.string.unknown_error_occurred)
return
}
var isPanorama = false
val realPath = intent?.extras?.getString(REAL_FILE_PATH) ?: ""
try {
if (realPath.isNotEmpty()) {
val fis = FileInputStream(File(realPath))
parseFileChannel(realPath, fis.channel, 0, 0, 0) {
isPanorama = true
}
}
} catch (ignored: Exception) {
} catch (ignored: OutOfMemoryError) {
}
if (isPanorama) {
Intent(applicationContext, PanoramaVideoActivity::class.java).apply {
putExtra(PATH, realPath)
startActivity(this)
}
} else {
val mimeType = getUriMimeType(mUri.toString(), newUri)
Intent(applicationContext, VideoPlayerActivity::class.java).apply {
setDataAndType(newUri, mimeType)
addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT)
if (intent.extras != null) {
putExtras(intent.extras!!)
}
startActivity(this)
}
}
finish()
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
initBottomActionsLayout()
}
private fun sendViewPagerIntent(path: String) {
Intent(this, ViewPagerActivity::class.java).apply {
putExtra(SKIP_AUTHENTICATION, intent.getBooleanExtra(SKIP_AUTHENTICATION, false))
putExtra(SHOW_FAVORITES, intent.getBooleanExtra(SHOW_FAVORITES, false))
putExtra(IS_VIEW_INTENT, true)
putExtra(IS_FROM_GALLERY, mIsFromGallery)
putExtra(PATH, path)
startActivity(this)
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.photo_video_menu, menu)
val visibleBottomActions = if (config.bottomActions) config.visibleBottomActions else 0
menu.apply {
findItem(R.id.menu_set_as).isVisible = mMedium?.isImage() == true && visibleBottomActions and BOTTOM_ACTION_SET_AS == 0
findItem(R.id.menu_edit).isVisible = mMedium?.isImage() == true && mUri?.scheme == "file" && visibleBottomActions and BOTTOM_ACTION_EDIT == 0
findItem(R.id.menu_properties).isVisible = mUri?.scheme == "file" && visibleBottomActions and BOTTOM_ACTION_PROPERTIES == 0
findItem(R.id.menu_share).isVisible = visibleBottomActions and BOTTOM_ACTION_SHARE == 0
findItem(R.id.menu_show_on_map).isVisible = visibleBottomActions and BOTTOM_ACTION_SHOW_ON_MAP == 0
}
updateMenuItemColors(menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (mMedium == null || mUri == null) {
return true
}
when (item.itemId) {
R.id.menu_set_as -> setAs(mUri!!.toString())
R.id.menu_open_with -> openPath(mUri!!.toString(), true)
R.id.menu_share -> sharePath(mUri!!.toString())
R.id.menu_edit -> openEditor(mUri!!.toString())
R.id.menu_properties -> showProperties()
R.id.menu_show_on_map -> showFileOnMap(mUri!!.toString())
else -> return super.onOptionsItemSelected(item)
}
return true
}
private fun showProperties() {
PropertiesDialog(this, mUri!!.path!!)
}
private fun isFileTypeVisible(path: String): Boolean {
val filter = config.filterMedia
return !(path.isImageFast() && filter and TYPE_IMAGES == 0 ||
path.isVideoFast() && filter and TYPE_VIDEOS == 0 ||
path.isGif() && filter and TYPE_GIFS == 0 ||
path.isRawFast() && filter and TYPE_RAWS == 0 ||
path.isSvg() && filter and TYPE_SVGS == 0 ||
path.isPortrait() && filter and TYPE_PORTRAITS == 0)
}
private fun initBottomActions() {
initBottomActionButtons()
initBottomActionsLayout()
}
private fun initBottomActionsLayout() {
bottom_actions.layoutParams.height = resources.getDimension(R.dimen.bottom_actions_height).toInt() + navigationBarHeight
if (config.bottomActions) {
bottom_actions.beVisible()
} else {
bottom_actions.beGone()
}
}
private fun initBottomActionButtons() {
arrayListOf(bottom_favorite, bottom_delete, bottom_rotate, bottom_properties, bottom_change_orientation, bottom_slideshow, bottom_show_on_map,
bottom_toggle_file_visibility, bottom_rename, bottom_copy, bottom_move, bottom_resize).forEach {
it.beGone()
}
val visibleBottomActions = if (config.bottomActions) config.visibleBottomActions else 0
bottom_edit.beVisibleIf(visibleBottomActions and BOTTOM_ACTION_EDIT != 0 && mMedium?.isImage() == true)
bottom_edit.setOnClickListener {
if (mUri != null && bottom_actions.alpha == 1f) {
openEditor(mUri!!.toString())
}
}
bottom_share.beVisibleIf(visibleBottomActions and BOTTOM_ACTION_SHARE != 0)
bottom_share.setOnClickListener {
if (mUri != null && bottom_actions.alpha == 1f) {
sharePath(mUri!!.toString())
}
}
bottom_set_as.beVisibleIf(visibleBottomActions and BOTTOM_ACTION_SET_AS != 0 && mMedium?.isImage() == true)
bottom_set_as.setOnClickListener {
setAs(mUri!!.toString())
}
bottom_show_on_map.beVisibleIf(visibleBottomActions and BOTTOM_ACTION_SHOW_ON_MAP != 0)
bottom_show_on_map.setOnClickListener {
showFileOnMap(mUri!!.toString())
}
}
override fun fragmentClicked() {
mIsFullScreen = !mIsFullScreen
if (mIsFullScreen) {
hideSystemUI(true)
} else {
showSystemUI(true)
}
val newAlpha = if (mIsFullScreen) 0f else 1f
top_shadow.animate().alpha(newAlpha).start()
if (!bottom_actions.isGone()) {
bottom_actions.animate().alpha(newAlpha).start()
}
}
override fun videoEnded() = false
override fun goToPrevItem() {}
override fun goToNextItem() {}
override fun launchViewVideoIntent(path: String) {}
override fun isSlideShowActive() = false
}

View file

@ -0,0 +1,385 @@
package com.simplemobiletools.gallery.pro.activities
import android.app.SearchManager
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.ViewGroup
import android.widget.RelativeLayout
import androidx.appcompat.widget.SearchView
import androidx.core.view.MenuItemCompat
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.VIEW_TYPE_GRID
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.commons.models.FileDirItem
import com.simplemobiletools.commons.views.MyGridLayoutManager
import com.simplemobiletools.gallery.pro.R
import com.simplemobiletools.gallery.pro.adapters.MediaAdapter
import com.simplemobiletools.gallery.pro.asynctasks.GetMediaAsynctask
import com.simplemobiletools.gallery.pro.extensions.*
import com.simplemobiletools.gallery.pro.helpers.*
import com.simplemobiletools.gallery.pro.interfaces.MediaOperationsListener
import com.simplemobiletools.gallery.pro.models.Medium
import com.simplemobiletools.gallery.pro.models.ThumbnailItem
import com.simplemobiletools.gallery.pro.models.ThumbnailSection
import kotlinx.android.synthetic.main.activity_media.*
import kotlinx.android.synthetic.main.activity_search.*
import kotlinx.android.synthetic.main.activity_search.media_empty_text_placeholder
import kotlinx.android.synthetic.main.activity_search.media_grid
import kotlinx.android.synthetic.main.activity_search.media_horizontal_fastscroller
import kotlinx.android.synthetic.main.activity_search.media_vertical_fastscroller
import java.io.File
class SearchActivity : SimpleActivity(), MediaOperationsListener {
private var mIsSearchOpen = false
private var mLastSearchedText = ""
private var mDateFormat = ""
private var mTimeFormat = ""
private var mSearchMenuItem: MenuItem? = null
private var mCurrAsyncTask: GetMediaAsynctask? = null
private var mAllMedia = ArrayList<ThumbnailItem>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_search)
media_empty_text_placeholder.setTextColor(config.textColor)
mDateFormat = config.dateFormat
mTimeFormat = getTimeFormat()
getAllMedia()
}
override fun onDestroy() {
super.onDestroy()
mCurrAsyncTask?.stopFetching()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_search, menu)
setupSearch(menu)
updateMenuItemColors(menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.toggle_filename -> toggleFilenameVisibility()
else -> return super.onOptionsItemSelected(item)
}
return true
}
private fun setupSearch(menu: Menu) {
val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
mSearchMenuItem = menu.findItem(R.id.search)
(mSearchMenuItem?.actionView as? SearchView)?.apply {
setSearchableInfo(searchManager.getSearchableInfo(componentName))
isSubmitButtonEnabled = false
setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String) = false
override fun onQueryTextChange(newText: String): Boolean {
if (mIsSearchOpen) {
mLastSearchedText = newText
textChanged(newText)
}
return true
}
})
}
MenuItemCompat.setOnActionExpandListener(mSearchMenuItem, object : MenuItemCompat.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
mIsSearchOpen = true
return true
}
// this triggers on device rotation too, avoid doing anything
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
if (mIsSearchOpen) {
mIsSearchOpen = false
mLastSearchedText = ""
}
return true
}
})
mSearchMenuItem?.expandActionView()
}
private fun textChanged(text: String) {
ensureBackgroundThread {
try {
val filtered = mAllMedia.filter { it is Medium && it.name.contains(text, true) } as ArrayList
filtered.sortBy { it is Medium && !it.name.startsWith(text, true) }
val grouped = MediaFetcher(applicationContext).groupMedia(filtered as ArrayList<Medium>, "")
runOnUiThread {
if (grouped.isEmpty()) {
media_empty_text_placeholder.text = getString(R.string.no_items_found)
media_empty_text_placeholder.beVisible()
} else {
media_empty_text_placeholder.beGone()
}
handleGridSpacing(grouped)
getMediaAdapter()?.updateMedia(grouped)
measureRecyclerViewContent(grouped)
}
} catch (ignored: Exception) {
}
}
}
private fun setupAdapter() {
val currAdapter = media_grid.adapter
if (currAdapter == null) {
val fastscroller = if (config.scrollHorizontally) media_horizontal_fastscroller else media_vertical_fastscroller
MediaAdapter(this, ArrayList(), this, false, false, "", media_grid, fastscroller) {
if (it is Medium) {
itemClicked(it.path)
}
}.apply {
media_grid.adapter = this
}
setupLayoutManager()
handleGridSpacing(mAllMedia)
measureRecyclerViewContent(mAllMedia)
} else if (mLastSearchedText.isEmpty()) {
(currAdapter as MediaAdapter).updateMedia(mAllMedia)
handleGridSpacing(mAllMedia)
measureRecyclerViewContent(mAllMedia)
} else {
textChanged(mLastSearchedText)
}
setupScrollDirection()
}
private fun handleGridSpacing(media: ArrayList<ThumbnailItem>) {
val viewType = config.getFolderViewType(SHOW_ALL)
if (viewType == VIEW_TYPE_GRID) {
if (media_grid.itemDecorationCount > 0) {
media_grid.removeItemDecorationAt(0)
}
val spanCount = config.mediaColumnCnt
val spacing = config.thumbnailSpacing
val decoration = GridSpacingItemDecoration(spanCount, spacing, config.scrollHorizontally, config.fileRoundedCorners, media, true)
media_grid.addItemDecoration(decoration)
}
}
private fun getMediaAdapter() = media_grid.adapter as? MediaAdapter
private fun toggleFilenameVisibility() {
config.displayFileNames = !config.displayFileNames
getMediaAdapter()?.updateDisplayFilenames(config.displayFileNames)
}
private fun itemClicked(path: String) {
val isVideo = path.isVideoFast()
if (isVideo) {
openPath(path, false)
} else {
Intent(this, ViewPagerActivity::class.java).apply {
putExtra(PATH, path)
putExtra(SHOW_ALL, false)
startActivity(this)
}
}
}
private fun setupLayoutManager() {
val viewType = config.getFolderViewType(SHOW_ALL)
if (viewType == VIEW_TYPE_GRID) {
setupGridLayoutManager()
} else {
setupListLayoutManager()
}
}
private fun setupGridLayoutManager() {
val layoutManager = media_grid.layoutManager as MyGridLayoutManager
if (config.scrollHorizontally) {
layoutManager.orientation = RecyclerView.HORIZONTAL
media_grid.layoutParams = RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)
} else {
layoutManager.orientation = RecyclerView.VERTICAL
media_grid.layoutParams = RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
}
layoutManager.spanCount = config.mediaColumnCnt
val adapter = getMediaAdapter()
layoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return if (adapter?.isASectionTitle(position) == true) {
layoutManager.spanCount
} else {
1
}
}
}
}
private fun setupListLayoutManager() {
val layoutManager = media_grid.layoutManager as MyGridLayoutManager
layoutManager.spanCount = 1
layoutManager.orientation = RecyclerView.VERTICAL
}
private fun setupScrollDirection() {
val viewType = config.getFolderViewType(SHOW_ALL)
val allowHorizontalScroll = config.scrollHorizontally && viewType == VIEW_TYPE_GRID
media_vertical_fastscroller.isHorizontal = false
media_vertical_fastscroller.beGoneIf(allowHorizontalScroll)
media_horizontal_fastscroller.isHorizontal = true
media_horizontal_fastscroller.beVisibleIf(allowHorizontalScroll)
val sorting = config.getFolderSorting(SHOW_ALL)
if (allowHorizontalScroll) {
media_horizontal_fastscroller.setViews(media_grid) {
media_horizontal_fastscroller.updateBubbleText(getBubbleTextItem(it, sorting))
}
} else {
media_vertical_fastscroller.setViews(media_grid) {
media_vertical_fastscroller.updateBubbleText(getBubbleTextItem(it, sorting))
}
}
}
private fun getBubbleTextItem(index: Int, sorting: Int): String {
var realIndex = index
val mediaAdapter = getMediaAdapter()
if (mediaAdapter?.isASectionTitle(index) == true) {
realIndex++
}
return mediaAdapter?.getItemBubbleText(realIndex, sorting, mDateFormat, mTimeFormat) ?: ""
}
private fun measureRecyclerViewContent(media: ArrayList<ThumbnailItem>) {
media_grid.onGlobalLayout {
if (config.scrollHorizontally) {
calculateContentWidth(media)
} else {
calculateContentHeight(media)
}
}
}
private fun calculateContentWidth(media: ArrayList<ThumbnailItem>) {
val layoutManager = media_grid.layoutManager as MyGridLayoutManager
val thumbnailWidth = layoutManager.getChildAt(0)?.width ?: 0
val fullWidth = ((media.size - 1) / layoutManager.spanCount + 1) * thumbnailWidth
media_horizontal_fastscroller.setContentWidth(fullWidth)
media_horizontal_fastscroller.setScrollToX(media_grid.computeHorizontalScrollOffset())
}
private fun calculateContentHeight(media: ArrayList<ThumbnailItem>) {
val layoutManager = media_grid.layoutManager as MyGridLayoutManager
val pathToCheck = SHOW_ALL
val hasSections = config.getFolderGrouping(pathToCheck) and GROUP_BY_NONE == 0 && !config.scrollHorizontally
val sectionTitleHeight = if (hasSections) layoutManager.getChildAt(0)?.height ?: 0 else 0
val thumbnailHeight = if (hasSections) layoutManager.getChildAt(1)?.height ?: 0 else layoutManager.getChildAt(0)?.height ?: 0
var fullHeight = 0
var curSectionItems = 0
media.forEach {
if (it is ThumbnailSection) {
fullHeight += sectionTitleHeight
if (curSectionItems != 0) {
val rows = ((curSectionItems - 1) / layoutManager.spanCount + 1)
fullHeight += rows * thumbnailHeight
}
curSectionItems = 0
} else {
curSectionItems++
}
}
fullHeight += ((curSectionItems - 1) / layoutManager.spanCount + 1) * thumbnailHeight
media_vertical_fastscroller.setContentHeight(fullHeight)
media_vertical_fastscroller.setScrollToY(media_grid.computeVerticalScrollOffset())
}
private fun getAllMedia() {
getCachedMedia("") {
if (it.isNotEmpty()) {
mAllMedia = it.clone() as ArrayList<ThumbnailItem>
}
runOnUiThread {
setupAdapter()
}
startAsyncTask(false)
}
}
private fun startAsyncTask(updateItems: Boolean) {
mCurrAsyncTask?.stopFetching()
mCurrAsyncTask = GetMediaAsynctask(applicationContext, "", showAll = true) {
mAllMedia = it.clone() as ArrayList<ThumbnailItem>
if (updateItems) {
textChanged(mLastSearchedText)
}
}
mCurrAsyncTask!!.execute()
}
override fun refreshItems() {
startAsyncTask(true)
}
override fun tryDeleteFiles(fileDirItems: ArrayList<FileDirItem>) {
val filtered = fileDirItems.filter { File(it.path).isFile && it.path.isMediaFile() } as ArrayList
if (filtered.isEmpty()) {
return
}
if (config.useRecycleBin && !filtered.first().path.startsWith(recycleBinPath)) {
val movingItems = resources.getQuantityString(R.plurals.moving_items_into_bin, filtered.size, filtered.size)
toast(movingItems)
movePathsInRecycleBin(filtered.map { it.path } as ArrayList<String>) {
if (it) {
deleteFilteredFiles(filtered)
} else {
toast(R.string.unknown_error_occurred)
}
}
} else {
val deletingItems = resources.getQuantityString(R.plurals.deleting_items, filtered.size, filtered.size)
toast(deletingItems)
deleteFilteredFiles(filtered)
}
}
private fun deleteFilteredFiles(filtered: ArrayList<FileDirItem>) {
deleteFiles(filtered) {
if (!it) {
toast(R.string.unknown_error_occurred)
return@deleteFiles
}
mAllMedia.removeAll { filtered.map { it.path }.contains((it as? Medium)?.path) }
ensureBackgroundThread {
val useRecycleBin = config.useRecycleBin
filtered.forEach {
if (it.path.startsWith(recycleBinPath) || !useRecycleBin) {
deleteDBPath(it.path)
}
}
}
}
}
override fun selectedPaths(paths: ArrayList<String>) {
}
override fun updateMediaGridDecoration(media: ArrayList<ThumbnailItem>) {
}
}

View file

@ -1,29 +1,40 @@
package com.simplemobiletools.gallery.activities
package com.simplemobiletools.gallery.pro.activities
import android.annotation.SuppressLint
import android.app.Activity
import android.app.WallpaperManager
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import com.simplemobiletools.commons.dialogs.RadioGroupDialog
import com.simplemobiletools.commons.extensions.checkAppSideloading
import com.simplemobiletools.commons.extensions.toast
import com.simplemobiletools.gallery.R
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.commons.helpers.isNougatPlus
import com.simplemobiletools.commons.models.RadioItem
import com.simplemobiletools.gallery.pro.R
import com.theartofdev.edmodo.cropper.CropImageView
import kotlinx.android.synthetic.main.view_crop_image.*
import kotlinx.android.synthetic.main.activity_set_wallpaper.*
import kotlinx.android.synthetic.main.bottom_set_wallpaper_actions.*
class SetWallpaperActivity : SimpleActivity(), CropImageView.OnCropImageCompleteListener {
private val PICK_IMAGE = 1
private var isLandscapeRatio = true
private var wallpaperFlag = -1
lateinit var uri: Uri
lateinit var wallpaperManager: WallpaperManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.view_crop_image)
setContentView(R.layout.activity_set_wallpaper)
if (checkAppSideloading()) {
return
}
if (intent.data == null) {
val pickIntent = Intent(applicationContext, MainActivity::class.java)
@ -34,10 +45,25 @@ class SetWallpaperActivity : SimpleActivity(), CropImageView.OnCropImageComplete
}
handleImage(intent)
setupBottomActions()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_set_wallpaper, menu)
updateMenuItemColors(menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.save -> confirmWallpaper()
else -> return super.onOptionsItemSelected(item)
}
return true
}
private fun handleImage(intent: Intent) {
uri = intent.data
uri = intent.data!!
if (uri.scheme != "file" && uri.scheme != "content") {
toast(R.string.unknown_file_location)
finish()
@ -53,56 +79,70 @@ class SetWallpaperActivity : SimpleActivity(), CropImageView.OnCropImageComplete
setupAspectRatio()
}
private fun setupBottomActions() {
bottom_set_wallpaper_aspect_ratio.setOnClickListener {
changeAspectRatio(!isLandscapeRatio)
}
bottom_set_wallpaper_rotate.setOnClickListener {
crop_image_view.rotateImage(90)
}
}
private fun setupAspectRatio() {
val wallpaperWidth = if (isLandscapeRatio) wallpaperManager.desiredMinimumWidth else wallpaperManager.desiredMinimumWidth / 2
crop_image_view.setAspectRatio(wallpaperWidth, wallpaperManager.desiredMinimumHeight)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_set_wallpaper, menu)
menu.findItem(R.id.portrait_aspect_ratio).isVisible = isLandscapeRatio
menu.findItem(R.id.landscape_aspect_ratio).isVisible = !isLandscapeRatio
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.save -> crop_image_view.getCroppedImageAsync()
R.id.rotate -> crop_image_view.rotateImage(90)
R.id.portrait_aspect_ratio -> changeAspectRatio(false)
R.id.landscape_aspect_ratio -> changeAspectRatio(true)
else -> return super.onOptionsItemSelected(item)
}
return true
bottom_set_wallpaper_aspect_ratio.setImageResource(if (isLandscapeRatio) R.drawable.ic_minimize else R.drawable.ic_maximize)
}
private fun changeAspectRatio(isLandscape: Boolean) {
isLandscapeRatio = isLandscape
setupAspectRatio()
invalidateOptionsMenu()
}
@SuppressLint("InlinedApi")
private fun confirmWallpaper() {
if (isNougatPlus()) {
val items = arrayListOf(
RadioItem(WallpaperManager.FLAG_SYSTEM, getString(R.string.home_screen)),
RadioItem(WallpaperManager.FLAG_LOCK, getString(R.string.lock_screen)),
RadioItem(WallpaperManager.FLAG_SYSTEM or WallpaperManager.FLAG_LOCK, getString(R.string.home_and_lock_screen)))
RadioGroupDialog(this, items) {
wallpaperFlag = it as Int
crop_image_view.getCroppedImageAsync()
}
} else {
crop_image_view.getCroppedImageAsync()
}
}
@SuppressLint("NewApi")
override fun onCropImageComplete(view: CropImageView?, result: CropImageView.CropResult) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && isDestroyed)
if (isDestroyed)
return
if (result.error == null) {
toast(R.string.setting_wallpaper)
Thread({
ensureBackgroundThread {
val bitmap = result.bitmap
val wantedHeight = wallpaperManager.desiredMinimumHeight
val ratio = wantedHeight / bitmap.height.toFloat()
val wantedWidth = (bitmap.width * ratio).toInt()
try {
wallpaperManager.setBitmap(Bitmap.createScaledBitmap(bitmap, wantedWidth, wantedHeight, true))
val scaledBitmap = Bitmap.createScaledBitmap(bitmap, wantedWidth, wantedHeight, true)
if (isNougatPlus()) {
wallpaperManager.setBitmap(scaledBitmap, null, true, wallpaperFlag)
} else {
wallpaperManager.setBitmap(scaledBitmap)
}
setResult(Activity.RESULT_OK)
} catch (e: OutOfMemoryError) {
toast(R.string.out_of_memory_error)
setResult(Activity.RESULT_CANCELED)
}
finish()
}).start()
}
} else {
toast("${getString(R.string.image_editing_failed)}: ${result.error.message}")
}

View file

@ -0,0 +1,852 @@
package com.simplemobiletools.gallery.pro.activities
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.text.TextUtils
import android.view.Menu
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.simplemobiletools.commons.dialogs.*
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.commons.models.RadioItem
import com.simplemobiletools.gallery.pro.R
import com.simplemobiletools.gallery.pro.dialogs.ChangeFileThumbnailStyleDialog
import com.simplemobiletools.gallery.pro.dialogs.ChangeFolderThumbnailStyleDialog
import com.simplemobiletools.gallery.pro.dialogs.ManageBottomActionsDialog
import com.simplemobiletools.gallery.pro.dialogs.ManageExtendedDetailsDialog
import com.simplemobiletools.gallery.pro.extensions.config
import com.simplemobiletools.gallery.pro.extensions.emptyTheRecycleBin
import com.simplemobiletools.gallery.pro.extensions.mediaDB
import com.simplemobiletools.gallery.pro.extensions.showRecycleBinEmptyingDialog
import com.simplemobiletools.gallery.pro.helpers.*
import com.simplemobiletools.gallery.pro.models.AlbumCover
import kotlinx.android.synthetic.main.activity_settings.*
import java.io.File
import java.io.InputStream
import java.util.*
class SettingsActivity : SimpleActivity() {
private val PICK_IMPORT_SOURCE_INTENT = 1
private var mRecycleBinContentSize = 0L
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
}
override fun onResume() {
super.onResume()
setupSettingItems()
}
private fun setupSettingItems() {
setupCustomizeColors()
setupUseEnglish()
setupChangeDateTimeFormat()
setupFileLoadingPriority()
setupManageIncludedFolders()
setupManageExcludedFolders()
setupManageHiddenFolders()
setupShowHiddenItems()
setupAutoplayVideos()
setupRememberLastVideo()
setupLoopVideos()
setupOpenVideosOnSeparateScreen()
setupMaxBrightness()
setupCropThumbnails()
setupDarkBackground()
setupScrollHorizontally()
setupScreenRotation()
setupHideSystemUI()
setupHiddenItemPasswordProtection()
setupAppPasswordProtection()
setupFileDeletionPasswordProtection()
setupDeleteEmptyFolders()
setupAllowDownGesture()
setupAllowRotatingWithGestures()
setupShowNotch()
setupBottomActions()
setupFileThumbnailStyle()
setupFolderThumbnailStyle()
setupKeepLastModified()
setupEnablePullToRefresh()
setupAllowZoomingImages()
setupShowHighestQuality()
setupAllowOneToOneZoom()
setupAllowInstantChange()
setupShowExtendedDetails()
setupHideExtendedDetails()
setupManageExtendedDetails()
setupSkipDeleteConfirmation()
setupManageBottomActions()
setupUseRecycleBin()
setupShowRecycleBin()
setupShowRecycleBinLast()
setupEmptyRecycleBin()
updateTextColors(settings_holder)
setupSectionColors()
setupClearCache()
setupExportSettings()
setupImportSettings()
invalidateOptionsMenu()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
updateMenuItemColors(menu)
return super.onCreateOptionsMenu(menu)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
super.onActivityResult(requestCode, resultCode, resultData)
if (requestCode == PICK_IMPORT_SOURCE_INTENT && resultCode == Activity.RESULT_OK && resultData != null && resultData.data != null) {
val inputStream = contentResolver.openInputStream(resultData.data!!)
parseFile(inputStream)
}
}
private fun setupSectionColors() {
val adjustedPrimaryColor = getAdjustedPrimaryColor()
arrayListOf(visibility_label, videos_label, thumbnails_label, scrolling_label, fullscreen_media_label, security_label,
file_operations_label, deep_zoomable_images_label, extended_details_label, bottom_actions_label, recycle_bin_label,
migrating_label).forEach {
it.setTextColor(adjustedPrimaryColor)
}
}
private fun setupCustomizeColors() {
settings_customize_colors_holder.setOnClickListener {
startCustomizationActivity()
}
}
private fun setupUseEnglish() {
settings_use_english_holder.beVisibleIf(config.wasUseEnglishToggled || Locale.getDefault().language != "en")
settings_use_english.isChecked = config.useEnglish
settings_use_english_holder.setOnClickListener {
settings_use_english.toggle()
config.useEnglish = settings_use_english.isChecked
System.exit(0)
}
}
private fun setupChangeDateTimeFormat() {
settings_change_date_time_format_holder.setOnClickListener {
ChangeDateTimeFormatDialog(this) {}
}
}
private fun setupFileLoadingPriority() {
settings_file_loading_priority.text = getFileLoadingPriorityText()
settings_file_loading_priority_holder.setOnClickListener {
val items = arrayListOf(
RadioItem(PRIORITY_SPEED, getString(R.string.speed)),
RadioItem(PRIORITY_COMPROMISE, getString(R.string.compromise)),
RadioItem(PRIORITY_VALIDITY, getString(R.string.avoid_showing_invalid_files)))
RadioGroupDialog(this@SettingsActivity, items, config.fileLoadingPriority) {
config.fileLoadingPriority = it as Int
settings_file_loading_priority.text = getFileLoadingPriorityText()
}
}
}
private fun getFileLoadingPriorityText() = getString(when (config.fileLoadingPriority) {
PRIORITY_SPEED -> R.string.speed
PRIORITY_COMPROMISE -> R.string.compromise
else -> R.string.avoid_showing_invalid_files
})
private fun setupManageIncludedFolders() {
settings_manage_included_folders_holder.setOnClickListener {
startActivity(Intent(this, IncludedFoldersActivity::class.java))
}
}
private fun setupManageExcludedFolders() {
settings_manage_excluded_folders_holder.setOnClickListener {
startActivity(Intent(this, ExcludedFoldersActivity::class.java))
}
}
private fun setupManageHiddenFolders() {
settings_manage_hidden_folders_holder.beVisibleIf(!isQPlus())
settings_manage_hidden_folders_holder.setOnClickListener {
handleHiddenFolderPasswordProtection {
startActivity(Intent(this, HiddenFoldersActivity::class.java))
}
}
}
private fun setupShowHiddenItems() {
settings_show_hidden_items.isChecked = config.showHiddenMedia
settings_show_hidden_items_holder.setOnClickListener {
if (config.showHiddenMedia) {
toggleHiddenItems()
} else {
handleHiddenFolderPasswordProtection {
toggleHiddenItems()
}
}
}
}
private fun toggleHiddenItems() {
settings_show_hidden_items.toggle()
config.showHiddenMedia = settings_show_hidden_items.isChecked
}
private fun setupAutoplayVideos() {
settings_autoplay_videos.isChecked = config.autoplayVideos
settings_autoplay_videos_holder.setOnClickListener {
settings_autoplay_videos.toggle()
config.autoplayVideos = settings_autoplay_videos.isChecked
}
}
private fun setupRememberLastVideo() {
settings_remember_last_video_position.isChecked = config.rememberLastVideoPosition
settings_remember_last_video_position_holder.setOnClickListener {
settings_remember_last_video_position.toggle()
config.rememberLastVideoPosition = settings_remember_last_video_position.isChecked
}
}
private fun setupLoopVideos() {
settings_loop_videos.isChecked = config.loopVideos
settings_loop_videos_holder.setOnClickListener {
settings_loop_videos.toggle()
config.loopVideos = settings_loop_videos.isChecked
}
}
private fun setupOpenVideosOnSeparateScreen() {
settings_open_videos_on_separate_screen.isChecked = config.openVideosOnSeparateScreen
settings_open_videos_on_separate_screen_holder.setOnClickListener {
settings_open_videos_on_separate_screen.toggle()
config.openVideosOnSeparateScreen = settings_open_videos_on_separate_screen.isChecked
}
}
private fun setupMaxBrightness() {
settings_max_brightness.isChecked = config.maxBrightness
settings_max_brightness_holder.setOnClickListener {
settings_max_brightness.toggle()
config.maxBrightness = settings_max_brightness.isChecked
}
}
private fun setupCropThumbnails() {
settings_crop_thumbnails.isChecked = config.cropThumbnails
settings_crop_thumbnails_holder.setOnClickListener {
settings_crop_thumbnails.toggle()
config.cropThumbnails = settings_crop_thumbnails.isChecked
}
}
private fun setupDarkBackground() {
settings_black_background.isChecked = config.blackBackground
settings_black_background_holder.setOnClickListener {
settings_black_background.toggle()
config.blackBackground = settings_black_background.isChecked
}
}
private fun setupScrollHorizontally() {
settings_scroll_horizontally.isChecked = config.scrollHorizontally
settings_scroll_horizontally_holder.setOnClickListener {
settings_scroll_horizontally.toggle()
config.scrollHorizontally = settings_scroll_horizontally.isChecked
if (config.scrollHorizontally) {
config.enablePullToRefresh = false
settings_enable_pull_to_refresh.isChecked = false
}
}
}
private fun setupHideSystemUI() {
settings_hide_system_ui.isChecked = config.hideSystemUI
settings_hide_system_ui_holder.setOnClickListener {
settings_hide_system_ui.toggle()
config.hideSystemUI = settings_hide_system_ui.isChecked
}
}
private fun setupHiddenItemPasswordProtection() {
settings_hidden_item_password_protection.isChecked = config.isHiddenPasswordProtectionOn
settings_hidden_item_password_protection_holder.setOnClickListener {
val tabToShow = if (config.isHiddenPasswordProtectionOn) config.hiddenProtectionType else SHOW_ALL_TABS
SecurityDialog(this, config.hiddenPasswordHash, tabToShow) { hash, type, success ->
if (success) {
val hasPasswordProtection = config.isHiddenPasswordProtectionOn
settings_hidden_item_password_protection.isChecked = !hasPasswordProtection
config.isHiddenPasswordProtectionOn = !hasPasswordProtection
config.hiddenPasswordHash = if (hasPasswordProtection) "" else hash
config.hiddenProtectionType = type
if (config.isHiddenPasswordProtectionOn) {
val confirmationTextId = if (config.hiddenProtectionType == PROTECTION_FINGERPRINT)
R.string.fingerprint_setup_successfully else R.string.protection_setup_successfully
ConfirmationDialog(this, "", confirmationTextId, R.string.ok, 0) { }
}
}
}
}
}
private fun setupAppPasswordProtection() {
settings_app_password_protection.isChecked = config.isAppPasswordProtectionOn
settings_app_password_protection_holder.setOnClickListener {
val tabToShow = if (config.isAppPasswordProtectionOn) config.appProtectionType else SHOW_ALL_TABS
SecurityDialog(this, config.appPasswordHash, tabToShow) { hash, type, success ->
if (success) {
val hasPasswordProtection = config.isAppPasswordProtectionOn
settings_app_password_protection.isChecked = !hasPasswordProtection
config.isAppPasswordProtectionOn = !hasPasswordProtection
config.appPasswordHash = if (hasPasswordProtection) "" else hash
config.appProtectionType = type
if (config.isAppPasswordProtectionOn) {
val confirmationTextId = if (config.appProtectionType == PROTECTION_FINGERPRINT)
R.string.fingerprint_setup_successfully else R.string.protection_setup_successfully
ConfirmationDialog(this, "", confirmationTextId, R.string.ok, 0) { }
}
}
}
}
}
private fun setupFileDeletionPasswordProtection() {
settings_file_deletion_password_protection.isChecked = config.isDeletePasswordProtectionOn
settings_file_deletion_password_protection_holder.setOnClickListener {
val tabToShow = if (config.isDeletePasswordProtectionOn) config.deleteProtectionType else SHOW_ALL_TABS
SecurityDialog(this, config.deletePasswordHash, tabToShow) { hash, type, success ->
if (success) {
val hasPasswordProtection = config.isDeletePasswordProtectionOn
settings_file_deletion_password_protection.isChecked = !hasPasswordProtection
config.isDeletePasswordProtectionOn = !hasPasswordProtection
config.deletePasswordHash = if (hasPasswordProtection) "" else hash
config.deleteProtectionType = type
if (config.isDeletePasswordProtectionOn) {
val confirmationTextId = if (config.deleteProtectionType == PROTECTION_FINGERPRINT)
R.string.fingerprint_setup_successfully else R.string.protection_setup_successfully
ConfirmationDialog(this, "", confirmationTextId, R.string.ok, 0) { }
}
}
}
}
}
private fun setupDeleteEmptyFolders() {
settings_delete_empty_folders.isChecked = config.deleteEmptyFolders
settings_delete_empty_folders_holder.setOnClickListener {
settings_delete_empty_folders.toggle()
config.deleteEmptyFolders = settings_delete_empty_folders.isChecked
}
}
private fun setupAllowDownGesture() {
settings_allow_down_gesture.isChecked = config.allowDownGesture
settings_allow_down_gesture_holder.setOnClickListener {
settings_allow_down_gesture.toggle()
config.allowDownGesture = settings_allow_down_gesture.isChecked
}
}
private fun setupAllowRotatingWithGestures() {
settings_allow_rotating_with_gestures.isChecked = config.allowRotatingWithGestures
settings_allow_rotating_with_gestures_holder.setOnClickListener {
settings_allow_rotating_with_gestures.toggle()
config.allowRotatingWithGestures = settings_allow_rotating_with_gestures.isChecked
}
}
private fun setupShowNotch() {
settings_show_notch_holder.beVisibleIf(isPiePlus())
settings_show_notch.isChecked = config.showNotch
settings_show_notch_holder.setOnClickListener {
settings_show_notch.toggle()
config.showNotch = settings_show_notch.isChecked
}
}
private fun setupFileThumbnailStyle() {
settings_file_thumbnail_style_holder.setOnClickListener {
ChangeFileThumbnailStyleDialog(this)
}
}
private fun setupFolderThumbnailStyle() {
settings_folder_thumbnail_style.text = getFolderStyleText()
settings_folder_thumbnail_style_holder.setOnClickListener {
ChangeFolderThumbnailStyleDialog(this) {
settings_folder_thumbnail_style.text = getFolderStyleText()
}
}
}
private fun getFolderStyleText() = getString(when (config.folderStyle) {
FOLDER_STYLE_SQUARE -> R.string.square
else -> R.string.rounded_corners
})
private fun setupKeepLastModified() {
settings_keep_last_modified.isChecked = config.keepLastModified
settings_keep_last_modified_holder.setOnClickListener {
settings_keep_last_modified.toggle()
config.keepLastModified = settings_keep_last_modified.isChecked
}
}
private fun setupEnablePullToRefresh() {
settings_enable_pull_to_refresh.isChecked = config.enablePullToRefresh
settings_enable_pull_to_refresh_holder.setOnClickListener {
settings_enable_pull_to_refresh.toggle()
config.enablePullToRefresh = settings_enable_pull_to_refresh.isChecked
}
}
private fun setupAllowZoomingImages() {
settings_allow_zooming_images.isChecked = config.allowZoomingImages
updateDeepZoomToggleButtons()
settings_allow_zooming_images_holder.setOnClickListener {
settings_allow_zooming_images.toggle()
config.allowZoomingImages = settings_allow_zooming_images.isChecked
updateDeepZoomToggleButtons()
}
}
private fun updateDeepZoomToggleButtons() {
settings_allow_rotating_with_gestures_holder.beVisibleIf(config.allowZoomingImages)
settings_show_highest_quality_holder.beVisibleIf(config.allowZoomingImages)
settings_allow_one_to_one_zoom_holder.beVisibleIf(config.allowZoomingImages)
}
private fun setupShowHighestQuality() {
settings_show_highest_quality.isChecked = config.showHighestQuality
settings_show_highest_quality_holder.setOnClickListener {
settings_show_highest_quality.toggle()
config.showHighestQuality = settings_show_highest_quality.isChecked
}
}
private fun setupAllowOneToOneZoom() {
settings_allow_one_to_one_zoom.isChecked = config.allowOneToOneZoom
settings_allow_one_to_one_zoom_holder.setOnClickListener {
settings_allow_one_to_one_zoom.toggle()
config.allowOneToOneZoom = settings_allow_one_to_one_zoom.isChecked
}
}
private fun setupAllowInstantChange() {
settings_allow_instant_change.isChecked = config.allowInstantChange
settings_allow_instant_change_holder.setOnClickListener {
settings_allow_instant_change.toggle()
config.allowInstantChange = settings_allow_instant_change.isChecked
}
}
private fun setupShowExtendedDetails() {
settings_show_extended_details.isChecked = config.showExtendedDetails
settings_show_extended_details_holder.setOnClickListener {
settings_show_extended_details.toggle()
config.showExtendedDetails = settings_show_extended_details.isChecked
settings_manage_extended_details_holder.beVisibleIf(config.showExtendedDetails)
settings_hide_extended_details_holder.beVisibleIf(config.showExtendedDetails)
}
}
private fun setupHideExtendedDetails() {
settings_hide_extended_details_holder.beVisibleIf(config.showExtendedDetails)
settings_hide_extended_details.isChecked = config.hideExtendedDetails
settings_hide_extended_details_holder.setOnClickListener {
settings_hide_extended_details.toggle()
config.hideExtendedDetails = settings_hide_extended_details.isChecked
}
}
private fun setupManageExtendedDetails() {
settings_manage_extended_details_holder.beVisibleIf(config.showExtendedDetails)
settings_manage_extended_details_holder.setOnClickListener {
ManageExtendedDetailsDialog(this) {
if (config.extendedDetails == 0) {
settings_show_extended_details_holder.callOnClick()
}
}
}
}
private fun setupSkipDeleteConfirmation() {
settings_skip_delete_confirmation.isChecked = config.skipDeleteConfirmation
settings_skip_delete_confirmation_holder.setOnClickListener {
settings_skip_delete_confirmation.toggle()
config.skipDeleteConfirmation = settings_skip_delete_confirmation.isChecked
}
}
private fun setupScreenRotation() {
settings_screen_rotation.text = getScreenRotationText()
settings_screen_rotation_holder.setOnClickListener {
val items = arrayListOf(
RadioItem(ROTATE_BY_SYSTEM_SETTING, getString(R.string.screen_rotation_system_setting)),
RadioItem(ROTATE_BY_DEVICE_ROTATION, getString(R.string.screen_rotation_device_rotation)),
RadioItem(ROTATE_BY_ASPECT_RATIO, getString(R.string.screen_rotation_aspect_ratio)))
RadioGroupDialog(this@SettingsActivity, items, config.screenRotation) {
config.screenRotation = it as Int
settings_screen_rotation.text = getScreenRotationText()
}
}
}
private fun getScreenRotationText() = getString(when (config.screenRotation) {
ROTATE_BY_SYSTEM_SETTING -> R.string.screen_rotation_system_setting
ROTATE_BY_DEVICE_ROTATION -> R.string.screen_rotation_device_rotation
else -> R.string.screen_rotation_aspect_ratio
})
private fun setupBottomActions() {
settings_bottom_actions.isChecked = config.bottomActions
settings_bottom_actions_holder.setOnClickListener {
settings_bottom_actions.toggle()
config.bottomActions = settings_bottom_actions.isChecked
settings_manage_bottom_actions_holder.beVisibleIf(config.bottomActions)
}
}
private fun setupManageBottomActions() {
settings_manage_bottom_actions_holder.beVisibleIf(config.bottomActions)
settings_manage_bottom_actions_holder.setOnClickListener {
ManageBottomActionsDialog(this) {
if (config.visibleBottomActions == 0) {
settings_bottom_actions_holder.callOnClick()
config.bottomActions = false
config.visibleBottomActions = DEFAULT_BOTTOM_ACTIONS
}
}
}
}
private fun setupUseRecycleBin() {
settings_empty_recycle_bin_holder.beVisibleIf(config.useRecycleBin)
settings_show_recycle_bin_holder.beVisibleIf(config.useRecycleBin)
settings_show_recycle_bin_last_holder.beVisibleIf(config.useRecycleBin && config.showRecycleBinAtFolders)
settings_use_recycle_bin.isChecked = config.useRecycleBin
settings_use_recycle_bin_holder.setOnClickListener {
settings_use_recycle_bin.toggle()
config.useRecycleBin = settings_use_recycle_bin.isChecked
settings_empty_recycle_bin_holder.beVisibleIf(config.useRecycleBin)
settings_show_recycle_bin_holder.beVisibleIf(config.useRecycleBin)
settings_show_recycle_bin_last_holder.beVisibleIf(config.useRecycleBin && config.showRecycleBinAtFolders)
}
}
private fun setupShowRecycleBin() {
settings_show_recycle_bin.isChecked = config.showRecycleBinAtFolders
settings_show_recycle_bin_holder.setOnClickListener {
settings_show_recycle_bin.toggle()
config.showRecycleBinAtFolders = settings_show_recycle_bin.isChecked
settings_show_recycle_bin_last_holder.beVisibleIf(config.useRecycleBin && config.showRecycleBinAtFolders)
}
}
private fun setupShowRecycleBinLast() {
settings_show_recycle_bin_last.isChecked = config.showRecycleBinLast
settings_show_recycle_bin_last_holder.setOnClickListener {
settings_show_recycle_bin_last.toggle()
config.showRecycleBinLast = settings_show_recycle_bin_last.isChecked
if (config.showRecycleBinLast) {
config.removePinnedFolders(setOf(RECYCLE_BIN))
}
}
}
private fun setupEmptyRecycleBin() {
ensureBackgroundThread {
try {
mRecycleBinContentSize = mediaDB.getDeletedMedia().sumByLong { it.size }
} catch (ignored: Exception) {
}
runOnUiThread {
settings_empty_recycle_bin_size.text = mRecycleBinContentSize.formatSize()
}
}
settings_empty_recycle_bin_holder.setOnClickListener {
if (mRecycleBinContentSize == 0L) {
toast(R.string.recycle_bin_empty)
} else {
showRecycleBinEmptyingDialog {
emptyTheRecycleBin()
mRecycleBinContentSize = 0L
settings_empty_recycle_bin_size.text = 0L.formatSize()
}
}
}
}
private fun setupClearCache() {
ensureBackgroundThread {
runOnUiThread {
settings_clear_cache_size.text = cacheDir.getProperSize(true).formatSize()
}
}
settings_clear_cache_holder.setOnClickListener {
ensureBackgroundThread {
cacheDir.deleteRecursively()
runOnUiThread {
settings_clear_cache_size.text = cacheDir.getProperSize(true).formatSize()
}
}
}
}
private fun setupExportSettings() {
settings_export_holder.setOnClickListener {
val configItems = LinkedHashMap<String, Any>().apply {
put(IS_USING_SHARED_THEME, config.isUsingSharedTheme)
put(TEXT_COLOR, config.textColor)
put(BACKGROUND_COLOR, config.backgroundColor)
put(PRIMARY_COLOR, config.primaryColor)
put(APP_ICON_COLOR, config.appIconColor)
put(USE_ENGLISH, config.useEnglish)
put(WAS_USE_ENGLISH_TOGGLED, config.wasUseEnglishToggled)
put(WIDGET_BG_COLOR, config.widgetBgColor)
put(WIDGET_TEXT_COLOR, config.widgetTextColor)
put(DATE_FORMAT, config.dateFormat)
put(USE_24_HOUR_FORMAT, config.use24HourFormat)
put(INCLUDED_FOLDERS, TextUtils.join(",", config.includedFolders))
put(EXCLUDED_FOLDERS, TextUtils.join(",", config.excludedFolders))
put(SHOW_HIDDEN_MEDIA, config.showHiddenMedia)
put(FILE_LOADING_PRIORITY, config.fileLoadingPriority)
put(AUTOPLAY_VIDEOS, config.autoplayVideos)
put(REMEMBER_LAST_VIDEO_POSITION, config.rememberLastVideoPosition)
put(LOOP_VIDEOS, config.loopVideos)
put(OPEN_VIDEOS_ON_SEPARATE_SCREEN, config.openVideosOnSeparateScreen)
put(ALLOW_VIDEO_GESTURES, config.allowVideoGestures)
put(ANIMATE_GIFS, config.animateGifs)
put(CROP_THUMBNAILS, config.cropThumbnails)
put(SHOW_THUMBNAIL_VIDEO_DURATION, config.showThumbnailVideoDuration)
put(SHOW_THUMBNAIL_FILE_TYPES, config.showThumbnailFileTypes)
put(SCROLL_HORIZONTALLY, config.scrollHorizontally)
put(ENABLE_PULL_TO_REFRESH, config.enablePullToRefresh)
put(MAX_BRIGHTNESS, config.maxBrightness)
put(BLACK_BACKGROUND, config.blackBackground)
put(HIDE_SYSTEM_UI, config.hideSystemUI)
put(ALLOW_INSTANT_CHANGE, config.allowInstantChange)
put(ALLOW_PHOTO_GESTURES, config.allowPhotoGestures)
put(ALLOW_DOWN_GESTURE, config.allowDownGesture)
put(ALLOW_ROTATING_WITH_GESTURES, config.allowRotatingWithGestures)
put(SHOW_NOTCH, config.showNotch)
put(SCREEN_ROTATION, config.screenRotation)
put(ALLOW_ZOOMING_IMAGES, config.allowZoomingImages)
put(SHOW_HIGHEST_QUALITY, config.showHighestQuality)
put(ALLOW_ONE_TO_ONE_ZOOM, config.allowOneToOneZoom)
put(SHOW_EXTENDED_DETAILS, config.showExtendedDetails)
put(HIDE_EXTENDED_DETAILS, config.hideExtendedDetails)
put(EXTENDED_DETAILS, config.extendedDetails)
put(DELETE_EMPTY_FOLDERS, config.deleteEmptyFolders)
put(KEEP_LAST_MODIFIED, config.keepLastModified)
put(SKIP_DELETE_CONFIRMATION, config.skipDeleteConfirmation)
put(BOTTOM_ACTIONS, config.bottomActions)
put(VISIBLE_BOTTOM_ACTIONS, config.visibleBottomActions)
put(USE_RECYCLE_BIN, config.useRecycleBin)
put(SHOW_RECYCLE_BIN_AT_FOLDERS, config.showRecycleBinAtFolders)
put(SHOW_RECYCLE_BIN_LAST, config.showRecycleBinLast)
put(SORT_ORDER, config.sorting)
put(DIRECTORY_SORT_ORDER, config.directorySorting)
put(GROUP_BY, config.groupBy)
put(GROUP_DIRECT_SUBFOLDERS, config.groupDirectSubfolders)
put(PINNED_FOLDERS, TextUtils.join(",", config.pinnedFolders))
put(DISPLAY_FILE_NAMES, config.displayFileNames)
put(FILTER_MEDIA, config.filterMedia)
put(DIR_COLUMN_CNT, config.dirColumnCnt)
put(MEDIA_COLUMN_CNT, config.mediaColumnCnt)
put(SHOW_ALL, config.showAll)
put(SHOW_WIDGET_FOLDER_NAME, config.showWidgetFolderName)
put(VIEW_TYPE_FILES, config.viewTypeFiles)
put(VIEW_TYPE_FOLDERS, config.viewTypeFolders)
put(SLIDESHOW_INTERVAL, config.slideshowInterval)
put(SLIDESHOW_INCLUDE_VIDEOS, config.slideshowIncludeVideos)
put(SLIDESHOW_INCLUDE_GIFS, config.slideshowIncludeGIFs)
put(SLIDESHOW_RANDOM_ORDER, config.slideshowRandomOrder)
put(SLIDESHOW_MOVE_BACKWARDS, config.slideshowMoveBackwards)
put(SLIDESHOW_LOOP, config.loopSlideshow)
put(LAST_EDITOR_CROP_ASPECT_RATIO, config.lastEditorCropAspectRatio)
put(LAST_EDITOR_CROP_OTHER_ASPECT_RATIO_X, config.lastEditorCropOtherAspectRatioX)
put(LAST_EDITOR_CROP_OTHER_ASPECT_RATIO_Y, config.lastEditorCropOtherAspectRatioY)
put(LAST_CONFLICT_RESOLUTION, config.lastConflictResolution)
put(LAST_CONFLICT_APPLY_TO_ALL, config.lastConflictApplyToAll)
put(EDITOR_BRUSH_COLOR, config.editorBrushColor)
put(EDITOR_BRUSH_HARDNESS, config.editorBrushHardness)
put(EDITOR_BRUSH_SIZE, config.editorBrushSize)
put(ALBUM_COVERS, config.albumCovers)
put(FOLDER_THUMBNAIL_STYLE, config.folderStyle)
put(FOLDER_MEDIA_COUNT, config.showFolderMediaCount)
put(LIMIT_FOLDER_TITLE, config.limitFolderTitle)
put(THUMBNAIL_SPACING, config.thumbnailSpacing)
put(FILE_ROUNDED_CORNERS, config.fileRoundedCorners)
}
exportSettings(configItems)
}
}
private fun setupImportSettings() {
settings_import_holder.setOnClickListener {
if (isQPlus()) {
Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "text/plain"
startActivityForResult(this, PICK_IMPORT_SOURCE_INTENT)
}
} else {
handlePermission(PERMISSION_READ_STORAGE) {
if (it) {
FilePickerDialog(this) {
ensureBackgroundThread {
parseFile(File(it).inputStream())
}
}
}
}
}
}
}
private fun parseFile(inputStream: InputStream?) {
if (inputStream == null) {
toast(R.string.unknown_error_occurred)
return
}
var importedItems = 0
val configValues = LinkedHashMap<String, Any>()
inputStream.bufferedReader().use {
while (true) {
try {
val line = it.readLine() ?: break
val split = line.split("=".toRegex(), 2)
if (split.size == 2) {
configValues[split[0]] = split[1]
}
importedItems++
} catch (e: Exception) {
showErrorToast(e)
}
}
}
for ((key, value) in configValues) {
when (key) {
IS_USING_SHARED_THEME -> config.isUsingSharedTheme = value.toBoolean()
TEXT_COLOR -> config.textColor = value.toInt()
BACKGROUND_COLOR -> config.backgroundColor = value.toInt()
PRIMARY_COLOR -> config.primaryColor = value.toInt()
APP_ICON_COLOR -> {
if (getAppIconColors().contains(value.toInt())) {
config.appIconColor = value.toInt()
checkAppIconColor()
}
}
USE_ENGLISH -> config.useEnglish = value.toBoolean()
WAS_USE_ENGLISH_TOGGLED -> config.wasUseEnglishToggled = value.toBoolean()
WIDGET_BG_COLOR -> config.widgetBgColor = value.toInt()
WIDGET_TEXT_COLOR -> config.widgetTextColor = value.toInt()
DATE_FORMAT -> config.dateFormat = value.toString()
USE_24_HOUR_FORMAT -> config.use24HourFormat = value.toBoolean()
INCLUDED_FOLDERS -> config.addIncludedFolders(value.toStringSet())
EXCLUDED_FOLDERS -> config.addExcludedFolders(value.toStringSet())
SHOW_HIDDEN_MEDIA -> config.showHiddenMedia = value.toBoolean()
FILE_LOADING_PRIORITY -> config.fileLoadingPriority = value.toInt()
AUTOPLAY_VIDEOS -> config.autoplayVideos = value.toBoolean()
REMEMBER_LAST_VIDEO_POSITION -> config.rememberLastVideoPosition = value.toBoolean()
LOOP_VIDEOS -> config.loopVideos = value.toBoolean()
OPEN_VIDEOS_ON_SEPARATE_SCREEN -> config.openVideosOnSeparateScreen = value.toBoolean()
ALLOW_VIDEO_GESTURES -> config.allowVideoGestures = value.toBoolean()
ANIMATE_GIFS -> config.animateGifs = value.toBoolean()
CROP_THUMBNAILS -> config.cropThumbnails = value.toBoolean()
SHOW_THUMBNAIL_VIDEO_DURATION -> config.showThumbnailVideoDuration = value.toBoolean()
SHOW_THUMBNAIL_FILE_TYPES -> config.showThumbnailFileTypes = value.toBoolean()
SCROLL_HORIZONTALLY -> config.scrollHorizontally = value.toBoolean()
ENABLE_PULL_TO_REFRESH -> config.enablePullToRefresh = value.toBoolean()
MAX_BRIGHTNESS -> config.maxBrightness = value.toBoolean()
BLACK_BACKGROUND -> config.blackBackground = value.toBoolean()
HIDE_SYSTEM_UI -> config.hideSystemUI = value.toBoolean()
ALLOW_INSTANT_CHANGE -> config.allowInstantChange = value.toBoolean()
ALLOW_PHOTO_GESTURES -> config.allowPhotoGestures = value.toBoolean()
ALLOW_DOWN_GESTURE -> config.allowDownGesture = value.toBoolean()
ALLOW_ROTATING_WITH_GESTURES -> config.allowRotatingWithGestures = value.toBoolean()
SHOW_NOTCH -> config.showNotch = value.toBoolean()
SCREEN_ROTATION -> config.screenRotation = value.toInt()
ALLOW_ZOOMING_IMAGES -> config.allowZoomingImages = value.toBoolean()
SHOW_HIGHEST_QUALITY -> config.showHighestQuality = value.toBoolean()
ALLOW_ONE_TO_ONE_ZOOM -> config.allowOneToOneZoom = value.toBoolean()
SHOW_EXTENDED_DETAILS -> config.showExtendedDetails = value.toBoolean()
HIDE_EXTENDED_DETAILS -> config.hideExtendedDetails = value.toBoolean()
EXTENDED_DETAILS -> config.extendedDetails = value.toInt()
DELETE_EMPTY_FOLDERS -> config.deleteEmptyFolders = value.toBoolean()
KEEP_LAST_MODIFIED -> config.keepLastModified = value.toBoolean()
SKIP_DELETE_CONFIRMATION -> config.skipDeleteConfirmation = value.toBoolean()
BOTTOM_ACTIONS -> config.bottomActions = value.toBoolean()
VISIBLE_BOTTOM_ACTIONS -> config.visibleBottomActions = value.toInt()
USE_RECYCLE_BIN -> config.useRecycleBin = value.toBoolean()
SHOW_RECYCLE_BIN_AT_FOLDERS -> config.showRecycleBinAtFolders = value.toBoolean()
SHOW_RECYCLE_BIN_LAST -> config.showRecycleBinLast = value.toBoolean()
SORT_ORDER -> config.sorting = value.toInt()
DIRECTORY_SORT_ORDER -> config.directorySorting = value.toInt()
GROUP_BY -> config.groupBy = value.toInt()
GROUP_DIRECT_SUBFOLDERS -> config.groupDirectSubfolders = value.toBoolean()
PINNED_FOLDERS -> config.addPinnedFolders(value.toStringSet())
DISPLAY_FILE_NAMES -> config.displayFileNames = value.toBoolean()
FILTER_MEDIA -> config.filterMedia = value.toInt()
DIR_COLUMN_CNT -> config.dirColumnCnt = value.toInt()
MEDIA_COLUMN_CNT -> config.mediaColumnCnt = value.toInt()
SHOW_ALL -> config.showAll = value.toBoolean()
SHOW_WIDGET_FOLDER_NAME -> config.showWidgetFolderName = value.toBoolean()
VIEW_TYPE_FILES -> config.viewTypeFiles = value.toInt()
VIEW_TYPE_FOLDERS -> config.viewTypeFolders = value.toInt()
SLIDESHOW_INTERVAL -> config.slideshowInterval = value.toInt()
SLIDESHOW_INCLUDE_VIDEOS -> config.slideshowIncludeVideos = value.toBoolean()
SLIDESHOW_INCLUDE_GIFS -> config.slideshowIncludeGIFs = value.toBoolean()
SLIDESHOW_RANDOM_ORDER -> config.slideshowRandomOrder = value.toBoolean()
SLIDESHOW_MOVE_BACKWARDS -> config.slideshowMoveBackwards = value.toBoolean()
SLIDESHOW_LOOP -> config.loopSlideshow = value.toBoolean()
LAST_EDITOR_CROP_ASPECT_RATIO -> config.lastEditorCropAspectRatio = value.toInt()
LAST_EDITOR_CROP_OTHER_ASPECT_RATIO_X -> config.lastEditorCropOtherAspectRatioX = value.toString().toFloat()
LAST_EDITOR_CROP_OTHER_ASPECT_RATIO_Y -> config.lastEditorCropOtherAspectRatioY = value.toString().toFloat()
LAST_CONFLICT_RESOLUTION -> config.lastConflictResolution = value.toInt()
LAST_CONFLICT_APPLY_TO_ALL -> config.lastConflictApplyToAll = value.toBoolean()
EDITOR_BRUSH_COLOR -> config.editorBrushColor = value.toInt()
EDITOR_BRUSH_HARDNESS -> config.editorBrushHardness = value.toString().toFloat()
EDITOR_BRUSH_SIZE -> config.editorBrushSize = value.toString().toFloat()
FOLDER_THUMBNAIL_STYLE -> config.folderStyle = value.toInt()
FOLDER_MEDIA_COUNT -> config.showFolderMediaCount = value.toInt()
LIMIT_FOLDER_TITLE -> config.limitFolderTitle = value.toBoolean()
THUMBNAIL_SPACING -> config.thumbnailSpacing = value.toInt()
FILE_ROUNDED_CORNERS -> config.fileRoundedCorners = value.toBoolean()
ALBUM_COVERS -> {
val existingCovers = config.parseAlbumCovers()
val existingCoverPaths = existingCovers.map { it.path }.toMutableList() as ArrayList<String>
val listType = object : TypeToken<List<AlbumCover>>() {}.type
val covers = Gson().fromJson<ArrayList<AlbumCover>>(value.toString(), listType) ?: ArrayList(1)
covers.filter { !existingCoverPaths.contains(it.path) && getDoesFilePathExist(it.tmb) }.forEach {
existingCovers.add(it)
}
config.albumCovers = Gson().toJson(existingCovers)
}
}
}
toast(if (configValues.size > 0) R.string.settings_imported_successfully else R.string.no_entries_for_importing)
runOnUiThread {
setupSettingItems()
}
}
}

View file

@ -0,0 +1,97 @@
package com.simplemobiletools.gallery.pro.activities
import android.annotation.SuppressLint
import android.database.ContentObserver
import android.net.Uri
import android.provider.MediaStore.Images
import android.provider.MediaStore.Video
import android.view.WindowManager
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.dialogs.FilePickerDialog
import com.simplemobiletools.commons.extensions.getParentPath
import com.simplemobiletools.commons.extensions.getRealPathFromURI
import com.simplemobiletools.commons.extensions.scanPathRecursively
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.commons.helpers.isPiePlus
import com.simplemobiletools.gallery.pro.R
import com.simplemobiletools.gallery.pro.extensions.addPathToDB
import com.simplemobiletools.gallery.pro.extensions.config
import com.simplemobiletools.gallery.pro.extensions.updateDirectoryPath
open class SimpleActivity : BaseSimpleActivity() {
val observer = object : ContentObserver(null) {
override fun onChange(selfChange: Boolean, uri: Uri) {
super.onChange(selfChange, uri)
val path = getRealPathFromURI(uri)
if (path != null) {
updateDirectoryPath(path.getParentPath())
addPathToDB(path)
}
}
}
override fun getAppIconIDs() = arrayListOf(
R.mipmap.ic_launcher_red,
R.mipmap.ic_launcher_pink,
R.mipmap.ic_launcher_purple,
R.mipmap.ic_launcher_deep_purple,
R.mipmap.ic_launcher_indigo,
R.mipmap.ic_launcher_blue,
R.mipmap.ic_launcher_light_blue,
R.mipmap.ic_launcher_cyan,
R.mipmap.ic_launcher_teal,
R.mipmap.ic_launcher_green,
R.mipmap.ic_launcher_light_green,
R.mipmap.ic_launcher_lime,
R.mipmap.ic_launcher_yellow,
R.mipmap.ic_launcher_amber,
R.mipmap.ic_launcher,
R.mipmap.ic_launcher_deep_orange,
R.mipmap.ic_launcher_brown,
R.mipmap.ic_launcher_blue_grey,
R.mipmap.ic_launcher_grey_black
)
override fun getAppLauncherName() = getString(R.string.app_launcher_name)
@SuppressLint("InlinedApi")
protected fun checkNotchSupport() {
if (isPiePlus()) {
val cutoutMode = when {
config.showNotch -> WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
else -> WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
}
window.attributes.layoutInDisplayCutoutMode = cutoutMode
if (config.showNotch) {
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
}
}
}
protected fun registerFileUpdateListener() {
try {
contentResolver.registerContentObserver(Images.Media.EXTERNAL_CONTENT_URI, true, observer)
contentResolver.registerContentObserver(Video.Media.EXTERNAL_CONTENT_URI, true, observer)
} catch (ignored: Exception) {
}
}
protected fun unregisterFileUpdateListener() {
try {
contentResolver.unregisterContentObserver(observer)
} catch (ignored: Exception) {
}
}
protected fun showAddIncludedFolderDialog(callback: () -> Unit) {
FilePickerDialog(this, config.lastFilepickerPath, false, config.shouldShowHidden, false, true) {
config.lastFilepickerPath = it
config.addIncludedFolder(it)
callback()
ensureBackgroundThread {
scanPathRecursively(it)
}
}
}
}

View file

@ -0,0 +1,44 @@
package com.simplemobiletools.gallery.pro.activities
import android.content.Intent
import com.simplemobiletools.commons.activities.BaseSplashActivity
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.gallery.pro.extensions.config
import com.simplemobiletools.gallery.pro.extensions.favoritesDB
import com.simplemobiletools.gallery.pro.extensions.getFavoriteFromPath
import com.simplemobiletools.gallery.pro.extensions.mediaDB
import com.simplemobiletools.gallery.pro.models.Favorite
class SplashActivity : BaseSplashActivity() {
override fun initActivity() {
// check if previously selected favorite items have been properly migrated into the new Favorites table
if (config.wereFavoritesMigrated) {
launchActivity()
} else {
if (config.appRunCount == 0) {
config.wereFavoritesMigrated = true
launchActivity()
} else {
config.wereFavoritesMigrated = true
ensureBackgroundThread {
val favorites = ArrayList<Favorite>()
val favoritePaths = mediaDB.getFavorites().map { it.path }.toMutableList() as ArrayList<String>
favoritePaths.forEach {
favorites.add(getFavoriteFromPath(it))
}
favoritesDB.insertAll(favorites)
runOnUiThread {
launchActivity()
}
}
}
}
}
private fun launchActivity() {
startActivity(Intent(this, MainActivity::class.java))
finish()
}
}

View file

@ -1,11 +1,11 @@
package com.simplemobiletools.gallery.activities
package com.simplemobiletools.gallery.pro.activities
import android.os.Bundle
class VideoActivity : PhotoVideoActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
PhotoVideoActivity.mIsVideo = true
mIsVideo = true
super.onCreate(savedInstanceState)
}
}

View file

@ -0,0 +1,633 @@
package com.simplemobiletools.gallery.pro.activities
import android.app.Activity
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.graphics.Color
import android.graphics.Point
import android.graphics.SurfaceTexture
import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.util.DisplayMetrics
import android.view.*
import android.widget.SeekBar
import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory
import com.google.android.exoplayer2.source.ExtractorMediaSource
import com.google.android.exoplayer2.source.TrackGroupArray
import com.google.android.exoplayer2.trackselection.TrackSelectionArray
import com.google.android.exoplayer2.upstream.ContentDataSource
import com.google.android.exoplayer2.upstream.DataSource
import com.google.android.exoplayer2.upstream.DataSpec
import com.google.android.exoplayer2.video.VideoListener
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.gallery.pro.R
import com.simplemobiletools.gallery.pro.extensions.*
import com.simplemobiletools.gallery.pro.helpers.*
import kotlinx.android.synthetic.main.activity_video_player.*
import kotlinx.android.synthetic.main.bottom_video_time_holder.*
open class VideoPlayerActivity : SimpleActivity(), SeekBar.OnSeekBarChangeListener, TextureView.SurfaceTextureListener {
private val PLAY_WHEN_READY_DRAG_DELAY = 100L
private var mIsFullscreen = false
private var mIsPlaying = false
private var mWasVideoStarted = false
private var mIsDragged = false
private var mIsOrientationLocked = false
private var mScreenWidth = 0
private var mCurrTime = 0
private var mDuration = 0
private var mDragThreshold = 0f
private var mTouchDownX = 0f
private var mTouchDownY = 0f
private var mTouchDownTime = 0L
private var mProgressAtDown = 0L
private var mCloseDownThreshold = 100f
private var mUri: Uri? = null
private var mExoPlayer: SimpleExoPlayer? = null
private var mVideoSize = Point(0, 0)
private var mTimerHandler = Handler()
private var mPlayWhenReadyHandler = Handler()
private var mIgnoreCloseDown = false
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_video_player)
setupOrientation()
checkNotchSupport()
initPlayer()
}
override fun onResume() {
super.onResume()
top_shadow.layoutParams.height = statusBarHeight + actionBarHeight
supportActionBar?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
window.statusBarColor = Color.TRANSPARENT
window.navigationBarColor = Color.TRANSPARENT
if (config.blackBackground) {
video_player_holder.background = ColorDrawable(Color.BLACK)
}
if (config.maxBrightness) {
val attributes = window.attributes
attributes.screenBrightness = 1f
window.attributes = attributes
}
updateTextColors(video_player_holder)
}
override fun onPause() {
super.onPause()
pauseVideo()
if (config.rememberLastVideoPosition && mWasVideoStarted) {
saveVideoProgress()
}
}
override fun onDestroy() {
super.onDestroy()
if (!isChangingConfigurations) {
pauseVideo()
video_curr_time.text = 0.getFormattedDuration()
releaseExoPlayer()
video_seekbar.progress = 0
mTimerHandler.removeCallbacksAndMessages(null)
mPlayWhenReadyHandler.removeCallbacksAndMessages(null)
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_video_player, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_change_orientation -> changeOrientation()
R.id.menu_open_with -> openPath(mUri!!.toString(), true)
R.id.menu_share -> shareMediumPath(mUri!!.toString())
else -> return super.onOptionsItemSelected(item)
}
return true
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
setVideoSize()
initTimeHolder()
video_surface_frame.onGlobalLayout {
video_surface_frame.controller.resetState()
}
}
private fun setupOrientation() {
if (!mIsOrientationLocked) {
if (config.screenRotation == ROTATE_BY_DEVICE_ROTATION) {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR
} else if (config.screenRotation == ROTATE_BY_SYSTEM_SETTING) {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}
}
}
private fun initPlayer() {
mUri = intent.data ?: return
supportActionBar?.title = getFilenameFromUri(mUri!!)
initTimeHolder()
showSystemUI(true)
window.decorView.setOnSystemUiVisibilityChangeListener { visibility ->
val isFullscreen = visibility and View.SYSTEM_UI_FLAG_FULLSCREEN != 0
fullscreenToggled(isFullscreen)
}
video_curr_time.setOnClickListener { doSkip(false) }
video_duration.setOnClickListener { doSkip(true) }
video_toggle_play_pause.setOnClickListener { togglePlayPause() }
video_surface_frame.setOnClickListener { toggleFullscreen() }
video_surface_frame.controller.settings.swallowDoubleTaps = true
video_next_file.beVisibleIf(intent.getBooleanExtra(SHOW_NEXT_ITEM, false))
video_next_file.setOnClickListener { handleNextFile() }
video_prev_file.beVisibleIf(intent.getBooleanExtra(SHOW_PREV_ITEM, false))
video_prev_file.setOnClickListener { handlePrevFile() }
val gestureDetector = GestureDetector(this, object : GestureDetector.SimpleOnGestureListener() {
override fun onDoubleTap(e: MotionEvent?): Boolean {
if (e != null) {
handleDoubleTap(e.rawX)
}
return true
}
})
video_surface_frame.setOnTouchListener { view, event ->
handleEvent(event)
gestureDetector.onTouchEvent(event)
false
}
initExoPlayer()
video_surface.surfaceTextureListener = this
if (config.allowVideoGestures) {
video_brightness_controller.initialize(this, slide_info, true, video_player_holder, singleTap = { x, y ->
toggleFullscreen()
}, doubleTap = { x, y ->
doSkip(false)
})
video_volume_controller.initialize(this, slide_info, false, video_player_holder, singleTap = { x, y ->
toggleFullscreen()
}, doubleTap = { x, y ->
doSkip(true)
})
} else {
video_brightness_controller.beGone()
video_volume_controller.beGone()
}
if (config.hideSystemUI) {
Handler().postDelayed({
fullscreenToggled(true)
}, HIDE_SYSTEM_UI_DELAY)
}
mDragThreshold = DRAG_THRESHOLD * resources.displayMetrics.density
}
private fun initExoPlayer() {
val dataSpec = DataSpec(mUri)
val fileDataSource = ContentDataSource(applicationContext)
try {
fileDataSource.open(dataSpec)
} catch (e: Exception) {
showErrorToast(e)
}
val factory = DataSource.Factory { fileDataSource }
val audioSource = ExtractorMediaSource(fileDataSource.uri, factory, DefaultExtractorsFactory(), null, null)
mExoPlayer = ExoPlayerFactory.newSimpleInstance(applicationContext).apply {
seekParameters = SeekParameters.CLOSEST_SYNC
audioStreamType = C.STREAM_TYPE_MUSIC
if (config.loopVideos) {
repeatMode = Player.REPEAT_MODE_ONE
}
prepare(audioSource)
}
initExoPlayerListeners()
}
private fun initExoPlayerListeners() {
mExoPlayer!!.addListener(object : Player.EventListener {
override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters?) {}
override fun onSeekProcessed() {}
override fun onTracksChanged(trackGroups: TrackGroupArray?, trackSelections: TrackSelectionArray?) {}
override fun onPlayerError(error: ExoPlaybackException?) {}
override fun onLoadingChanged(isLoading: Boolean) {}
override fun onPositionDiscontinuity(reason: Int) {
// Reset progress views when video loops.
if (reason == Player.DISCONTINUITY_REASON_PERIOD_TRANSITION) {
video_seekbar.progress = 0
video_curr_time.text = 0.getFormattedDuration()
}
}
override fun onRepeatModeChanged(repeatMode: Int) {}
override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {}
override fun onTimelineChanged(timeline: Timeline?, manifest: Any?, reason: Int) {}
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
when (playbackState) {
Player.STATE_READY -> videoPrepared()
Player.STATE_ENDED -> videoCompleted()
}
}
})
mExoPlayer!!.addVideoListener(object : VideoListener {
override fun onVideoSizeChanged(width: Int, height: Int, unappliedRotationDegrees: Int, pixelWidthHeightRatio: Float) {
mVideoSize.x = width
mVideoSize.y = height
setVideoSize()
}
override fun onRenderedFirstFrame() {}
})
}
private fun videoPrepared() {
if (!mWasVideoStarted) {
video_toggle_play_pause.beVisible()
mDuration = (mExoPlayer!!.duration / 1000).toInt()
video_seekbar.max = mDuration
video_duration.text = mDuration.getFormattedDuration()
setPosition(mCurrTime)
if (config.rememberLastVideoPosition) {
setLastVideoSavedPosition()
}
if (config.autoplayVideos) {
resumeVideo()
} else {
video_toggle_play_pause.setImageResource(R.drawable.ic_play_outline_vector)
}
}
}
private fun handleDoubleTap(x: Float) {
val instantWidth = mScreenWidth / 7
when {
x <= instantWidth -> doSkip(false)
x >= mScreenWidth - instantWidth -> doSkip(true)
else -> togglePlayPause()
}
}
private fun resumeVideo() {
video_toggle_play_pause.setImageResource(R.drawable.ic_pause_outline_vector)
if (mExoPlayer == null) {
return
}
val wasEnded = didVideoEnd()
if (wasEnded) {
setPosition(0)
}
mWasVideoStarted = true
mIsPlaying = true
mExoPlayer?.playWhenReady = true
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
private fun pauseVideo() {
video_toggle_play_pause.setImageResource(R.drawable.ic_play_outline_vector)
if (mExoPlayer == null) {
return
}
mIsPlaying = false
if (!didVideoEnd()) {
mExoPlayer?.playWhenReady = false
}
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
private fun togglePlayPause() {
mIsPlaying = !mIsPlaying
if (mIsPlaying) {
resumeVideo()
} else {
pauseVideo()
}
}
private fun setPosition(seconds: Int) {
mExoPlayer?.seekTo(seconds * 1000L)
video_seekbar.progress = seconds
video_curr_time.text = seconds.getFormattedDuration()
}
private fun setLastVideoSavedPosition() {
val pos = config.getLastVideoPosition(mUri.toString())
if (pos > 0) {
setPosition(pos)
}
}
private fun videoCompleted() {
if (mExoPlayer == null) {
return
}
clearLastVideoSavedProgress()
mCurrTime = (mExoPlayer!!.duration / 1000).toInt()
video_seekbar.progress = video_seekbar.max
video_curr_time.text = mDuration.getFormattedDuration()
pauseVideo()
}
private fun didVideoEnd(): Boolean {
val currentPos = mExoPlayer?.currentPosition ?: 0
val duration = mExoPlayer?.duration ?: 0
return currentPos != 0L && currentPos >= duration
}
private fun saveVideoProgress() {
if (!didVideoEnd()) {
config.saveLastVideoPosition(mUri.toString(), mExoPlayer!!.currentPosition.toInt() / 1000)
}
}
private fun clearLastVideoSavedProgress() {
config.removeLastVideoPosition(mUri.toString())
}
private fun setVideoSize() {
val videoProportion = mVideoSize.x.toFloat() / mVideoSize.y.toFloat()
val display = windowManager.defaultDisplay
val screenWidth: Int
val screenHeight: Int
val realMetrics = DisplayMetrics()
display.getRealMetrics(realMetrics)
screenWidth = realMetrics.widthPixels
screenHeight = realMetrics.heightPixels
val screenProportion = screenWidth.toFloat() / screenHeight.toFloat()
video_surface.layoutParams.apply {
if (videoProportion > screenProportion) {
width = screenWidth
height = (screenWidth.toFloat() / videoProportion).toInt()
} else {
width = (videoProportion * screenHeight.toFloat()).toInt()
height = screenHeight
}
video_surface.layoutParams = this
}
val multiplier = if (screenWidth > screenHeight) 0.5 else 0.8
mScreenWidth = (screenWidth * multiplier).toInt()
if (config.screenRotation == ROTATE_BY_ASPECT_RATIO) {
if (mVideoSize.x > mVideoSize.y) {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
} else if (mVideoSize.x < mVideoSize.y) {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
}
}
private fun changeOrientation() {
mIsOrientationLocked = true
requestedOrientation = if (resources.configuration.orientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
} else {
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
}
private fun toggleFullscreen() {
fullscreenToggled(!mIsFullscreen)
}
private fun fullscreenToggled(isFullScreen: Boolean) {
mIsFullscreen = isFullScreen
if (isFullScreen) {
hideSystemUI(true)
} else {
showSystemUI(true)
}
val newAlpha = if (isFullScreen) 0f else 1f
arrayOf(video_prev_file, video_toggle_play_pause, video_next_file, video_curr_time, video_seekbar, video_duration, top_shadow, video_bottom_gradient).forEach {
it.animate().alpha(newAlpha).start()
}
video_seekbar.setOnSeekBarChangeListener(if (mIsFullscreen) null else this)
arrayOf(video_prev_file, video_next_file, video_curr_time, video_duration).forEach {
it.isClickable = !mIsFullscreen
}
}
private fun initTimeHolder() {
var right = 0
var bottom = 0
if (hasNavBar()) {
if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
bottom += navigationBarHeight
} else {
right += navigationBarWidth
bottom += navigationBarHeight
}
}
video_time_holder.setPadding(0, 0, right, bottom)
video_seekbar.setOnSeekBarChangeListener(this)
video_seekbar.max = mDuration
video_duration.text = mDuration.getFormattedDuration()
video_curr_time.text = mCurrTime.getFormattedDuration()
setupTimer()
}
private fun setupTimer() {
runOnUiThread(object : Runnable {
override fun run() {
if (mExoPlayer != null && !mIsDragged && mIsPlaying) {
mCurrTime = (mExoPlayer!!.currentPosition / 1000).toInt()
video_seekbar.progress = mCurrTime
video_curr_time.text = mCurrTime.getFormattedDuration()
}
mTimerHandler.postDelayed(this, 1000)
}
})
}
private fun doSkip(forward: Boolean) {
if (mExoPlayer == null) {
return
}
val curr = mExoPlayer!!.currentPosition
val newProgress = if (forward) curr + FAST_FORWARD_VIDEO_MS else curr - FAST_FORWARD_VIDEO_MS
val roundProgress = Math.round(newProgress / 1000f)
val limitedProgress = Math.max(Math.min(mExoPlayer!!.duration.toInt() / 1000, roundProgress), 0)
setPosition(limitedProgress)
if (!mIsPlaying) {
togglePlayPause()
}
}
private fun handleEvent(event: MotionEvent) {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
mTouchDownX = event.x
mTouchDownY = event.y
mTouchDownTime = System.currentTimeMillis()
mProgressAtDown = mExoPlayer!!.currentPosition
}
MotionEvent.ACTION_POINTER_DOWN -> mIgnoreCloseDown = true
MotionEvent.ACTION_MOVE -> {
val diffX = event.x - mTouchDownX
val diffY = event.y - mTouchDownY
if (mIsDragged || (Math.abs(diffX) > mDragThreshold && Math.abs(diffX) > Math.abs(diffY)) && video_surface_frame.controller.state.zoom == 1f) {
if (!mIsDragged) {
arrayOf(video_curr_time, video_seekbar, video_duration).forEach {
it.animate().alpha(1f).start()
}
}
mIgnoreCloseDown = true
mIsDragged = true
var percent = ((diffX / mScreenWidth) * 100).toInt()
percent = Math.min(100, Math.max(-100, percent))
val skipLength = (mDuration * 1000f) * (percent / 100f)
var newProgress = mProgressAtDown + skipLength
newProgress = Math.max(Math.min(mExoPlayer!!.duration.toFloat(), newProgress), 0f)
val newSeconds = (newProgress / 1000).toInt()
setPosition(newSeconds)
resetPlayWhenReady()
}
}
MotionEvent.ACTION_UP -> {
val diffX = mTouchDownX - event.x
val diffY = mTouchDownY - event.y
val downGestureDuration = System.currentTimeMillis() - mTouchDownTime
if (config.allowDownGesture && !mIgnoreCloseDown && Math.abs(diffY) > Math.abs(diffX) && diffY < -mCloseDownThreshold &&
downGestureDuration < MAX_CLOSE_DOWN_GESTURE_DURATION &&
video_surface_frame.controller.state.zoom == 1f) {
supportFinishAfterTransition()
}
mIgnoreCloseDown = false
if (mIsDragged) {
if (mIsFullscreen) {
arrayOf(video_curr_time, video_seekbar, video_duration).forEach {
it.animate().alpha(0f).start()
}
}
if (!mIsPlaying) {
togglePlayPause()
}
}
mIsDragged = false
}
}
}
private fun handleNextFile() {
Intent().apply {
putExtra(GO_TO_NEXT_ITEM, true)
setResult(Activity.RESULT_OK, this)
}
finish()
}
private fun handlePrevFile() {
Intent().apply {
putExtra(GO_TO_PREV_ITEM, true)
setResult(Activity.RESULT_OK, this)
}
finish()
}
private fun resetPlayWhenReady() {
mExoPlayer?.playWhenReady = false
mPlayWhenReadyHandler.removeCallbacksAndMessages(null)
mPlayWhenReadyHandler.postDelayed({
mExoPlayer?.playWhenReady = true
}, PLAY_WHEN_READY_DRAG_DELAY)
}
private fun releaseExoPlayer() {
mExoPlayer?.stop()
ensureBackgroundThread {
mExoPlayer?.release()
mExoPlayer = null
}
}
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
if (mExoPlayer != null && fromUser) {
setPosition(progress)
resetPlayWhenReady()
}
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {
mIsDragged = true
}
override fun onStopTrackingTouch(seekBar: SeekBar?) {
if (mExoPlayer == null)
return
if (mIsPlaying) {
mExoPlayer!!.playWhenReady = true
} else {
togglePlayPause()
}
mIsDragged = false
}
override fun onSurfaceTextureUpdated(surface: SurfaceTexture?) {
}
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture?) = false
override fun onSurfaceTextureAvailable(surface: SurfaceTexture?, width: Int, height: Int) {
ensureBackgroundThread {
mExoPlayer?.setVideoSurface(Surface(video_surface!!.surfaceTexture))
}
}
override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture?, width: Int, height: Int) {}
}

View file

@ -0,0 +1,183 @@
package com.simplemobiletools.gallery.pro.activities
import android.app.Activity
import android.appwidget.AppWidgetManager
import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.widget.RelativeLayout
import android.widget.RemoteViews
import com.bumptech.glide.signature.ObjectKey
import com.simplemobiletools.commons.dialogs.ColorPickerDialog
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.gallery.pro.R
import com.simplemobiletools.gallery.pro.dialogs.PickDirectoryDialog
import com.simplemobiletools.gallery.pro.extensions.*
import com.simplemobiletools.gallery.pro.helpers.MyWidgetProvider
import com.simplemobiletools.gallery.pro.helpers.ROUNDED_CORNERS_NONE
import com.simplemobiletools.gallery.pro.models.Directory
import com.simplemobiletools.gallery.pro.models.Widget
import kotlinx.android.synthetic.main.activity_widget_config.*
class WidgetConfigureActivity : SimpleActivity() {
private var mBgAlpha = 0f
private var mWidgetId = 0
private var mBgColor = 0
private var mBgColorWithoutTransparency = 0
private var mTextColor = 0
private var mFolderPath = ""
private var mDirectories = ArrayList<Directory>()
public override fun onCreate(savedInstanceState: Bundle?) {
useDynamicTheme = false
super.onCreate(savedInstanceState)
setResult(RESULT_CANCELED)
setContentView(R.layout.activity_widget_config)
initVariables()
mWidgetId = intent.extras?.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID) ?: AppWidgetManager.INVALID_APPWIDGET_ID
if (mWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
finish()
}
config_save.setOnClickListener { saveConfig() }
config_bg_color.setOnClickListener { pickBackgroundColor() }
config_text_color.setOnClickListener { pickTextColor() }
folder_picker_value.setOnClickListener { changeSelectedFolder() }
config_image_holder.setOnClickListener { changeSelectedFolder() }
folder_picker_show_folder_name.isChecked = config.showWidgetFolderName
handleFolderNameDisplay()
folder_picker_show_folder_name_holder.setOnClickListener {
folder_picker_show_folder_name.toggle()
handleFolderNameDisplay()
}
updateTextColors(folder_picker_holder)
folder_picker_holder.background = ColorDrawable(config.backgroundColor)
getCachedDirectories(false, false) {
mDirectories = it
val path = it.firstOrNull()?.path
if (path != null) {
updateFolderImage(path)
}
}
}
private fun initVariables() {
mBgColor = config.widgetBgColor
mBgAlpha = Color.alpha(mBgColor) / 255f
mBgColorWithoutTransparency = Color.rgb(Color.red(mBgColor), Color.green(mBgColor), Color.blue(mBgColor))
config_bg_seekbar.apply {
progress = (mBgAlpha * 100).toInt()
onSeekBarChangeListener {
mBgAlpha = it / 100f
updateBackgroundColor()
}
}
updateBackgroundColor()
mTextColor = config.widgetTextColor
updateTextColor()
}
private fun saveConfig() {
val views = RemoteViews(packageName, R.layout.widget)
views.setBackgroundColor(R.id.widget_holder, mBgColor)
AppWidgetManager.getInstance(this).updateAppWidget(mWidgetId, views)
config.showWidgetFolderName = folder_picker_show_folder_name.isChecked
val widget = Widget(null, mWidgetId, mFolderPath)
ensureBackgroundThread {
widgetsDB.insertOrUpdate(widget)
}
storeWidgetColors()
requestWidgetUpdate()
Intent().apply {
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mWidgetId)
setResult(Activity.RESULT_OK, this)
}
finish()
}
private fun storeWidgetColors() {
config.apply {
widgetBgColor = mBgColor
widgetTextColor = mTextColor
}
}
private fun requestWidgetUpdate() {
Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE, null, this, MyWidgetProvider::class.java).apply {
putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, intArrayOf(mWidgetId))
sendBroadcast(this)
}
}
private fun updateBackgroundColor() {
mBgColor = mBgColorWithoutTransparency.adjustAlpha(mBgAlpha)
config_save.setBackgroundColor(mBgColor)
config_image_holder.setBackgroundColor(mBgColor)
config_bg_color.setFillWithStroke(mBgColor, Color.BLACK)
}
private fun updateTextColor() {
config_save.setTextColor(mTextColor)
config_folder_name.setTextColor(mTextColor)
config_text_color.setFillWithStroke(mTextColor, Color.BLACK)
}
private fun pickBackgroundColor() {
ColorPickerDialog(this, mBgColorWithoutTransparency) { wasPositivePressed, color ->
if (wasPositivePressed) {
mBgColorWithoutTransparency = color
updateBackgroundColor()
}
}
}
private fun pickTextColor() {
ColorPickerDialog(this, mTextColor) { wasPositivePressed, color ->
if (wasPositivePressed) {
mTextColor = color
updateTextColor()
}
}
}
private fun changeSelectedFolder() {
PickDirectoryDialog(this, "", false, true) {
updateFolderImage(it)
}
}
private fun updateFolderImage(folderPath: String) {
mFolderPath = folderPath
runOnUiThread {
folder_picker_value.text = getFolderNameFromPath(folderPath)
config_folder_name.text = getFolderNameFromPath(folderPath)
}
ensureBackgroundThread {
val path = directoryDao.getDirectoryThumbnail(folderPath)
if (path != null) {
runOnUiThread {
val signature = ObjectKey(System.currentTimeMillis().toString())
loadJpg(path, config_image, config.cropThumbnails, ROUNDED_CORNERS_NONE, signature)
}
}
}
}
private fun handleFolderNameDisplay() {
val showFolderName = folder_picker_show_folder_name.isChecked
config_folder_name.beVisibleIf(showFolderName)
(config_image.layoutParams as RelativeLayout.LayoutParams).bottomMargin = if (showFolderName) 0 else resources.getDimension(R.dimen.normal_margin).toInt()
}
}

View file

@ -0,0 +1,760 @@
package com.simplemobiletools.gallery.pro.adapters
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.ShortcutInfo
import android.content.pm.ShortcutManager
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Icon
import android.text.TextUtils
import android.view.Menu
import android.view.View
import android.view.ViewGroup
import android.widget.RelativeLayout
import com.bumptech.glide.Glide
import com.google.gson.Gson
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.adapters.MyRecyclerViewAdapter
import com.simplemobiletools.commons.dialogs.*
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.commons.models.FileDirItem
import com.simplemobiletools.commons.views.FastScroller
import com.simplemobiletools.commons.views.MyRecyclerView
import com.simplemobiletools.gallery.pro.R
import com.simplemobiletools.gallery.pro.activities.MediaActivity
import com.simplemobiletools.gallery.pro.dialogs.ConfirmDeleteFolderDialog
import com.simplemobiletools.gallery.pro.dialogs.ExcludeFolderDialog
import com.simplemobiletools.gallery.pro.dialogs.PickMediumDialog
import com.simplemobiletools.gallery.pro.extensions.*
import com.simplemobiletools.gallery.pro.helpers.*
import com.simplemobiletools.gallery.pro.interfaces.DirectoryOperationsListener
import com.simplemobiletools.gallery.pro.models.AlbumCover
import com.simplemobiletools.gallery.pro.models.Directory
import kotlinx.android.synthetic.main.directory_item_grid_square.view.dir_check
import kotlinx.android.synthetic.main.directory_item_grid_square.view.dir_location
import kotlinx.android.synthetic.main.directory_item_grid_square.view.dir_lock
import kotlinx.android.synthetic.main.directory_item_grid_square.view.dir_name
import kotlinx.android.synthetic.main.directory_item_grid_square.view.dir_pin
import kotlinx.android.synthetic.main.directory_item_grid_square.view.dir_thumbnail
import kotlinx.android.synthetic.main.directory_item_list.view.*
import java.io.File
class DirectoryAdapter(activity: BaseSimpleActivity, var dirs: ArrayList<Directory>, val listener: DirectoryOperationsListener?, recyclerView: MyRecyclerView,
val isPickIntent: Boolean, fastScroller: FastScroller? = null, itemClick: (Any) -> Unit) :
MyRecyclerViewAdapter(activity, recyclerView, fastScroller, itemClick) {
private val config = activity.config
private val isListViewType = config.viewTypeFolders == VIEW_TYPE_LIST
private var pinnedFolders = config.pinnedFolders
private var scrollHorizontally = config.scrollHorizontally
private var animateGifs = config.animateGifs
private var cropThumbnails = config.cropThumbnails
private var groupDirectSubfolders = config.groupDirectSubfolders
private var currentDirectoriesHash = dirs.hashCode()
private var lockedFolderPaths = ArrayList<String>()
private var showMediaCount = config.showFolderMediaCount
private var folderStyle = config.folderStyle
private var limitFolderTitle = config.limitFolderTitle
init {
setupDragListener(true)
fillLockedFolders()
}
override fun getActionMenuId() = R.menu.cab_directories
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val layoutType = when {
isListViewType -> R.layout.directory_item_list
folderStyle == FOLDER_STYLE_SQUARE -> R.layout.directory_item_grid_square
else -> R.layout.directory_item_grid_rounded_corners
}
return createViewHolder(layoutType, parent)
}
override fun onBindViewHolder(holder: MyRecyclerViewAdapter.ViewHolder, position: Int) {
val dir = dirs.getOrNull(position) ?: return
holder.bindView(dir, true, !isPickIntent) { itemView, adapterPosition ->
setupView(itemView, dir)
}
bindViewHolder(holder)
}
override fun getItemCount() = dirs.size
override fun prepareActionMode(menu: Menu) {
val selectedPaths = getSelectedPaths()
if (selectedPaths.isEmpty()) {
return
}
val isOneItemSelected = isOneItemSelected()
menu.apply {
findItem(R.id.cab_rename).isVisible = !selectedPaths.contains(FAVORITES) && !selectedPaths.contains(RECYCLE_BIN)
findItem(R.id.cab_change_cover_image).isVisible = isOneItemSelected
findItem(R.id.cab_lock).isVisible = selectedPaths.any { !config.isFolderProtected(it) }
findItem(R.id.cab_unlock).isVisible = selectedPaths.any { config.isFolderProtected(it) }
findItem(R.id.cab_empty_recycle_bin).isVisible = isOneItemSelected && selectedPaths.first() == RECYCLE_BIN
findItem(R.id.cab_empty_disable_recycle_bin).isVisible = isOneItemSelected && selectedPaths.first() == RECYCLE_BIN
findItem(R.id.cab_create_shortcut).isVisible = isOreoPlus() && isOneItemSelected
checkHideBtnVisibility(this, selectedPaths)
checkPinBtnVisibility(this, selectedPaths)
}
}
override fun actionItemPressed(id: Int) {
if (selectedKeys.isEmpty()) {
return
}
when (id) {
R.id.cab_properties -> showProperties()
R.id.cab_rename -> renameDir()
R.id.cab_pin -> pinFolders(true)
R.id.cab_unpin -> pinFolders(false)
R.id.cab_empty_recycle_bin -> tryEmptyRecycleBin(true)
R.id.cab_empty_disable_recycle_bin -> emptyAndDisableRecycleBin()
R.id.cab_hide -> toggleFoldersVisibility(true)
R.id.cab_unhide -> toggleFoldersVisibility(false)
R.id.cab_exclude -> tryExcludeFolder()
R.id.cab_lock -> tryLockFolder()
R.id.cab_unlock -> unlockFolder()
R.id.cab_copy_to -> copyMoveTo(true)
R.id.cab_move_to -> moveFilesTo()
R.id.cab_select_all -> selectAll()
R.id.cab_create_shortcut -> tryCreateShortcut()
R.id.cab_delete -> askConfirmDelete()
R.id.cab_select_photo -> tryChangeAlbumCover(false)
R.id.cab_use_default -> tryChangeAlbumCover(true)
}
}
override fun getSelectableItemCount() = dirs.size
override fun getIsItemSelectable(position: Int) = true
override fun getItemSelectionKey(position: Int) = dirs.getOrNull(position)?.path?.hashCode()
override fun getItemKeyPosition(key: Int) = dirs.indexOfFirst { it.path.hashCode() == key }
override fun onActionModeCreated() {}
override fun onActionModeDestroyed() {}
override fun onViewRecycled(holder: ViewHolder) {
super.onViewRecycled(holder)
if (!activity.isDestroyed) {
Glide.with(activity).clear(holder.itemView.dir_thumbnail!!)
}
}
private fun checkHideBtnVisibility(menu: Menu, selectedPaths: ArrayList<String>) {
menu.findItem(R.id.cab_hide).isVisible = selectedPaths.any { !it.doesThisOrParentHaveNoMedia(HashMap(), null) }
menu.findItem(R.id.cab_unhide).isVisible = selectedPaths.any { it.doesThisOrParentHaveNoMedia(HashMap(), null) }
}
private fun checkPinBtnVisibility(menu: Menu, selectedPaths: ArrayList<String>) {
val pinnedFolders = config.pinnedFolders
menu.findItem(R.id.cab_pin).isVisible = selectedPaths.any { !pinnedFolders.contains(it) }
menu.findItem(R.id.cab_unpin).isVisible = selectedPaths.any { pinnedFolders.contains(it) }
}
private fun showProperties() {
if (selectedKeys.size <= 1) {
val path = getFirstSelectedItemPath() ?: return
if (path != FAVORITES && path != RECYCLE_BIN) {
activity.handleLockedFolderOpening(path) { success ->
if (success) {
PropertiesDialog(activity, path, config.shouldShowHidden)
}
}
}
} else {
PropertiesDialog(activity, getSelectedPaths().filter {
it != FAVORITES && it != RECYCLE_BIN && !config.isFolderProtected(it)
}.toMutableList(), config.shouldShowHidden)
}
}
private fun renameDir() {
if (selectedKeys.size == 1) {
val firstDir = getFirstSelectedItem() ?: return
val sourcePath = firstDir.path
val dir = File(sourcePath)
if (activity.isAStorageRootFolder(dir.absolutePath)) {
activity.toast(R.string.rename_folder_root)
return
}
activity.handleLockedFolderOpening(sourcePath) { success ->
if (success) {
RenameItemDialog(activity, dir.absolutePath) {
activity.runOnUiThread {
firstDir.apply {
path = it
name = it.getFilenameFromPath()
tmb = File(it, tmb.getFilenameFromPath()).absolutePath
}
updateDirs(dirs)
ensureBackgroundThread {
try {
activity.directoryDao.updateDirectoryAfterRename(firstDir.tmb, firstDir.name, firstDir.path, sourcePath)
listener?.refreshItems()
} catch (e: Exception) {
activity.showErrorToast(e)
}
}
}
}
}
}
} else {
val paths = getSelectedPaths().filter { !activity.isAStorageRootFolder(it) && !config.isFolderProtected(it) } as ArrayList<String>
RenameItemsDialog(activity, paths) {
listener?.refreshItems()
}
}
}
private fun toggleFoldersVisibility(hide: Boolean) {
val selectedPaths = getSelectedPaths()
if (hide && selectedPaths.contains(RECYCLE_BIN)) {
config.showRecycleBinAtFolders = false
if (selectedPaths.size == 1) {
listener?.refreshItems()
finishActMode()
}
}
if (hide) {
if (config.wasHideFolderTooltipShown) {
hideFolders(selectedPaths)
} else {
config.wasHideFolderTooltipShown = true
ConfirmationDialog(activity, activity.getString(R.string.hide_folder_description)) {
hideFolders(selectedPaths)
}
}
} else {
selectedPaths.filter { it != FAVORITES && it != RECYCLE_BIN && (selectedPaths.size == 1 || !config.isFolderProtected(it)) }.forEach {
val path = it
activity.handleLockedFolderOpening(path) { success ->
if (success) {
if (path.containsNoMedia()) {
activity.removeNoMedia(path) {
if (config.shouldShowHidden) {
updateFolderNames()
} else {
activity.runOnUiThread {
listener?.refreshItems()
finishActMode()
}
}
}
}
}
}
}
}
}
private fun hideFolders(paths: ArrayList<String>) {
for (path in paths) {
activity.handleLockedFolderOpening(path) { success ->
if (success) {
hideFolder(path)
}
}
}
}
private fun tryEmptyRecycleBin(askConfirmation: Boolean) {
if (askConfirmation) {
activity.showRecycleBinEmptyingDialog {
emptyRecycleBin()
}
} else {
emptyRecycleBin()
}
}
private fun emptyRecycleBin() {
activity.handleLockedFolderOpening(RECYCLE_BIN) { success ->
if (success) {
activity.emptyTheRecycleBin {
listener?.refreshItems()
}
}
}
}
private fun emptyAndDisableRecycleBin() {
activity.handleLockedFolderOpening(RECYCLE_BIN) { success ->
if (success) {
activity.showRecycleBinEmptyingDialog {
activity.emptyAndDisableTheRecycleBin {
listener?.refreshItems()
}
}
}
}
}
private fun updateFolderNames() {
val includedFolders = config.includedFolders
val hidden = activity.getString(R.string.hidden)
dirs.forEach {
it.name = activity.checkAppendingHidden(it.path, hidden, includedFolders, ArrayList())
}
listener?.updateDirectories(dirs.toMutableList() as ArrayList)
activity.runOnUiThread {
updateDirs(dirs)
}
}
private fun hideFolder(path: String) {
activity.addNoMedia(path) {
if (config.shouldShowHidden) {
updateFolderNames()
} else {
val affectedPositions = ArrayList<Int>()
val includedFolders = config.includedFolders
val newDirs = dirs.filterIndexed { index, directory ->
val removeDir = directory.path.doesThisOrParentHaveNoMedia(HashMap(), null) && !includedFolders.contains(directory.path)
if (removeDir) {
affectedPositions.add(index)
}
!removeDir
} as ArrayList<Directory>
activity.runOnUiThread {
affectedPositions.sortedDescending().forEach {
notifyItemRemoved(it)
}
currentDirectoriesHash = newDirs.hashCode()
dirs = newDirs
finishActMode()
listener?.updateDirectories(newDirs)
}
}
}
}
private fun tryExcludeFolder() {
val selectedPaths = getSelectedPaths()
val paths = selectedPaths.filter { it != PATH && it != RECYCLE_BIN && it != FAVORITES }.toSet()
if (selectedPaths.contains(RECYCLE_BIN)) {
config.showRecycleBinAtFolders = false
if (selectedPaths.size == 1) {
listener?.refreshItems()
finishActMode()
}
}
if (paths.size == 1) {
ExcludeFolderDialog(activity, paths.toMutableList()) {
listener?.refreshItems()
finishActMode()
}
} else if (paths.size > 1) {
config.addExcludedFolders(paths)
listener?.refreshItems()
finishActMode()
}
}
private fun tryLockFolder() {
if (config.wasFolderLockingNoticeShown) {
lockFolder()
} else {
FolderLockingNoticeDialog(activity) {
lockFolder()
}
}
}
private fun lockFolder() {
SecurityDialog(activity, "", SHOW_ALL_TABS) { hash, type, success ->
if (success) {
getSelectedPaths().filter { !config.isFolderProtected(it) }.forEach {
config.addFolderProtection(it, hash, type)
lockedFolderPaths.add(it)
}
listener?.refreshItems()
finishActMode()
}
}
}
private fun unlockFolder() {
val paths = getSelectedPaths()
val firstPath = paths.first()
val tabToShow = config.getFolderProtectionType(firstPath)
val hashToCheck = config.getFolderProtectionHash(firstPath)
SecurityDialog(activity, hashToCheck, tabToShow) { hash, type, success ->
if (success) {
paths.filter { config.isFolderProtected(it) && config.getFolderProtectionType(it) == tabToShow && config.getFolderProtectionHash(it) == hashToCheck }.forEach {
config.removeFolderProtection(it)
lockedFolderPaths.remove(it)
}
listener?.refreshItems()
finishActMode()
}
}
}
private fun pinFolders(pin: Boolean) {
if (pin) {
config.addPinnedFolders(getSelectedPaths().toHashSet())
} else {
config.removePinnedFolders(getSelectedPaths().toHashSet())
}
currentDirectoriesHash = 0
pinnedFolders = config.pinnedFolders
listener?.recheckPinnedFolders()
}
private fun moveFilesTo() {
activity.handleDeletePasswordProtection {
copyMoveTo(false)
}
}
private fun copyMoveTo(isCopyOperation: Boolean) {
val paths = ArrayList<String>()
val showHidden = config.shouldShowHidden
getSelectedPaths().forEach {
val filter = config.filterMedia
File(it).listFiles()?.filter {
!File(it.absolutePath).isDirectory &&
it.absolutePath.isMediaFile() && (showHidden || !it.name.startsWith('.')) &&
((it.isImageFast() && filter and TYPE_IMAGES != 0) ||
(it.isVideoFast() && filter and TYPE_VIDEOS != 0) ||
(it.isGif() && filter and TYPE_GIFS != 0) ||
(it.isRawFast() && filter and TYPE_RAWS != 0) ||
(it.isSvg() && filter and TYPE_SVGS != 0))
}?.mapTo(paths) { it.absolutePath }
}
val fileDirItems = paths.map { FileDirItem(it, it.getFilenameFromPath()) } as ArrayList<FileDirItem>
activity.tryCopyMoveFilesTo(fileDirItems, isCopyOperation) {
val destinationPath = it
val newPaths = fileDirItems.map { "$destinationPath/${it.name}" }.toMutableList() as java.util.ArrayList<String>
activity.rescanPaths(newPaths) {
activity.fixDateTaken(newPaths, false)
}
config.tempFolderPath = ""
listener?.refreshItems()
finishActMode()
}
}
private fun tryCreateShortcut() {
activity.handleLockedFolderOpening(getFirstSelectedItemPath() ?: "") { success ->
if (success) {
createShortcut()
}
}
}
@SuppressLint("NewApi")
private fun createShortcut() {
val manager = activity.getSystemService(ShortcutManager::class.java)
if (manager.isRequestPinShortcutSupported) {
val dir = getFirstSelectedItem() ?: return
val path = dir.path
val drawable = resources.getDrawable(R.drawable.shortcut_image).mutate()
val coverThumbnail = config.parseAlbumCovers().firstOrNull { it.tmb == dir.path }?.tmb ?: dir.tmb
activity.getShortcutImage(coverThumbnail, drawable) {
val intent = Intent(activity, MediaActivity::class.java)
intent.action = Intent.ACTION_VIEW
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
intent.putExtra(DIRECTORY, path)
val shortcut = ShortcutInfo.Builder(activity, path)
.setShortLabel(dir.name)
.setIcon(Icon.createWithBitmap(drawable.convertToBitmap()))
.setIntent(intent)
.build()
manager.requestPinShortcut(shortcut, null)
}
}
}
private fun askConfirmDelete() {
when {
config.isDeletePasswordProtectionOn -> activity.handleDeletePasswordProtection {
deleteFolders()
}
config.skipDeleteConfirmation -> deleteFolders()
else -> {
val itemsCnt = selectedKeys.size
val items = if (itemsCnt == 1) {
var folder = getSelectedPaths().first().getFilenameFromPath()
if (folder == RECYCLE_BIN) {
folder = activity.getString(R.string.recycle_bin)
}
"\"$folder\""
} else {
resources.getQuantityString(R.plurals.delete_items, itemsCnt, itemsCnt)
}
val fileDirItem = getFirstSelectedItem() ?: return
val baseString = if (!config.useRecycleBin || (isOneItemSelected() && fileDirItem.isRecycleBin()) || (isOneItemSelected() && fileDirItem.areFavorites())) {
R.string.deletion_confirmation
} else {
R.string.move_to_recycle_bin_confirmation
}
val question = String.format(resources.getString(baseString), items)
val warning = resources.getQuantityString(R.plurals.delete_warning, itemsCnt, itemsCnt)
ConfirmDeleteFolderDialog(activity, question, warning) {
deleteFolders()
}
}
}
}
private fun deleteFolders() {
if (selectedKeys.isEmpty()) {
return
}
var SAFPath = ""
val selectedDirs = getSelectedItems()
selectedDirs.forEach {
val path = it.path
if (activity.needsStupidWritePermissions(path) && config.treeUri.isEmpty()) {
SAFPath = path
}
}
activity.handleSAFDialog(SAFPath) {
if (!it) {
return@handleSAFDialog
}
var foldersToDelete = ArrayList<File>(selectedKeys.size)
selectedDirs.forEach {
if (it.areFavorites() || it.isRecycleBin()) {
if (it.isRecycleBin()) {
tryEmptyRecycleBin(false)
} else {
ensureBackgroundThread {
activity.mediaDB.clearFavorites()
activity.favoritesDB.clearFavorites()
listener?.refreshItems()
}
}
if (selectedKeys.size == 1) {
finishActMode()
}
} else {
foldersToDelete.add(File(it.path))
}
}
if (foldersToDelete.size == 1) {
activity.handleLockedFolderOpening(foldersToDelete.first().absolutePath) { success ->
if (success) {
listener?.deleteFolders(foldersToDelete)
}
}
} else {
foldersToDelete = foldersToDelete.filter { !config.isFolderProtected(it.absolutePath) }.toMutableList() as ArrayList<File>
listener?.deleteFolders(foldersToDelete)
}
}
}
private fun tryChangeAlbumCover(useDefault: Boolean) {
activity.handleLockedFolderOpening(getFirstSelectedItemPath() ?: "") { success ->
if (success) {
changeAlbumCover(useDefault)
}
}
}
private fun changeAlbumCover(useDefault: Boolean) {
if (selectedKeys.size != 1)
return
val path = getFirstSelectedItemPath() ?: return
if (useDefault) {
val albumCovers = getAlbumCoversWithout(path)
storeCovers(albumCovers)
} else {
pickMediumFrom(path, path)
}
}
private fun pickMediumFrom(targetFolder: String, path: String) {
PickMediumDialog(activity, path) {
if (File(it).isDirectory) {
pickMediumFrom(targetFolder, it)
} else {
val albumCovers = getAlbumCoversWithout(path)
val cover = AlbumCover(targetFolder, it)
albumCovers.add(cover)
storeCovers(albumCovers)
}
}
}
private fun getAlbumCoversWithout(path: String) = config.parseAlbumCovers().filterNot { it.path == path } as ArrayList
private fun storeCovers(albumCovers: ArrayList<AlbumCover>) {
config.albumCovers = Gson().toJson(albumCovers)
finishActMode()
listener?.refreshItems()
}
private fun getSelectedItems() = selectedKeys.mapNotNull { getItemWithKey(it) } as ArrayList<Directory>
private fun getSelectedPaths() = getSelectedItems().map { it.path } as ArrayList<String>
private fun getFirstSelectedItem() = getItemWithKey(selectedKeys.first())
private fun getFirstSelectedItemPath() = getFirstSelectedItem()?.path
private fun getItemWithKey(key: Int): Directory? = dirs.firstOrNull { it.path.hashCode() == key }
private fun fillLockedFolders() {
lockedFolderPaths.clear()
dirs.map { it.path }.filter { config.isFolderProtected(it) }.forEach {
lockedFolderPaths.add(it)
}
}
fun updateDirs(newDirs: ArrayList<Directory>) {
val directories = newDirs.clone() as ArrayList<Directory>
if (directories.hashCode() != currentDirectoriesHash) {
currentDirectoriesHash = directories.hashCode()
dirs = directories
fillLockedFolders()
notifyDataSetChanged()
finishActMode()
}
}
fun updateAnimateGifs(animateGifs: Boolean) {
this.animateGifs = animateGifs
notifyDataSetChanged()
}
fun updateCropThumbnails(cropThumbnails: Boolean) {
this.cropThumbnails = cropThumbnails
notifyDataSetChanged()
}
private fun setupView(view: View, directory: Directory) {
val isSelected = selectedKeys.contains(directory.path.hashCode())
view.apply {
dir_path?.text = "${directory.path.substringBeforeLast("/")}/"
val thumbnailType = when {
directory.tmb.isVideoFast() -> TYPE_VIDEOS
directory.tmb.isGif() -> TYPE_GIFS
directory.tmb.isRawFast() -> TYPE_RAWS
directory.tmb.isSvg() -> TYPE_SVGS
else -> TYPE_IMAGES
}
dir_check?.beVisibleIf(isSelected)
if (isSelected) {
dir_check.background?.applyColorFilter(primaryColor)
}
if (isListViewType) {
dir_holder.isSelected = isSelected
}
if (scrollHorizontally && !isListViewType && folderStyle == FOLDER_STYLE_ROUNDED_CORNERS) {
(dir_thumbnail.layoutParams as RelativeLayout.LayoutParams).addRule(RelativeLayout.ABOVE, dir_name.id)
val photoCntParams = (photo_cnt.layoutParams as RelativeLayout.LayoutParams)
val nameParams = (dir_name.layoutParams as RelativeLayout.LayoutParams)
nameParams.removeRule(RelativeLayout.BELOW)
if (config.showFolderMediaCount == FOLDER_MEDIA_CNT_LINE) {
nameParams.addRule(RelativeLayout.ABOVE, photo_cnt.id)
nameParams.removeRule(RelativeLayout.ALIGN_PARENT_BOTTOM)
photoCntParams.removeRule(RelativeLayout.BELOW)
photoCntParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM)
} else {
nameParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM)
}
}
if (lockedFolderPaths.contains(directory.path)) {
dir_lock.beVisible()
dir_lock.background = ColorDrawable(config.backgroundColor)
dir_lock.applyColorFilter(config.backgroundColor.getContrastColor())
} else {
dir_lock.beGone()
val roundedCorners = when {
isListViewType -> ROUNDED_CORNERS_SMALL
folderStyle == FOLDER_STYLE_SQUARE -> ROUNDED_CORNERS_NONE
else -> ROUNDED_CORNERS_BIG
}
activity.loadImage(thumbnailType, directory.tmb, dir_thumbnail, scrollHorizontally, animateGifs, cropThumbnails, roundedCorners, directory.getKey())
}
dir_pin.beVisibleIf(pinnedFolders.contains(directory.path))
dir_location.beVisibleIf(directory.location != LOCATION_INTERNAL)
if (dir_location.isVisible()) {
dir_location.setImageResource(if (directory.location == LOCATION_SD) R.drawable.ic_sd_card_vector else R.drawable.ic_usb_vector)
}
photo_cnt.text = directory.subfoldersMediaCount.toString()
photo_cnt.beVisibleIf(showMediaCount == FOLDER_MEDIA_CNT_LINE)
if (limitFolderTitle) {
dir_name.setSingleLine()
dir_name.ellipsize = TextUtils.TruncateAt.MIDDLE
}
var nameCount = directory.name
if (showMediaCount == FOLDER_MEDIA_CNT_BRACKETS) {
nameCount += " (${directory.subfoldersMediaCount})"
}
if (groupDirectSubfolders) {
if (directory.subfoldersCount > 1) {
nameCount += " [${directory.subfoldersCount}]"
}
}
dir_name.text = nameCount
if (isListViewType || folderStyle == FOLDER_STYLE_ROUNDED_CORNERS) {
photo_cnt.setTextColor(textColor)
dir_name.setTextColor(textColor)
dir_location.applyColorFilter(textColor)
}
if (isListViewType) {
dir_path.setTextColor(textColor)
dir_pin.applyColorFilter(textColor)
dir_location.applyColorFilter(textColor)
}
}
}
}

View file

@ -0,0 +1,58 @@
package com.simplemobiletools.gallery.pro.adapters
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.simplemobiletools.gallery.pro.R
import com.simplemobiletools.gallery.pro.models.FilterItem
import kotlinx.android.synthetic.main.editor_filter_item.view.*
import java.util.*
class FiltersAdapter(val context: Context, val filterItems: ArrayList<FilterItem>, val itemClick: (Int) -> Unit) : RecyclerView.Adapter<FiltersAdapter.ViewHolder>() {
private var currentSelection = filterItems.first()
private var strokeBackground = context.resources.getDrawable(R.drawable.stroke_background)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bindView(filterItems[position])
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.editor_filter_item, parent, false)
return ViewHolder(view)
}
override fun getItemCount() = filterItems.size
fun getCurrentFilter() = currentSelection
private fun setCurrentFilter(position: Int) {
val filterItem = filterItems.getOrNull(position) ?: return
if (currentSelection != filterItem) {
currentSelection = filterItem
notifyDataSetChanged()
itemClick.invoke(position)
}
}
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
fun bindView(filterItem: FilterItem): View {
itemView.apply {
editor_filter_item_label.text = filterItem.filter.name
editor_filter_item_thumbnail.setImageBitmap(filterItem.bitmap)
editor_filter_item_thumbnail.background = if (getCurrentFilter() == filterItem) {
strokeBackground
} else {
null
}
setOnClickListener {
setCurrentFilter(adapterPosition)
}
}
return itemView
}
}
}

View file

@ -0,0 +1,89 @@
package com.simplemobiletools.gallery.pro.adapters
import android.view.Menu
import android.view.View
import android.view.ViewGroup
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.adapters.MyRecyclerViewAdapter
import com.simplemobiletools.commons.interfaces.RefreshRecyclerViewListener
import com.simplemobiletools.commons.views.MyRecyclerView
import com.simplemobiletools.gallery.pro.R
import com.simplemobiletools.gallery.pro.extensions.config
import kotlinx.android.synthetic.main.item_manage_folder.view.*
import java.util.*
class ManageFoldersAdapter(activity: BaseSimpleActivity, var folders: ArrayList<String>, val isShowingExcludedFolders: Boolean, val listener: RefreshRecyclerViewListener?,
recyclerView: MyRecyclerView, itemClick: (Any) -> Unit) : MyRecyclerViewAdapter(activity, recyclerView, null, itemClick) {
private val config = activity.config
init {
setupDragListener(true)
}
override fun getActionMenuId() = R.menu.cab_remove_only
override fun prepareActionMode(menu: Menu) {}
override fun actionItemPressed(id: Int) {
when (id) {
R.id.cab_remove -> removeSelection()
}
}
override fun getSelectableItemCount() = folders.size
override fun getIsItemSelectable(position: Int) = true
override fun getItemSelectionKey(position: Int) = folders.getOrNull(position)?.hashCode()
override fun getItemKeyPosition(key: Int) = folders.indexOfFirst { it.hashCode() == key }
override fun onActionModeCreated() {}
override fun onActionModeDestroyed() {}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = createViewHolder(R.layout.item_manage_folder, parent)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val folder = folders[position]
holder.bindView(folder, true, true) { itemView, adapterPosition ->
setupView(itemView, folder)
}
bindViewHolder(holder)
}
override fun getItemCount() = folders.size
private fun getSelectedItems() = folders.filter { selectedKeys.contains(it.hashCode()) } as ArrayList<String>
private fun setupView(view: View, folder: String) {
view.apply {
manage_folder_holder?.isSelected = selectedKeys.contains(folder.hashCode())
manage_folder_title.apply {
text = folder
setTextColor(config.textColor)
}
}
}
private fun removeSelection() {
val removeFolders = ArrayList<String>(selectedKeys.size)
val positions = getSelectedItemPositions()
getSelectedItems().forEach {
removeFolders.add(it)
if (isShowingExcludedFolders) {
config.removeExcludedFolder(it)
} else {
config.removeIncludedFolder(it)
}
}
folders.removeAll(removeFolders)
removeSelectedItems(positions)
if (folders.isEmpty()) {
listener?.refreshItems()
}
}
}

View file

@ -0,0 +1,106 @@
package com.simplemobiletools.gallery.pro.adapters
import android.view.Menu
import android.view.View
import android.view.ViewGroup
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.adapters.MyRecyclerViewAdapter
import com.simplemobiletools.commons.extensions.isPathOnSD
import com.simplemobiletools.commons.interfaces.RefreshRecyclerViewListener
import com.simplemobiletools.commons.views.MyRecyclerView
import com.simplemobiletools.gallery.pro.R
import com.simplemobiletools.gallery.pro.extensions.config
import com.simplemobiletools.gallery.pro.extensions.removeNoMedia
import kotlinx.android.synthetic.main.item_manage_folder.view.*
import java.util.*
class ManageHiddenFoldersAdapter(activity: BaseSimpleActivity, var folders: ArrayList<String>, val listener: RefreshRecyclerViewListener?,
recyclerView: MyRecyclerView, itemClick: (Any) -> Unit) : MyRecyclerViewAdapter(activity, recyclerView, null, itemClick) {
private val config = activity.config
init {
setupDragListener(true)
}
override fun getActionMenuId() = R.menu.cab_hidden_folders
override fun prepareActionMode(menu: Menu) {}
override fun actionItemPressed(id: Int) {
when (id) {
R.id.cab_unhide -> tryUnhideFolders()
}
}
override fun getSelectableItemCount() = folders.size
override fun getIsItemSelectable(position: Int) = true
override fun getItemSelectionKey(position: Int) = folders.getOrNull(position)?.hashCode()
override fun getItemKeyPosition(key: Int) = folders.indexOfFirst { it.hashCode() == key }
override fun onActionModeCreated() {}
override fun onActionModeDestroyed() {}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = createViewHolder(R.layout.item_manage_folder, parent)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val folder = folders[position]
holder.bindView(folder, true, true) { itemView, adapterPosition ->
setupView(itemView, folder)
}
bindViewHolder(holder)
}
override fun getItemCount() = folders.size
private fun getSelectedItems() = folders.filter { selectedKeys.contains(it.hashCode()) } as ArrayList<String>
private fun setupView(view: View, folder: String) {
view.apply {
manage_folder_holder?.isSelected = selectedKeys.contains(folder.hashCode())
manage_folder_title.apply {
text = folder
setTextColor(config.textColor)
}
}
}
private fun tryUnhideFolders() {
val removeFolders = ArrayList<String>(selectedKeys.size)
val sdCardPaths = ArrayList<String>()
getSelectedItems().forEach {
if (activity.isPathOnSD(it)) {
sdCardPaths.add(it)
}
}
if (sdCardPaths.isNotEmpty()) {
activity.handleSAFDialog(sdCardPaths.first()) {
if (it) {
unhideFolders(removeFolders)
}
}
} else {
unhideFolders(removeFolders)
}
}
private fun unhideFolders(removeFolders: ArrayList<String>) {
val position = getSelectedItemPositions()
getSelectedItems().forEach {
removeFolders.add(it)
activity.removeNoMedia(it)
}
folders.removeAll(removeFolders)
removeSelectedItems(position)
if (folders.isEmpty()) {
listener?.refreshItems()
}
}
}

View file

@ -0,0 +1,588 @@
package com.simplemobiletools.gallery.pro.adapters
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.ShortcutInfo
import android.content.pm.ShortcutManager
import android.graphics.drawable.Icon
import android.os.Handler
import android.os.Looper
import android.view.Menu
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import com.bumptech.glide.Glide
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.adapters.MyRecyclerViewAdapter
import com.simplemobiletools.commons.dialogs.PropertiesDialog
import com.simplemobiletools.commons.dialogs.RenameDialog
import com.simplemobiletools.commons.dialogs.RenameItemDialog
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.commons.models.FileDirItem
import com.simplemobiletools.commons.views.FastScroller
import com.simplemobiletools.commons.views.MyRecyclerView
import com.simplemobiletools.gallery.pro.R
import com.simplemobiletools.gallery.pro.activities.ViewPagerActivity
import com.simplemobiletools.gallery.pro.dialogs.DeleteWithRememberDialog
import com.simplemobiletools.gallery.pro.extensions.*
import com.simplemobiletools.gallery.pro.helpers.*
import com.simplemobiletools.gallery.pro.interfaces.MediaOperationsListener
import com.simplemobiletools.gallery.pro.models.Medium
import com.simplemobiletools.gallery.pro.models.ThumbnailItem
import com.simplemobiletools.gallery.pro.models.ThumbnailSection
import kotlinx.android.synthetic.main.photo_video_item_grid.view.*
import kotlinx.android.synthetic.main.thumbnail_section.view.*
import java.util.*
class MediaAdapter(activity: BaseSimpleActivity, var media: ArrayList<ThumbnailItem>, val listener: MediaOperationsListener?, val isAGetIntent: Boolean,
val allowMultiplePicks: Boolean, val path: String, recyclerView: MyRecyclerView, fastScroller: FastScroller? = null, itemClick: (Any) -> Unit) :
MyRecyclerViewAdapter(activity, recyclerView, fastScroller, itemClick) {
private val INSTANT_LOAD_DURATION = 2000L
private val IMAGE_LOAD_DELAY = 100L
private val ITEM_SECTION = 0
private val ITEM_MEDIUM = 1
private val config = activity.config
private val viewType = config.getFolderViewType(if (config.showAll) SHOW_ALL else path)
private val isListViewType = viewType == VIEW_TYPE_LIST
private var visibleItemPaths = ArrayList<String>()
private var rotatedImagePaths = ArrayList<String>()
private var loadImageInstantly = false
private var delayHandler = Handler(Looper.getMainLooper())
private var currentMediaHash = media.hashCode()
private val hasOTGConnected = activity.hasOTGConnected()
private var scrollHorizontally = config.scrollHorizontally
private var animateGifs = config.animateGifs
private var cropThumbnails = config.cropThumbnails
private var displayFilenames = config.displayFileNames
private var showFileTypes = config.showThumbnailFileTypes
init {
setupDragListener(true)
enableInstantLoad()
}
override fun getActionMenuId() = R.menu.cab_media
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val layoutType = if (viewType == ITEM_SECTION) {
R.layout.thumbnail_section
} else {
if (isListViewType) {
R.layout.photo_video_item_list
} else {
R.layout.photo_video_item_grid
}
}
return createViewHolder(layoutType, parent)
}
override fun onBindViewHolder(holder: MyRecyclerViewAdapter.ViewHolder, position: Int) {
val tmbItem = media.getOrNull(position) ?: return
if (tmbItem is Medium) {
visibleItemPaths.add(tmbItem.path)
}
val allowLongPress = (!isAGetIntent || allowMultiplePicks) && tmbItem is Medium
holder.bindView(tmbItem, tmbItem is Medium, allowLongPress) { itemView, adapterPosition ->
if (tmbItem is Medium) {
setupThumbnail(itemView, tmbItem)
} else {
setupSection(itemView, tmbItem as ThumbnailSection)
}
}
bindViewHolder(holder)
}
override fun getItemCount() = media.size
override fun getItemViewType(position: Int): Int {
val tmbItem = media[position]
return if (tmbItem is ThumbnailSection) {
ITEM_SECTION
} else {
ITEM_MEDIUM
}
}
override fun prepareActionMode(menu: Menu) {
val selectedItems = getSelectedItems()
if (selectedItems.isEmpty()) {
return
}
val isOneItemSelected = isOneItemSelected()
val selectedPaths = selectedItems.map { it.path } as ArrayList<String>
val isInRecycleBin = selectedItems.firstOrNull()?.getIsInRecycleBin() == true
menu.apply {
findItem(R.id.cab_rename).isVisible = !isInRecycleBin
findItem(R.id.cab_add_to_favorites).isVisible = !isInRecycleBin
findItem(R.id.cab_fix_date_taken).isVisible = !isInRecycleBin
findItem(R.id.cab_move_to).isVisible = !isInRecycleBin
findItem(R.id.cab_open_with).isVisible = isOneItemSelected
findItem(R.id.cab_confirm_selection).isVisible = isAGetIntent && allowMultiplePicks && selectedKeys.isNotEmpty()
findItem(R.id.cab_restore_recycle_bin_files).isVisible = selectedPaths.all { it.startsWith(activity.recycleBinPath) }
findItem(R.id.cab_create_shortcut).isVisible = isOreoPlus() && isOneItemSelected
checkHideBtnVisibility(this, selectedItems)
checkFavoriteBtnVisibility(this, selectedItems)
}
}
override fun actionItemPressed(id: Int) {
if (selectedKeys.isEmpty()) {
return
}
when (id) {
R.id.cab_confirm_selection -> confirmSelection()
R.id.cab_properties -> showProperties()
R.id.cab_rename -> renameFile()
R.id.cab_edit -> editFile()
R.id.cab_hide -> toggleFileVisibility(true)
R.id.cab_unhide -> toggleFileVisibility(false)
R.id.cab_add_to_favorites -> toggleFavorites(true)
R.id.cab_remove_from_favorites -> toggleFavorites(false)
R.id.cab_restore_recycle_bin_files -> restoreFiles()
R.id.cab_share -> shareMedia()
R.id.cab_rotate_right -> rotateSelection(90)
R.id.cab_rotate_left -> rotateSelection(270)
R.id.cab_rotate_one_eighty -> rotateSelection(180)
R.id.cab_copy_to -> copyMoveTo(true)
R.id.cab_move_to -> moveFilesTo()
R.id.cab_create_shortcut -> createShortcut()
R.id.cab_select_all -> selectAll()
R.id.cab_open_with -> openPath()
R.id.cab_fix_date_taken -> fixDateTaken()
R.id.cab_set_as -> setAs()
R.id.cab_delete -> checkDeleteConfirmation()
}
}
override fun getSelectableItemCount() = media.filter { it is Medium }.size
override fun getIsItemSelectable(position: Int) = !isASectionTitle(position)
override fun getItemSelectionKey(position: Int) = (media.getOrNull(position) as? Medium)?.path?.hashCode()
override fun getItemKeyPosition(key: Int) = media.indexOfFirst { (it as? Medium)?.path?.hashCode() == key }
override fun onActionModeCreated() {}
override fun onActionModeDestroyed() {}
override fun onViewRecycled(holder: ViewHolder) {
super.onViewRecycled(holder)
if (!activity.isDestroyed) {
val itemView = holder.itemView
visibleItemPaths.remove(itemView.medium_name?.tag)
val tmb = itemView.medium_thumbnail
if (tmb != null) {
Glide.with(activity).clear(tmb)
}
}
}
fun isASectionTitle(position: Int) = media.getOrNull(position) is ThumbnailSection
private fun checkHideBtnVisibility(menu: Menu, selectedItems: ArrayList<Medium>) {
val isInRecycleBin = selectedItems.firstOrNull()?.getIsInRecycleBin() == true
menu.findItem(R.id.cab_hide).isVisible = !isInRecycleBin && selectedItems.any { !it.isHidden() }
menu.findItem(R.id.cab_unhide).isVisible = !isInRecycleBin && selectedItems.any { it.isHidden() }
}
private fun checkFavoriteBtnVisibility(menu: Menu, selectedItems: ArrayList<Medium>) {
menu.findItem(R.id.cab_add_to_favorites).isVisible = selectedItems.none { it.getIsInRecycleBin() } && selectedItems.any { !it.isFavorite }
menu.findItem(R.id.cab_remove_from_favorites).isVisible = selectedItems.none { it.getIsInRecycleBin() } && selectedItems.any { it.isFavorite }
}
private fun confirmSelection() {
listener?.selectedPaths(getSelectedPaths())
}
private fun showProperties() {
if (selectedKeys.size <= 1) {
val path = getFirstSelectedItemPath() ?: return
PropertiesDialog(activity, path, config.shouldShowHidden)
} else {
val paths = getSelectedPaths()
PropertiesDialog(activity, paths, config.shouldShowHidden)
}
}
private fun renameFile() {
if (selectedKeys.size == 1) {
val oldPath = getFirstSelectedItemPath() ?: return
RenameItemDialog(activity, oldPath) {
ensureBackgroundThread {
activity.updateDBMediaPath(oldPath, it)
activity.runOnUiThread {
enableInstantLoad()
listener?.refreshItems()
finishActMode()
}
}
}
} else {
RenameDialog(activity, getSelectedPaths(), true) {
enableInstantLoad()
listener?.refreshItems()
finishActMode()
}
}
}
private fun editFile() {
val path = getFirstSelectedItemPath() ?: return
activity.openEditor(path)
}
private fun openPath() {
val path = getFirstSelectedItemPath() ?: return
activity.openPath(path, true)
}
private fun setAs() {
val path = getFirstSelectedItemPath() ?: return
activity.setAs(path)
}
private fun toggleFileVisibility(hide: Boolean) {
ensureBackgroundThread {
getSelectedItems().forEach {
activity.toggleFileVisibility(it.path, hide)
}
activity.runOnUiThread {
listener?.refreshItems()
finishActMode()
}
}
}
private fun toggleFavorites(add: Boolean) {
ensureBackgroundThread {
getSelectedItems().forEach {
it.isFavorite = add
activity.updateFavorite(it.path, add)
}
activity.runOnUiThread {
listener?.refreshItems()
finishActMode()
}
}
}
private fun restoreFiles() {
activity.restoreRecycleBinPaths(getSelectedPaths()) {
listener?.refreshItems()
finishActMode()
}
}
private fun shareMedia() {
if (selectedKeys.size == 1 && selectedKeys.first() != -1) {
activity.shareMediumPath(getSelectedItems().first().path)
} else if (selectedKeys.size > 1) {
activity.shareMediaPaths(getSelectedPaths())
}
}
private fun rotateSelection(degrees: Int) {
activity.toast(R.string.saving)
ensureBackgroundThread {
val paths = getSelectedPaths().filter { it.isImageFast() }
var fileCnt = paths.size
rotatedImagePaths.clear()
paths.forEach {
rotatedImagePaths.add(it)
activity.saveRotatedImageToFile(it, it, degrees, true) {
fileCnt--
if (fileCnt == 0) {
activity.runOnUiThread {
listener?.refreshItems()
finishActMode()
}
}
}
}
}
}
private fun moveFilesTo() {
activity.handleDeletePasswordProtection {
copyMoveTo(false)
}
}
private fun copyMoveTo(isCopyOperation: Boolean) {
val paths = getSelectedPaths()
val recycleBinPath = activity.recycleBinPath
val fileDirItems = paths.asSequence().filter { isCopyOperation || !it.startsWith(recycleBinPath) }.map {
FileDirItem(it, it.getFilenameFromPath())
}.toMutableList() as ArrayList
if (!isCopyOperation && paths.any { it.startsWith(recycleBinPath) }) {
activity.toast(R.string.moving_recycle_bin_items_disabled, Toast.LENGTH_LONG)
}
if (fileDirItems.isEmpty()) {
return
}
activity.tryCopyMoveFilesTo(fileDirItems, isCopyOperation) {
val destinationPath = it
config.tempFolderPath = ""
activity.applicationContext.rescanFolderMedia(destinationPath)
activity.applicationContext.rescanFolderMedia(fileDirItems.first().getParentPath())
val newPaths = fileDirItems.map { "$destinationPath/${it.name}" }.toMutableList() as ArrayList<String>
activity.rescanPaths(newPaths) {
activity.fixDateTaken(newPaths, false)
}
if (!isCopyOperation) {
listener?.refreshItems()
activity.updateFavoritePaths(fileDirItems, destinationPath)
}
}
}
@SuppressLint("NewApi")
private fun createShortcut() {
val manager = activity.getSystemService(ShortcutManager::class.java)
if (manager.isRequestPinShortcutSupported) {
val path = getSelectedPaths().first()
val drawable = resources.getDrawable(R.drawable.shortcut_image).mutate()
activity.getShortcutImage(path, drawable) {
val intent = Intent(activity, ViewPagerActivity::class.java).apply {
putExtra(PATH, path)
putExtra(SHOW_ALL, config.showAll)
putExtra(SHOW_FAVORITES, path == FAVORITES)
putExtra(SHOW_RECYCLE_BIN, path == RECYCLE_BIN)
action = Intent.ACTION_VIEW
flags = flags or Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val shortcut = ShortcutInfo.Builder(activity, path)
.setShortLabel(path.getFilenameFromPath())
.setIcon(Icon.createWithBitmap(drawable.convertToBitmap()))
.setIntent(intent)
.build()
manager.requestPinShortcut(shortcut, null)
}
}
}
private fun fixDateTaken() {
ensureBackgroundThread {
activity.fixDateTaken(getSelectedPaths(), true) {
listener?.refreshItems()
finishActMode()
}
}
}
private fun checkDeleteConfirmation() {
if (config.isDeletePasswordProtectionOn) {
activity.handleDeletePasswordProtection {
deleteFiles()
}
} else if (config.tempSkipDeleteConfirmation || config.skipDeleteConfirmation) {
deleteFiles()
} else {
askConfirmDelete()
}
}
private fun askConfirmDelete() {
val itemsCnt = selectedKeys.size
val firstPath = getSelectedPaths().first()
val items = if (itemsCnt == 1) {
"\"${firstPath.getFilenameFromPath()}\""
} else {
resources.getQuantityString(R.plurals.delete_items, itemsCnt, itemsCnt)
}
val isRecycleBin = firstPath.startsWith(activity.recycleBinPath)
val baseString = if (config.useRecycleBin && !isRecycleBin) R.string.move_to_recycle_bin_confirmation else R.string.deletion_confirmation
val question = String.format(resources.getString(baseString), items)
DeleteWithRememberDialog(activity, question) {
config.tempSkipDeleteConfirmation = it
deleteFiles()
}
}
private fun deleteFiles() {
if (selectedKeys.isEmpty()) {
return
}
val SAFPath = getSelectedPaths().firstOrNull { activity.needsStupidWritePermissions(it) } ?: getFirstSelectedItemPath() ?: return
activity.handleSAFDialog(SAFPath) {
if (!it) {
return@handleSAFDialog
}
val fileDirItems = ArrayList<FileDirItem>(selectedKeys.size)
val removeMedia = ArrayList<Medium>(selectedKeys.size)
val positions = getSelectedItemPositions()
getSelectedItems().forEach {
fileDirItems.add(FileDirItem(it.path, it.name))
removeMedia.add(it)
}
media.removeAll(removeMedia)
listener?.tryDeleteFiles(fileDirItems)
listener?.updateMediaGridDecoration(media)
removeSelectedItems(positions)
currentMediaHash = media.hashCode()
}
}
private fun getSelectedItems() = selectedKeys.mapNotNull { getItemWithKey(it) } as ArrayList<Medium>
private fun getSelectedPaths() = getSelectedItems().map { it.path } as ArrayList<String>
private fun getFirstSelectedItemPath() = getItemWithKey(selectedKeys.first())?.path
private fun getItemWithKey(key: Int): Medium? = media.firstOrNull { (it as? Medium)?.path?.hashCode() == key } as? Medium
fun updateMedia(newMedia: ArrayList<ThumbnailItem>) {
val thumbnailItems = newMedia.clone() as ArrayList<ThumbnailItem>
if (thumbnailItems.hashCode() != currentMediaHash) {
currentMediaHash = thumbnailItems.hashCode()
media = thumbnailItems
enableInstantLoad()
notifyDataSetChanged()
finishActMode()
}
}
fun updateDisplayFilenames(displayFilenames: Boolean) {
this.displayFilenames = displayFilenames
enableInstantLoad()
notifyDataSetChanged()
}
fun updateAnimateGifs(animateGifs: Boolean) {
this.animateGifs = animateGifs
notifyDataSetChanged()
}
fun updateCropThumbnails(cropThumbnails: Boolean) {
this.cropThumbnails = cropThumbnails
notifyDataSetChanged()
}
fun updateShowFileTypes(showFileTypes: Boolean) {
this.showFileTypes = showFileTypes
notifyDataSetChanged()
}
private fun enableInstantLoad() {
loadImageInstantly = true
delayHandler.postDelayed({
loadImageInstantly = false
}, INSTANT_LOAD_DURATION)
}
fun getItemBubbleText(position: Int, sorting: Int, dateFormat: String, timeFormat: String): String {
return (media[position] as? Medium)?.getBubbleText(sorting, activity, dateFormat, timeFormat) ?: ""
}
private fun setupThumbnail(view: View, medium: Medium) {
val isSelected = selectedKeys.contains(medium.path.hashCode())
view.apply {
val padding = if (config.thumbnailSpacing <= 1) {
config.thumbnailSpacing
} else {
0
}
media_item_holder.setPadding(padding, padding, padding, padding)
play_outline.beVisibleIf(medium.isVideo() || medium.isPortrait())
if (medium.isVideo()) {
play_outline.setImageResource(R.drawable.ic_play_outline_vector)
play_outline.beVisible()
} else if (medium.isPortrait()) {
play_outline.setImageResource(R.drawable.ic_portrait_photo_vector)
play_outline.beVisibleIf(showFileTypes)
}
if (showFileTypes && (medium.isGIF() || medium.isRaw() || medium.isSVG())) {
file_type.setText(when (medium.type) {
TYPE_GIFS -> R.string.gif
TYPE_RAWS -> R.string.raw
else -> R.string.svg
})
file_type.beVisible()
} else {
file_type.beGone()
}
medium_name.beVisibleIf(displayFilenames || isListViewType)
medium_name.text = medium.name
medium_name.tag = medium.path
val showVideoDuration = medium.isVideo() && config.showThumbnailVideoDuration
if (showVideoDuration) {
video_duration.text = medium.videoDuration.getFormattedDuration()
}
video_duration.beVisibleIf(showVideoDuration)
medium_check?.beVisibleIf(isSelected)
if (isSelected) {
medium_check?.background?.applyColorFilter(primaryColor)
}
if (isListViewType) {
media_item_holder.isSelected = isSelected
}
var path = medium.path
if (hasOTGConnected && context.isPathOnOTG(path)) {
path = path.getOTGPublicPath(context)
}
val roundedCorners = when {
isListViewType -> ROUNDED_CORNERS_SMALL
config.fileRoundedCorners -> ROUNDED_CORNERS_BIG
else -> ROUNDED_CORNERS_NONE
}
if (loadImageInstantly) {
activity.loadImage(medium.type, path, medium_thumbnail, scrollHorizontally, animateGifs, cropThumbnails, roundedCorners, medium.getKey(),
rotatedImagePaths)
} else {
medium_thumbnail.setImageDrawable(null)
medium_thumbnail.isHorizontalScrolling = scrollHorizontally
delayHandler.postDelayed({
val isVisible = visibleItemPaths.contains(medium.path)
if (isVisible) {
activity.loadImage(medium.type, path, medium_thumbnail, scrollHorizontally, animateGifs, cropThumbnails, roundedCorners,
medium.getKey(), rotatedImagePaths)
}
}, IMAGE_LOAD_DELAY)
}
if (isListViewType) {
medium_name.setTextColor(textColor)
play_outline.applyColorFilter(textColor)
}
}
}
private fun setupSection(view: View, section: ThumbnailSection) {
view.apply {
thumbnail_section.text = section.title
thumbnail_section.setTextColor(textColor)
}
}
}

View file

@ -0,0 +1,67 @@
package com.simplemobiletools.gallery.pro.adapters
import android.os.Bundle
import android.os.Parcelable
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentStatePagerAdapter
import androidx.viewpager.widget.PagerAdapter
import com.simplemobiletools.gallery.pro.activities.ViewPagerActivity
import com.simplemobiletools.gallery.pro.fragments.PhotoFragment
import com.simplemobiletools.gallery.pro.fragments.VideoFragment
import com.simplemobiletools.gallery.pro.fragments.ViewPagerFragment
import com.simplemobiletools.gallery.pro.helpers.MEDIUM
import com.simplemobiletools.gallery.pro.helpers.SHOULD_INIT_FRAGMENT
import com.simplemobiletools.gallery.pro.models.Medium
class MyPagerAdapter(val activity: ViewPagerActivity, fm: FragmentManager, val media: MutableList<Medium>) : FragmentStatePagerAdapter(fm) {
private val fragments = HashMap<Int, ViewPagerFragment>()
var shouldInitFragment = true
override fun getCount() = media.size
override fun getItem(position: Int): Fragment {
val medium = media[position]
val bundle = Bundle()
bundle.putSerializable(MEDIUM, medium)
bundle.putBoolean(SHOULD_INIT_FRAGMENT, shouldInitFragment)
val fragment = if (medium.isVideo()) {
VideoFragment()
} else {
PhotoFragment()
}
fragment.arguments = bundle
fragment.listener = activity
return fragment
}
override fun getItemPosition(item: Any) = PagerAdapter.POSITION_NONE
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val fragment = super.instantiateItem(container, position) as ViewPagerFragment
fragments[position] = fragment
return fragment
}
override fun destroyItem(container: ViewGroup, position: Int, any: Any) {
fragments.remove(position)
super.destroyItem(container, position, any)
}
fun getCurrentFragment(position: Int) = fragments[position]
fun toggleFullscreen(isFullscreen: Boolean) {
for ((pos, fragment) in fragments) {
fragment.fullscreenToggled(isFullscreen)
}
}
// try fixing TransactionTooLargeException crash on Android Nougat, tip from https://stackoverflow.com/a/43193425/1967672
override fun saveState(): Parcelable? {
val bundle = super.saveState() as Bundle?
bundle?.putParcelableArray("states", null)
return bundle
}
}

View file

@ -0,0 +1,88 @@
package com.simplemobiletools.gallery.pro.adapters
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.signature.ObjectKey
import com.simplemobiletools.commons.extensions.getFileKey
import com.simplemobiletools.gallery.pro.R
import kotlinx.android.synthetic.main.portrait_photo_item.view.*
import java.util.*
class PortraitPhotosAdapter(val context: Context, val photos: ArrayList<String>, val sideElementWidth: Int, val itemClick: (Int, Int) -> Unit) :
RecyclerView.Adapter<PortraitPhotosAdapter.ViewHolder>() {
var currentSelectionIndex = -1
var views = HashMap<Int, View>()
private var strokeBackground = context.resources.getDrawable(R.drawable.stroke_background)
private val itemWidth = context.resources.getDimension(R.dimen.portrait_photos_stripe_height).toInt()
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bindView(photos[position], position)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.portrait_photo_item, parent, false)
return ViewHolder(view)
}
override fun getItemCount() = photos.size
fun setCurrentPhoto(position: Int) {
if (currentSelectionIndex != position) {
currentSelectionIndex = position
notifyDataSetChanged()
}
}
fun performClickOn(position: Int) {
views[position]?.performClick()
}
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
fun bindView(photo: String, position: Int): View {
itemView.apply {
portrait_photo_item_thumbnail.layoutParams.width = if (position == 0 || position == photos.size - 1) {
sideElementWidth
} else {
itemWidth
}
portrait_photo_item_thumbnail.background = if (photo.isEmpty() || position != currentSelectionIndex) {
null
} else {
strokeBackground
}
val options = RequestOptions()
.signature(ObjectKey(photo.getFileKey()))
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.centerCrop()
Glide.with(context)
.load(photo)
.transition(DrawableTransitionOptions.withCrossFade())
.apply(options)
.into(portrait_photo_item_thumbnail)
if (photo.isNotEmpty()) {
isClickable = true
views[position] = this
setOnClickListener {
itemClick(position, x.toInt())
setCurrentPhoto(position)
}
} else {
isClickable = false
}
}
return itemView
}
}
}

View file

@ -0,0 +1,64 @@
package com.simplemobiletools.gallery.pro.asynctasks
import android.content.Context
import android.os.AsyncTask
import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.gallery.pro.extensions.config
import com.simplemobiletools.gallery.pro.extensions.getFavoritePaths
import com.simplemobiletools.gallery.pro.helpers.*
import com.simplemobiletools.gallery.pro.models.Medium
import com.simplemobiletools.gallery.pro.models.ThumbnailItem
import java.util.*
class GetMediaAsynctask(val context: Context, val mPath: String, val isPickImage: Boolean = false, val isPickVideo: Boolean = false,
val showAll: Boolean, val callback: (media: ArrayList<ThumbnailItem>) -> Unit) :
AsyncTask<Void, Void, ArrayList<ThumbnailItem>>() {
private val mediaFetcher = MediaFetcher(context)
override fun doInBackground(vararg params: Void): ArrayList<ThumbnailItem> {
val pathToUse = if (showAll) SHOW_ALL else mPath
val folderGrouping = context.config.getFolderGrouping(pathToUse)
val fileSorting = context.config.getFolderSorting(pathToUse)
val getProperDateTaken = fileSorting and SORT_BY_DATE_TAKEN != 0 ||
folderGrouping and GROUP_BY_DATE_TAKEN_DAILY != 0 ||
folderGrouping and GROUP_BY_DATE_TAKEN_MONTHLY != 0
val getProperLastModified = fileSorting and SORT_BY_DATE_MODIFIED != 0 ||
folderGrouping and GROUP_BY_LAST_MODIFIED_DAILY != 0 ||
folderGrouping and GROUP_BY_LAST_MODIFIED_MONTHLY != 0
val getProperFileSize = fileSorting and SORT_BY_SIZE != 0
val favoritePaths = context.getFavoritePaths()
val getVideoDurations = context.config.showThumbnailVideoDuration
val lastModifieds = if (getProperLastModified) mediaFetcher.getLastModifieds() else HashMap()
val dateTakens = if (getProperDateTaken) mediaFetcher.getDateTakens() else HashMap()
val media = if (showAll) {
val foldersToScan = mediaFetcher.getFoldersToScan().filter { it != RECYCLE_BIN && it != FAVORITES && !context.config.isFolderProtected(it) }
val media = ArrayList<Medium>()
foldersToScan.forEach {
val newMedia = mediaFetcher.getFilesFrom(it, isPickImage, isPickVideo, getProperDateTaken, getProperLastModified, getProperFileSize,
favoritePaths, getVideoDurations, lastModifieds, dateTakens)
media.addAll(newMedia)
}
mediaFetcher.sortMedia(media, context.config.getFolderSorting(SHOW_ALL))
media
} else {
mediaFetcher.getFilesFrom(mPath, isPickImage, isPickVideo, getProperDateTaken, getProperLastModified, getProperFileSize, favoritePaths,
getVideoDurations, lastModifieds, dateTakens)
}
return mediaFetcher.groupMedia(media, pathToUse)
}
override fun onPostExecute(media: ArrayList<ThumbnailItem>) {
super.onPostExecute(media)
callback(media)
}
fun stopFetching() {
mediaFetcher.shouldStop = true
cancel(true)
}
}

View file

@ -0,0 +1,85 @@
package com.simplemobiletools.gallery.pro.databases
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import com.simplemobiletools.gallery.pro.interfaces.*
import com.simplemobiletools.gallery.pro.models.*
@Database(entities = [Directory::class, Medium::class, Widget::class, DateTaken::class, Favorite::class], version = 9)
abstract class GalleryDatabase : RoomDatabase() {
abstract fun DirectoryDao(): DirectoryDao
abstract fun MediumDao(): MediumDao
abstract fun WidgetsDao(): WidgetsDao
abstract fun DateTakensDao(): DateTakensDao
abstract fun FavoritesDao(): FavoritesDao
companion object {
private var db: GalleryDatabase? = null
fun getInstance(context: Context): GalleryDatabase {
if (db == null) {
synchronized(GalleryDatabase::class) {
if (db == null) {
db = Room.databaseBuilder(context.applicationContext, GalleryDatabase::class.java, "gallery.db")
.fallbackToDestructiveMigration()
.addMigrations(MIGRATION_4_5)
.addMigrations(MIGRATION_5_6)
.addMigrations(MIGRATION_6_7)
.addMigrations(MIGRATION_7_8)
.addMigrations(MIGRATION_8_9)
.build()
}
}
}
return db!!
}
fun destroyInstance() {
db = null
}
private val MIGRATION_4_5 = object : Migration(4, 5) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE media ADD COLUMN video_duration INTEGER default 0 NOT NULL")
}
}
private val MIGRATION_5_6 = object : Migration(5, 6) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `widgets` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `widget_id` INTEGER NOT NULL, `folder_path` TEXT NOT NULL)")
database.execSQL("CREATE UNIQUE INDEX `index_widgets_widget_id` ON `widgets` (`widget_id`)")
}
}
private val MIGRATION_6_7 = object : Migration(6, 7) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `date_takens` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `full_path` TEXT NOT NULL, `filename` TEXT NOT NULL, `parent_path` TEXT NOT NULL, `date_taken` INTEGER NOT NULL, `last_fixed` INTEGER NOT NULL)")
database.execSQL("CREATE TABLE IF NOT EXISTS `favorites` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `full_path` TEXT NOT NULL, `filename` TEXT NOT NULL, `parent_path` TEXT NOT NULL)")
database.execSQL("CREATE UNIQUE INDEX `index_date_takens_full_path` ON `date_takens` (`full_path`)")
database.execSQL("CREATE UNIQUE INDEX `index_favorites_full_path` ON `favorites` (`full_path`)")
}
}
private val MIGRATION_7_8 = object : Migration(7, 8) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE directories ADD COLUMN sort_value TEXT default '' NOT NULL")
}
}
private val MIGRATION_8_9 = object : Migration(8, 9) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE date_takens ADD COLUMN last_modified INTEGER default 0 NOT NULL")
}
}
}
}

View file

@ -0,0 +1,70 @@
package com.simplemobiletools.gallery.pro.dialogs
import android.content.DialogInterface
import android.view.View
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.dialogs.RadioGroupDialog
import com.simplemobiletools.commons.extensions.setupDialogStuff
import com.simplemobiletools.commons.models.RadioItem
import com.simplemobiletools.gallery.pro.R
import com.simplemobiletools.gallery.pro.extensions.config
import kotlinx.android.synthetic.main.dialog_change_file_thumbnail_style.view.*
class ChangeFileThumbnailStyleDialog(val activity: BaseSimpleActivity) : DialogInterface.OnClickListener {
private var config = activity.config
private var view: View
private var thumbnailSpacing = config.thumbnailSpacing
init {
view = activity.layoutInflater.inflate(R.layout.dialog_change_file_thumbnail_style, null).apply {
dialog_file_style_rounded_corners.isChecked = config.fileRoundedCorners
dialog_file_style_animate_gifs.isChecked = config.animateGifs
dialog_file_style_show_thumbnail_video_duration.isChecked = config.showThumbnailVideoDuration
dialog_file_style_show_thumbnail_file_types.isChecked = config.showThumbnailFileTypes
dialog_file_style_rounded_corners_holder.setOnClickListener { dialog_file_style_rounded_corners.toggle() }
dialog_file_style_animate_gifs_holder.setOnClickListener { dialog_file_style_animate_gifs.toggle() }
dialog_file_style_show_thumbnail_video_duration_holder.setOnClickListener { dialog_file_style_show_thumbnail_video_duration.toggle() }
dialog_file_style_show_thumbnail_file_types_holder.setOnClickListener { dialog_file_style_show_thumbnail_file_types.toggle() }
dialog_file_style_spacing_holder.setOnClickListener {
val items = arrayListOf(
RadioItem(0, "0x"),
RadioItem(1, "1x"),
RadioItem(2, "2x"),
RadioItem(4, "4x"),
RadioItem(8, "8x"),
RadioItem(16, "16x"),
RadioItem(32, "32x"),
RadioItem(64, "64x"))
RadioGroupDialog(activity, items, thumbnailSpacing) {
thumbnailSpacing = it as Int
updateThumbnailSpacingText()
}
}
}
updateThumbnailSpacingText()
AlertDialog.Builder(activity)
.setPositiveButton(R.string.ok, this)
.setNegativeButton(R.string.cancel, null)
.create().apply {
activity.setupDialogStuff(view, this)
}
}
override fun onClick(dialog: DialogInterface, which: Int) {
config.fileRoundedCorners = view.dialog_file_style_rounded_corners.isChecked
config.animateGifs = view.dialog_file_style_animate_gifs.isChecked
config.showThumbnailVideoDuration = view.dialog_file_style_show_thumbnail_video_duration.isChecked
config.showThumbnailFileTypes = view.dialog_file_style_show_thumbnail_file_types.isChecked
config.thumbnailSpacing = thumbnailSpacing
}
private fun updateThumbnailSpacingText() {
view.dialog_file_style_spacing.text = "${thumbnailSpacing}x"
}
}

View file

@ -0,0 +1,134 @@
package com.simplemobiletools.gallery.pro.dialogs
import android.content.DialogInterface
import android.view.View
import android.widget.RelativeLayout
import androidx.appcompat.app.AlertDialog
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.CenterCrop
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.RequestOptions
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.beGone
import com.simplemobiletools.commons.extensions.beVisible
import com.simplemobiletools.commons.extensions.setupDialogStuff
import com.simplemobiletools.gallery.pro.R
import com.simplemobiletools.gallery.pro.extensions.config
import com.simplemobiletools.gallery.pro.helpers.*
import kotlinx.android.synthetic.main.dialog_change_folder_thumbnail_style.view.*
import kotlinx.android.synthetic.main.directory_item_grid_square.view.*
class ChangeFolderThumbnailStyleDialog(val activity: BaseSimpleActivity, val callback: () -> Unit) : DialogInterface.OnClickListener {
private var config = activity.config
private var view: View
init {
view = activity.layoutInflater.inflate(R.layout.dialog_change_folder_thumbnail_style, null).apply {
dialog_folder_limit_title.isChecked = config.limitFolderTitle
}
AlertDialog.Builder(activity)
.setPositiveButton(R.string.ok, this)
.setNegativeButton(R.string.cancel, null)
.create().apply {
activity.setupDialogStuff(view, this) {
setupStyle()
setupMediaCount()
updateSample()
}
}
}
private fun setupStyle() {
val styleRadio = view.dialog_radio_folder_style
styleRadio.setOnCheckedChangeListener { group, checkedId ->
updateSample()
}
val styleBtn = when (config.folderStyle) {
FOLDER_STYLE_SQUARE -> styleRadio.dialog_radio_folder_square
else -> styleRadio.dialog_radio_folder_rounded_corners
}
styleBtn.isChecked = true
}
private fun setupMediaCount() {
val countRadio = view.dialog_radio_folder_count_holder
countRadio.setOnCheckedChangeListener { group, checkedId ->
updateSample()
}
val countBtn = when (config.showFolderMediaCount) {
FOLDER_MEDIA_CNT_LINE -> countRadio.dialog_radio_folder_count_line
FOLDER_MEDIA_CNT_BRACKETS -> countRadio.dialog_radio_folder_count_brackets
else -> countRadio.dialog_radio_folder_count_none
}
countBtn.isChecked = true
}
private fun updateSample() {
val photoCount = 36
val folderName = "Camera"
view.apply {
val useRoundedCornersLayout = dialog_radio_folder_style.checkedRadioButtonId == R.id.dialog_radio_folder_rounded_corners
dialog_folder_sample_holder.removeAllViews()
val layout = if (useRoundedCornersLayout) R.layout.directory_item_grid_rounded_corners else R.layout.directory_item_grid_square
val sampleView = activity.layoutInflater.inflate(layout, null)
dialog_folder_sample_holder.addView(sampleView)
sampleView.layoutParams.width = activity.resources.getDimension(R.dimen.sample_thumbnail_size).toInt()
(sampleView.layoutParams as RelativeLayout.LayoutParams).addRule(RelativeLayout.CENTER_HORIZONTAL)
when (dialog_radio_folder_count_holder.checkedRadioButtonId) {
R.id.dialog_radio_folder_count_line -> {
dir_name.text = folderName
photo_cnt.text = photoCount.toString()
photo_cnt.beVisible()
}
R.id.dialog_radio_folder_count_brackets -> {
photo_cnt.beGone()
dir_name.text = "$folderName ($photoCount)"
}
else -> {
dir_name.text = folderName
photo_cnt?.beGone()
}
}
val options = RequestOptions().centerCrop()
var builder = Glide.with(activity)
.load(R.drawable.sample_logo)
.apply(options)
if (useRoundedCornersLayout) {
val cornerRadius = resources.getDimension(R.dimen.rounded_corner_radius_big).toInt()
builder = builder.transform(CenterCrop(), RoundedCorners(cornerRadius))
dir_name.setTextColor(activity.config.textColor)
photo_cnt.setTextColor(activity.config.textColor)
}
builder.into(dir_thumbnail)
}
}
override fun onClick(dialog: DialogInterface, which: Int) {
val style = when (view.dialog_radio_folder_style.checkedRadioButtonId) {
R.id.dialog_radio_folder_square -> FOLDER_STYLE_SQUARE
else -> FOLDER_STYLE_ROUNDED_CORNERS
}
val count = when (view.dialog_radio_folder_count_holder.checkedRadioButtonId) {
R.id.dialog_radio_folder_count_line -> FOLDER_MEDIA_CNT_LINE
R.id.dialog_radio_folder_count_brackets -> FOLDER_MEDIA_CNT_BRACKETS
else -> FOLDER_MEDIA_CNT_NONE
}
config.folderStyle = style
config.showFolderMediaCount = count
config.limitFolderTitle = view.dialog_folder_limit_title.isChecked
callback()
}
}

View file

@ -0,0 +1,90 @@
package com.simplemobiletools.gallery.pro.dialogs
import android.content.DialogInterface
import android.view.View
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.beVisibleIf
import com.simplemobiletools.commons.extensions.setupDialogStuff
import com.simplemobiletools.gallery.pro.R
import com.simplemobiletools.gallery.pro.extensions.config
import com.simplemobiletools.gallery.pro.helpers.*
import kotlinx.android.synthetic.main.dialog_change_grouping.view.*
class ChangeGroupingDialog(val activity: BaseSimpleActivity, val path: String = "", val callback: () -> Unit) :
DialogInterface.OnClickListener {
private var currGrouping = 0
private var config = activity.config
private val pathToUse = if (path.isEmpty()) SHOW_ALL else path
private var view: View
init {
view = activity.layoutInflater.inflate(R.layout.dialog_change_grouping, null).apply {
grouping_dialog_use_for_this_folder.isChecked = config.hasCustomGrouping(pathToUse)
grouping_dialog_radio_folder.beVisibleIf(path.isEmpty())
}
AlertDialog.Builder(activity)
.setPositiveButton(R.string.ok, this)
.setNegativeButton(R.string.cancel, null)
.create().apply {
activity.setupDialogStuff(view, this, R.string.group_by)
}
currGrouping = config.getFolderGrouping(pathToUse)
setupGroupRadio()
setupOrderRadio()
}
private fun setupGroupRadio() {
val groupingRadio = view.grouping_dialog_radio_grouping
val groupBtn = when {
currGrouping and GROUP_BY_NONE != 0 -> groupingRadio.grouping_dialog_radio_none
currGrouping and GROUP_BY_LAST_MODIFIED_DAILY != 0 -> groupingRadio.grouping_dialog_radio_last_modified_daily
currGrouping and GROUP_BY_LAST_MODIFIED_MONTHLY != 0 -> groupingRadio.grouping_dialog_radio_last_modified_monthly
currGrouping and GROUP_BY_DATE_TAKEN_DAILY != 0 -> groupingRadio.grouping_dialog_radio_date_taken_daily
currGrouping and GROUP_BY_DATE_TAKEN_MONTHLY != 0 -> groupingRadio.grouping_dialog_radio_date_taken_monthly
currGrouping and GROUP_BY_FILE_TYPE != 0 -> groupingRadio.grouping_dialog_radio_file_type
currGrouping and GROUP_BY_EXTENSION != 0 -> groupingRadio.grouping_dialog_radio_extension
else -> groupingRadio.grouping_dialog_radio_folder
}
groupBtn.isChecked = true
}
private fun setupOrderRadio() {
val orderRadio = view.grouping_dialog_radio_order
var orderBtn = orderRadio.grouping_dialog_radio_ascending
if (currGrouping and GROUP_DESCENDING != 0) {
orderBtn = orderRadio.grouping_dialog_radio_descending
}
orderBtn.isChecked = true
}
override fun onClick(dialog: DialogInterface, which: Int) {
val groupingRadio = view.grouping_dialog_radio_grouping
var grouping = when (groupingRadio.checkedRadioButtonId) {
R.id.grouping_dialog_radio_none -> GROUP_BY_NONE
R.id.grouping_dialog_radio_last_modified_daily -> GROUP_BY_LAST_MODIFIED_DAILY
R.id.grouping_dialog_radio_last_modified_monthly -> GROUP_BY_LAST_MODIFIED_MONTHLY
R.id.grouping_dialog_radio_date_taken_daily -> GROUP_BY_DATE_TAKEN_DAILY
R.id.grouping_dialog_radio_date_taken_monthly -> GROUP_BY_DATE_TAKEN_MONTHLY
R.id.grouping_dialog_radio_file_type -> GROUP_BY_FILE_TYPE
R.id.grouping_dialog_radio_extension -> GROUP_BY_EXTENSION
else -> GROUP_BY_FOLDER
}
if (view.grouping_dialog_radio_order.checkedRadioButtonId == R.id.grouping_dialog_radio_descending) {
grouping = grouping or GROUP_DESCENDING
}
if (view.grouping_dialog_use_for_this_folder.isChecked) {
config.saveFolderGrouping(pathToUse, grouping)
} else {
config.removeFolderGrouping(pathToUse)
config.groupBy = grouping
}
callback()
}
}

View file

@ -0,0 +1,108 @@
package com.simplemobiletools.gallery.pro.dialogs
import android.content.DialogInterface
import android.view.View
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.beVisibleIf
import com.simplemobiletools.commons.extensions.isVisible
import com.simplemobiletools.commons.extensions.setupDialogStuff
import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.gallery.pro.R
import com.simplemobiletools.gallery.pro.extensions.config
import com.simplemobiletools.gallery.pro.helpers.SHOW_ALL
import kotlinx.android.synthetic.main.dialog_change_sorting.view.*
class ChangeSortingDialog(val activity: BaseSimpleActivity, val isDirectorySorting: Boolean, val showFolderCheckbox: Boolean,
val path: String = "", val callback: () -> Unit) :
DialogInterface.OnClickListener {
private var currSorting = 0
private var config = activity.config
private var pathToUse = if (!isDirectorySorting && path.isEmpty()) SHOW_ALL else path
private var view: View
init {
currSorting = if (isDirectorySorting) config.directorySorting else config.getFolderSorting(pathToUse)
view = activity.layoutInflater.inflate(R.layout.dialog_change_sorting, null).apply {
use_for_this_folder_divider.beVisibleIf(showFolderCheckbox || (currSorting and SORT_BY_NAME != 0 || currSorting and SORT_BY_PATH != 0))
sorting_dialog_numeric_sorting.beVisibleIf(showFolderCheckbox && (currSorting and SORT_BY_NAME != 0 || currSorting and SORT_BY_PATH != 0))
sorting_dialog_numeric_sorting.isChecked = currSorting and SORT_USE_NUMERIC_VALUE != 0
sorting_dialog_use_for_this_folder.beVisibleIf(showFolderCheckbox)
sorting_dialog_use_for_this_folder.isChecked = config.hasCustomSorting(pathToUse)
sorting_dialog_bottom_note.beVisibleIf(!isDirectorySorting)
}
AlertDialog.Builder(activity)
.setPositiveButton(R.string.ok, this)
.setNegativeButton(R.string.cancel, null)
.create().apply {
activity.setupDialogStuff(view, this, R.string.sort_by)
}
setupSortRadio()
setupOrderRadio()
}
private fun setupSortRadio() {
val sortingRadio = view.sorting_dialog_radio_sorting
sortingRadio.setOnCheckedChangeListener { group, checkedId ->
val isSortingByNameOrPath = checkedId == sortingRadio.sorting_dialog_radio_name.id || checkedId == sortingRadio.sorting_dialog_radio_path.id
view.sorting_dialog_numeric_sorting.beVisibleIf(isSortingByNameOrPath)
view.use_for_this_folder_divider.beVisibleIf(view.sorting_dialog_numeric_sorting.isVisible() || view.sorting_dialog_use_for_this_folder.isVisible())
}
val sortBtn = when {
currSorting and SORT_BY_PATH != 0 -> sortingRadio.sorting_dialog_radio_path
currSorting and SORT_BY_SIZE != 0 -> sortingRadio.sorting_dialog_radio_size
currSorting and SORT_BY_DATE_MODIFIED != 0 -> sortingRadio.sorting_dialog_radio_last_modified
currSorting and SORT_BY_DATE_TAKEN != 0 -> sortingRadio.sorting_dialog_radio_date_taken
currSorting and SORT_BY_RANDOM != 0 -> sortingRadio.sorting_dialog_radio_random
else -> sortingRadio.sorting_dialog_radio_name
}
sortBtn.isChecked = true
}
private fun setupOrderRadio() {
val orderRadio = view.sorting_dialog_radio_order
var orderBtn = orderRadio.sorting_dialog_radio_ascending
if (currSorting and SORT_DESCENDING != 0) {
orderBtn = orderRadio.sorting_dialog_radio_descending
}
orderBtn.isChecked = true
}
override fun onClick(dialog: DialogInterface, which: Int) {
val sortingRadio = view.sorting_dialog_radio_sorting
var sorting = when (sortingRadio.checkedRadioButtonId) {
R.id.sorting_dialog_radio_name -> SORT_BY_NAME
R.id.sorting_dialog_radio_path -> SORT_BY_PATH
R.id.sorting_dialog_radio_size -> SORT_BY_SIZE
R.id.sorting_dialog_radio_last_modified -> SORT_BY_DATE_MODIFIED
R.id.sorting_dialog_radio_random -> SORT_BY_RANDOM
else -> SORT_BY_DATE_TAKEN
}
if (view.sorting_dialog_radio_order.checkedRadioButtonId == R.id.sorting_dialog_radio_descending) {
sorting = sorting or SORT_DESCENDING
}
if (view.sorting_dialog_numeric_sorting.isChecked) {
sorting = sorting or SORT_USE_NUMERIC_VALUE
}
if (isDirectorySorting) {
config.directorySorting = sorting
} else {
if (view.sorting_dialog_use_for_this_folder.isChecked) {
config.saveCustomSorting(pathToUse, sorting)
} else {
config.removeCustomSorting(pathToUse)
config.sorting = sorting
}
}
callback()
}
}

View file

@ -0,0 +1,73 @@
package com.simplemobiletools.gallery.pro.dialogs
import android.view.View
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.beVisibleIf
import com.simplemobiletools.commons.extensions.setupDialogStuff
import com.simplemobiletools.commons.helpers.VIEW_TYPE_GRID
import com.simplemobiletools.commons.helpers.VIEW_TYPE_LIST
import com.simplemobiletools.gallery.pro.R
import com.simplemobiletools.gallery.pro.extensions.config
import com.simplemobiletools.gallery.pro.helpers.SHOW_ALL
import kotlinx.android.synthetic.main.dialog_change_view_type.view.*
class ChangeViewTypeDialog(val activity: BaseSimpleActivity, val fromFoldersView: Boolean, val path: String = "", val callback: () -> Unit) {
private var view: View
private var config = activity.config
private var pathToUse = if (path.isEmpty()) SHOW_ALL else path
init {
view = activity.layoutInflater.inflate(R.layout.dialog_change_view_type, null).apply {
val viewToCheck = if (fromFoldersView) {
if (config.viewTypeFolders == VIEW_TYPE_GRID) {
change_view_type_dialog_radio_grid.id
} else {
change_view_type_dialog_radio_list.id
}
} else {
val currViewType = config.getFolderViewType(pathToUse)
if (currViewType == VIEW_TYPE_GRID) {
change_view_type_dialog_radio_grid.id
} else {
change_view_type_dialog_radio_list.id
}
}
change_view_type_dialog_radio.check(viewToCheck)
change_view_type_dialog_group_direct_subfolders.apply {
beVisibleIf(fromFoldersView)
isChecked = config.groupDirectSubfolders
}
change_view_type_dialog_use_for_this_folder.apply {
beVisibleIf(!fromFoldersView)
isChecked = config.hasCustomViewType(pathToUse)
}
}
AlertDialog.Builder(activity)
.setPositiveButton(R.string.ok) { dialog, which -> dialogConfirmed() }
.setNegativeButton(R.string.cancel, null)
.create().apply {
activity.setupDialogStuff(view, this)
}
}
private fun dialogConfirmed() {
val viewType = if (view.change_view_type_dialog_radio.checkedRadioButtonId == view.change_view_type_dialog_radio_grid.id) VIEW_TYPE_GRID else VIEW_TYPE_LIST
if (fromFoldersView) {
config.viewTypeFolders = viewType
config.groupDirectSubfolders = view.change_view_type_dialog_group_direct_subfolders.isChecked
} else {
if (view.change_view_type_dialog_use_for_this_folder.isChecked) {
config.saveFolderViewType(pathToUse, viewType)
} else {
config.removeFolderViewType(pathToUse)
config.viewTypeFiles = viewType
}
}
callback()
}
}

View file

@ -0,0 +1,31 @@
package com.simplemobiletools.gallery.pro.dialogs
import android.app.Activity
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.extensions.setupDialogStuff
import com.simplemobiletools.gallery.pro.R
import kotlinx.android.synthetic.main.dialog_confirm_delete_folder.view.*
class ConfirmDeleteFolderDialog(activity: Activity, message: String, warningMessage: String, val callback: () -> Unit) {
var dialog: AlertDialog
init {
val view = activity.layoutInflater.inflate(R.layout.dialog_confirm_delete_folder, null)
view.message.text = message
view.message_warning.text = warningMessage
val builder = AlertDialog.Builder(activity)
.setPositiveButton(R.string.yes) { dialog, which -> dialogConfirmed() }
builder.setNegativeButton(R.string.no, null)
dialog = builder.create().apply {
activity.setupDialogStuff(view, this)
}
}
private fun dialogConfirmed() {
dialog.dismiss()
callback()
}
}

View file

@ -0,0 +1,39 @@
package com.simplemobiletools.gallery.pro.dialogs
import android.widget.EditText
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.setupDialogStuff
import com.simplemobiletools.commons.extensions.showKeyboard
import com.simplemobiletools.commons.extensions.value
import com.simplemobiletools.gallery.pro.R
import kotlinx.android.synthetic.main.dialog_custom_aspect_ratio.view.*
class CustomAspectRatioDialog(val activity: BaseSimpleActivity, val defaultCustomAspectRatio: Pair<Float, Float>?, val callback: (aspectRatio: Pair<Float, Float>) -> Unit) {
init {
val view = activity.layoutInflater.inflate(R.layout.dialog_custom_aspect_ratio, null).apply {
aspect_ratio_width.setText(defaultCustomAspectRatio?.first?.toInt()?.toString() ?: "")
aspect_ratio_height.setText(defaultCustomAspectRatio?.second?.toInt()?.toString() ?: "")
}
AlertDialog.Builder(activity)
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.cancel, null)
.create().apply {
activity.setupDialogStuff(view, this) {
showKeyboard(view.aspect_ratio_width)
getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
val width = getViewValue(view.aspect_ratio_width)
val height = getViewValue(view.aspect_ratio_height)
callback(Pair(width, height))
dismiss()
}
}
}
}
private fun getViewValue(view: EditText): Float {
val textValue = view.value
return if (textValue.isEmpty()) 0f else textValue.toFloat()
}
}

View file

@ -0,0 +1,28 @@
package com.simplemobiletools.gallery.pro.dialogs
import android.app.Activity
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.extensions.setupDialogStuff
import com.simplemobiletools.gallery.pro.R
import kotlinx.android.synthetic.main.dialog_delete_with_remember.view.*
class DeleteWithRememberDialog(val activity: Activity, val message: String, val callback: (remember: Boolean) -> Unit) {
private var dialog: AlertDialog
val view = activity.layoutInflater.inflate(R.layout.dialog_delete_with_remember, null)!!
init {
view.delete_remember_title.text = message
val builder = AlertDialog.Builder(activity)
.setPositiveButton(R.string.yes) { dialog, which -> dialogConfirmed() }
.setNegativeButton(R.string.no, null)
dialog = builder.create().apply {
activity.setupDialogStuff(view, this)
}
}
private fun dialogConfirmed() {
dialog.dismiss()
callback(view.delete_remember_checkbox.isChecked)
}
}

View file

@ -1,25 +1,23 @@
package com.simplemobiletools.gallery.dialogs
package com.simplemobiletools.gallery.pro.dialogs
import android.support.v7.app.AlertDialog
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.RadioButton
import android.widget.RadioGroup
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.beVisibleIf
import com.simplemobiletools.commons.extensions.getBasePath
import com.simplemobiletools.commons.extensions.setupDialogStuff
import com.simplemobiletools.gallery.R
import com.simplemobiletools.gallery.activities.SimpleActivity
import com.simplemobiletools.gallery.extensions.config
import com.simplemobiletools.gallery.pro.R
import com.simplemobiletools.gallery.pro.extensions.config
import kotlinx.android.synthetic.main.dialog_exclude_folder.view.*
class ExcludeFolderDialog(val activity: SimpleActivity, val selectedPaths: List<String>, val callback: () -> Unit) {
var dialog: AlertDialog? = null
class ExcludeFolderDialog(val activity: BaseSimpleActivity, val selectedPaths: List<String>, val callback: () -> Unit) {
val alternativePaths = getAlternativePathsList()
var radioGroup: RadioGroup? = null
init {
val view = LayoutInflater.from(activity).inflate(R.layout.dialog_exclude_folder, null).apply {
val view = activity.layoutInflater.inflate(R.layout.dialog_exclude_folder, null).apply {
exclude_folder_parent.beVisibleIf(alternativePaths.size > 1)
radioGroup = exclude_folder_radio_group
@ -36,7 +34,7 @@ class ExcludeFolderDialog(val activity: SimpleActivity, val selectedPaths: List<
}
AlertDialog.Builder(activity)
.setPositiveButton(R.string.ok, { dialog, which -> dialogConfirmed() })
.setPositiveButton(R.string.ok) { dialog, which -> dialogConfirmed() }
.setNegativeButton(R.string.cancel, null)
.create().apply {
activity.setupDialogStuff(view, this)
@ -46,7 +44,7 @@ class ExcludeFolderDialog(val activity: SimpleActivity, val selectedPaths: List<
private fun dialogConfirmed() {
val path = if (alternativePaths.isEmpty()) selectedPaths[0] else alternativePaths[radioGroup!!.checkedRadioButtonId]
activity.config.addExcludedFolder(path)
callback.invoke()
callback()
}
private fun getAlternativePathsList(): List<String> {

View file

@ -0,0 +1,55 @@
package com.simplemobiletools.gallery.pro.dialogs
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.setupDialogStuff
import com.simplemobiletools.gallery.pro.R
import com.simplemobiletools.gallery.pro.extensions.config
import com.simplemobiletools.gallery.pro.helpers.*
import kotlinx.android.synthetic.main.dialog_filter_media.view.*
class FilterMediaDialog(val activity: BaseSimpleActivity, val callback: (result: Int) -> Unit) {
private var view = activity.layoutInflater.inflate(R.layout.dialog_filter_media, null)
init {
val filterMedia = activity.config.filterMedia
view.apply {
filter_media_images.isChecked = filterMedia and TYPE_IMAGES != 0
filter_media_videos.isChecked = filterMedia and TYPE_VIDEOS != 0
filter_media_gifs.isChecked = filterMedia and TYPE_GIFS != 0
filter_media_raws.isChecked = filterMedia and TYPE_RAWS != 0
filter_media_svgs.isChecked = filterMedia and TYPE_SVGS != 0
filter_media_portraits.isChecked = filterMedia and TYPE_PORTRAITS != 0
}
AlertDialog.Builder(activity)
.setPositiveButton(R.string.ok) { dialog, which -> dialogConfirmed() }
.setNegativeButton(R.string.cancel, null)
.create().apply {
activity.setupDialogStuff(view, this, R.string.filter_media)
}
}
private fun dialogConfirmed() {
var result = 0
if (view.filter_media_images.isChecked)
result += TYPE_IMAGES
if (view.filter_media_videos.isChecked)
result += TYPE_VIDEOS
if (view.filter_media_gifs.isChecked)
result += TYPE_GIFS
if (view.filter_media_raws.isChecked)
result += TYPE_RAWS
if (view.filter_media_svgs.isChecked)
result += TYPE_SVGS
if (view.filter_media_portraits.isChecked)
result += TYPE_PORTRAITS
if (result == 0) {
result = getDefaultFileFilter()
}
activity.config.filterMedia = result
callback(result)
}
}

View file

@ -0,0 +1,80 @@
package com.simplemobiletools.gallery.pro.dialogs
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.setupDialogStuff
import com.simplemobiletools.gallery.pro.R
import com.simplemobiletools.gallery.pro.extensions.config
import com.simplemobiletools.gallery.pro.helpers.*
import kotlinx.android.synthetic.main.dialog_manage_bottom_actions.view.*
class ManageBottomActionsDialog(val activity: BaseSimpleActivity, val callback: (result: Int) -> Unit) {
private var view = activity.layoutInflater.inflate(R.layout.dialog_manage_bottom_actions, null)
init {
val actions = activity.config.visibleBottomActions
view.apply {
manage_bottom_actions_toggle_favorite.isChecked = actions and BOTTOM_ACTION_TOGGLE_FAVORITE != 0
manage_bottom_actions_edit.isChecked = actions and BOTTOM_ACTION_EDIT != 0
manage_bottom_actions_share.isChecked = actions and BOTTOM_ACTION_SHARE != 0
manage_bottom_actions_delete.isChecked = actions and BOTTOM_ACTION_DELETE != 0
manage_bottom_actions_rotate.isChecked = actions and BOTTOM_ACTION_ROTATE != 0
manage_bottom_actions_properties.isChecked = actions and BOTTOM_ACTION_PROPERTIES != 0
manage_bottom_actions_change_orientation.isChecked = actions and BOTTOM_ACTION_CHANGE_ORIENTATION != 0
manage_bottom_actions_slideshow.isChecked = actions and BOTTOM_ACTION_SLIDESHOW != 0
manage_bottom_actions_show_on_map.isChecked = actions and BOTTOM_ACTION_SHOW_ON_MAP != 0
manage_bottom_actions_toggle_visibility.isChecked = actions and BOTTOM_ACTION_TOGGLE_VISIBILITY != 0
manage_bottom_actions_rename.isChecked = actions and BOTTOM_ACTION_RENAME != 0
manage_bottom_actions_set_as.isChecked = actions and BOTTOM_ACTION_SET_AS != 0
manage_bottom_actions_copy.isChecked = actions and BOTTOM_ACTION_COPY != 0
manage_bottom_actions_move.isChecked = actions and BOTTOM_ACTION_MOVE != 0
manage_bottom_actions_resize.isChecked = actions and BOTTOM_ACTION_RESIZE != 0
}
AlertDialog.Builder(activity)
.setPositiveButton(R.string.ok) { dialog, which -> dialogConfirmed() }
.setNegativeButton(R.string.cancel, null)
.create().apply {
activity.setupDialogStuff(view, this)
}
}
private fun dialogConfirmed() {
var result = 0
view.apply {
if (manage_bottom_actions_toggle_favorite.isChecked)
result += BOTTOM_ACTION_TOGGLE_FAVORITE
if (manage_bottom_actions_edit.isChecked)
result += BOTTOM_ACTION_EDIT
if (manage_bottom_actions_share.isChecked)
result += BOTTOM_ACTION_SHARE
if (manage_bottom_actions_delete.isChecked)
result += BOTTOM_ACTION_DELETE
if (manage_bottom_actions_rotate.isChecked)
result += BOTTOM_ACTION_ROTATE
if (manage_bottom_actions_properties.isChecked)
result += BOTTOM_ACTION_PROPERTIES
if (manage_bottom_actions_change_orientation.isChecked)
result += BOTTOM_ACTION_CHANGE_ORIENTATION
if (manage_bottom_actions_slideshow.isChecked)
result += BOTTOM_ACTION_SLIDESHOW
if (manage_bottom_actions_show_on_map.isChecked)
result += BOTTOM_ACTION_SHOW_ON_MAP
if (manage_bottom_actions_toggle_visibility.isChecked)
result += BOTTOM_ACTION_TOGGLE_VISIBILITY
if (manage_bottom_actions_rename.isChecked)
result += BOTTOM_ACTION_RENAME
if (manage_bottom_actions_set_as.isChecked)
result += BOTTOM_ACTION_SET_AS
if (manage_bottom_actions_copy.isChecked)
result += BOTTOM_ACTION_COPY
if (manage_bottom_actions_move.isChecked)
result += BOTTOM_ACTION_MOVE
if (manage_bottom_actions_resize.isChecked)
result += BOTTOM_ACTION_RESIZE
}
activity.config.visibleBottomActions = result
callback(result)
}
}

View file

@ -0,0 +1,62 @@
package com.simplemobiletools.gallery.pro.dialogs
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.setupDialogStuff
import com.simplemobiletools.gallery.pro.R
import com.simplemobiletools.gallery.pro.extensions.config
import com.simplemobiletools.gallery.pro.helpers.*
import kotlinx.android.synthetic.main.dialog_manage_extended_details.view.*
class ManageExtendedDetailsDialog(val activity: BaseSimpleActivity, val callback: (result: Int) -> Unit) {
private var view = activity.layoutInflater.inflate(R.layout.dialog_manage_extended_details, null)
init {
val details = activity.config.extendedDetails
view.apply {
manage_extended_details_name.isChecked = details and EXT_NAME != 0
manage_extended_details_path.isChecked = details and EXT_PATH != 0
manage_extended_details_size.isChecked = details and EXT_SIZE != 0
manage_extended_details_resolution.isChecked = details and EXT_RESOLUTION != 0
manage_extended_details_last_modified.isChecked = details and EXT_LAST_MODIFIED != 0
manage_extended_details_date_taken.isChecked = details and EXT_DATE_TAKEN != 0
manage_extended_details_camera.isChecked = details and EXT_CAMERA_MODEL != 0
manage_extended_details_exif.isChecked = details and EXT_EXIF_PROPERTIES != 0
manage_extended_details_gps_coordinates.isChecked = details and EXT_GPS != 0
}
AlertDialog.Builder(activity)
.setPositiveButton(R.string.ok) { dialog, which -> dialogConfirmed() }
.setNegativeButton(R.string.cancel, null)
.create().apply {
activity.setupDialogStuff(view, this)
}
}
private fun dialogConfirmed() {
var result = 0
view.apply {
if (manage_extended_details_name.isChecked)
result += EXT_NAME
if (manage_extended_details_path.isChecked)
result += EXT_PATH
if (manage_extended_details_size.isChecked)
result += EXT_SIZE
if (manage_extended_details_resolution.isChecked)
result += EXT_RESOLUTION
if (manage_extended_details_last_modified.isChecked)
result += EXT_LAST_MODIFIED
if (manage_extended_details_date_taken.isChecked)
result += EXT_DATE_TAKEN
if (manage_extended_details_camera.isChecked)
result += EXT_CAMERA_MODEL
if (manage_extended_details_exif.isChecked)
result += EXT_EXIF_PROPERTIES
if (manage_extended_details_gps_coordinates.isChecked)
result += EXT_GPS
}
activity.config.extendedDetails = result
callback(result)
}
}

View file

@ -0,0 +1,74 @@
package com.simplemobiletools.gallery.pro.dialogs
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.setupDialogStuff
import com.simplemobiletools.gallery.pro.R
import kotlinx.android.synthetic.main.dialog_other_aspect_ratio.view.*
class OtherAspectRatioDialog(val activity: BaseSimpleActivity, val lastOtherAspectRatio: Pair<Float, Float>?, val callback: (aspectRatio: Pair<Float, Float>) -> Unit) {
private val dialog: AlertDialog
init {
val view = activity.layoutInflater.inflate(R.layout.dialog_other_aspect_ratio, null).apply {
other_aspect_ratio_2_1.setOnClickListener { ratioPicked(Pair(2f, 1f)) }
other_aspect_ratio_3_2.setOnClickListener { ratioPicked(Pair(3f, 2f)) }
other_aspect_ratio_4_3.setOnClickListener { ratioPicked(Pair(4f, 3f)) }
other_aspect_ratio_5_3.setOnClickListener { ratioPicked(Pair(5f, 3f)) }
other_aspect_ratio_16_9.setOnClickListener { ratioPicked(Pair(16f, 9f)) }
other_aspect_ratio_19_9.setOnClickListener { ratioPicked(Pair(19f, 9f)) }
other_aspect_ratio_custom.setOnClickListener { customRatioPicked() }
other_aspect_ratio_1_2.setOnClickListener { ratioPicked(Pair(1f, 2f)) }
other_aspect_ratio_2_3.setOnClickListener { ratioPicked(Pair(2f, 3f)) }
other_aspect_ratio_3_4.setOnClickListener { ratioPicked(Pair(3f, 4f)) }
other_aspect_ratio_3_5.setOnClickListener { ratioPicked(Pair(3f, 5f)) }
other_aspect_ratio_9_16.setOnClickListener { ratioPicked(Pair(9f, 16f)) }
other_aspect_ratio_9_19.setOnClickListener { ratioPicked(Pair(9f, 19f)) }
val radio1SelectedItemId = when (lastOtherAspectRatio) {
Pair(2f, 1f) -> other_aspect_ratio_2_1.id
Pair(3f, 2f) -> other_aspect_ratio_3_2.id
Pair(4f, 3f) -> other_aspect_ratio_4_3.id
Pair(5f, 3f) -> other_aspect_ratio_5_3.id
Pair(16f, 9f) -> other_aspect_ratio_16_9.id
Pair(19f, 9f) -> other_aspect_ratio_19_9.id
else -> 0
}
other_aspect_ratio_dialog_radio_1.check(radio1SelectedItemId)
val radio2SelectedItemId = when (lastOtherAspectRatio) {
Pair(1f, 2f) -> other_aspect_ratio_1_2.id
Pair(2f, 3f) -> other_aspect_ratio_2_3.id
Pair(3f, 4f) -> other_aspect_ratio_3_4.id
Pair(3f, 5f) -> other_aspect_ratio_3_5.id
Pair(9f, 16f) -> other_aspect_ratio_9_16.id
Pair(9f, 19f) -> other_aspect_ratio_9_19.id
else -> 0
}
other_aspect_ratio_dialog_radio_2.check(radio2SelectedItemId)
if (radio1SelectedItemId == 0 && radio2SelectedItemId == 0) {
other_aspect_ratio_dialog_radio_1.check(other_aspect_ratio_custom.id)
}
}
dialog = AlertDialog.Builder(activity)
.setNegativeButton(R.string.cancel, null)
.create().apply {
activity.setupDialogStuff(view, this)
}
}
private fun customRatioPicked() {
CustomAspectRatioDialog(activity, lastOtherAspectRatio) {
callback(it)
dialog.dismiss()
}
}
private fun ratioPicked(pair: Pair<Float, Float>) {
callback(pair)
dialog.dismiss()
}
}

View file

@ -0,0 +1,161 @@
package com.simplemobiletools.gallery.pro.dialogs
import android.view.KeyEvent
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.RecyclerView
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.dialogs.FilePickerDialog
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.VIEW_TYPE_GRID
import com.simplemobiletools.commons.views.MyGridLayoutManager
import com.simplemobiletools.gallery.pro.R
import com.simplemobiletools.gallery.pro.adapters.DirectoryAdapter
import com.simplemobiletools.gallery.pro.extensions.*
import com.simplemobiletools.gallery.pro.models.Directory
import kotlinx.android.synthetic.main.dialog_directory_picker.view.*
class PickDirectoryDialog(val activity: BaseSimpleActivity, val sourcePath: String, showOtherFolderButton: Boolean, val showFavoritesBin: Boolean,
val callback: (path: String) -> Unit) {
private var dialog: AlertDialog
private var shownDirectories = ArrayList<Directory>()
private var allDirectories = ArrayList<Directory>()
private var openedSubfolders = arrayListOf("")
private var view = activity.layoutInflater.inflate(R.layout.dialog_directory_picker, null)
private var isGridViewType = activity.config.viewTypeFolders == VIEW_TYPE_GRID
private var showHidden = activity.config.shouldShowHidden
private var currentPathPrefix = ""
init {
(view.directories_grid.layoutManager as MyGridLayoutManager).apply {
orientation = if (activity.config.scrollHorizontally && isGridViewType) RecyclerView.HORIZONTAL else RecyclerView.VERTICAL
spanCount = if (isGridViewType) activity.config.dirColumnCnt else 1
}
val builder = AlertDialog.Builder(activity)
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.cancel, null)
.setOnKeyListener { dialogInterface, i, keyEvent ->
if (keyEvent.action == KeyEvent.ACTION_UP && i == KeyEvent.KEYCODE_BACK) {
backPressed()
}
true
}
if (showOtherFolderButton) {
builder.setNeutralButton(R.string.other_folder) { dialogInterface, i -> showOtherFolder() }
}
dialog = builder.create().apply {
activity.setupDialogStuff(view, this, R.string.select_destination) {
view.directories_show_hidden.beVisibleIf(!context.config.shouldShowHidden)
view.directories_show_hidden.setOnClickListener {
activity.handleHiddenFolderPasswordProtection {
view.directories_show_hidden.beGone()
showHidden = true
fetchDirectories(true)
}
}
}
}
fetchDirectories(false)
}
private fun fetchDirectories(forceShowHidden: Boolean) {
activity.getCachedDirectories(forceShowHidden = forceShowHidden) {
if (it.isNotEmpty()) {
it.forEach {
it.subfoldersMediaCount = it.mediaCnt
}
activity.runOnUiThread {
gotDirectories(activity.addTempFolderIfNeeded(it))
}
}
}
}
private fun showOtherFolder() {
FilePickerDialog(activity, sourcePath, false, showHidden, true, true) {
activity.handleLockedFolderOpening(it) { success ->
if (success) {
callback(it)
}
}
}
}
private fun gotDirectories(newDirs: ArrayList<Directory>) {
if (allDirectories.isEmpty()) {
allDirectories = newDirs.clone() as ArrayList<Directory>
}
val distinctDirs = newDirs.filter { showFavoritesBin || (!it.isRecycleBin() && !it.areFavorites()) }.distinctBy { it.path.getDistinctPath() }.toMutableList() as ArrayList<Directory>
val sortedDirs = activity.getSortedDirectories(distinctDirs)
val dirs = activity.getDirsToShow(sortedDirs, allDirectories, currentPathPrefix).clone() as ArrayList<Directory>
if (dirs.hashCode() == shownDirectories.hashCode()) {
return
}
shownDirectories = dirs
val adapter = DirectoryAdapter(activity, dirs.clone() as ArrayList<Directory>, null, view.directories_grid, true) {
val clickedDir = it as Directory
val path = clickedDir.path
if (clickedDir.subfoldersCount == 1 || !activity.config.groupDirectSubfolders) {
if (path.trimEnd('/') == sourcePath) {
activity.toast(R.string.source_and_destination_same)
return@DirectoryAdapter
} else {
activity.handleLockedFolderOpening(path) { success ->
if (success) {
callback(path)
}
}
dialog.dismiss()
}
} else {
currentPathPrefix = path
openedSubfolders.add(path)
gotDirectories(allDirectories)
}
}
val scrollHorizontally = activity.config.scrollHorizontally && isGridViewType
val sorting = activity.config.directorySorting
val dateFormat = activity.config.dateFormat
val timeFormat = activity.getTimeFormat()
view.apply {
directories_grid.adapter = adapter
directories_vertical_fastscroller.isHorizontal = false
directories_vertical_fastscroller.beGoneIf(scrollHorizontally)
directories_horizontal_fastscroller.isHorizontal = true
directories_horizontal_fastscroller.beVisibleIf(scrollHorizontally)
if (scrollHorizontally) {
directories_horizontal_fastscroller.setViews(directories_grid) {
directories_horizontal_fastscroller.updateBubbleText(dirs[it].getBubbleText(sorting, activity, dateFormat, timeFormat))
}
} else {
directories_vertical_fastscroller.setViews(directories_grid) {
directories_vertical_fastscroller.updateBubbleText(dirs[it].getBubbleText(sorting, activity, dateFormat, timeFormat))
}
}
}
}
private fun backPressed() {
if (activity.config.groupDirectSubfolders) {
if (currentPathPrefix.isEmpty()) {
dialog.dismiss()
} else {
openedSubfolders.removeAt(openedSubfolders.size - 1)
currentPathPrefix = openedSubfolders.last()
gotDirectories(allDirectories)
}
} else {
dialog.dismiss()
}
}
}

View file

@ -0,0 +1,102 @@
package com.simplemobiletools.gallery.pro.dialogs
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.RecyclerView
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.beGoneIf
import com.simplemobiletools.commons.extensions.beVisibleIf
import com.simplemobiletools.commons.extensions.getTimeFormat
import com.simplemobiletools.commons.extensions.setupDialogStuff
import com.simplemobiletools.commons.helpers.VIEW_TYPE_GRID
import com.simplemobiletools.commons.views.MyGridLayoutManager
import com.simplemobiletools.gallery.pro.R
import com.simplemobiletools.gallery.pro.adapters.MediaAdapter
import com.simplemobiletools.gallery.pro.asynctasks.GetMediaAsynctask
import com.simplemobiletools.gallery.pro.extensions.config
import com.simplemobiletools.gallery.pro.extensions.getCachedMedia
import com.simplemobiletools.gallery.pro.helpers.SHOW_ALL
import com.simplemobiletools.gallery.pro.models.Medium
import com.simplemobiletools.gallery.pro.models.ThumbnailItem
import kotlinx.android.synthetic.main.dialog_medium_picker.view.*
class PickMediumDialog(val activity: BaseSimpleActivity, val path: String, val callback: (path: String) -> Unit) {
var dialog: AlertDialog
var shownMedia = ArrayList<ThumbnailItem>()
val view = activity.layoutInflater.inflate(R.layout.dialog_medium_picker, null)
val viewType = activity.config.getFolderViewType(if (activity.config.showAll) SHOW_ALL else path)
var isGridViewType = viewType == VIEW_TYPE_GRID
init {
(view.media_grid.layoutManager as MyGridLayoutManager).apply {
orientation = if (activity.config.scrollHorizontally && isGridViewType) RecyclerView.HORIZONTAL else RecyclerView.VERTICAL
spanCount = if (isGridViewType) activity.config.mediaColumnCnt else 1
}
dialog = AlertDialog.Builder(activity)
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.cancel, null)
.setNeutralButton(R.string.other_folder) { dialogInterface, i -> showOtherFolder() }
.create().apply {
activity.setupDialogStuff(view, this, R.string.select_photo)
}
activity.getCachedMedia(path) {
val media = it.filter { it is Medium } as ArrayList
if (media.isNotEmpty()) {
activity.runOnUiThread {
gotMedia(media)
}
}
}
GetMediaAsynctask(activity, path, false, false, false) {
gotMedia(it)
}.execute()
}
private fun showOtherFolder() {
PickDirectoryDialog(activity, path, true, true) {
callback(it)
dialog.dismiss()
}
}
private fun gotMedia(media: ArrayList<ThumbnailItem>) {
if (media.hashCode() == shownMedia.hashCode())
return
shownMedia = media
val adapter = MediaAdapter(activity, shownMedia.clone() as ArrayList<ThumbnailItem>, null, true, false, path, view.media_grid, null) {
if (it is Medium) {
callback(it.path)
dialog.dismiss()
}
}
val scrollHorizontally = activity.config.scrollHorizontally && isGridViewType
val sorting = activity.config.getFolderSorting(if (path.isEmpty()) SHOW_ALL else path)
val dateFormat = activity.config.dateFormat
val timeFormat = activity.getTimeFormat()
view.apply {
media_grid.adapter = adapter
media_vertical_fastscroller.isHorizontal = false
media_vertical_fastscroller.beGoneIf(scrollHorizontally)
media_horizontal_fastscroller.isHorizontal = true
media_horizontal_fastscroller.beVisibleIf(scrollHorizontally)
if (scrollHorizontally) {
media_horizontal_fastscroller.setViews(media_grid) {
val medium = (media[it] as? Medium)
media_horizontal_fastscroller.updateBubbleText(medium?.getBubbleText(sorting, activity, dateFormat, timeFormat) ?: "")
}
} else {
media_vertical_fastscroller.setViews(media_grid) {
val medium = (media[it] as? Medium)
media_vertical_fastscroller.updateBubbleText(medium?.getBubbleText(sorting, activity, dateFormat, timeFormat) ?: "")
}
}
}
}
}

View file

@ -0,0 +1,76 @@
package com.simplemobiletools.gallery.pro.dialogs
import android.graphics.Point
import android.widget.EditText
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.gallery.pro.R
import kotlinx.android.synthetic.main.dialog_resize_image.view.*
class ResizeDialog(val activity: BaseSimpleActivity, val size: Point, val callback: (newSize: Point) -> Unit) {
init {
val view = activity.layoutInflater.inflate(R.layout.dialog_resize_image, null)
val widthView = view.image_width
val heightView = view.image_height
widthView.setText(size.x.toString())
heightView.setText(size.y.toString())
val ratio = size.x / size.y.toFloat()
widthView.onTextChangeListener {
if (widthView.hasFocus()) {
var width = getViewValue(widthView)
if (width > size.x) {
widthView.setText(size.x.toString())
width = size.x
}
if (view.keep_aspect_ratio.isChecked) {
heightView.setText((width / ratio).toInt().toString())
}
}
}
heightView.onTextChangeListener {
if (heightView.hasFocus()) {
var height = getViewValue(heightView)
if (height > size.y) {
heightView.setText(size.y.toString())
height = size.y
}
if (view.keep_aspect_ratio.isChecked) {
widthView.setText((height * ratio).toInt().toString())
}
}
}
AlertDialog.Builder(activity)
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.cancel, null)
.create().apply {
activity.setupDialogStuff(view, this, R.string.resize_and_save) {
showKeyboard(view.image_width)
getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
val width = getViewValue(widthView)
val height = getViewValue(heightView)
if (width <= 0 || height <= 0) {
activity.toast(R.string.invalid_values)
return@setOnClickListener
}
val newSize = Point(getViewValue(widthView), getViewValue(heightView))
callback(newSize)
dismiss()
}
}
}
}
private fun getViewValue(view: EditText): Int {
val textValue = view.value
return if (textValue.isEmpty()) 0 else textValue.toInt()
}
}

View file

@ -0,0 +1,125 @@
package com.simplemobiletools.gallery.pro.dialogs
import android.graphics.Point
import android.widget.EditText
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.dialogs.ConfirmationDialog
import com.simplemobiletools.commons.dialogs.FilePickerDialog
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.gallery.pro.R
import com.simplemobiletools.gallery.pro.extensions.config
import kotlinx.android.synthetic.main.dialog_resize_image_with_path.view.*
class ResizeWithPathDialog(val activity: BaseSimpleActivity, val size: Point, val path: String, val callback: (newSize: Point, newPath: String) -> Unit) {
init {
var realPath = path.getParentPath()
val view = activity.layoutInflater.inflate(R.layout.dialog_resize_image_with_path, null).apply {
image_path.text = "${activity.humanizePath(realPath).trimEnd('/')}/"
val fullName = path.getFilenameFromPath()
val dotAt = fullName.lastIndexOf(".")
var name = fullName
if (dotAt > 0) {
name = fullName.substring(0, dotAt)
val extension = fullName.substring(dotAt + 1)
image_extension.setText(extension)
}
image_name.setText(name)
image_path.setOnClickListener {
FilePickerDialog(activity, realPath, false, activity.config.shouldShowHidden, true, true) {
image_path.text = activity.humanizePath(it)
realPath = it
}
}
}
val widthView = view.image_width
val heightView = view.image_height
widthView.setText(size.x.toString())
heightView.setText(size.y.toString())
val ratio = size.x / size.y.toFloat()
widthView.onTextChangeListener {
if (widthView.hasFocus()) {
var width = getViewValue(widthView)
if (width > size.x) {
widthView.setText(size.x.toString())
width = size.x
}
heightView.setText((width / ratio).toInt().toString())
}
}
heightView.onTextChangeListener {
if (heightView.hasFocus()) {
var height = getViewValue(heightView)
if (height > size.y) {
heightView.setText(size.y.toString())
height = size.y
}
widthView.setText((height * ratio).toInt().toString())
}
}
AlertDialog.Builder(activity)
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.cancel, null)
.create().apply {
activity.setupDialogStuff(view, this) {
showKeyboard(view.image_width)
getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
val width = getViewValue(widthView)
val height = getViewValue(heightView)
if (width <= 0 || height <= 0) {
activity.toast(R.string.invalid_values)
return@setOnClickListener
}
val newSize = Point(getViewValue(widthView), getViewValue(heightView))
val filename = view.image_name.value
val extension = view.image_extension.value
if (filename.isEmpty()) {
activity.toast(R.string.filename_cannot_be_empty)
return@setOnClickListener
}
if (extension.isEmpty()) {
activity.toast(R.string.extension_cannot_be_empty)
return@setOnClickListener
}
val newFilename = "$filename.$extension"
val newPath = "${realPath.trimEnd('/')}/$newFilename"
if (!newFilename.isAValidFilename()) {
activity.toast(R.string.filename_invalid_characters)
return@setOnClickListener
}
if (activity.getDoesFilePathExist(newPath)) {
val title = String.format(activity.getString(R.string.file_already_exists_overwrite), newFilename)
ConfirmationDialog(activity, title) {
callback(newSize, newPath)
dismiss()
}
} else {
callback(newSize, newPath)
dismiss()
}
}
}
}
}
private fun getViewValue(view: EditText): Int {
val textValue = view.value
return if (textValue.isEmpty()) 0 else textValue.toInt()
}
}

View file

@ -0,0 +1,86 @@
package com.simplemobiletools.gallery.pro.dialogs
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.dialogs.ConfirmationDialog
import com.simplemobiletools.commons.dialogs.FilePickerDialog
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.gallery.pro.R
import kotlinx.android.synthetic.main.dialog_save_as.view.*
class SaveAsDialog(val activity: BaseSimpleActivity, val path: String, val appendFilename: Boolean, val cancelCallback: (() -> Unit)? = null,
val callback: (savePath: String) -> Unit) {
init {
var realPath = path.getParentPath()
val view = activity.layoutInflater.inflate(R.layout.dialog_save_as, null).apply {
save_as_path.text = "${activity.humanizePath(realPath).trimEnd('/')}/"
val fullName = path.getFilenameFromPath()
val dotAt = fullName.lastIndexOf(".")
var name = fullName
if (dotAt > 0) {
name = fullName.substring(0, dotAt)
val extension = fullName.substring(dotAt + 1)
save_as_extension.setText(extension)
}
if (appendFilename) {
name += "_1"
}
save_as_name.setText(name)
save_as_path.setOnClickListener {
activity.hideKeyboard(save_as_path)
FilePickerDialog(activity, realPath, false, false, true, true) {
save_as_path.text = activity.humanizePath(it)
realPath = it
}
}
}
AlertDialog.Builder(activity)
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.cancel) { dialog, which -> cancelCallback?.invoke() }
.setOnCancelListener { cancelCallback?.invoke() }
.create().apply {
activity.setupDialogStuff(view, this, R.string.save_as) {
showKeyboard(view.save_as_name)
getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
val filename = view.save_as_name.value
val extension = view.save_as_extension.value
if (filename.isEmpty()) {
activity.toast(R.string.filename_cannot_be_empty)
return@setOnClickListener
}
if (extension.isEmpty()) {
activity.toast(R.string.extension_cannot_be_empty)
return@setOnClickListener
}
val newFilename = "$filename.$extension"
val newPath = "${realPath.trimEnd('/')}/$newFilename"
if (!newFilename.isAValidFilename()) {
activity.toast(R.string.filename_invalid_characters)
return@setOnClickListener
}
if (activity.getDoesFilePathExist(newPath)) {
val title = String.format(activity.getString(R.string.file_already_exists_overwrite), newFilename)
ConfirmationDialog(activity, title) {
callback(newPath)
dismiss()
}
} else {
callback(newPath)
dismiss()
}
}
}
}
}
}

View file

@ -0,0 +1,135 @@
package com.simplemobiletools.gallery.pro.dialogs
import android.view.View
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.dialogs.RadioGroupDialog
import com.simplemobiletools.commons.extensions.hideKeyboard
import com.simplemobiletools.commons.extensions.setupDialogStuff
import com.simplemobiletools.commons.extensions.value
import com.simplemobiletools.commons.models.RadioItem
import com.simplemobiletools.gallery.pro.R
import com.simplemobiletools.gallery.pro.extensions.config
import com.simplemobiletools.gallery.pro.helpers.SLIDESHOW_ANIMATION_FADE
import com.simplemobiletools.gallery.pro.helpers.SLIDESHOW_ANIMATION_NONE
import com.simplemobiletools.gallery.pro.helpers.SLIDESHOW_ANIMATION_SLIDE
import com.simplemobiletools.gallery.pro.helpers.SLIDESHOW_DEFAULT_INTERVAL
import kotlinx.android.synthetic.main.dialog_slideshow.view.*
class SlideshowDialog(val activity: BaseSimpleActivity, val callback: () -> Unit) {
val view: View
init {
view = activity.layoutInflater.inflate(R.layout.dialog_slideshow, null).apply {
interval_value.setOnClickListener {
val text = interval_value.text
if (text.isNotEmpty()) {
text.replace(0, 1, text.subSequence(0, 1), 0, 1)
interval_value.selectAll()
}
}
interval_value.setOnFocusChangeListener { v, hasFocus ->
if (!hasFocus)
activity.hideKeyboard(v)
}
animation_holder.setOnClickListener {
val items = arrayListOf(
RadioItem(SLIDESHOW_ANIMATION_NONE, activity.getString(R.string.no_animation)),
RadioItem(SLIDESHOW_ANIMATION_SLIDE, activity.getString(R.string.slide)),
RadioItem(SLIDESHOW_ANIMATION_FADE, activity.getString(R.string.fade)))
RadioGroupDialog(activity, items, activity.config.slideshowAnimation) {
activity.config.slideshowAnimation = it as Int
animation_value.text = getAnimationText()
}
}
include_videos_holder.setOnClickListener {
interval_value.clearFocus()
include_videos.toggle()
}
include_gifs_holder.setOnClickListener {
interval_value.clearFocus()
include_gifs.toggle()
}
random_order_holder.setOnClickListener {
interval_value.clearFocus()
random_order.toggle()
}
move_backwards_holder.setOnClickListener {
interval_value.clearFocus()
move_backwards.toggle()
}
loop_slideshow_holder.setOnClickListener {
interval_value.clearFocus()
loop_slideshow.toggle()
}
}
setupValues()
AlertDialog.Builder(activity)
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.cancel, null)
.create().apply {
activity.setupDialogStuff(view, this) {
hideKeyboard()
getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
storeValues()
callback()
dismiss()
}
}
}
}
private fun setupValues() {
val config = activity.config
view.apply {
interval_value.setText(config.slideshowInterval.toString())
animation_value.text = getAnimationText()
include_videos.isChecked = config.slideshowIncludeVideos
include_gifs.isChecked = config.slideshowIncludeGIFs
random_order.isChecked = config.slideshowRandomOrder
move_backwards.isChecked = config.slideshowMoveBackwards
loop_slideshow.isChecked = config.loopSlideshow
}
}
private fun storeValues() {
var interval = view.interval_value.text.toString()
if (interval.trim('0').isEmpty())
interval = SLIDESHOW_DEFAULT_INTERVAL.toString()
activity.config.apply {
slideshowAnimation = getAnimationValue(view.animation_value.value)
slideshowInterval = interval.toInt()
slideshowIncludeVideos = view.include_videos.isChecked
slideshowIncludeGIFs = view.include_gifs.isChecked
slideshowRandomOrder = view.random_order.isChecked
slideshowMoveBackwards = view.move_backwards.isChecked
loopSlideshow = view.loop_slideshow.isChecked
}
}
private fun getAnimationText(): String {
return when (activity.config.slideshowAnimation) {
SLIDESHOW_ANIMATION_SLIDE -> activity.getString(R.string.slide)
SLIDESHOW_ANIMATION_FADE -> activity.getString(R.string.fade)
else -> activity.getString(R.string.no_animation)
}
}
private fun getAnimationValue(text: String): Int {
return when (text) {
activity.getString(R.string.slide) -> SLIDESHOW_ANIMATION_SLIDE
activity.getString(R.string.fade) -> SLIDESHOW_ANIMATION_FADE
else -> SLIDESHOW_ANIMATION_NONE
}
}
}

View file

@ -0,0 +1,675 @@
package com.simplemobiletools.gallery.pro.extensions
import android.annotation.TargetApi
import android.app.Activity
import android.content.ContentProviderOperation
import android.content.ContentValues
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Matrix
import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import android.provider.MediaStore.Files
import android.provider.MediaStore.Images
import android.util.DisplayMetrics
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.exifinterface.media.ExifInterface
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.dialogs.ConfirmationDialog
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.commons.models.FAQItem
import com.simplemobiletools.commons.models.FileDirItem
import com.simplemobiletools.gallery.pro.BuildConfig
import com.simplemobiletools.gallery.pro.R
import com.simplemobiletools.gallery.pro.activities.SimpleActivity
import com.simplemobiletools.gallery.pro.dialogs.PickDirectoryDialog
import com.simplemobiletools.gallery.pro.helpers.RECYCLE_BIN
import com.simplemobiletools.gallery.pro.models.DateTaken
import com.squareup.picasso.Picasso
import java.io.*
import java.text.SimpleDateFormat
import java.util.*
fun Activity.sharePath(path: String) {
sharePathIntent(path, BuildConfig.APPLICATION_ID)
}
fun Activity.sharePaths(paths: ArrayList<String>) {
sharePathsIntent(paths, BuildConfig.APPLICATION_ID)
}
fun Activity.shareMediumPath(path: String) {
sharePath(path)
}
fun Activity.shareMediaPaths(paths: ArrayList<String>) {
sharePaths(paths)
}
fun Activity.setAs(path: String) {
setAsIntent(path, BuildConfig.APPLICATION_ID)
}
fun Activity.openPath(path: String, forceChooser: Boolean, extras: HashMap<String, Boolean> = HashMap()) {
openPathIntent(path, forceChooser, BuildConfig.APPLICATION_ID, extras = extras)
}
fun Activity.openEditor(path: String, forceChooser: Boolean = false) {
val newPath = path.removePrefix("file://")
openEditorIntent(newPath, forceChooser, BuildConfig.APPLICATION_ID)
}
fun Activity.launchCamera() {
val intent = Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA)
if (intent.resolveActivity(packageManager) != null) {
startActivity(intent)
} else {
toast(R.string.no_app_found)
}
}
fun SimpleActivity.launchAbout() {
val licenses = LICENSE_GLIDE or LICENSE_CROPPER or LICENSE_RTL or LICENSE_SUBSAMPLING or LICENSE_PATTERN or LICENSE_REPRINT or LICENSE_GIF_DRAWABLE or
LICENSE_PICASSO or LICENSE_EXOPLAYER or LICENSE_PANORAMA_VIEW or LICENSE_SANSELAN or LICENSE_FILTERS or LICENSE_GESTURE_VIEWS or
LICENSE_APNG
val faqItems = arrayListOf(
FAQItem(R.string.faq_5_title_commons, R.string.faq_5_text_commons),
FAQItem(R.string.faq_1_title, R.string.faq_1_text),
FAQItem(R.string.faq_2_title, R.string.faq_2_text),
FAQItem(R.string.faq_3_title, R.string.faq_3_text),
FAQItem(R.string.faq_4_title, R.string.faq_4_text),
FAQItem(R.string.faq_5_title, R.string.faq_5_text),
FAQItem(R.string.faq_6_title, R.string.faq_6_text),
FAQItem(R.string.faq_7_title, R.string.faq_7_text),
FAQItem(R.string.faq_8_title, R.string.faq_8_text),
FAQItem(R.string.faq_10_title, R.string.faq_10_text),
FAQItem(R.string.faq_11_title, R.string.faq_11_text),
FAQItem(R.string.faq_12_title, R.string.faq_12_text),
FAQItem(R.string.faq_13_title, R.string.faq_13_text),
FAQItem(R.string.faq_14_title, R.string.faq_14_text),
FAQItem(R.string.faq_15_title, R.string.faq_15_text),
FAQItem(R.string.faq_2_title_commons, R.string.faq_2_text_commons),
FAQItem(R.string.faq_6_title_commons, R.string.faq_6_text_commons),
FAQItem(R.string.faq_7_title_commons, R.string.faq_7_text_commons),
FAQItem(R.string.faq_9_title_commons, R.string.faq_9_text_commons),
FAQItem(R.string.faq_10_title_commons, R.string.faq_10_text_commons))
startAboutActivity(R.string.app_name, licenses, BuildConfig.VERSION_NAME, faqItems, true)
}
fun AppCompatActivity.showSystemUI(toggleActionBarVisibility: Boolean) {
if (toggleActionBarVisibility) {
supportActionBar?.show()
}
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
}
fun AppCompatActivity.hideSystemUI(toggleActionBarVisibility: Boolean) {
if (toggleActionBarVisibility) {
supportActionBar?.hide()
}
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
View.SYSTEM_UI_FLAG_LOW_PROFILE or
View.SYSTEM_UI_FLAG_FULLSCREEN or
View.SYSTEM_UI_FLAG_IMMERSIVE
}
fun BaseSimpleActivity.addNoMedia(path: String, callback: () -> Unit) {
val file = File(path, NOMEDIA)
if (getDoesFilePathExist(file.absolutePath)) {
callback()
return
}
if (needsStupidWritePermissions(path)) {
handleSAFDialog(file.absolutePath) {
if (!it) {
return@handleSAFDialog
}
val fileDocument = getDocumentFile(path)
if (fileDocument?.exists() == true && fileDocument.isDirectory) {
fileDocument.createFile("", NOMEDIA)
addNoMediaIntoMediaStore(file.absolutePath)
callback()
} else {
toast(R.string.unknown_error_occurred)
callback()
}
}
} else {
try {
if (file.createNewFile()) {
addNoMediaIntoMediaStore(file.absolutePath)
} else {
toast(R.string.unknown_error_occurred)
}
} catch (e: Exception) {
showErrorToast(e)
}
callback()
}
}
fun BaseSimpleActivity.addNoMediaIntoMediaStore(path: String) {
try {
val content = ContentValues().apply {
put(Files.FileColumns.TITLE, NOMEDIA)
put(Files.FileColumns.DATA, path)
put(Files.FileColumns.MEDIA_TYPE, Files.FileColumns.MEDIA_TYPE_NONE)
}
contentResolver.insert(Files.getContentUri("external"), content)
} catch (e: Exception) {
showErrorToast(e)
}
}
fun BaseSimpleActivity.removeNoMedia(path: String, callback: (() -> Unit)? = null) {
val file = File(path, NOMEDIA)
if (!getDoesFilePathExist(file.absolutePath)) {
callback?.invoke()
return
}
tryDeleteFileDirItem(file.toFileDirItem(applicationContext), false, false) {
callback?.invoke()
deleteFromMediaStore(file.absolutePath)
rescanFolderMedia(path)
}
}
fun BaseSimpleActivity.toggleFileVisibility(oldPath: String, hide: Boolean, callback: ((newPath: String) -> Unit)? = null) {
val path = oldPath.getParentPath()
var filename = oldPath.getFilenameFromPath()
if ((hide && filename.startsWith('.')) || (!hide && !filename.startsWith('.'))) {
callback?.invoke(oldPath)
return
}
filename = if (hide) {
".${filename.trimStart('.')}"
} else {
filename.substring(1, filename.length)
}
val newPath = "$path/$filename"
renameFile(oldPath, newPath) {
callback?.invoke(newPath)
ensureBackgroundThread {
updateDBMediaPath(oldPath, newPath)
}
}
}
fun BaseSimpleActivity.tryCopyMoveFilesTo(fileDirItems: ArrayList<FileDirItem>, isCopyOperation: Boolean, callback: (destinationPath: String) -> Unit) {
if (fileDirItems.isEmpty()) {
toast(R.string.unknown_error_occurred)
return
}
val source = fileDirItems[0].getParentPath()
PickDirectoryDialog(this, source, true, false) {
val destination = it
handleSAFDialog(source) {
if (it) {
copyMoveFilesTo(fileDirItems, source.trimEnd('/'), destination, isCopyOperation, true, config.shouldShowHidden, callback)
}
}
}
}
fun BaseSimpleActivity.tryDeleteFileDirItem(fileDirItem: FileDirItem, allowDeleteFolder: Boolean = false, deleteFromDatabase: Boolean,
callback: ((wasSuccess: Boolean) -> Unit)? = null) {
deleteFile(fileDirItem, allowDeleteFolder) {
if (deleteFromDatabase) {
ensureBackgroundThread {
deleteDBPath(fileDirItem.path)
runOnUiThread {
callback?.invoke(it)
}
}
} else {
callback?.invoke(it)
}
}
}
fun BaseSimpleActivity.movePathsInRecycleBin(paths: ArrayList<String>, callback: ((wasSuccess: Boolean) -> Unit)?) {
ensureBackgroundThread {
var pathsCnt = paths.size
val OTGPath = config.OTGPath
for (source in paths) {
if (OTGPath.isNotEmpty() && source.startsWith(OTGPath)) {
var inputStream: InputStream? = null
var out: OutputStream? = null
try {
val destination = "$recycleBinPath/$source"
val fileDocument = getSomeDocumentFile(source)
inputStream = applicationContext.contentResolver.openInputStream(fileDocument?.uri!!)
out = getFileOutputStreamSync(destination, source.getMimeType())
var copiedSize = 0L
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
var bytes = inputStream!!.read(buffer)
while (bytes >= 0) {
out!!.write(buffer, 0, bytes)
copiedSize += bytes
bytes = inputStream.read(buffer)
}
out?.flush()
if (fileDocument.getItemSize(true) == copiedSize && getDoesFilePathExist(destination)) {
mediaDB.updateDeleted("$RECYCLE_BIN$source", System.currentTimeMillis(), source)
pathsCnt--
}
} catch (e: Exception) {
showErrorToast(e)
return@ensureBackgroundThread
} finally {
inputStream?.close()
out?.close()
}
} else {
val file = File(source)
val internalFile = File(recycleBinPath, source)
val lastModified = file.lastModified()
try {
if (file.copyRecursively(internalFile, true)) {
mediaDB.updateDeleted("$RECYCLE_BIN$source", System.currentTimeMillis(), source)
pathsCnt--
if (config.keepLastModified) {
internalFile.setLastModified(lastModified)
}
}
} catch (e: Exception) {
showErrorToast(e)
return@ensureBackgroundThread
}
}
}
callback?.invoke(pathsCnt == 0)
}
}
fun BaseSimpleActivity.restoreRecycleBinPath(path: String, callback: () -> Unit) {
restoreRecycleBinPaths(arrayListOf(path), callback)
}
fun BaseSimpleActivity.restoreRecycleBinPaths(paths: ArrayList<String>, callback: () -> Unit) {
ensureBackgroundThread {
val newPaths = ArrayList<String>()
for (source in paths) {
val destination = source.removePrefix(recycleBinPath)
val lastModified = File(source).lastModified()
val isShowingSAF = handleSAFDialog(destination) {}
if (isShowingSAF) {
return@ensureBackgroundThread
}
var inputStream: InputStream? = null
var out: OutputStream? = null
try {
out = getFileOutputStreamSync(destination, source.getMimeType())
inputStream = getFileInputStreamSync(source)
var copiedSize = 0L
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
var bytes = inputStream!!.read(buffer)
while (bytes >= 0) {
out!!.write(buffer, 0, bytes)
copiedSize += bytes
bytes = inputStream.read(buffer)
}
out?.flush()
if (File(source).length() == copiedSize) {
mediaDB.updateDeleted(destination.removePrefix(recycleBinPath), 0, "$RECYCLE_BIN$destination")
}
newPaths.add(destination)
if (config.keepLastModified) {
File(destination).setLastModified(lastModified)
}
} catch (e: Exception) {
showErrorToast(e)
} finally {
inputStream?.close()
out?.close()
}
}
runOnUiThread {
callback()
}
rescanPaths(newPaths) {
fixDateTaken(newPaths, false)
}
}
}
fun BaseSimpleActivity.emptyTheRecycleBin(callback: (() -> Unit)? = null) {
ensureBackgroundThread {
try {
recycleBin.deleteRecursively()
mediaDB.clearRecycleBin()
directoryDao.deleteRecycleBin()
toast(R.string.recycle_bin_emptied)
callback?.invoke()
} catch (e: Exception) {
toast(R.string.unknown_error_occurred)
}
}
}
fun BaseSimpleActivity.emptyAndDisableTheRecycleBin(callback: () -> Unit) {
ensureBackgroundThread {
emptyTheRecycleBin {
config.useRecycleBin = false
callback()
}
}
}
fun BaseSimpleActivity.showRecycleBinEmptyingDialog(callback: () -> Unit) {
ConfirmationDialog(this, "", R.string.empty_recycle_bin_confirmation, R.string.yes, R.string.no) {
callback()
}
}
fun BaseSimpleActivity.updateFavoritePaths(fileDirItems: ArrayList<FileDirItem>, destination: String) {
ensureBackgroundThread {
fileDirItems.forEach {
val newPath = "$destination/${it.name}"
updateDBMediaPath(it.path, newPath)
}
}
}
fun Activity.hasNavBar(): Boolean {
val display = windowManager.defaultDisplay
val realDisplayMetrics = DisplayMetrics()
display.getRealMetrics(realDisplayMetrics)
val displayMetrics = DisplayMetrics()
display.getMetrics(displayMetrics)
return (realDisplayMetrics.widthPixels - displayMetrics.widthPixels > 0) || (realDisplayMetrics.heightPixels - displayMetrics.heightPixels > 0)
}
fun Activity.fixDateTaken(paths: ArrayList<String>, showToasts: Boolean, hasRescanned: Boolean = false, callback: (() -> Unit)? = null) {
val BATCH_SIZE = 50
if (showToasts) {
toast(R.string.fixing)
}
val pathsToRescan = ArrayList<String>()
try {
var didUpdateFile = false
val operations = ArrayList<ContentProviderOperation>()
ensureBackgroundThread {
val dateTakens = ArrayList<DateTaken>()
for (path in paths) {
val dateTime = ExifInterface(path).getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL)
?: ExifInterface(path).getAttribute(ExifInterface.TAG_DATETIME) ?: continue
// some formats contain a "T" in the middle, some don't
// sample dates: 2015-07-26T14:55:23, 2018:09:05 15:09:05
val t = if (dateTime.substring(10, 11) == "T") "\'T\'" else " "
val separator = dateTime.substring(4, 5)
val format = "yyyy${separator}MM${separator}dd${t}kk:mm:ss"
val formatter = SimpleDateFormat(format, Locale.getDefault())
val timestamp = formatter.parse(dateTime).time
val uri = getFileUri(path)
ContentProviderOperation.newUpdate(uri).apply {
val selection = "${Images.Media.DATA} = ?"
val selectionArgs = arrayOf(path)
withSelection(selection, selectionArgs)
withValue(Images.Media.DATE_TAKEN, timestamp)
operations.add(build())
}
if (operations.size % BATCH_SIZE == 0) {
contentResolver.applyBatch(MediaStore.AUTHORITY, operations)
operations.clear()
}
mediaDB.updateFavoriteDateTaken(path, timestamp)
didUpdateFile = true
val dateTaken = DateTaken(null, path, path.getFilenameFromPath(), path.getParentPath(), timestamp, (System.currentTimeMillis() / 1000).toInt(), File(path).lastModified())
dateTakens.add(dateTaken)
if (!hasRescanned && getFileDateTaken(path) == 0L) {
pathsToRescan.add(path)
}
}
if (!didUpdateFile) {
if (showToasts) {
toast(R.string.no_date_takens_found)
}
runOnUiThread {
callback?.invoke()
}
return@ensureBackgroundThread
}
val resultSize = contentResolver.applyBatch(MediaStore.AUTHORITY, operations).size
if (resultSize == 0) {
didUpdateFile = false
}
if (hasRescanned || pathsToRescan.isEmpty()) {
if (dateTakens.isNotEmpty()) {
dateTakensDB.insertAll(dateTakens)
}
runOnUiThread {
if (showToasts) {
toast(if (didUpdateFile) R.string.dates_fixed_successfully else R.string.unknown_error_occurred)
}
callback?.invoke()
}
} else {
rescanPaths(pathsToRescan) {
fixDateTaken(paths, showToasts, true)
}
}
}
} catch (e: Exception) {
if (showToasts) {
showErrorToast(e)
}
}
}
fun BaseSimpleActivity.saveRotatedImageToFile(oldPath: String, newPath: String, degrees: Int, showToasts: Boolean, callback: () -> Unit) {
var newDegrees = degrees
if (newDegrees < 0) {
newDegrees += 360
}
if (oldPath == newPath && oldPath.isJpg()) {
if (tryRotateByExif(oldPath, newDegrees, showToasts, callback)) {
return
}
}
val tmpPath = "$recycleBinPath/.tmp_${newPath.getFilenameFromPath()}"
val tmpFileDirItem = FileDirItem(tmpPath, tmpPath.getFilenameFromPath())
try {
getFileOutputStream(tmpFileDirItem) {
if (it == null) {
if (showToasts) {
toast(R.string.unknown_error_occurred)
}
return@getFileOutputStream
}
val oldLastModified = File(oldPath).lastModified()
if (oldPath.isJpg()) {
copyFile(oldPath, tmpPath)
saveExifRotation(ExifInterface(tmpPath), newDegrees)
} else {
val inputstream = getFileInputStreamSync(oldPath)
val bitmap = BitmapFactory.decodeStream(inputstream)
saveFile(tmpPath, bitmap, it as FileOutputStream, newDegrees)
}
copyFile(tmpPath, newPath)
rescanPaths(arrayListOf(newPath))
fileRotatedSuccessfully(newPath, oldLastModified)
it.flush()
it.close()
callback.invoke()
}
} catch (e: OutOfMemoryError) {
if (showToasts) {
toast(R.string.out_of_memory_error)
}
} catch (e: Exception) {
if (showToasts) {
showErrorToast(e)
}
} finally {
tryDeleteFileDirItem(tmpFileDirItem, false, true)
}
}
@TargetApi(Build.VERSION_CODES.N)
fun Activity.tryRotateByExif(path: String, degrees: Int, showToasts: Boolean, callback: () -> Unit): Boolean {
return try {
val file = File(path)
val oldLastModified = file.lastModified()
if (saveImageRotation(path, degrees)) {
fileRotatedSuccessfully(path, oldLastModified)
callback.invoke()
if (showToasts) {
toast(R.string.file_saved)
}
true
} else {
false
}
} catch (e: Exception) {
if (showToasts) {
showErrorToast(e)
}
false
}
}
fun Activity.fileRotatedSuccessfully(path: String, lastModified: Long) {
if (config.keepLastModified) {
File(path).setLastModified(lastModified)
updateLastModified(path, lastModified)
}
Picasso.get().invalidate(path.getFileKey(lastModified))
// we cannot refresh a specific image in Glide Cache, so just clear it all
val glide = Glide.get(applicationContext)
glide.clearDiskCache()
runOnUiThread {
glide.clearMemory()
}
}
fun BaseSimpleActivity.copyFile(source: String, destination: String) {
var inputStream: InputStream? = null
var out: OutputStream? = null
try {
out = getFileOutputStreamSync(destination, source.getMimeType())
inputStream = getFileInputStreamSync(source)
inputStream!!.copyTo(out!!)
} catch (e: Exception) {
showErrorToast(e)
} finally {
inputStream?.close()
out?.close()
}
}
fun saveFile(path: String, bitmap: Bitmap, out: FileOutputStream, degrees: Int) {
val matrix = Matrix()
matrix.postRotate(degrees.toFloat())
val bmp = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
bmp.compress(path.getCompressionFormat(), 90, out)
}
fun Activity.getShortcutImage(tmb: String, drawable: Drawable, callback: () -> Unit) {
ensureBackgroundThread {
val options = RequestOptions()
.format(DecodeFormat.PREFER_ARGB_8888)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.fitCenter()
val size = resources.getDimension(R.dimen.shortcut_size).toInt()
val builder = Glide.with(this)
.asDrawable()
.load(tmb)
.apply(options)
.centerCrop()
.into(size, size)
try {
(drawable as LayerDrawable).setDrawableByLayerId(R.id.shortcut_image, builder.get())
} catch (e: Exception) {
}
runOnUiThread {
callback()
}
}
}
@TargetApi(Build.VERSION_CODES.N)
fun Activity.showFileOnMap(path: String) {
val exif = try {
if (path.startsWith("content://") && isNougatPlus()) {
ExifInterface(contentResolver.openInputStream(Uri.parse(path))!!)
} else {
ExifInterface(path)
}
} catch (e: Exception) {
showErrorToast(e)
return
}
val latLon = FloatArray(2)
if (exif.getLatLong(latLon)) {
showLocationOnMap("${latLon[0]}, ${latLon[1]}")
} else {
toast(R.string.unknown_location)
}
}

View file

@ -0,0 +1,33 @@
package com.simplemobiletools.gallery.pro.extensions
import com.simplemobiletools.gallery.pro.helpers.*
import com.simplemobiletools.gallery.pro.models.Medium
fun ArrayList<Medium>.getDirMediaTypes(): Int {
var types = 0
if (any { it.isImage() }) {
types += TYPE_IMAGES
}
if (any { it.isVideo() }) {
types += TYPE_VIDEOS
}
if (any { it.isGIF() }) {
types += TYPE_GIFS
}
if (any { it.isRaw() }) {
types += TYPE_RAWS
}
if (any { it.isSVG() }) {
types += TYPE_SVGS
}
if (any { it.isPortrait() }) {
types += TYPE_PORTRAITS
}
return types
}

View file

@ -0,0 +1,978 @@
package com.simplemobiletools.gallery.pro.extensions
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.database.Cursor
import android.graphics.drawable.PictureDrawable
import android.media.AudioManager
import android.provider.MediaStore.Files
import android.provider.MediaStore.Images
import android.widget.ImageView
import com.bumptech.glide.Glide
import com.bumptech.glide.Priority
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.resource.bitmap.CenterCrop
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.signature.ObjectKey
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.gallery.pro.R
import com.simplemobiletools.gallery.pro.activities.SettingsActivity
import com.simplemobiletools.gallery.pro.asynctasks.GetMediaAsynctask
import com.simplemobiletools.gallery.pro.databases.GalleryDatabase
import com.simplemobiletools.gallery.pro.helpers.*
import com.simplemobiletools.gallery.pro.interfaces.*
import com.simplemobiletools.gallery.pro.models.*
import com.simplemobiletools.gallery.pro.svg.SvgSoftwareLayerSetter
import com.simplemobiletools.gallery.pro.views.MySquareImageView
import pl.droidsonroids.gif.GifDrawable
import java.io.File
import java.io.FileInputStream
import java.nio.ByteBuffer
import java.nio.channels.FileChannel
import java.util.HashSet
import java.util.LinkedHashSet
import kotlin.Comparator
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
import kotlin.collections.set
val Context.audioManager get() = getSystemService(Context.AUDIO_SERVICE) as AudioManager
fun Context.getHumanizedFilename(path: String): String {
val humanized = humanizePath(path)
return humanized.substring(humanized.lastIndexOf("/") + 1)
}
fun Context.launchSettings() {
startActivity(Intent(applicationContext, SettingsActivity::class.java))
}
val Context.config: Config get() = Config.newInstance(applicationContext)
val Context.widgetsDB: WidgetsDao get() = GalleryDatabase.getInstance(applicationContext).WidgetsDao()
val Context.mediaDB: MediumDao get() = GalleryDatabase.getInstance(applicationContext).MediumDao()
val Context.directoryDao: DirectoryDao get() = GalleryDatabase.getInstance(applicationContext).DirectoryDao()
val Context.favoritesDB: FavoritesDao get() = GalleryDatabase.getInstance(applicationContext).FavoritesDao()
val Context.dateTakensDB: DateTakensDao get() = GalleryDatabase.getInstance(applicationContext).DateTakensDao()
val Context.recycleBin: File get() = filesDir
val Context.recycleBinPath: String get() = filesDir.absolutePath
fun Context.movePinnedDirectoriesToFront(dirs: ArrayList<Directory>): ArrayList<Directory> {
val foundFolders = ArrayList<Directory>()
val pinnedFolders = config.pinnedFolders
dirs.forEach {
if (pinnedFolders.contains(it.path)) {
foundFolders.add(it)
}
}
dirs.removeAll(foundFolders)
dirs.addAll(0, foundFolders)
if (config.tempFolderPath.isNotEmpty()) {
val newFolder = dirs.firstOrNull { it.path == config.tempFolderPath }
if (newFolder != null) {
dirs.remove(newFolder)
dirs.add(0, newFolder)
}
}
if (config.showRecycleBinAtFolders && config.showRecycleBinLast) {
val binIndex = dirs.indexOfFirst { it.isRecycleBin() }
if (binIndex != -1) {
val bin = dirs.removeAt(binIndex)
dirs.add(bin)
}
}
return dirs
}
@Suppress("UNCHECKED_CAST")
fun Context.getSortedDirectories(source: ArrayList<Directory>): ArrayList<Directory> {
val sorting = config.directorySorting
val dirs = source.clone() as ArrayList<Directory>
if (sorting and SORT_BY_RANDOM != 0) {
dirs.shuffle()
return movePinnedDirectoriesToFront(dirs)
}
dirs.sortWith(Comparator { o1, o2 ->
o1 as Directory
o2 as Directory
var result = when {
sorting and SORT_BY_NAME != 0 -> {
if (sorting and SORT_USE_NUMERIC_VALUE != 0) {
AlphanumericComparator().compare(o1.sortValue.toLowerCase(), o2.sortValue.toLowerCase())
} else {
o1.sortValue.toLowerCase().compareTo(o2.sortValue.toLowerCase())
}
}
sorting and SORT_BY_PATH != 0 -> {
if (sorting and SORT_USE_NUMERIC_VALUE != 0) {
AlphanumericComparator().compare(o1.sortValue.toLowerCase(), o2.sortValue.toLowerCase())
} else {
o1.sortValue.toLowerCase().compareTo(o2.sortValue.toLowerCase())
}
}
sorting and SORT_BY_PATH != 0 -> AlphanumericComparator().compare(o1.sortValue.toLowerCase(), o2.sortValue.toLowerCase())
sorting and SORT_BY_SIZE != 0 -> (o1.sortValue.toLongOrNull() ?: 0).compareTo(o2.sortValue.toLongOrNull() ?: 0)
sorting and SORT_BY_DATE_MODIFIED != 0 -> (o1.sortValue.toLongOrNull() ?: 0).compareTo(o2.sortValue.toLongOrNull() ?: 0)
else -> (o1.sortValue.toLongOrNull() ?: 0).compareTo(o2.sortValue.toLongOrNull() ?: 0)
}
if (sorting and SORT_DESCENDING != 0) {
result *= -1
}
result
})
return movePinnedDirectoriesToFront(dirs)
}
fun Context.getDirsToShow(dirs: ArrayList<Directory>, allDirs: ArrayList<Directory>, currentPathPrefix: String): ArrayList<Directory> {
return if (config.groupDirectSubfolders) {
dirs.forEach {
it.subfoldersCount = 0
it.subfoldersMediaCount = it.mediaCnt
}
val parentDirs = getDirectParentSubfolders(dirs, currentPathPrefix)
updateSubfolderCounts(dirs, parentDirs)
// show the current folder as an available option too, not just subfolders
if (currentPathPrefix.isNotEmpty()) {
val currentFolder = allDirs.firstOrNull { parentDirs.firstOrNull { it.path == currentPathPrefix } == null && it.path == currentPathPrefix }
currentFolder?.apply {
subfoldersCount = 1
parentDirs.add(this)
}
}
getSortedDirectories(parentDirs)
} else {
dirs.forEach { it.subfoldersMediaCount = it.mediaCnt }
dirs
}
}
fun Context.getDirectParentSubfolders(dirs: ArrayList<Directory>, currentPathPrefix: String): ArrayList<Directory> {
val folders = dirs.map { it.path }.sorted().toMutableSet() as HashSet<String>
val currentPaths = LinkedHashSet<String>()
val foldersWithoutMediaFiles = ArrayList<String>()
var newDirId = 1000L
for (path in folders) {
if (path == RECYCLE_BIN || path == FAVORITES) {
continue
}
if (currentPathPrefix.isNotEmpty()) {
if (!path.startsWith(currentPathPrefix, true)) {
continue
}
if (!File(path).parent.equals(currentPathPrefix, true)) {
continue
}
}
if (currentPathPrefix.isNotEmpty() && path == currentPathPrefix || File(path).parent.equals(currentPathPrefix, true)) {
currentPaths.add(path)
} else if (folders.any { !it.equals(path, true) && (File(path).parent.equals(it, true) || File(it).parent.equals(File(path).parent, true)) }) {
// if we have folders like
// /storage/emulated/0/Pictures/Images and
// /storage/emulated/0/Pictures/Screenshots,
// but /storage/emulated/0/Pictures is empty, still Pictures with the first folders thumbnails and proper other info
val parent = File(path).parent
if (parent != null && !folders.contains(parent) && dirs.none { it.path == parent }) {
currentPaths.add(parent)
val isSortingAscending = config.sorting.isSortingAscending()
val subDirs = dirs.filter { File(it.path).parent.equals(File(path).parent, true) } as ArrayList<Directory>
if (subDirs.isNotEmpty()) {
val lastModified = if (isSortingAscending) {
subDirs.minBy { it.modified }?.modified
} else {
subDirs.maxBy { it.modified }?.modified
} ?: 0
val dateTaken = if (isSortingAscending) {
subDirs.minBy { it.taken }?.taken
} else {
subDirs.maxBy { it.taken }?.taken
} ?: 0
var mediaTypes = 0
subDirs.forEach {
mediaTypes = mediaTypes or it.types
}
val directory = Directory(newDirId++,
parent,
subDirs.first().tmb,
getFolderNameFromPath(parent),
subDirs.sumBy { it.mediaCnt },
lastModified,
dateTaken,
subDirs.sumByLong { it.size },
getPathLocation(parent),
mediaTypes,
"")
directory.containsMediaFilesDirectly = false
dirs.add(directory)
currentPaths.add(parent)
foldersWithoutMediaFiles.add(parent)
}
}
} else {
currentPaths.add(path)
}
}
var areDirectSubfoldersAvailable = false
currentPaths.forEach {
val path = it
currentPaths.forEach {
if (!foldersWithoutMediaFiles.contains(it) && !it.equals(path, true) && File(it).parent?.equals(path, true) == true) {
areDirectSubfoldersAvailable = true
}
}
}
if (currentPathPrefix.isEmpty() && folders.contains(RECYCLE_BIN)) {
currentPaths.add(RECYCLE_BIN)
}
if (currentPathPrefix.isEmpty() && folders.contains(FAVORITES)) {
currentPaths.add(FAVORITES)
}
if (folders.size == currentPaths.size) {
return dirs.filter { currentPaths.contains(it.path) } as ArrayList<Directory>
}
folders.clear()
folders.addAll(currentPaths)
val dirsToShow = dirs.filter { folders.contains(it.path) } as ArrayList<Directory>
return if (areDirectSubfoldersAvailable) {
getDirectParentSubfolders(dirsToShow, currentPathPrefix)
} else {
dirsToShow
}
}
fun Context.updateSubfolderCounts(children: ArrayList<Directory>, parentDirs: ArrayList<Directory>) {
for (child in children) {
var longestSharedPath = ""
for (parentDir in parentDirs) {
if (parentDir.path == child.path) {
longestSharedPath = child.path
continue
}
if (child.path.startsWith(parentDir.path, true) && parentDir.path.length > longestSharedPath.length) {
longestSharedPath = parentDir.path
}
}
// make sure we count only the proper direct subfolders, grouped the same way as on the main screen
parentDirs.firstOrNull { it.path == longestSharedPath }?.apply {
if (path.equals(child.path, true) || path.equals(File(child.path).parent, true) || children.any { it.path.equals(File(child.path).parent, true) }) {
if (child.containsMediaFilesDirectly) {
subfoldersCount++
}
if (path != child.path) {
subfoldersMediaCount += child.mediaCnt
}
}
}
}
}
fun Context.getNoMediaFolders(callback: (folders: ArrayList<String>) -> Unit) {
ensureBackgroundThread {
callback(getNoMediaFoldersSync())
}
}
fun Context.getNoMediaFoldersSync(): ArrayList<String> {
val folders = ArrayList<String>()
val uri = Files.getContentUri("external")
val projection = arrayOf(Files.FileColumns.DATA)
val selection = "${Files.FileColumns.MEDIA_TYPE} = ? AND ${Files.FileColumns.TITLE} LIKE ?"
val selectionArgs = arrayOf(Files.FileColumns.MEDIA_TYPE_NONE.toString(), "%$NOMEDIA%")
val sortOrder = "${Files.FileColumns.DATE_MODIFIED} DESC"
val OTGPath = config.OTGPath
var cursor: Cursor? = null
try {
cursor = contentResolver.query(uri, projection, selection, selectionArgs, sortOrder)
if (cursor?.moveToFirst() == true) {
do {
val path = cursor.getStringValue(Files.FileColumns.DATA) ?: continue
val noMediaFile = File(path)
if (getDoesFilePathExist(noMediaFile.absolutePath, OTGPath) && noMediaFile.name == NOMEDIA) {
folders.add(noMediaFile.parent)
}
} while (cursor.moveToNext())
}
} catch (ignored: Exception) {
} finally {
cursor?.close()
}
return folders
}
fun Context.rescanFolderMedia(path: String) {
ensureBackgroundThread {
rescanFolderMediaSync(path)
}
}
fun Context.rescanFolderMediaSync(path: String) {
getCachedMedia(path) {
val cached = it
GetMediaAsynctask(applicationContext, path, false, false, false) {
ensureBackgroundThread {
val newMedia = it
val media = newMedia.filter { it is Medium } as ArrayList<Medium>
try {
mediaDB.insertAll(media)
cached.forEach {
if (!newMedia.contains(it)) {
val mediumPath = (it as? Medium)?.path
if (mediumPath != null) {
deleteDBPath(mediumPath)
}
}
}
} catch (ignored: Exception) {
}
}
}.execute()
}
}
fun Context.storeDirectoryItems(items: ArrayList<Directory>) {
ensureBackgroundThread {
directoryDao.insertAll(items)
}
}
fun Context.checkAppendingHidden(path: String, hidden: String, includedFolders: MutableSet<String>, noMediaFolders: ArrayList<String>): String {
val dirName = getFolderNameFromPath(path)
val folderNoMediaStatuses = java.util.HashMap<String, Boolean>()
noMediaFolders.forEach { folder ->
folderNoMediaStatuses["$folder/$NOMEDIA"] = true
}
return if (path.doesThisOrParentHaveNoMedia(folderNoMediaStatuses, null) && !path.isThisOrParentIncluded(includedFolders)) {
"$dirName $hidden"
} else {
dirName
}
}
fun Context.getFolderNameFromPath(path: String): String {
return when (path) {
internalStoragePath -> getString(R.string.internal)
sdCardPath -> getString(R.string.sd_card)
otgPath -> getString(R.string.usb)
FAVORITES -> getString(R.string.favorites)
RECYCLE_BIN -> getString(R.string.recycle_bin)
else -> path.getFilenameFromPath()
}
}
fun Context.loadImage(type: Int, path: String, target: MySquareImageView, horizontalScroll: Boolean, animateGifs: Boolean, cropThumbnails: Boolean,
roundCorners: Int, signature: ObjectKey, skipMemoryCacheAtPaths: ArrayList<String>? = null) {
target.isHorizontalScrolling = horizontalScroll
if (type == TYPE_IMAGES || type == TYPE_VIDEOS || type == TYPE_RAWS || type == TYPE_PORTRAITS) {
if (type == TYPE_IMAGES && path.isPng()) {
loadPng(path, target, cropThumbnails, roundCorners, signature, skipMemoryCacheAtPaths)
} else {
loadJpg(path, target, cropThumbnails, roundCorners, signature, skipMemoryCacheAtPaths)
}
} else if (type == TYPE_GIFS) {
if (!animateGifs) {
loadStaticGIF(path, target, cropThumbnails, roundCorners, signature, skipMemoryCacheAtPaths)
return
}
try {
val gifDrawable = GifDrawable(path)
target.setImageDrawable(gifDrawable)
gifDrawable.start()
target.scaleType = if (cropThumbnails) ImageView.ScaleType.CENTER_CROP else ImageView.ScaleType.FIT_CENTER
} catch (e: Exception) {
loadStaticGIF(path, target, cropThumbnails, roundCorners, signature, skipMemoryCacheAtPaths)
} catch (e: OutOfMemoryError) {
loadStaticGIF(path, target, cropThumbnails, roundCorners, signature, skipMemoryCacheAtPaths)
}
} else if (type == TYPE_SVGS) {
loadSVG(path, target, cropThumbnails, roundCorners, signature)
}
}
fun Context.addTempFolderIfNeeded(dirs: ArrayList<Directory>): ArrayList<Directory> {
val tempFolderPath = config.tempFolderPath
return if (tempFolderPath.isNotEmpty()) {
val directories = ArrayList<Directory>()
val newFolder = Directory(null, tempFolderPath, "", tempFolderPath.getFilenameFromPath(), 0, 0, 0, 0L, getPathLocation(tempFolderPath), 0, "")
directories.add(newFolder)
directories.addAll(dirs)
directories
} else {
dirs
}
}
fun Context.getPathLocation(path: String): Int {
return when {
isPathOnSD(path) -> LOCATION_SD
isPathOnOTG(path) -> LOCATION_OTG
else -> LOCATION_INTERNAL
}
}
fun Context.loadPng(path: String, target: MySquareImageView, cropThumbnails: Boolean, roundCorners: Int, signature: ObjectKey, skipMemoryCacheAtPaths: ArrayList<String>? = null) {
val options = RequestOptions()
.signature(signature)
.skipMemoryCache(skipMemoryCacheAtPaths?.contains(path) == true)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.priority(Priority.LOW)
.format(DecodeFormat.PREFER_ARGB_8888)
if (cropThumbnails) options.centerCrop() else options.fitCenter()
var builder = Glide.with(applicationContext)
.asBitmap()
.load(path)
.apply(options)
if (roundCorners != ROUNDED_CORNERS_NONE) {
val cornerSize = if (roundCorners == ROUNDED_CORNERS_SMALL) R.dimen.rounded_corner_radius_small else R.dimen.rounded_corner_radius_big
val cornerRadius = resources.getDimension(cornerSize).toInt()
builder = builder.transform(CenterCrop(), RoundedCorners(cornerRadius))
}
builder.into(target)
}
fun Context.loadJpg(path: String, target: MySquareImageView, cropThumbnails: Boolean, roundCorners: Int, signature: ObjectKey, skipMemoryCacheAtPaths: ArrayList<String>? = null) {
val options = RequestOptions()
.signature(signature)
.skipMemoryCache(skipMemoryCacheAtPaths?.contains(path) == true)
.priority(Priority.LOW)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
if (cropThumbnails) options.centerCrop() else options.fitCenter()
var builder = Glide.with(applicationContext)
.load(path)
.apply(options)
.transition(DrawableTransitionOptions.withCrossFade())
if (roundCorners != ROUNDED_CORNERS_NONE) {
val cornerSize = if (roundCorners == ROUNDED_CORNERS_SMALL) R.dimen.rounded_corner_radius_small else R.dimen.rounded_corner_radius_big
val cornerRadius = resources.getDimension(cornerSize).toInt()
builder = builder.transform(CenterCrop(), RoundedCorners(cornerRadius))
}
builder.into(target)
}
fun Context.loadStaticGIF(path: String, target: MySquareImageView, cropThumbnails: Boolean, roundCorners: Int, signature: ObjectKey, skipMemoryCacheAtPaths: ArrayList<String>? = null) {
val options = RequestOptions()
.signature(signature)
.skipMemoryCache(skipMemoryCacheAtPaths?.contains(path) == true)
.priority(Priority.LOW)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
if (cropThumbnails) options.centerCrop() else options.fitCenter()
var builder = Glide.with(applicationContext)
.asBitmap() // make sure the GIF wont animate
.load(path)
.apply(options)
if (roundCorners != ROUNDED_CORNERS_NONE) {
val cornerSize = if (roundCorners == ROUNDED_CORNERS_SMALL) R.dimen.rounded_corner_radius_small else R.dimen.rounded_corner_radius_big
val cornerRadius = resources.getDimension(cornerSize).toInt()
builder = builder.transform(CenterCrop(), RoundedCorners(cornerRadius))
}
builder.into(target)
}
fun Context.loadSVG(path: String, target: MySquareImageView, cropThumbnails: Boolean, roundCorners: Int, signature: ObjectKey) {
target.scaleType = if (cropThumbnails) ImageView.ScaleType.CENTER_CROP else ImageView.ScaleType.FIT_CENTER
val options = RequestOptions().signature(signature)
var builder = Glide.with(applicationContext)
.`as`(PictureDrawable::class.java)
.listener(SvgSoftwareLayerSetter())
.load(path)
.apply(options)
.transition(DrawableTransitionOptions.withCrossFade())
if (roundCorners != ROUNDED_CORNERS_NONE) {
val cornerSize = if (roundCorners == ROUNDED_CORNERS_SMALL) R.dimen.rounded_corner_radius_small else R.dimen.rounded_corner_radius_big
val cornerRadius = resources.getDimension(cornerSize).toInt()
builder = builder.transform(CenterCrop(), RoundedCorners(cornerRadius))
}
builder.into(target)
}
fun Context.getCachedDirectories(getVideosOnly: Boolean = false, getImagesOnly: Boolean = false, forceShowHidden: Boolean = false, callback: (ArrayList<Directory>) -> Unit) {
ensureBackgroundThread {
val directories = try {
directoryDao.getAll() as ArrayList<Directory>
} catch (e: Exception) {
ArrayList<Directory>()
}
if (!config.showRecycleBinAtFolders) {
directories.removeAll { it.isRecycleBin() }
}
val shouldShowHidden = config.shouldShowHidden || forceShowHidden
val excludedPaths = config.excludedFolders
val includedPaths = config.includedFolders
val folderNoMediaStatuses = HashMap<String, Boolean>()
val noMediaFolders = getNoMediaFoldersSync()
noMediaFolders.forEach { folder ->
folderNoMediaStatuses["$folder/$NOMEDIA"] = true
}
var filteredDirectories = directories.filter {
it.path.shouldFolderBeVisible(excludedPaths, includedPaths, shouldShowHidden, folderNoMediaStatuses) { path, hasNoMedia ->
folderNoMediaStatuses[path] = hasNoMedia
}
} as ArrayList<Directory>
val filterMedia = config.filterMedia
filteredDirectories = (when {
getVideosOnly -> filteredDirectories.filter { it.types and TYPE_VIDEOS != 0 }
getImagesOnly -> filteredDirectories.filter { it.types and TYPE_IMAGES != 0 }
else -> filteredDirectories.filter {
(filterMedia and TYPE_IMAGES != 0 && it.types and TYPE_IMAGES != 0) ||
(filterMedia and TYPE_VIDEOS != 0 && it.types and TYPE_VIDEOS != 0) ||
(filterMedia and TYPE_GIFS != 0 && it.types and TYPE_GIFS != 0) ||
(filterMedia and TYPE_RAWS != 0 && it.types and TYPE_RAWS != 0) ||
(filterMedia and TYPE_SVGS != 0 && it.types and TYPE_SVGS != 0) ||
(filterMedia and TYPE_PORTRAITS != 0 && it.types and TYPE_PORTRAITS != 0)
}
}) as ArrayList<Directory>
if (shouldShowHidden) {
val hiddenString = resources.getString(R.string.hidden)
filteredDirectories.forEach {
val noMediaPath = "${it.path}/$NOMEDIA"
val hasNoMedia = if (folderNoMediaStatuses.keys.contains(noMediaPath)) {
folderNoMediaStatuses[noMediaPath]!!
} else {
it.path.doesThisOrParentHaveNoMedia(folderNoMediaStatuses) { path, hasNoMedia ->
val newPath = "$path/$NOMEDIA"
folderNoMediaStatuses[newPath] = hasNoMedia
}
}
it.name = if (hasNoMedia && !it.path.isThisOrParentIncluded(includedPaths)) {
"${it.name.removeSuffix(hiddenString).trim()} $hiddenString"
} else {
it.name.removeSuffix(hiddenString).trim()
}
}
}
val clone = filteredDirectories.clone() as ArrayList<Directory>
callback(clone.distinctBy { it.path.getDistinctPath() } as ArrayList<Directory>)
removeInvalidDBDirectories(filteredDirectories)
}
}
fun Context.getCachedMedia(path: String, getVideosOnly: Boolean = false, getImagesOnly: Boolean = false, callback: (ArrayList<ThumbnailItem>) -> Unit) {
ensureBackgroundThread {
val mediaFetcher = MediaFetcher(this)
val foldersToScan = if (path.isEmpty()) mediaFetcher.getFoldersToScan() else arrayListOf(path)
var media = ArrayList<Medium>()
if (path == FAVORITES) {
media.addAll(mediaDB.getFavorites())
}
if (path == RECYCLE_BIN) {
media.addAll(getUpdatedDeletedMedia())
}
if (config.filterMedia and TYPE_PORTRAITS != 0) {
val foldersToAdd = ArrayList<String>()
for (folder in foldersToScan) {
val allFiles = File(folder).listFiles() ?: continue
allFiles.filter { it.name.startsWith("img_", true) && it.isDirectory }.forEach {
foldersToAdd.add(it.absolutePath)
}
}
foldersToScan.addAll(foldersToAdd)
}
val shouldShowHidden = config.shouldShowHidden
foldersToScan.filter { path.isNotEmpty() || !config.isFolderProtected(it) }.forEach {
try {
val currMedia = mediaDB.getMediaFromPath(it)
media.addAll(currMedia)
} catch (ignored: Exception) {
}
}
if (!shouldShowHidden) {
media = media.filter { !it.path.contains("/.") } as ArrayList<Medium>
}
val filterMedia = config.filterMedia
media = (when {
getVideosOnly -> media.filter { it.type == TYPE_VIDEOS }
getImagesOnly -> media.filter { it.type == TYPE_IMAGES }
else -> media.filter {
(filterMedia and TYPE_IMAGES != 0 && it.type == TYPE_IMAGES) ||
(filterMedia and TYPE_VIDEOS != 0 && it.type == TYPE_VIDEOS) ||
(filterMedia and TYPE_GIFS != 0 && it.type == TYPE_GIFS) ||
(filterMedia and TYPE_RAWS != 0 && it.type == TYPE_RAWS) ||
(filterMedia and TYPE_SVGS != 0 && it.type == TYPE_SVGS) ||
(filterMedia and TYPE_PORTRAITS != 0 && it.type == TYPE_PORTRAITS)
}
}) as ArrayList<Medium>
val pathToUse = if (path.isEmpty()) SHOW_ALL else path
mediaFetcher.sortMedia(media, config.getFolderSorting(pathToUse))
val grouped = mediaFetcher.groupMedia(media, pathToUse)
callback(grouped.clone() as ArrayList<ThumbnailItem>)
val OTGPath = config.OTGPath
try {
val mediaToDelete = ArrayList<Medium>()
// creating a new thread intentionally, do not reuse the common background thread
Thread {
media.filter { !getDoesFilePathExist(it.path, OTGPath) }.forEach {
if (it.path.startsWith(recycleBinPath)) {
deleteDBPath(it.path)
} else {
mediaToDelete.add(it)
}
}
if (mediaToDelete.isNotEmpty()) {
mediaDB.deleteMedia(*mediaToDelete.toTypedArray())
mediaToDelete.filter { it.isFavorite }.forEach {
favoritesDB.deleteFavoritePath(it.path)
}
}
}.start()
} catch (ignored: Exception) {
}
}
}
fun Context.removeInvalidDBDirectories(dirs: ArrayList<Directory>? = null) {
val dirsToCheck = dirs ?: directoryDao.getAll()
val OTGPath = config.OTGPath
dirsToCheck.filter { !it.areFavorites() && !it.isRecycleBin() && !getDoesFilePathExist(it.path, OTGPath) && it.path != config.tempFolderPath }.forEach {
try {
directoryDao.deleteDirPath(it.path)
} catch (ignored: Exception) {
}
}
}
fun Context.updateDBMediaPath(oldPath: String, newPath: String) {
val newFilename = newPath.getFilenameFromPath()
val newParentPath = newPath.getParentPath()
try {
mediaDB.updateMedium(newFilename, newPath, newParentPath, oldPath)
favoritesDB.updateFavorite(newFilename, newPath, newParentPath, oldPath)
} catch (ignored: Exception) {
}
}
fun Context.updateDBDirectory(directory: Directory) {
try {
directoryDao.updateDirectory(directory.path, directory.tmb, directory.mediaCnt, directory.modified, directory.taken, directory.size, directory.types, directory.sortValue)
} catch (ignored: Exception) {
}
}
fun Context.getOTGFolderChildren(path: String) = getDocumentFile(path)?.listFiles()
fun Context.getOTGFolderChildrenNames(path: String) = getOTGFolderChildren(path)?.map { it.name }?.toMutableList()
fun Context.getFavoritePaths(): ArrayList<String> {
return try {
favoritesDB.getValidFavoritePaths() as ArrayList<String>
} catch (e: Exception) {
ArrayList()
}
}
fun Context.getFavoriteFromPath(path: String) = Favorite(null, path, path.getFilenameFromPath(), path.getParentPath())
fun Context.updateFavorite(path: String, isFavorite: Boolean) {
if (isFavorite) {
favoritesDB.insert(getFavoriteFromPath(path))
} else {
favoritesDB.deleteFavoritePath(path)
}
}
// remove the "recycle_bin" from the file path prefix, replace it with real bin path /data/user...
fun Context.getUpdatedDeletedMedia(): ArrayList<Medium> {
val media = try {
mediaDB.getDeletedMedia() as ArrayList<Medium>
} catch (ignored: Exception) {
ArrayList<Medium>()
}
media.forEach {
it.path = File(recycleBinPath, it.path.removePrefix(RECYCLE_BIN)).toString()
}
return media
}
fun Context.deleteDBPath(path: String) {
deleteMediumWithPath(path.replaceFirst(recycleBinPath, RECYCLE_BIN))
}
fun Context.deleteMediumWithPath(path: String) {
try {
mediaDB.deleteMediumPath(path)
} catch (ignored: Exception) {
}
}
fun Context.updateWidgets() {
val widgetIDs = AppWidgetManager.getInstance(applicationContext).getAppWidgetIds(ComponentName(applicationContext, MyWidgetProvider::class.java))
if (widgetIDs.isNotEmpty()) {
Intent(applicationContext, MyWidgetProvider::class.java).apply {
action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, widgetIDs)
sendBroadcast(this)
}
}
}
// based on https://github.com/sannies/mp4parser/blob/master/examples/src/main/java/com/google/code/mp4parser/example/PrintStructure.java
fun Context.parseFileChannel(path: String, fc: FileChannel, level: Int, start: Long, end: Long, callback: () -> Unit) {
val FILE_CHANNEL_CONTAINERS = arrayListOf("moov", "trak", "mdia", "minf", "udta", "stbl")
try {
var iteration = 0
var currEnd = end
fc.position(start)
if (currEnd <= 0) {
currEnd = start + fc.size()
}
while (currEnd - fc.position() > 8) {
// just a check to avoid deadloop at some videos
if (iteration++ > 50) {
return
}
val begin = fc.position()
val byteBuffer = ByteBuffer.allocate(8)
fc.read(byteBuffer)
byteBuffer.rewind()
val size = IsoTypeReader.readUInt32(byteBuffer)
val type = IsoTypeReader.read4cc(byteBuffer)
val newEnd = begin + size
if (type == "uuid") {
val fis = FileInputStream(File(path))
fis.skip(begin)
val sb = StringBuilder()
val buffer = ByteArray(1024)
while (sb.length < size) {
val n = fis.read(buffer)
if (n != -1) {
sb.append(String(buffer, 0, n))
} else {
break
}
}
val xmlString = sb.toString().toLowerCase()
if (xmlString.contains("gspherical:projectiontype>equirectangular") || xmlString.contains("gspherical:projectiontype=\"equirectangular\"")) {
callback.invoke()
}
return
}
if (FILE_CHANNEL_CONTAINERS.contains(type)) {
parseFileChannel(path, fc, level + 1, begin + 8, newEnd, callback)
}
fc.position(newEnd)
}
} catch (ignored: Exception) {
}
}
fun Context.addPathToDB(path: String) {
ensureBackgroundThread {
if (!getDoesFilePathExist(path)) {
return@ensureBackgroundThread
}
val type = when {
path.isVideoFast() -> TYPE_VIDEOS
path.isGif() -> TYPE_GIFS
path.isRawFast() -> TYPE_RAWS
path.isSvg() -> TYPE_SVGS
path.isPortrait() -> TYPE_PORTRAITS
else -> TYPE_IMAGES
}
try {
val isFavorite = favoritesDB.isFavorite(path)
val videoDuration = if (type == TYPE_VIDEOS) getDuration(path) ?: 0 else 0
val medium = Medium(null, path.getFilenameFromPath(), path, path.getParentPath(), System.currentTimeMillis(), System.currentTimeMillis(),
File(path).length(), type, videoDuration, isFavorite, 0L)
mediaDB.insert(medium)
} catch (ignored: Exception) {
}
}
}
fun Context.createDirectoryFromMedia(path: String, curMedia: ArrayList<Medium>, albumCovers: ArrayList<AlbumCover>, hiddenString: String,
includedFolders: MutableSet<String>, getProperFileSize: Boolean, noMediaFolders: ArrayList<String>): Directory {
val OTGPath = config.OTGPath
val grouped = MediaFetcher(this).groupMedia(curMedia, path)
var thumbnail: String? = null
albumCovers.forEach {
if (it.path == path && getDoesFilePathExist(it.tmb, OTGPath)) {
thumbnail = it.tmb
}
}
if (thumbnail == null) {
val sortedMedia = grouped.filter { it is Medium }.toMutableList() as ArrayList<Medium>
thumbnail = sortedMedia.firstOrNull { getDoesFilePathExist(it.path, OTGPath) }?.path ?: ""
}
if (config.OTGPath.isNotEmpty() && thumbnail!!.startsWith(config.OTGPath)) {
thumbnail = thumbnail!!.getOTGPublicPath(applicationContext)
}
val isSortingAscending = config.directorySorting.isSortingAscending()
val defaultMedium = Medium(0, "", "", "", 0L, 0L, 0L, 0, 0, false, 0L)
val firstItem = curMedia.firstOrNull() ?: defaultMedium
val lastItem = curMedia.lastOrNull() ?: defaultMedium
val dirName = checkAppendingHidden(path, hiddenString, includedFolders, noMediaFolders)
val lastModified = if (isSortingAscending) Math.min(firstItem.modified, lastItem.modified) else Math.max(firstItem.modified, lastItem.modified)
val dateTaken = if (isSortingAscending) Math.min(firstItem.taken, lastItem.taken) else Math.max(firstItem.taken, lastItem.taken)
val size = if (getProperFileSize) curMedia.sumByLong { it.size } else 0L
val mediaTypes = curMedia.getDirMediaTypes()
val sortValue = getDirectorySortingValue(curMedia, path, dirName, size)
return Directory(null, path, thumbnail!!, dirName, curMedia.size, lastModified, dateTaken, size, getPathLocation(path), mediaTypes, sortValue)
}
fun Context.getDirectorySortingValue(media: ArrayList<Medium>, path: String, name: String, size: Long): String {
val sorting = config.directorySorting
val sorted = when {
sorting and SORT_BY_NAME != 0 -> return name
sorting and SORT_BY_PATH != 0 -> return path
sorting and SORT_BY_SIZE != 0 -> return size.toString()
sorting and SORT_BY_DATE_MODIFIED != 0 -> media.sortedBy { it.modified }
sorting and SORT_BY_DATE_TAKEN != 0 -> media.sortedBy { it.taken }
else -> media
}
val relevantMedium = if (sorting.isSortingAscending()) {
sorted.firstOrNull() ?: return ""
} else {
sorted.lastOrNull() ?: return ""
}
val result: Any = when {
sorting and SORT_BY_DATE_MODIFIED != 0 -> relevantMedium.modified
sorting and SORT_BY_DATE_TAKEN != 0 -> relevantMedium.taken
else -> 0
}
return result.toString()
}
fun Context.updateDirectoryPath(path: String) {
val mediaFetcher = MediaFetcher(applicationContext)
val getImagesOnly = false
val getVideosOnly = false
val hiddenString = getString(R.string.hidden)
val albumCovers = config.parseAlbumCovers()
val includedFolders = config.includedFolders
val noMediaFolders = getNoMediaFoldersSync()
val sorting = config.getFolderSorting(path)
val grouping = config.getFolderGrouping(path)
val getProperDateTaken = config.directorySorting and SORT_BY_DATE_TAKEN != 0 ||
sorting and SORT_BY_DATE_TAKEN != 0 ||
grouping and GROUP_BY_DATE_TAKEN_DAILY != 0 ||
grouping and GROUP_BY_DATE_TAKEN_MONTHLY != 0
val getProperLastModified = config.directorySorting and SORT_BY_DATE_MODIFIED != 0 ||
sorting and SORT_BY_DATE_MODIFIED != 0 ||
grouping and GROUP_BY_LAST_MODIFIED_DAILY != 0 ||
grouping and GROUP_BY_LAST_MODIFIED_MONTHLY != 0
val getProperFileSize = config.directorySorting and SORT_BY_SIZE != 0
val lastModifieds = if (getProperLastModified) mediaFetcher.getFolderLastModifieds(path) else HashMap()
val dateTakens = mediaFetcher.getFolderDateTakens(path)
val favoritePaths = getFavoritePaths()
val curMedia = mediaFetcher.getFilesFrom(path, getImagesOnly, getVideosOnly, getProperDateTaken, getProperLastModified, getProperFileSize,
favoritePaths, false, lastModifieds, dateTakens)
val directory = createDirectoryFromMedia(path, curMedia, albumCovers, hiddenString, includedFolders, getProperFileSize, noMediaFolders)
updateDBDirectory(directory)
}
fun Context.getFileDateTaken(path: String): Long {
val projection = arrayOf(
Images.Media.DATE_TAKEN
)
val uri = Files.getContentUri("external")
val selection = "${Images.Media.DATA} = ?"
val selectionArgs = arrayOf(path)
try {
val cursor = contentResolver.query(uri, projection, selection, selectionArgs, null)
cursor?.use {
if (cursor.moveToFirst()) {
return cursor.getLongValue(Images.Media.DATE_TAKEN)
}
}
} catch (ignored: Exception) {
}
return 0L
}

View file

@ -0,0 +1,57 @@
package com.simplemobiletools.gallery.pro.extensions
import androidx.exifinterface.media.ExifInterface
import java.lang.reflect.Field
import java.lang.reflect.Modifier
fun ExifInterface.copyNonDimensionAttributesTo(destination: ExifInterface) {
val attributes = ExifInterfaceAttributes.AllNonDimensionAttributes
attributes.forEach {
val value = getAttribute(it)
if (value != null) {
destination.setAttribute(it, value)
}
}
try {
destination.saveAttributes()
} catch (ignored: Exception) {
}
}
private class ExifInterfaceAttributes {
companion object {
val AllNonDimensionAttributes = getAllNonDimensionExifAttributes()
private fun getAllNonDimensionExifAttributes(): List<String> {
val tagFields = ExifInterface::class.java.fields.filter { field -> isExif(field) }
val excludeAttributes = arrayListOf(
ExifInterface.TAG_IMAGE_LENGTH,
ExifInterface.TAG_IMAGE_WIDTH,
ExifInterface.TAG_PIXEL_X_DIMENSION,
ExifInterface.TAG_PIXEL_Y_DIMENSION,
ExifInterface.TAG_THUMBNAIL_IMAGE_LENGTH,
ExifInterface.TAG_THUMBNAIL_IMAGE_WIDTH,
ExifInterface.TAG_ORIENTATION)
return tagFields
.map { tagField -> tagField.get(null) as String }
.filter { x -> !excludeAttributes.contains(x) }
.distinct()
}
private fun isExif(field: Field): Boolean {
return field.type == String::class.java &&
isPublicStaticFinal(field.modifiers) &&
field.name.startsWith("TAG_")
}
private const val publicStaticFinal = Modifier.PUBLIC or Modifier.STATIC or Modifier.FINAL
private fun isPublicStaticFinal(modifiers: Int): Boolean {
return modifiers and publicStaticFinal > 0
}
}
}

View file

@ -0,0 +1,5 @@
package com.simplemobiletools.gallery.pro.extensions
import com.simplemobiletools.commons.models.FileDirItem
fun FileDirItem.isDownloadsFolder() = path.isDownloadsFolder()

View file

@ -0,0 +1,5 @@
package com.simplemobiletools.gallery.pro.extensions
import com.simplemobiletools.commons.helpers.SORT_DESCENDING
fun Int.isSortingAscending() = this and SORT_DESCENDING == 0

View file

@ -1,4 +1,4 @@
package com.simplemobiletools.gallery.extensions
package com.simplemobiletools.gallery.pro.extensions
import android.content.Context
import android.content.res.Resources
@ -6,11 +6,10 @@ import android.util.TypedValue
fun Resources.getActionBarHeight(context: Context): Int {
val tv = TypedValue()
var height = 0
if (context.theme.resolveAttribute(android.R.attr.actionBarSize, tv, true)) {
height = TypedValue.complexToDimensionPixelSize(tv.data, displayMetrics)
}
return height
return if (context.theme.resolveAttribute(android.R.attr.actionBarSize, tv, true)) {
TypedValue.complexToDimensionPixelSize(tv.data, displayMetrics)
} else
0
}
fun Resources.getStatusBarHeight(): Int {

View file

@ -0,0 +1,87 @@
package com.simplemobiletools.gallery.pro.extensions
import android.os.Environment
import com.simplemobiletools.commons.helpers.NOMEDIA
import java.io.File
import java.io.IOException
fun String.isThisOrParentIncluded(includedPaths: MutableSet<String>) = includedPaths.any { equals(it, true) } || includedPaths.any { "$this/".startsWith("$it/", true) }
fun String.isThisOrParentExcluded(excludedPaths: MutableSet<String>) = excludedPaths.any { equals(it, true) } || excludedPaths.any { "$this/".startsWith("$it/", true) }
// cache which folders contain .nomedia files to avoid checking them over and over again
fun String.shouldFolderBeVisible(excludedPaths: MutableSet<String>, includedPaths: MutableSet<String>, showHidden: Boolean,
folderNoMediaStatuses: HashMap<String, Boolean>, callback: (path: String, hasNoMedia: Boolean) -> Unit): Boolean {
if (isEmpty()) {
return false
}
val file = File(this)
val filename = file.name
if (filename.startsWith("img_", true) && file.isDirectory) {
val files = file.list()
if (files != null) {
if (files.any { it.contains("burst", true) }) {
return false
}
}
}
if (!showHidden && filename.startsWith('.')) {
return false
} else if (includedPaths.contains(this)) {
return true
}
val containsNoMedia = if (showHidden) {
false
} else {
folderNoMediaStatuses.getOrElse("$this/$NOMEDIA", { false }) || File(this, NOMEDIA).exists()
}
return if (!showHidden && containsNoMedia) {
false
} else if (excludedPaths.contains(this)) {
false
} else if (isThisOrParentIncluded(includedPaths)) {
true
} else if (isThisOrParentExcluded(excludedPaths)) {
false
} else if (!showHidden) {
var containsNoMediaOrDot = containsNoMedia || contains("/.")
if (!containsNoMediaOrDot) {
var curPath = this
for (i in 0 until count { it == '/' } - 1) {
curPath = curPath.substringBeforeLast('/')
val pathToCheck = "$curPath/$NOMEDIA"
if (folderNoMediaStatuses.contains(pathToCheck)) {
if (folderNoMediaStatuses[pathToCheck] == true) {
containsNoMediaOrDot = true
break
}
} else {
val noMediaExists = folderNoMediaStatuses.getOrElse(pathToCheck, { false }) || File(pathToCheck).exists()
callback(pathToCheck, noMediaExists)
if (noMediaExists) {
containsNoMediaOrDot = true
break
}
}
}
}
!containsNoMediaOrDot
} else {
true
}
}
// recognize /sdcard/DCIM as the same folder as /storage/emulated/0/DCIM
fun String.getDistinctPath(): String {
return try {
File(this).canonicalPath.toLowerCase()
} catch (e: IOException) {
toLowerCase()
}
}
fun String.isDownloadsFolder() = equals(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString(), true)

View file

@ -0,0 +1,13 @@
package com.simplemobiletools.gallery.pro.extensions
import android.os.SystemClock
import android.view.MotionEvent
import android.view.View
fun View.sendFakeClick(x: Float, y: Float) {
val uptime = SystemClock.uptimeMillis()
val event = MotionEvent.obtain(uptime, uptime, MotionEvent.ACTION_DOWN, x, y, 0)
dispatchTouchEvent(event)
event.action = MotionEvent.ACTION_UP
dispatchTouchEvent(event)
}

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