Repo created

This commit is contained in:
Fr4nz D13trich 2025-11-22 13:51:39 +01:00
parent e09986deae
commit fa69fd81a1
48 changed files with 5156 additions and 0 deletions

11
.gitignore vendored Normal file
View file

@ -0,0 +1,11 @@
.externalNativeBuild
.gradle
.idea
*.iml
android.keystore
build
local.properties
MAVEN
tags
mupdf-*-android-viewer.apk
mupdf-*-android-viewer--app-release.aab

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "jni"]
path = jni
url = ../mupdf-android-fitz.git

661
COPYING Normal file
View file

@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are 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.
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.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
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 Affero 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. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
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 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 work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero 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 Affero 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 Affero 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 Affero 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 Affero 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
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 AGPL, see
<http://www.gnu.org/licenses/>.

47
Makefile Normal file
View file

@ -0,0 +1,47 @@
# This is a very simple Makefile that calls 'gradlew' to do the heavy lifting.
default: debug
debug:
./gradlew --warning-mode=all assembleDebug bundleDebug
release:
./gradlew --warning-mode=all assembleRelease bundleRelease
install:
./gradlew --warning-mode=all installDebug
install-release:
./gradlew --warning-mode=all installRelease
lint:
./gradlew --warning-mode=all lint
archive:
./gradlew --warning-mode=all publishReleasePublicationToLocalRepository
sync: archive
rsync -av --chmod=g+w --chown=:gs-web \
$(HOME)/MAVEN/com/artifex/mupdf/viewer/$(shell git describe --tags)/ \
ghostscript.com:/var/www/maven.ghostscript.com/com/artifex/mupdf/viewer/$(shell git describe --tags)/
rsync -av --chmod=g+w --chown=:gs-web \
$(HOME)/MAVEN/com/artifex/mupdf/viewer/maven-metadata.xml* \
ghostscript.com:/var/www/maven.ghostscript.com/com/artifex/mupdf/viewer/
tarball: release
cp app/build/outputs/apk/release/app-universal-release.apk \
mupdf-$(shell git describe --tags)-android-viewer.apk
cp app/build/outputs/bundle/release/app-release.aab \
mupdf-$(shell git describe --tags)-android-viewer-app-release.aab
synctarball: tarball
rsync -av --chmod=g+w --chown=:gs-web \
mupdf-$(shell git describe --tags)-android-viewer.apk \
ghostscript.com:/var/www/mupdf.com/downloads/archive/mupdf-$(shell git describe --tags)-android-viewer.apk
rsync -av --chmod=g+w --chown=:gs-web \
mupdf-$(shell git describe --tags)-android-viewer-app-release.aab \
ghostscript.com:/var/www/mupdf.com/downloads/archive/mupdf-$(shell git describe --tags)-android-viewer-app-release.aab
run: install
adb shell am start -n com.artifex.mupdf.viewer.app/.LibraryActivity
run-release: install-release
adb shell am start -n com.artifex.mupdf.viewer.app/.LibraryActivity
clean:
rm -rf .gradle build
rm -rf jni/.cxx jni/.externalNativeBuild jni/.gradle jni/build
rm -rf lib/.gradle lib/build
rm -rf app/.gradle app/build

143
README Normal file
View file

@ -0,0 +1,143 @@
# MuPDF Android Viewer
This project is a simplified variant of the full MuPDF Android app that only
supports viewing documents. The annotation editing and form filling features
are not present here.
This project builds both a viewer library and a viewer application.
The viewer library can be used to view PDF and other documents.
The application is a simple file chooser that shows a list of documents on the
external storage on your device, and hands off the selected file to the viewer
library.
## License
MuPDF is Copyright (c) 2006-2017 Artifex Software, Inc.
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero 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 Affero General Public License along
with this program. If not, see <http://www.gnu.org/licenses/>.
## Prerequisites
You need a working Android development environment, consisting of the Android
SKD and the Android NDK. The easiest way is to use Android Studio to download
and install the SDK and NDK.
## Building
Download the project using Git:
$ git clone git://git.ghostscript.com/mupdf-android-viewer.git
Edit the local.properties file to point to your Android SDK directory:
$ echo sdk.dir=$HOME/Android/Sdk > local.properties
If all tools have been installed as per the prerequisites, build the app using
the gradle wrapper:
$ ./gradlew assembleRelease
If all has gone well, you can now find the app APKs in app/build/outputs/apk/,
with one APK for each ABI. There is also a universal APK which supports all
ABIs.
The library component can be found in lib/build/outputs/aar/lib-release.aar.
## Running
To run the app in the android emulator, first you'll need to set up an "Android
Virtual Device" for the emulator. Run "android avd" and create a new device.
You can also use Android Studio to set up a virtual device. Use the x86 ABI for
best emulator performance.
Then launch the emulator, or connect a device with USB debugging enabled:
$ emulator -avd MyVirtualDevice &
Then copy some test files to the device:
$ adb push file.pdf /mnt/sdcard/Download
Then install the app on the device:
$ ./gradlew installDebug
To start the installed app on the device:
$ adb shell am start -n com.artifex.mupdf.viewer.app/.LibraryActivity
To see the error and debugging message log:
$ adb logcat
## Building the JNI library locally
The viewer library here is 100% pure java, but it uses the MuPDF fitz library,
which provides access to PDF rendering and other low level functionality.
The default is to use the JNI library artifact from the Ghostscript Maven
repository.
If you want to build the JNI code yourself, you will need to check out the
'jni' submodule recursively. You will also need a working host development
environment with a C compiler and GNU Make.
Either clone the original project with the --recursive flag, or initialize all
the submodules recursively by hand:
mupdf-mini $ git submodule update --init
mupdf-mini $ cd jni
mupdf-mini/jni $ git submodule update --init
mupdf-mini/jni $ cd libmupdf
mupdf-mini/jni/libmupdf $ git submodule update --init
Then you need to run the 'make generate' step in the libmupdf directory:
mupdf-mini/jni/libmupdf $ make generate
Once this is done, the build system should pick up the local JNI library
instead of using the Maven artifact.
## Release
To do a release you MUST first change the package name!
Do NOT use the com.artifex domain for your custom app!
In order to sign a release build, you will need to create a key and a key
store.
$ keytool -genkey -v -keystore app/android.keystore -alias MyKey \
-validity 3650 -keysize 2048 -keyalg RSA
Then add the following entries to app/gradle.properties:
release_storeFile=android.keystore
release_storePassword=<your keystore password>
release_keyAlias=MyKey
release_keyPassword=<your key password>
If your keystore has been set up properly, you can now build a signed release.
## Maven
The library component of this project can be packaged as a Maven artifact.
The default is to create the Maven artifact in the 'MAVEN' directory. You can
copy thoes files to the distribution site manually, or you can change the
uploadArchives repository in build.gradle before running the uploadArchives
task.
$ ./gradlew uploadArchives
Good Luck!

54
app/build.gradle Normal file
View file

@ -0,0 +1,54 @@
apply plugin: 'com.android.application'
group = 'com.artifex.mupdf'
version = '1.26.11a'
dependencies {
if (file('../lib/build.gradle').isFile())
api project(':lib')
else
api 'com.artifex.mupdf:viewer:1.26.11a'
}
android {
namespace 'com.artifex.mupdf.viewer.app'
compileSdkVersion 33
defaultConfig {
minSdkVersion 21
targetSdkVersion 35
versionName '1.26.11a'
versionCode 180
}
splits {
abi {
enable true
universalApk true
}
}
bundle {
abi {
enableSplit true
}
}
if (project.hasProperty('release_storeFile')) {
signingConfigs {
release {
storeFile file(release_storeFile)
storePassword release_storePassword
keyAlias release_keyAlias
keyPassword release_keyPassword
}
}
buildTypes {
release {
signingConfig signingConfigs.release
ndk {
debugSymbolLevel 'FULL'
}
}
}
}
}

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:installLocation="auto"
>
<application
android:label="MuPDF viewer"
android:icon="@drawable/ic_mupdf"
>
<activity
android:name=".LibraryActivity"
android:exported="true"
>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,68 @@
package com.artifex.mupdf.viewer.app;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import com.artifex.mupdf.fitz.Document; /* for file name recognition */
import com.artifex.mupdf.viewer.DocumentActivity;
public class LibraryActivity extends Activity
{
private final String APP = "MuPDF";
protected final int FILE_REQUEST = 42;
protected boolean selectingDocument;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
selectingDocument = false;
}
public void onStart() {
super.onStart();
if (!selectingDocument)
{
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[] {
// open the mime-types we know about
"application/pdf",
"application/vnd.ms-xpsdocument",
"application/oxps",
"application/x-cbz",
"application/vnd.comicbook+zip",
"application/epub+zip",
"application/x-fictionbook",
"application/x-mobipocket-ebook",
// ... and the ones android doesn't know about
"application/octet-stream"
});
startActivityForResult(intent, FILE_REQUEST);
selectingDocument = true;
}
}
public void onActivityResult(int request, int result, Intent data) {
if (request == FILE_REQUEST && result == Activity.RESULT_OK) {
if (data != null) {
Intent intent = new Intent(this, DocumentActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
intent.setAction(Intent.ACTION_VIEW);
intent.setDataAndType(data.getData(), data.getType());
intent.putExtra(getComponentName().getPackageName() + ".ReturnToLibraryActivity", 1);
startActivity(intent);
}
if (android.os.Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.S_V2)
finish();
} else if (request == FILE_REQUEST && result == Activity.RESULT_CANCELED) {
finish();
}
selectingDocument = false;
}
}

View file

@ -0,0 +1,52 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
android:viewportWidth="48"
android:viewportHeight="48"
android:width="48dp"
android:height="48dp">
<path
android:pathData="M33.935 3.923h0.495c3.29 0 5.951 -0.436 8.334 -2.214 1.197 -0.883 2.927 0.303 1.814 2.286a9.1 9.1 0 0 1 -2.89 3.12 7.4 7.4 0 0 1 1.947 0.533c2.237 0.931 3.773 2.83 4.366 5.007a7.56 7.56 0 0 1 -6.64 0.363 7 7 0 0 1 -0.835 -0.423v24.65c0 2.406 -0.967 2.914 -2.878 2.914h-5.624v5.395c0 0.58 -0.472 1.04 -1.04 1.04h-2.105c-0.29 0 -0.544 -0.109 -0.738 -0.302a1.03 1.03 0 0 1 -0.314 -0.738v-1.137c0 -1.306 -1.162 -1.306 -1.162 0v1.137c0 0.58 -0.471 1.04 -1.04 1.04h-2.104a1.05 1.05 0 0 1 -1.052 -1.04c0 -4.487 -8.007 -4.536 -8.007 0 0 0.58 -0.484 1.04 -1.053 1.04h-2.104c-0.29 0 -0.544 -0.109 -0.738 -0.302a1.03 1.03 0 0 1 -0.314 -0.738v-1.137c0 -1.21 -1.162 -1.21 -1.162 0v1.137c0 0.58 -0.483 1.04 -1.052 1.04H5.935a1.05 1.05 0 0 1 -1.053 -1.04V32.636c-1.947 0.786 -2.346 2.032 -2.382 3.653 1.753 1.536 1.657 4.5 -0.545 6.604 -2.467 -2.117 -2.419 -5.286 -0.858 -6.531 0.193 -7.91 10.026 -10.608 11.284 -17.925V14.3h0.024v-1.717a7 7 0 0 1 -0.87 0.435c-2.226 0.944 -4.657 0.726 -6.628 -0.363 0.592 -2.177 2.14 -4.076 4.366 -5.007a7.7 7.7 0 0 1 1.947 -0.532 7 7 0 0 1 -0.387 -0.266h0.012a9 9 0 0 1 -2.503 -2.843C7.229 2.012 8.958 0.814 10.156 1.71c2.407 1.79 5.394 2.214 8.321 2.214z">
<aapt:attr
name="android:fillColor">
<gradient
android:startX="23.99889"
android:startY="46.60577"
android:endX="23.99889"
android:endY="0.8045481"
android:tileMode="clamp">
<item
android:color="#FD4D07"
android:offset="0" />
<item
android:color="#FD8F2F"
android:offset="1" />
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M29.895 6.62l3.87 4.185 3.907 4.221v22.267h-22.4V6.62Z"
android:fillColor="#FFFFFF" />
<path
android:pathData="M37.672 37.293v-10.22c-3.23 -0.545 -7.354 -1.137 -11.212 -1.137 -4.233 0 -7.74 0.665 -11.2 1.306v10.063h22.412z"
android:fillColor="#FCEAB6" />
<path
android:pathData="M18.816 21.932h15.312v1.33H18.816Z"
android:fillColor="#FCEAB6" />
<path
android:pathData="M18.816 16.369h15.288v1.318H18.816Z"
android:fillColor="#FCEAB6" />
<path
android:pathData="M18.816 10.805h10.438v1.318H18.816Z"
android:fillColor="#FCEAB6" />
<path
android:pathData="M32.193 16.961A1.657 2.141 0 0 1 28.879 16.961A1.657 2.141 0 0 1 32.193 16.961Z"
android:fillColor="#FF7C21" />
<path
android:pathData="M24.053 16.961A1.657 2.141 0 0 1 20.739 16.961A1.657 2.141 0 0 1 24.053 16.961Z"
android:fillColor="#FF7C21" />
<path
android:pathData="M23.17 31.62A2.322 3.096 0 0 1 18.526 31.62A2.322 3.096 0 0 1 23.17 31.62Z"
android:fillColor="#FF610F" />
<path
android:pathData="M34.418 31.62A2.322 3.096 0 0 1 29.774 31.62A2.322 3.096 0 0 1 34.418 31.62Z"
android:fillColor="#FF610F" />
</vector>

27
build.gradle Normal file
View file

@ -0,0 +1,27 @@
buildscript {
repositories {
if (project.hasProperty('MAVEN_REPO')) {
maven { url MAVEN_REPO }
} else {
maven { url "file://${System.properties['user.home']}/MAVEN" }
}
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.5.2'
}
}
allprojects {
repositories {
if (project.hasProperty('MAVEN_REPO')) {
maven { url MAVEN_REPO }
} else {
maven { url "file://${System.properties['user.home']}/MAVEN" }
}
maven { url 'https://maven.ghostscript.com/' }
google()
mavenCentral()
}
}

1
gradle.properties Normal file
View file

@ -0,0 +1 @@
android.useAndroidX=true

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

Binary file not shown.

View file

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

188
gradlew vendored Executable file
View file

@ -0,0 +1,188 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

100
gradlew.bat vendored Normal file
View file

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

68
lib/build.gradle Normal file
View file

@ -0,0 +1,68 @@
apply plugin: 'com.android.library'
apply plugin: 'maven-publish'
group = 'com.artifex.mupdf'
version = '1.26.11a'
dependencies {
implementation 'androidx.appcompat:appcompat:1.1.+'
if (file('../jni/build.gradle').isFile())
api project(':jni')
else
api 'com.artifex.mupdf:fitz:1.26.11'
}
android {
namespace 'com.artifex.mupdf.viewer'
compileSdkVersion 33
defaultConfig {
minSdkVersion 21
targetSdkVersion 35
}
publishing {
singleVariant("release") {
withSourcesJar()
}
}
}
project.afterEvaluate {
publishing {
publications {
release(MavenPublication) {
artifactId 'viewer'
artifact(bundleReleaseAar)
pom {
name = 'viewer'
url = 'http://www.mupdf.com'
licenses {
license {
name = 'GNU Affero General Public License'
url = 'https://www.gnu.org/licenses/agpl-3.0.html'
}
}
}
pom.withXml {
final dependenciesNode = asNode().appendNode('dependencies')
configurations.implementation.allDependencies.each {
def dependencyNode = dependenciesNode.appendNode('dependency')
dependencyNode.appendNode('groupId', it.group)
dependencyNode.appendNode('artifactId', it.name)
dependencyNode.appendNode('version', it.version)
}
}
}
}
repositories {
maven {
name 'Local'
if (project.hasProperty('MAVEN_REPO')) {
url = MAVEN_REPO
} else {
url = "file://${System.properties['user.home']}/MAVEN"
}
}
}
}
}

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application>
<activity
android:name=".DocumentActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:exported="true"
>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
<!-- list the mime-types we know about -->
<data android:mimeType="application/pdf" />
<data android:mimeType="application/vnd.ms-xpsdocument" />
<data android:mimeType="application/oxps" />
<data android:mimeType="application/vnd.comicbook+zip" />
<data android:mimeType="application/x-cbz" />
<data android:mimeType="application/epub+zip" />
<data android:mimeType="application/x-fictionbook" />
<data android:mimeType="application/x-mobipocket-ebook" />
<!-- list application/octet-stream to catch the ones android doesn't recognize -->
<data android:mimeType="application/octet-stream" />
</intent-filter>
</activity>
<activity
android:name=".OutlineActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,88 @@
package com.artifex.mupdf.viewer;
import android.os.AsyncTask;
import android.util.Log;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
// Ideally this would be a subclass of AsyncTask, however the cancel() method is final, and cannot
// be overridden. I felt that having two different, but similar cancel methods was a bad idea.
public class CancellableAsyncTask<Params, Result>
{
private final String APP = "MuPDF";
private final AsyncTask<Params, Void, Result> asyncTask;
private final CancellableTaskDefinition<Params, Result> ourTask;
public void onPreExecute()
{
}
public void onPostExecute(Result result)
{
}
public CancellableAsyncTask(final CancellableTaskDefinition<Params, Result> task)
{
if (task == null)
throw new IllegalArgumentException();
this.ourTask = task;
asyncTask = new AsyncTask<Params, Void, Result>()
{
@Override
protected Result doInBackground(Params... params)
{
return task.doInBackground(params);
}
@Override
protected void onPreExecute()
{
CancellableAsyncTask.this.onPreExecute();
}
@Override
protected void onPostExecute(Result result)
{
CancellableAsyncTask.this.onPostExecute(result);
task.doCleanup();
}
@Override
protected void onCancelled(Result result)
{
task.doCleanup();
}
};
}
public void cancel()
{
this.asyncTask.cancel(true);
ourTask.doCancel();
try
{
this.asyncTask.get();
}
catch (InterruptedException e)
{
}
catch (ExecutionException e)
{
}
catch (CancellationException e)
{
}
}
public void execute(Params ... params)
{
asyncTask.execute(params);
}
}

View file

@ -0,0 +1,8 @@
package com.artifex.mupdf.viewer;
public interface CancellableTaskDefinition <Params, Result>
{
public Result doInBackground(Params ... params);
public void doCancel();
public void doCleanup();
}

View file

@ -0,0 +1,95 @@
package com.artifex.mupdf.viewer;
import com.artifex.mupdf.fitz.SeekableInputStream;
import android.util.Log;
import android.content.ContentResolver;
import android.net.Uri;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
public class ContentInputStream implements SeekableInputStream {
private final String APP = "MuPDF";
protected ContentResolver cr;
protected Uri uri;
protected InputStream is;
protected long length, p;
protected boolean mustReopenStream;
public ContentInputStream(ContentResolver cr, Uri uri, long size) throws IOException {
this.cr = cr;
this.uri = uri;
length = size;
mustReopenStream = false;
reopenStream();
}
public long seek(long offset, int whence) throws IOException {
long newp = p;
switch (whence) {
case SEEK_SET:
newp = offset;
break;
case SEEK_CUR:
newp = p + offset;
break;
case SEEK_END:
if (length < 0) {
byte[] buf = new byte[16384];
int k;
while ((k = is.read(buf)) != -1)
p += k;
length = p;
}
newp = length + offset;
break;
}
if (newp < p) {
if (!mustReopenStream) {
try {
is.skip(newp - p);
} catch (IOException x) {
Log.i(APP, "Unable to skip backwards, reopening input stream");
mustReopenStream = true;
}
}
if (mustReopenStream) {
reopenStream();
is.skip(newp);
}
} else if (newp > p) {
is.skip(newp - p);
}
return p = newp;
}
public long position() throws IOException {
return p;
}
public int read(byte[] buf) throws IOException {
int n = is.read(buf);
if (n > 0)
p += n;
else if (n < 0 && length < 0)
length = p;
return n;
}
public void reopenStream() throws IOException {
if (is != null)
{
is.close();
is = null;
}
is = cr.openInputStream(uri);
p = 0;
}
}

View file

@ -0,0 +1,844 @@
package com.artifex.mupdf.viewer;
import com.artifex.mupdf.fitz.SeekableInputStream;
import android.Manifest;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface.OnCancelListener;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.RectShape;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.provider.OpenableColumns;
import android.text.Editable;
import android.text.TextWatcher;
import android.text.method.PasswordTransformationMethod;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem.OnMenuItemClickListener;
import android.view.MenuItem;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.view.animation.Animation;
import android.view.animation.TranslateAnimation;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.PopupMenu;
import android.widget.RelativeLayout;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.ViewAnimator;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Locale;
public class DocumentActivity extends Activity
{
private final String APP = "MuPDF";
/* The core rendering instance */
enum TopBarMode {Main, Search, More};
private final int OUTLINE_REQUEST=0;
private MuPDFCore core;
private String mDocTitle;
private String mDocKey;
private ReaderView mDocView;
private View mButtonsView;
private boolean mButtonsVisible;
private EditText mPasswordView;
private TextView mDocNameView;
private SeekBar mPageSlider;
private int mPageSliderRes;
private TextView mPageNumberView;
private ImageButton mSearchButton;
private ImageButton mOutlineButton;
private ViewAnimator mTopBarSwitcher;
private ImageButton mLinkButton;
private TopBarMode mTopBarMode = TopBarMode.Main;
private ImageButton mSearchBack;
private ImageButton mSearchFwd;
private ImageButton mSearchClose;
private EditText mSearchText;
private SearchTask mSearchTask;
private AlertDialog.Builder mAlertBuilder;
private boolean mLinkHighlight = false;
private final Handler mHandler = new Handler();
private boolean mAlertsActive= false;
private AlertDialog mAlertDialog;
private ArrayList<OutlineActivity.Item> mFlatOutline;
private boolean mReturnToLibraryActivity = false;
protected int mDisplayDPI;
private int mLayoutEM = 10;
private int mLayoutW = 312;
private int mLayoutH = 504;
protected View mLayoutButton;
protected PopupMenu mLayoutPopupMenu;
private String toHex(byte[] digest) {
StringBuilder builder = new StringBuilder(2 * digest.length);
for (byte b : digest)
builder.append(String.format("%02x", b));
return builder.toString();
}
private MuPDFCore openBuffer(byte buffer[], String magic)
{
try
{
core = new MuPDFCore(buffer, magic);
}
catch (Exception e)
{
Log.e(APP, "Error opening document buffer: " + e);
return null;
}
return core;
}
private MuPDFCore openStream(SeekableInputStream stm, String magic)
{
try
{
core = new MuPDFCore(stm, magic);
}
catch (Exception e)
{
Log.e(APP, "Error opening document stream: " + e);
return null;
}
return core;
}
private MuPDFCore openCore(Uri uri, long size, String mimetype) throws IOException {
ContentResolver cr = getContentResolver();
Log.i(APP, "Opening document " + uri);
InputStream is = cr.openInputStream(uri);
byte[] buf = null;
int used = -1;
try {
final int limit = 8 * 1024 * 1024;
if (size < 0) { // size is unknown
buf = new byte[limit];
used = is.read(buf);
boolean atEOF = is.read() == -1;
if (used < 0 || (used == limit && !atEOF)) // no or partial data
buf = null;
} else if (size <= limit) { // size is known and below limit
buf = new byte[(int) size];
used = is.read(buf);
if (used < 0 || used < size) // no or partial data
buf = null;
}
if (buf != null && buf.length != used) {
byte[] newbuf = new byte[used];
System.arraycopy(buf, 0, newbuf, 0, used);
buf = newbuf;
}
} catch (OutOfMemoryError e) {
buf = null;
} finally {
is.close();
}
if (buf != null) {
Log.i(APP, " Opening document from memory buffer of size " + buf.length);
return openBuffer(buf, mimetype);
} else {
Log.i(APP, " Opening document from stream");
return openStream(new ContentInputStream(cr, uri, size), mimetype);
}
}
private void showCannotOpenDialog(String reason) {
Resources res = getResources();
AlertDialog alert = mAlertBuilder.create();
setTitle(String.format(Locale.ROOT, res.getString(R.string.cannot_open_document_Reason), reason));
alert.setButton(AlertDialog.BUTTON_POSITIVE, getString(R.string.dismiss),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
finish();
}
});
alert.show();
}
/** Called when the activity is first created. */
@Override
public void onCreate(final Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
mDisplayDPI = (int)metrics.densityDpi;
mAlertBuilder = new AlertDialog.Builder(this);
if (core == null) {
if (savedInstanceState != null && savedInstanceState.containsKey("DocTitle")) {
mDocTitle = savedInstanceState.getString("DocTitle");
}
}
if (core == null) {
Intent intent = getIntent();
SeekableInputStream file;
mReturnToLibraryActivity = intent.getIntExtra(getComponentName().getPackageName() + ".ReturnToLibraryActivity", 0) != 0;
if (Intent.ACTION_VIEW.equals(intent.getAction())) {
Uri uri = intent.getData();
String mimetype = getIntent().getType();
if (uri == null) {
showCannotOpenDialog("No document uri to open");
return;
}
mDocKey = uri.toString();
Log.i(APP, "OPEN URI " + uri.toString());
Log.i(APP, " MAGIC (Intent) " + mimetype);
mDocTitle = null;
long size = -1;
Cursor cursor = null;
try {
cursor = getContentResolver().query(uri, null, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
int idx;
idx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
if (idx >= 0 && cursor.getType(idx) == Cursor.FIELD_TYPE_STRING)
mDocTitle = cursor.getString(idx);
idx = cursor.getColumnIndex(OpenableColumns.SIZE);
if (idx >= 0 && cursor.getType(idx) == Cursor.FIELD_TYPE_INTEGER)
size = cursor.getLong(idx);
if (size == 0)
size = -1;
}
} catch (Exception x) {
// Ignore any exception and depend on default values for title
// and size (unless one was decoded
} finally {
if (cursor != null)
cursor.close();
}
Log.i(APP, " NAME " + mDocTitle);
Log.i(APP, " SIZE " + size);
if (mimetype == null || mimetype.equals("application/octet-stream")) {
mimetype = getContentResolver().getType(uri);
Log.i(APP, " MAGIC (Resolved) " + mimetype);
}
if (mimetype == null || mimetype.equals("application/octet-stream")) {
mimetype = mDocTitle;
Log.i(APP, " MAGIC (Filename) " + mimetype);
}
try {
core = openCore(uri, size, mimetype);
SearchTaskResult.set(null);
} catch (Exception x) {
showCannotOpenDialog(x.toString());
return;
}
}
if (core != null && core.needsPassword()) {
requestPassword(savedInstanceState);
return;
}
if (core != null && core.countPages() == 0)
{
core = null;
}
}
if (core == null)
{
AlertDialog alert = mAlertBuilder.create();
alert.setTitle(R.string.cannot_open_document);
alert.setButton(AlertDialog.BUTTON_POSITIVE, getString(R.string.dismiss),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
finish();
}
});
alert.setOnCancelListener(new OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
finish();
}
});
alert.show();
return;
}
createUI(savedInstanceState);
}
public void requestPassword(final Bundle savedInstanceState) {
mPasswordView = new EditText(this);
mPasswordView.setInputType(EditorInfo.TYPE_TEXT_VARIATION_PASSWORD);
mPasswordView.setTransformationMethod(new PasswordTransformationMethod());
AlertDialog alert = mAlertBuilder.create();
alert.setTitle(R.string.enter_password);
alert.setView(mPasswordView);
alert.setButton(AlertDialog.BUTTON_POSITIVE, getString(R.string.okay),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
if (core.authenticatePassword(mPasswordView.getText().toString())) {
createUI(savedInstanceState);
} else {
requestPassword(savedInstanceState);
}
}
});
alert.setButton(AlertDialog.BUTTON_NEGATIVE, getString(R.string.cancel),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
finish();
}
});
alert.show();
}
public void relayoutDocument() {
int loc = core.layout(mDocView.mCurrent, mLayoutW, mLayoutH, mLayoutEM);
mFlatOutline = null;
mDocView.mHistory.clear();
mDocView.refresh();
mDocView.setDisplayedViewIndex(loc);
}
public void createUI(Bundle savedInstanceState) {
if (core == null)
return;
// Now create the UI.
// First create the document view
mDocView = new ReaderView(this) {
@Override
protected void onMoveToChild(int i) {
if (core == null)
return;
mPageNumberView.setText(String.format(Locale.ROOT, "%d / %d", i + 1, core.countPages()));
mPageSlider.setMax((core.countPages() - 1) * mPageSliderRes);
mPageSlider.setProgress(i * mPageSliderRes);
super.onMoveToChild(i);
}
@Override
protected void onTapMainDocArea() {
if (!mButtonsVisible) {
showButtons();
} else {
if (mTopBarMode == TopBarMode.Main)
hideButtons();
}
}
@Override
protected void onDocMotion() {
hideButtons();
}
@Override
public void onSizeChanged(int w, int h, int oldw, int oldh) {
if (core.isReflowable()) {
mLayoutW = w * 72 / mDisplayDPI;
mLayoutH = h * 72 / mDisplayDPI;
relayoutDocument();
} else {
refresh();
}
}
};
mDocView.setAdapter(new PageAdapter(this, core));
mSearchTask = new SearchTask(this, core) {
@Override
protected void onTextFound(SearchTaskResult result) {
SearchTaskResult.set(result);
// Ask the ReaderView to move to the resulting page
mDocView.setDisplayedViewIndex(result.pageNumber);
// Make the ReaderView act on the change to SearchTaskResult
// via overridden onChildSetup method.
mDocView.resetupChildren();
}
};
// Make the buttons overlay, and store all its
// controls in variables
makeButtonsView();
// Set up the page slider
int smax = Math.max(core.countPages()-1,1);
mPageSliderRes = ((10 + smax - 1)/smax) * 2;
// Set the file-name text
String docTitle = core.getTitle();
if (docTitle != null)
mDocNameView.setText(docTitle);
else
mDocNameView.setText(mDocTitle);
// Activate the seekbar
mPageSlider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
public void onStopTrackingTouch(SeekBar seekBar) {
mDocView.pushHistory();
mDocView.setDisplayedViewIndex((seekBar.getProgress()+mPageSliderRes/2)/mPageSliderRes);
}
public void onStartTrackingTouch(SeekBar seekBar) {}
public void onProgressChanged(SeekBar seekBar, int progress,
boolean fromUser) {
updatePageNumView((progress+mPageSliderRes/2)/mPageSliderRes);
}
});
// Activate the search-preparing button
mSearchButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
searchModeOn();
}
});
mSearchClose.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
searchModeOff();
}
});
// Search invoking buttons are disabled while there is no text specified
mSearchBack.setEnabled(false);
mSearchFwd.setEnabled(false);
mSearchBack.setColorFilter(Color.argb(255, 128, 128, 128));
mSearchFwd.setColorFilter(Color.argb(255, 128, 128, 128));
// React to interaction with the text widget
mSearchText.addTextChangedListener(new TextWatcher() {
public void afterTextChanged(Editable s) {
boolean haveText = s.toString().length() > 0;
setButtonEnabled(mSearchBack, haveText);
setButtonEnabled(mSearchFwd, haveText);
// Remove any previous search results
if (SearchTaskResult.get() != null && !mSearchText.getText().toString().equals(SearchTaskResult.get().txt)) {
SearchTaskResult.set(null);
mDocView.resetupChildren();
}
}
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {}
public void onTextChanged(CharSequence s, int start, int before,
int count) {}
});
//React to Done button on keyboard
mSearchText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_DONE)
search(1);
return false;
}
});
mSearchText.setOnKeyListener(new View.OnKeyListener() {
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER)
search(1);
return false;
}
});
// Activate search invoking buttons
mSearchBack.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
search(-1);
}
});
mSearchFwd.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
search(1);
}
});
mLinkButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
setLinkHighlight(!mLinkHighlight);
}
});
if (core.isReflowable()) {
mLayoutButton.setVisibility(View.VISIBLE);
mLayoutPopupMenu = new PopupMenu(this, mLayoutButton);
mLayoutPopupMenu.getMenuInflater().inflate(R.menu.layout_menu, mLayoutPopupMenu.getMenu());
mLayoutPopupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
float oldLayoutEM = mLayoutEM;
int id = item.getItemId();
if (id == R.id.action_layout_6pt) mLayoutEM = 6;
else if (id == R.id.action_layout_7pt) mLayoutEM = 7;
else if (id == R.id.action_layout_8pt) mLayoutEM = 8;
else if (id == R.id.action_layout_9pt) mLayoutEM = 9;
else if (id == R.id.action_layout_10pt) mLayoutEM = 10;
else if (id == R.id.action_layout_11pt) mLayoutEM = 11;
else if (id == R.id.action_layout_12pt) mLayoutEM = 12;
else if (id == R.id.action_layout_13pt) mLayoutEM = 13;
else if (id == R.id.action_layout_14pt) mLayoutEM = 14;
else if (id == R.id.action_layout_15pt) mLayoutEM = 15;
else if (id == R.id.action_layout_16pt) mLayoutEM = 16;
if (oldLayoutEM != mLayoutEM)
relayoutDocument();
return true;
}
});
mLayoutButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
mLayoutPopupMenu.show();
}
});
}
if (core.hasOutline()) {
mOutlineButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
if (mFlatOutline == null)
mFlatOutline = core.getOutline();
if (mFlatOutline != null) {
Intent intent = new Intent(DocumentActivity.this, OutlineActivity.class);
Bundle bundle = new Bundle();
bundle.putInt("POSITION", mDocView.getDisplayedViewIndex());
bundle.putSerializable("OUTLINE", mFlatOutline);
intent.putExtra("PALLETBUNDLE", Pallet.sendBundle(bundle));
startActivityForResult(intent, OUTLINE_REQUEST);
}
}
});
} else {
mOutlineButton.setVisibility(View.GONE);
}
// Reenstate last state if it was recorded
SharedPreferences prefs = getPreferences(Context.MODE_PRIVATE);
mDocView.setDisplayedViewIndex(prefs.getInt("page"+mDocKey, 0));
if (savedInstanceState == null || !savedInstanceState.getBoolean("ButtonsHidden", false))
showButtons();
if(savedInstanceState != null && savedInstanceState.getBoolean("SearchMode", false))
searchModeOn();
// Stick the document view and the buttons overlay into a parent view
RelativeLayout layout = new RelativeLayout(this);
layout.setBackgroundColor(Color.DKGRAY);
layout.addView(mDocView);
layout.addView(mButtonsView);
setContentView(layout);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case OUTLINE_REQUEST:
if (resultCode >= RESULT_FIRST_USER && mDocView != null) {
mDocView.pushHistory();
mDocView.setDisplayedViewIndex(resultCode-RESULT_FIRST_USER);
}
break;
}
super.onActivityResult(requestCode, resultCode, data);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (mDocKey != null && mDocView != null) {
if (mDocTitle != null)
outState.putString("DocTitle", mDocTitle);
// Store current page in the prefs against the file name,
// so that we can pick it up each time the file is loaded
// Other info is needed only for screen-orientation change,
// so it can go in the bundle
SharedPreferences prefs = getPreferences(Context.MODE_PRIVATE);
SharedPreferences.Editor edit = prefs.edit();
edit.putInt("page"+mDocKey, mDocView.getDisplayedViewIndex());
edit.apply();
}
if (!mButtonsVisible)
outState.putBoolean("ButtonsHidden", true);
if (mTopBarMode == TopBarMode.Search)
outState.putBoolean("SearchMode", true);
}
@Override
protected void onPause() {
super.onPause();
if (mSearchTask != null)
mSearchTask.stop();
if (mDocKey != null && mDocView != null) {
SharedPreferences prefs = getPreferences(Context.MODE_PRIVATE);
SharedPreferences.Editor edit = prefs.edit();
edit.putInt("page"+mDocKey, mDocView.getDisplayedViewIndex());
edit.apply();
}
}
public void onDestroy()
{
if (mDocView != null) {
mDocView.applyToChildren(new ReaderView.ViewMapper() {
@Override
public void applyToView(View view) {
((PageView)view).releaseBitmaps();
}
});
}
if (core != null)
core.onDestroy();
core = null;
super.onDestroy();
}
private void setButtonEnabled(ImageButton button, boolean enabled) {
button.setEnabled(enabled);
button.setColorFilter(enabled ? Color.argb(255, 255, 255, 255) : Color.argb(255, 128, 128, 128));
}
private void setLinkHighlight(boolean highlight) {
mLinkHighlight = highlight;
// LINK_COLOR tint
mLinkButton.setColorFilter(highlight ? Color.argb(0xFF, 0x00, 0x66, 0xCC) : Color.argb(0xFF, 255, 255, 255));
// Inform pages of the change.
mDocView.setLinksEnabled(highlight);
}
private void showButtons() {
if (core == null)
return;
if (!mButtonsVisible) {
mButtonsVisible = true;
// Update page number text and slider
int index = mDocView.getDisplayedViewIndex();
updatePageNumView(index);
mPageSlider.setMax((core.countPages()-1)*mPageSliderRes);
mPageSlider.setProgress(index * mPageSliderRes);
if (mTopBarMode == TopBarMode.Search) {
mSearchText.requestFocus();
showKeyboard();
}
Animation anim = new TranslateAnimation(0, 0, -mTopBarSwitcher.getHeight(), 0);
anim.setDuration(200);
anim.setAnimationListener(new Animation.AnimationListener() {
public void onAnimationStart(Animation animation) {
mTopBarSwitcher.setVisibility(View.VISIBLE);
}
public void onAnimationRepeat(Animation animation) {}
public void onAnimationEnd(Animation animation) {}
});
mTopBarSwitcher.startAnimation(anim);
anim = new TranslateAnimation(0, 0, mPageSlider.getHeight(), 0);
anim.setDuration(200);
anim.setAnimationListener(new Animation.AnimationListener() {
public void onAnimationStart(Animation animation) {
mPageSlider.setVisibility(View.VISIBLE);
}
public void onAnimationRepeat(Animation animation) {}
public void onAnimationEnd(Animation animation) {
mPageNumberView.setVisibility(View.VISIBLE);
}
});
mPageSlider.startAnimation(anim);
}
}
private void hideButtons() {
if (mButtonsVisible) {
mButtonsVisible = false;
hideKeyboard();
Animation anim = new TranslateAnimation(0, 0, 0, -mTopBarSwitcher.getHeight());
anim.setDuration(200);
anim.setAnimationListener(new Animation.AnimationListener() {
public void onAnimationStart(Animation animation) {}
public void onAnimationRepeat(Animation animation) {}
public void onAnimationEnd(Animation animation) {
mTopBarSwitcher.setVisibility(View.INVISIBLE);
}
});
mTopBarSwitcher.startAnimation(anim);
anim = new TranslateAnimation(0, 0, 0, mPageSlider.getHeight());
anim.setDuration(200);
anim.setAnimationListener(new Animation.AnimationListener() {
public void onAnimationStart(Animation animation) {
mPageNumberView.setVisibility(View.INVISIBLE);
}
public void onAnimationRepeat(Animation animation) {}
public void onAnimationEnd(Animation animation) {
mPageSlider.setVisibility(View.INVISIBLE);
}
});
mPageSlider.startAnimation(anim);
}
}
private void searchModeOn() {
if (mTopBarMode != TopBarMode.Search) {
mTopBarMode = TopBarMode.Search;
//Focus on EditTextWidget
mSearchText.requestFocus();
showKeyboard();
mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal());
}
}
private void searchModeOff() {
if (mTopBarMode == TopBarMode.Search) {
mTopBarMode = TopBarMode.Main;
hideKeyboard();
mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal());
SearchTaskResult.set(null);
// Make the ReaderView act on the change to mSearchTaskResult
// via overridden onChildSetup method.
mDocView.resetupChildren();
}
}
private void updatePageNumView(int index) {
if (core == null)
return;
mPageNumberView.setText(String.format(Locale.ROOT, "%d / %d", index + 1, core.countPages()));
}
private void makeButtonsView() {
mButtonsView = getLayoutInflater().inflate(R.layout.document_activity, null);
mDocNameView = (TextView)mButtonsView.findViewById(R.id.docNameText);
mPageSlider = (SeekBar)mButtonsView.findViewById(R.id.pageSlider);
mPageNumberView = (TextView)mButtonsView.findViewById(R.id.pageNumber);
mSearchButton = (ImageButton)mButtonsView.findViewById(R.id.searchButton);
mOutlineButton = (ImageButton)mButtonsView.findViewById(R.id.outlineButton);
mTopBarSwitcher = (ViewAnimator)mButtonsView.findViewById(R.id.switcher);
mSearchBack = (ImageButton)mButtonsView.findViewById(R.id.searchBack);
mSearchFwd = (ImageButton)mButtonsView.findViewById(R.id.searchForward);
mSearchClose = (ImageButton)mButtonsView.findViewById(R.id.searchClose);
mSearchText = (EditText)mButtonsView.findViewById(R.id.searchText);
mLinkButton = (ImageButton)mButtonsView.findViewById(R.id.linkButton);
mLayoutButton = mButtonsView.findViewById(R.id.layoutButton);
mTopBarSwitcher.setVisibility(View.INVISIBLE);
mPageNumberView.setVisibility(View.INVISIBLE);
mPageSlider.setVisibility(View.INVISIBLE);
}
private void showKeyboard() {
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null)
imm.showSoftInput(mSearchText, 0);
}
private void hideKeyboard() {
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null)
imm.hideSoftInputFromWindow(mSearchText.getWindowToken(), 0);
}
private void search(int direction) {
hideKeyboard();
int displayPage = mDocView.getDisplayedViewIndex();
SearchTaskResult r = SearchTaskResult.get();
int searchPage = r != null ? r.pageNumber : -1;
mSearchTask.go(mSearchText.getText().toString(), direction, displayPage, searchPage);
}
@Override
public boolean onSearchRequested() {
if (mButtonsVisible && mTopBarMode == TopBarMode.Search) {
hideButtons();
} else {
showButtons();
searchModeOn();
}
return super.onSearchRequested();
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
if (mButtonsVisible && mTopBarMode != TopBarMode.Search) {
hideButtons();
} else {
showButtons();
searchModeOff();
}
return super.onPrepareOptionsMenu(menu);
}
@Override
protected void onStart() {
super.onStart();
}
@Override
protected void onStop() {
super.onStop();
}
@Override
public void onBackPressed() {
if (mDocView == null || (mDocView != null && !mDocView.popHistory())) {
super.onBackPressed();
if (mReturnToLibraryActivity) {
Intent intent = getPackageManager().getLaunchIntentForPackage(getComponentName().getPackageName());
startActivity(intent);
}
}
}
}

View file

@ -0,0 +1,40 @@
package com.artifex.mupdf.viewer;
import com.artifex.mupdf.fitz.Cookie;
public abstract class MuPDFCancellableTaskDefinition<Params, Result> implements CancellableTaskDefinition<Params, Result>
{
private Cookie cookie;
public MuPDFCancellableTaskDefinition()
{
this.cookie = new Cookie();
}
@Override
public void doCancel()
{
if (cookie == null)
return;
cookie.abort();
}
@Override
public void doCleanup()
{
if (cookie == null)
return;
cookie.destroy();
cookie = null;
}
@Override
public final Result doInBackground(Params ... params)
{
return doInBackground(cookie, params);
}
public abstract Result doInBackground(Cookie cookie, Params ... params);
}

View file

@ -0,0 +1,232 @@
package com.artifex.mupdf.viewer;
import com.artifex.mupdf.fitz.Cookie;
import com.artifex.mupdf.fitz.DisplayList;
import com.artifex.mupdf.fitz.Document;
import com.artifex.mupdf.fitz.Link;
import com.artifex.mupdf.fitz.Matrix;
import com.artifex.mupdf.fitz.Outline;
import com.artifex.mupdf.fitz.Page;
import com.artifex.mupdf.fitz.Quad;
import com.artifex.mupdf.fitz.Rect;
import com.artifex.mupdf.fitz.RectI;
import com.artifex.mupdf.fitz.SeekableInputStream;
import com.artifex.mupdf.fitz.android.AndroidDrawDevice;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.PointF;
import android.util.Log;
import java.util.ArrayList;
public class MuPDFCore
{
private final String APP = "MuPDF";
private int resolution;
private Document doc;
private Outline[] outline;
private int pageCount = -1;
private boolean reflowable = false;
private int currentPage;
private Page page;
private float pageWidth;
private float pageHeight;
private DisplayList displayList;
/* Default to "A Format" pocket book size. */
private int layoutW = 312;
private int layoutH = 504;
private int layoutEM = 10;
private MuPDFCore(Document doc) {
this.doc = doc;
doc.layout(layoutW, layoutH, layoutEM);
pageCount = doc.countPages();
reflowable = doc.isReflowable();
resolution = 160;
currentPage = -1;
}
public MuPDFCore(byte buffer[], String magic) {
this(Document.openDocument(buffer, magic));
}
public MuPDFCore(SeekableInputStream stm, String magic) {
this(Document.openDocument(stm, magic));
}
public String getTitle() {
return doc.getMetaData(Document.META_INFO_TITLE);
}
public int countPages() {
return pageCount;
}
public boolean isReflowable() {
return reflowable;
}
public synchronized int layout(int oldPage, int w, int h, int em) {
if (w != layoutW || h != layoutH || em != layoutEM) {
System.out.println("LAYOUT: " + w + "," + h);
layoutW = w;
layoutH = h;
layoutEM = em;
long mark = doc.makeBookmark(doc.locationFromPageNumber(oldPage));
doc.layout(layoutW, layoutH, layoutEM);
currentPage = -1;
pageCount = doc.countPages();
outline = null;
try {
outline = doc.loadOutline();
} catch (Exception ex) {
/* ignore error */
}
return doc.pageNumberFromLocation(doc.findBookmark(mark));
}
return oldPage;
}
private synchronized void gotoPage(int pageNum) {
/* TODO: page cache */
if (pageNum > pageCount-1)
pageNum = pageCount-1;
else if (pageNum < 0)
pageNum = 0;
if (pageNum != currentPage) {
if (page != null)
page.destroy();
page = null;
if (displayList != null)
displayList.destroy();
displayList = null;
page = null;
pageWidth = 0;
pageHeight = 0;
currentPage = -1;
if (doc != null) {
page = doc.loadPage(pageNum);
Rect b = page.getBounds();
pageWidth = b.x1 - b.x0;
pageHeight = b.y1 - b.y0;
}
currentPage = pageNum;
}
}
public synchronized PointF getPageSize(int pageNum) {
gotoPage(pageNum);
return new PointF(pageWidth, pageHeight);
}
public synchronized void onDestroy() {
if (displayList != null)
displayList.destroy();
displayList = null;
if (page != null)
page.destroy();
page = null;
if (doc != null)
doc.destroy();
doc = null;
}
public synchronized void drawPage(Bitmap bm, int pageNum,
int pageW, int pageH,
int patchX, int patchY,
int patchW, int patchH,
Cookie cookie) {
gotoPage(pageNum);
if (displayList == null && page != null)
try {
displayList = page.toDisplayList();
} catch (Exception ex) {
displayList = null;
}
if (displayList == null || page == null)
return;
float zoom = resolution / 72;
Matrix ctm = new Matrix(zoom, zoom);
RectI bbox = new RectI(page.getBounds().transform(ctm));
float xscale = (float)pageW / (float)(bbox.x1-bbox.x0);
float yscale = (float)pageH / (float)(bbox.y1-bbox.y0);
ctm.scale(xscale, yscale);
AndroidDrawDevice dev = new AndroidDrawDevice(bm, patchX, patchY);
try {
displayList.run(dev, ctm, cookie);
dev.close();
} finally {
dev.destroy();
}
}
public synchronized void updatePage(Bitmap bm, int pageNum,
int pageW, int pageH,
int patchX, int patchY,
int patchW, int patchH,
Cookie cookie) {
drawPage(bm, pageNum, pageW, pageH, patchX, patchY, patchW, patchH, cookie);
}
public synchronized Link[] getPageLinks(int pageNum) {
gotoPage(pageNum);
return page != null ? page.getLinks() : null;
}
public synchronized int resolveLink(Link link) {
return doc.pageNumberFromLocation(doc.resolveLink(link));
}
public synchronized Quad[][] searchPage(int pageNum, String text) {
gotoPage(pageNum);
return page.search(text);
}
public synchronized boolean hasOutline() {
if (outline == null) {
try {
outline = doc.loadOutline();
} catch (Exception ex) {
/* ignore error */
}
}
return outline != null;
}
private void flattenOutlineNodes(ArrayList<OutlineActivity.Item> result, Outline list[], String indent) {
for (Outline node : list) {
if (node.title != null) {
int page = doc.pageNumberFromLocation(doc.resolveLink(node));
result.add(new OutlineActivity.Item(indent + node.title, page));
}
if (node.down != null)
flattenOutlineNodes(result, node.down, indent + " ");
}
}
public synchronized ArrayList<OutlineActivity.Item> getOutline() {
ArrayList<OutlineActivity.Item> result = new ArrayList<OutlineActivity.Item>();
flattenOutlineNodes(result, outline, "");
return result;
}
public synchronized boolean needsPassword() {
return doc.needsPassword();
}
public synchronized boolean authenticatePassword(String password) {
boolean authenticated = doc.authenticatePassword(password);
pageCount = doc.countPages();
reflowable = doc.isReflowable();
return authenticated;
}
}

View file

@ -0,0 +1,63 @@
package com.artifex.mupdf.viewer;
import android.app.ListActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import java.io.Serializable;
import java.util.ArrayList;
public class OutlineActivity extends ListActivity
{
private final String APP = "MuPDF";
public static class Item implements Serializable {
public String title;
public int page;
public Item(String title, int page) {
this.title = title;
this.page = page;
}
public String toString() {
return title;
}
}
protected ArrayAdapter<Item> adapter;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
adapter = new ArrayAdapter<Item>(this, android.R.layout.simple_list_item_1);
setListAdapter(adapter);
int idx = getIntent().getIntExtra("PALLETBUNDLE", -1);
Bundle bundle = Pallet.receiveBundle(idx);
if (bundle != null) {
int currentPage = bundle.getInt("POSITION");
ArrayList<Item> outline = (ArrayList<Item>)bundle.getSerializable("OUTLINE");
int found = -1;
for (int i = 0; i < outline.size(); ++i) {
Item item = outline.get(i);
if (found < 0 && item.page >= currentPage)
found = i;
adapter.add(item);
}
if (found >= 0)
setSelection(found);
}
}
protected void onListItemClick(ListView l, View v, int position, long id) {
Item item = adapter.getItem(position);
setResult(RESULT_FIRST_USER + item.page);
finish();
}
}

View file

@ -0,0 +1,105 @@
package com.artifex.mupdf.viewer;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.graphics.PointF;
import android.util.Log;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.os.AsyncTask;
public class PageAdapter extends BaseAdapter {
private final String APP = "MuPDF";
private final Context mContext;
private final MuPDFCore mCore;
private final SparseArray<PointF> mPageSizes = new SparseArray<PointF>();
private Bitmap mSharedHqBm;
public PageAdapter(Context c, MuPDFCore core) {
mContext = c;
mCore = core;
}
public int getCount() {
try {
return mCore.countPages();
} catch (RuntimeException e) {
return 0;
}
}
public Object getItem(int position) {
return null;
}
public long getItemId(int position) {
return 0;
}
public synchronized void releaseBitmaps()
{
// recycle and release the shared bitmap.
if (mSharedHqBm!=null)
mSharedHqBm.recycle();
mSharedHqBm = null;
}
public void refresh() {
mPageSizes.clear();
}
public synchronized View getView(final int position, View convertView, ViewGroup parent) {
final PageView pageView;
if (convertView == null) {
if (mSharedHqBm == null || mSharedHqBm.getWidth() != parent.getWidth() || mSharedHqBm.getHeight() != parent.getHeight())
{
if (parent.getWidth() > 0 && parent.getHeight() > 0)
mSharedHqBm = Bitmap.createBitmap(parent.getWidth(), parent.getHeight(), Bitmap.Config.ARGB_8888);
else
mSharedHqBm = null;
}
pageView = new PageView(mContext, mCore, new Point(parent.getWidth(), parent.getHeight()), mSharedHqBm);
} else {
pageView = (PageView) convertView;
}
PointF pageSize = mPageSizes.get(position);
if (pageSize != null) {
// We already know the page size. Set it up
// immediately
pageView.setPage(position, pageSize);
} else {
// Page size as yet unknown. Blank it for now, and
// start a background task to find the size
pageView.blank(position);
AsyncTask<Void,Void,PointF> sizingTask = new AsyncTask<Void,Void,PointF>() {
@Override
protected PointF doInBackground(Void... arg0) {
try {
return mCore.getPageSize(position);
} catch (RuntimeException e) {
return null;
}
}
@Override
protected void onPostExecute(PointF result) {
super.onPostExecute(result);
// We now know the page size
mPageSizes.put(position, result);
// Check that this view hasn't been reused for
// another page since we started
if (pageView.getPage() == position)
pageView.setPage(position, result);
}
};
sizingTask.execute((Void)null);
}
return pageView;
}
}

View file

@ -0,0 +1,672 @@
package com.artifex.mupdf.viewer;
import com.artifex.mupdf.fitz.Cookie;
import com.artifex.mupdf.fitz.Link;
import com.artifex.mupdf.fitz.Quad;
import java.util.ArrayList;
import java.util.Iterator;
import android.annotation.TargetApi;
import android.app.AlertDialog;
import android.content.ClipData;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Bitmap.Config;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.FileUriExposedException;
import android.os.Handler;
import android.text.method.PasswordTransformationMethod;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.Toast;
import android.os.AsyncTask;
// Make our ImageViews opaque to optimize redraw
class OpaqueImageView extends ImageView {
public OpaqueImageView(Context context) {
super(context);
}
@Override
public boolean isOpaque() {
return true;
}
}
public class PageView extends ViewGroup {
private final String APP = "MuPDF";
private final MuPDFCore mCore;
private static final int HIGHLIGHT_COLOR = 0x80cc6600;
private static final int LINK_COLOR = 0x800066cc;
private static final int BOX_COLOR = 0xFF4444FF;
private static final int BACKGROUND_COLOR = 0xFFFFFFFF;
private static final int PROGRESS_DIALOG_DELAY = 200;
protected final Context mContext;
protected int mPageNumber;
private Point mParentSize;
protected Point mSize; // Size of page at minimum zoom
protected float mSourceScale;
private ImageView mEntire; // Image rendered at minimum zoom
private Bitmap mEntireBm;
private Matrix mEntireMat;
private AsyncTask<Void,Void,Link[]> mGetLinkInfo;
private CancellableAsyncTask<Void, Boolean> mDrawEntire;
private Point mPatchViewSize; // View size on the basis of which the patch was created
private Rect mPatchArea;
private ImageView mPatch;
private Bitmap mPatchBm;
private CancellableAsyncTask<Void, Boolean> mDrawPatch;
private Quad mSearchBoxes[][];
protected Link mLinks[];
private View mSearchView;
private boolean mIsBlank;
private boolean mHighlightLinks;
private ImageView mErrorIndicator;
private ProgressBar mBusyIndicator;
private final Handler mHandler = new Handler();
public PageView(Context c, MuPDFCore core, Point parentSize, Bitmap sharedHqBm) {
super(c);
mContext = c;
mCore = core;
mParentSize = parentSize;
setBackgroundColor(BACKGROUND_COLOR);
mEntireBm = Bitmap.createBitmap(parentSize.x, parentSize.y, Config.ARGB_8888);
mPatchBm = sharedHqBm;
mEntireMat = new Matrix();
}
private void reinit() {
// Cancel pending render task
if (mDrawEntire != null) {
mDrawEntire.cancel();
mDrawEntire = null;
}
if (mDrawPatch != null) {
mDrawPatch.cancel();
mDrawPatch = null;
}
if (mGetLinkInfo != null) {
mGetLinkInfo.cancel(true);
mGetLinkInfo = null;
}
mIsBlank = true;
mPageNumber = 0;
if (mSize == null)
mSize = mParentSize;
if (mEntire != null) {
mEntire.setImageBitmap(null);
mEntire.invalidate();
}
if (mPatch != null) {
mPatch.setImageBitmap(null);
mPatch.invalidate();
}
mPatchViewSize = null;
mPatchArea = null;
mSearchBoxes = null;
mLinks = null;
clearRenderError();
}
public void releaseResources() {
reinit();
if (mBusyIndicator != null) {
removeView(mBusyIndicator);
mBusyIndicator = null;
}
clearRenderError();
}
public void releaseBitmaps() {
reinit();
// recycle bitmaps before releasing them.
if (mEntireBm!=null)
mEntireBm.recycle();
mEntireBm = null;
if (mPatchBm!=null)
mPatchBm.recycle();
mPatchBm = null;
}
public void blank(int page) {
reinit();
mPageNumber = page;
if (mBusyIndicator == null) {
mBusyIndicator = new ProgressBar(mContext);
mBusyIndicator.setIndeterminate(true);
addView(mBusyIndicator);
}
setBackgroundColor(BACKGROUND_COLOR);
}
protected void clearRenderError() {
if (mErrorIndicator == null)
return;
removeView(mErrorIndicator);
mErrorIndicator = null;
invalidate();
}
protected void setRenderError(String why) {
int page = mPageNumber;
reinit();
mPageNumber = page;
if (mBusyIndicator != null) {
removeView(mBusyIndicator);
mBusyIndicator = null;
}
if (mSearchView != null) {
removeView(mSearchView);
mSearchView = null;
}
if (mErrorIndicator == null) {
mErrorIndicator = new OpaqueImageView(mContext);
mErrorIndicator.setScaleType(ImageView.ScaleType.CENTER);
addView(mErrorIndicator);
Drawable mErrorIcon = getResources().getDrawable(R.drawable.ic_error_red_24dp);
mErrorIndicator.setImageDrawable(mErrorIcon);
mErrorIndicator.setBackgroundColor(BACKGROUND_COLOR);
}
setBackgroundColor(Color.TRANSPARENT);
mErrorIndicator.bringToFront();
mErrorIndicator.invalidate();
}
public void setPage(int page, PointF size) {
// Cancel pending render task
if (mDrawEntire != null) {
mDrawEntire.cancel();
mDrawEntire = null;
}
mIsBlank = false;
// Highlights may be missing because mIsBlank was true on last draw
if (mSearchView != null)
mSearchView.invalidate();
mPageNumber = page;
if (size == null) {
setRenderError("Error loading page");
size = new PointF(612, 792);
}
// Calculate scaled size that fits within the screen limits
// This is the size at minimum zoom
mSourceScale = Math.min(mParentSize.x/size.x, mParentSize.y/size.y);
Point newSize = new Point((int)(size.x*mSourceScale), (int)(size.y*mSourceScale));
mSize = newSize;
if (mErrorIndicator != null)
return;
if (mEntire == null) {
mEntire = new OpaqueImageView(mContext);
mEntire.setScaleType(ImageView.ScaleType.MATRIX);
addView(mEntire);
}
mEntire.setImageBitmap(null);
mEntire.invalidate();
// Get the link info in the background
mGetLinkInfo = new AsyncTask<Void,Void,Link[]>() {
protected Link[] doInBackground(Void... v) {
return getLinkInfo();
}
protected void onPostExecute(Link[] v) {
mLinks = v;
if (mSearchView != null)
mSearchView.invalidate();
}
};
mGetLinkInfo.execute();
// Render the page in the background
mDrawEntire = new CancellableAsyncTask<Void, Boolean>(getDrawPageTask(mEntireBm, mSize.x, mSize.y, 0, 0, mSize.x, mSize.y)) {
@Override
public void onPreExecute() {
setBackgroundColor(BACKGROUND_COLOR);
mEntire.setImageBitmap(null);
mEntire.invalidate();
if (mBusyIndicator == null) {
mBusyIndicator = new ProgressBar(mContext);
mBusyIndicator.setIndeterminate(true);
addView(mBusyIndicator);
mBusyIndicator.setVisibility(INVISIBLE);
mHandler.postDelayed(new Runnable() {
public void run() {
if (mBusyIndicator != null)
mBusyIndicator.setVisibility(VISIBLE);
}
}, PROGRESS_DIALOG_DELAY);
}
}
@Override
public void onPostExecute(Boolean result) {
removeView(mBusyIndicator);
mBusyIndicator = null;
if (result.booleanValue()) {
clearRenderError();
mEntire.setImageBitmap(mEntireBm);
mEntire.invalidate();
} else {
setRenderError("Error rendering page");
}
setBackgroundColor(Color.TRANSPARENT);
}
};
mDrawEntire.execute();
if (mSearchView == null) {
mSearchView = new View(mContext) {
@Override
protected void onDraw(final Canvas canvas) {
super.onDraw(canvas);
// Work out current total scale factor
// from source to view
final float scale = mSourceScale*(float)getWidth()/(float)mSize.x;
final Paint paint = new Paint();
if (!mIsBlank && mSearchBoxes != null) {
paint.setColor(HIGHLIGHT_COLOR);
for (Quad[] searchBox : mSearchBoxes) {
for (Quad q : searchBox) {
Path path = new Path();
path.moveTo(q.ul_x * scale, q.ul_y * scale);
path.lineTo(q.ll_x * scale, q.ll_y * scale);
path.lineTo(q.lr_x * scale, q.lr_y * scale);
path.lineTo(q.ur_x * scale, q.ur_y * scale);
path.close();
canvas.drawPath(path, paint);
}
}
}
if (!mIsBlank && mLinks != null && mHighlightLinks) {
paint.setColor(LINK_COLOR);
for (Link link : mLinks)
canvas.drawRect(link.getBounds().x0*scale, link.getBounds().y0*scale,
link.getBounds().x1*scale, link.getBounds().y1*scale,
paint);
}
}
};
addView(mSearchView);
}
requestLayout();
}
public void setSearchBoxes(Quad searchBoxes[][]) {
mSearchBoxes = searchBoxes;
if (mSearchView != null)
mSearchView.invalidate();
}
public void setLinkHighlighting(boolean f) {
mHighlightLinks = f;
if (mSearchView != null)
mSearchView.invalidate();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int x, y;
switch(View.MeasureSpec.getMode(widthMeasureSpec)) {
case View.MeasureSpec.UNSPECIFIED:
x = mSize.x;
break;
default:
x = View.MeasureSpec.getSize(widthMeasureSpec);
}
switch(View.MeasureSpec.getMode(heightMeasureSpec)) {
case View.MeasureSpec.UNSPECIFIED:
y = mSize.y;
break;
default:
y = View.MeasureSpec.getSize(heightMeasureSpec);
}
setMeasuredDimension(x, y);
if (mBusyIndicator != null) {
int limit = Math.min(mParentSize.x, mParentSize.y)/2;
mBusyIndicator.measure(View.MeasureSpec.AT_MOST | limit, View.MeasureSpec.AT_MOST | limit);
}
if (mErrorIndicator != null) {
int limit = Math.min(mParentSize.x, mParentSize.y)/2;
mErrorIndicator.measure(View.MeasureSpec.AT_MOST | limit, View.MeasureSpec.AT_MOST | limit);
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
int w = right-left;
int h = bottom-top;
if (mEntire != null) {
if (mEntire.getWidth() != w || mEntire.getHeight() != h) {
mEntireMat.setScale(w/(float)mSize.x, h/(float)mSize.y);
mEntire.setImageMatrix(mEntireMat);
mEntire.invalidate();
}
mEntire.layout(0, 0, w, h);
}
if (mSearchView != null) {
mSearchView.layout(0, 0, w, h);
}
if (mPatchViewSize != null) {
if (mPatchViewSize.x != w || mPatchViewSize.y != h) {
// Zoomed since patch was created
mPatchViewSize = null;
mPatchArea = null;
if (mPatch != null) {
mPatch.setImageBitmap(null);
mPatch.invalidate();
}
} else {
mPatch.layout(mPatchArea.left, mPatchArea.top, mPatchArea.right, mPatchArea.bottom);
}
}
if (mBusyIndicator != null) {
int bw = mBusyIndicator.getMeasuredWidth();
int bh = mBusyIndicator.getMeasuredHeight();
mBusyIndicator.layout((w-bw)/2, (h-bh)/2, (w+bw)/2, (h+bh)/2);
}
if (mErrorIndicator != null) {
int bw = (int) (8.5 * mErrorIndicator.getMeasuredWidth());
int bh = (int) (11 * mErrorIndicator.getMeasuredHeight());
mErrorIndicator.layout((w-bw)/2, (h-bh)/2, (w+bw)/2, (h+bh)/2);
}
}
public void updateHq(boolean update) {
if (mErrorIndicator != null) {
if (mPatch != null) {
mPatch.setImageBitmap(null);
mPatch.invalidate();
}
return;
}
Rect viewArea = new Rect(getLeft(),getTop(),getRight(),getBottom());
if (viewArea.width() == mSize.x || viewArea.height() == mSize.y) {
// If the viewArea's size matches the unzoomed size, there is no need for an hq patch
if (mPatch != null) {
mPatch.setImageBitmap(null);
mPatch.invalidate();
}
} else {
final Point patchViewSize = new Point(viewArea.width(), viewArea.height());
final Rect patchArea = new Rect(0, 0, mParentSize.x, mParentSize.y);
// Intersect and test that there is an intersection
if (!patchArea.intersect(viewArea))
return;
// Offset patch area to be relative to the view top left
patchArea.offset(-viewArea.left, -viewArea.top);
boolean area_unchanged = patchArea.equals(mPatchArea) && patchViewSize.equals(mPatchViewSize);
// If being asked for the same area as last time and not because of an update then nothing to do
if (area_unchanged && !update)
return;
boolean completeRedraw = !(area_unchanged && update);
// Stop the drawing of previous patch if still going
if (mDrawPatch != null) {
mDrawPatch.cancel();
mDrawPatch = null;
}
// Create and add the image view if not already done
if (mPatch == null) {
mPatch = new OpaqueImageView(mContext);
mPatch.setScaleType(ImageView.ScaleType.MATRIX);
addView(mPatch);
if (mSearchView != null)
mSearchView.bringToFront();
}
CancellableTaskDefinition<Void, Boolean> task;
if (completeRedraw)
task = getDrawPageTask(mPatchBm, patchViewSize.x, patchViewSize.y,
patchArea.left, patchArea.top,
patchArea.width(), patchArea.height());
else
task = getUpdatePageTask(mPatchBm, patchViewSize.x, patchViewSize.y,
patchArea.left, patchArea.top,
patchArea.width(), patchArea.height());
mDrawPatch = new CancellableAsyncTask<Void, Boolean>(task) {
public void onPostExecute(Boolean result) {
if (result.booleanValue()) {
mPatchViewSize = patchViewSize;
mPatchArea = patchArea;
clearRenderError();
mPatch.setImageBitmap(mPatchBm);
mPatch.invalidate();
//requestLayout();
// Calling requestLayout here doesn't lead to a later call to layout. No idea
// why, but apparently others have run into the problem.
mPatch.layout(mPatchArea.left, mPatchArea.top, mPatchArea.right, mPatchArea.bottom);
} else {
setRenderError("Error rendering patch");
}
}
};
mDrawPatch.execute();
}
}
public void update() {
// Cancel pending render task
if (mDrawEntire != null) {
mDrawEntire.cancel();
mDrawEntire = null;
}
if (mDrawPatch != null) {
mDrawPatch.cancel();
mDrawPatch = null;
}
// Render the page in the background
mDrawEntire = new CancellableAsyncTask<Void, Boolean>(getUpdatePageTask(mEntireBm, mSize.x, mSize.y, 0, 0, mSize.x, mSize.y)) {
public void onPostExecute(Boolean result) {
if (result.booleanValue()) {
clearRenderError();
mEntire.setImageBitmap(mEntireBm);
mEntire.invalidate();
} else {
setRenderError("Error updating page");
}
}
};
mDrawEntire.execute();
updateHq(true);
}
public void removeHq() {
// Stop the drawing of the patch if still going
if (mDrawPatch != null) {
mDrawPatch.cancel();
mDrawPatch = null;
}
// And get rid of it
mPatchViewSize = null;
mPatchArea = null;
if (mPatch != null) {
mPatch.setImageBitmap(null);
mPatch.invalidate();
}
}
public int getPage() {
return mPageNumber;
}
@Override
public boolean isOpaque() {
return true;
}
public int hitLink(Link link) {
if (link.isExternal()) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(link.getURI()));
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); // API>=21: FLAG_ACTIVITY_NEW_DOCUMENT
try {
mContext.startActivity(intent);
} catch (FileUriExposedException x) {
Log.e(APP, x.toString());
Toast.makeText(getContext(), "Android does not allow following file:// link: " + link.getURI(), Toast.LENGTH_LONG).show();
} catch (Throwable x) {
Log.e(APP, x.toString());
Toast.makeText(getContext(), x.getMessage(), Toast.LENGTH_LONG).show();
}
return 0;
} else {
return mCore.resolveLink(link);
}
}
public int hitLink(float x, float y) {
// Since link highlighting was implemented, the super class
// PageView has had sufficient information to be able to
// perform this method directly. Making that change would
// make MuPDFCore.hitLinkPage superfluous.
float scale = mSourceScale*(float)getWidth()/(float)mSize.x;
float docRelX = (x - getLeft())/scale;
float docRelY = (y - getTop())/scale;
if (mLinks != null)
for (Link l: mLinks)
if (l.getBounds().contains(docRelX, docRelY))
return hitLink(l);
return 0;
}
protected CancellableTaskDefinition<Void, Boolean> getDrawPageTask(final Bitmap bm, final int sizeX, final int sizeY,
final int patchX, final int patchY, final int patchWidth, final int patchHeight) {
return new MuPDFCancellableTaskDefinition<Void, Boolean>() {
@Override
public Boolean doInBackground(Cookie cookie, Void ... params) {
if (bm == null)
return new Boolean(false);
// Workaround bug in Android Honeycomb 3.x, where the bitmap generation count
// is not incremented when drawing.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB &&
Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH)
bm.eraseColor(0);
try {
mCore.drawPage(bm, mPageNumber, sizeX, sizeY, patchX, patchY, patchWidth, patchHeight, cookie);
return new Boolean(true);
} catch (RuntimeException e) {
return new Boolean(false);
}
}
};
}
protected CancellableTaskDefinition<Void, Boolean> getUpdatePageTask(final Bitmap bm, final int sizeX, final int sizeY,
final int patchX, final int patchY, final int patchWidth, final int patchHeight)
{
return new MuPDFCancellableTaskDefinition<Void, Boolean>() {
@Override
public Boolean doInBackground(Cookie cookie, Void ... params) {
if (bm == null)
return new Boolean(false);
// Workaround bug in Android Honeycomb 3.x, where the bitmap generation count
// is not incremented when drawing.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB &&
Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH)
bm.eraseColor(0);
try {
mCore.updatePage(bm, mPageNumber, sizeX, sizeY, patchX, patchY, patchWidth, patchHeight, cookie);
return new Boolean(true);
} catch (RuntimeException e) {
return new Boolean(false);
}
}
};
}
protected Link[] getLinkInfo() {
try {
return mCore.getPageLinks(mPageNumber);
} catch (RuntimeException e) {
return null;
}
}
}

View file

@ -0,0 +1,39 @@
package com.artifex.mupdf.viewer;
import android.os.Bundle;
import java.util.HashMap;
import java.util.Map;
public class Pallet {
private static Pallet instance = new Pallet();
private final Map<Integer, Object> pallet = new HashMap<>();
private int sequenceNumber = 0;
private Pallet() {
}
private static Pallet getInstance() {
return instance;
}
public static int sendBundle(Bundle bundle) {
Pallet instance = getInstance();
int i = instance.sequenceNumber++;
if (instance.sequenceNumber < 0)
instance.sequenceNumber = 0;
instance.pallet.put(new Integer(i), bundle);
return i;
}
public static Bundle receiveBundle(int number) {
Bundle bundle = (Bundle) getInstance().pallet.get(new Integer(number));
if (bundle != null)
getInstance().pallet.remove(new Integer(number));
return bundle;
}
public static boolean hasBundle(int number) {
return getInstance().pallet.containsKey(new Integer(number));
}
}

View file

@ -0,0 +1,980 @@
package com.artifex.mupdf.viewer;
import com.artifex.mupdf.fitz.Link;
import java.util.LinkedList;
import java.util.NoSuchElementException;
import java.util.Stack;
import android.content.Context;
import android.graphics.Point;
import android.graphics.Rect;
import android.net.Uri;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.SparseArray;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.view.WindowManager;
import android.widget.Adapter;
import android.widget.AdapterView;
import android.widget.Scroller;
public class ReaderView
extends AdapterView<Adapter>
implements GestureDetector.OnGestureListener, ScaleGestureDetector.OnScaleGestureListener, Runnable {
private final String APP = "MuPDF";
private Context mContext;
private boolean mLinksEnabled = false;
private boolean tapDisabled = false;
private int tapPageMargin;
private static final int MOVING_DIAGONALLY = 0;
private static final int MOVING_LEFT = 1;
private static final int MOVING_RIGHT = 2;
private static final int MOVING_UP = 3;
private static final int MOVING_DOWN = 4;
private static final int FLING_MARGIN = 100;
private static final int GAP = 20;
private static final float MIN_SCALE = 1.0f;
private static final float MAX_SCALE = 64.0f;
private static final boolean HORIZONTAL_SCROLLING = true;
private PageAdapter mAdapter;
protected int mCurrent; // Adapter's index for the current view
private boolean mResetLayout;
private final SparseArray<View>
mChildViews = new SparseArray<View>(3);
// Shadows the children of the adapter view
// but with more sensible indexing
private final LinkedList<View>
mViewCache = new LinkedList<View>();
private boolean mUserInteracting; // Whether the user is interacting
private boolean mScaling; // Whether the user is currently pinch zooming
private float mScale = 1.0f;
private int mXScroll; // Scroll amounts recorded from events.
private int mYScroll; // and then accounted for in onLayout
private GestureDetector mGestureDetector;
private ScaleGestureDetector mScaleGestureDetector;
private Scroller mScroller;
private Stepper mStepper;
private int mScrollerLastX;
private int mScrollerLastY;
private float mLastScaleFocusX;
private float mLastScaleFocusY;
protected Stack<Integer> mHistory;
public interface ViewMapper {
void applyToView(View view);
}
public ReaderView(Context context) {
super(context);
setup(context);
}
public ReaderView(Context context, AttributeSet attrs) {
super(context, attrs);
setup(context);
}
public ReaderView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setup(context);
}
private void setup(Context context)
{
mContext = context;
mGestureDetector = new GestureDetector(context, this);
mScaleGestureDetector = new ScaleGestureDetector(context, this);
mScroller = new Scroller(context);
mStepper = new Stepper(this, this);
mHistory = new Stack<Integer>();
// Get the screen size etc to customise tap margins.
// We calculate the size of 1 inch of the screen for tapping.
// On some devices the dpi values returned are wrong, so we
// sanity check it: we first restrict it so that we are never
// less than 100 pixels (the smallest Android device screen
// dimension I've seen is 480 pixels or so). Then we check
// to ensure we are never more than 1/5 of the screen width.
DisplayMetrics dm = new DisplayMetrics();
WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
wm.getDefaultDisplay().getMetrics(dm);
tapPageMargin = (int)dm.xdpi;
if (tapPageMargin < 100)
tapPageMargin = 100;
if (tapPageMargin > dm.widthPixels/5)
tapPageMargin = dm.widthPixels/5;
}
public boolean popHistory() {
if (mHistory.empty())
return false;
setDisplayedViewIndex(mHistory.pop());
return true;
}
public void pushHistory() {
mHistory.push(mCurrent);
}
public void clearHistory() {
mHistory.clear();
}
public int getDisplayedViewIndex() {
return mCurrent;
}
public void setDisplayedViewIndex(int i) {
if (0 <= i && i < mAdapter.getCount()) {
onMoveOffChild(mCurrent);
mCurrent = i;
onMoveToChild(i);
mResetLayout = true;
requestLayout();
}
}
public void moveToNext() {
View v = mChildViews.get(mCurrent+1);
if (v != null)
slideViewOntoScreen(v);
}
public void moveToPrevious() {
View v = mChildViews.get(mCurrent-1);
if (v != null)
slideViewOntoScreen(v);
}
// When advancing down the page, we want to advance by about
// 90% of a screenful. But we'd be happy to advance by between
// 80% and 95% if it means we hit the bottom in a whole number
// of steps.
private int smartAdvanceAmount(int screenHeight, int max) {
int advance = (int)(screenHeight * 0.9 + 0.5);
int leftOver = max % advance;
int steps = max / advance;
if (leftOver == 0) {
// We'll make it exactly. No adjustment
} else if ((float)leftOver / steps <= screenHeight * 0.05) {
// We can adjust up by less than 5% to make it exact.
advance += (int)((float)leftOver/steps + 0.5);
} else {
int overshoot = advance - leftOver;
if ((float)overshoot / steps <= screenHeight * 0.1) {
// We can adjust down by less than 10% to make it exact.
advance -= (int)((float)overshoot/steps + 0.5);
}
}
if (advance > max)
advance = max;
return advance;
}
public void smartMoveForwards() {
View v = mChildViews.get(mCurrent);
if (v == null)
return;
// The following code works in terms of where the screen is on the views;
// so for example, if the currentView is at (-100,-100), the visible
// region would be at (100,100). If the previous page was (2000, 3000) in
// size, the visible region of the previous page might be (2100 + GAP, 100)
// (i.e. off the previous page). This is different to the way the rest of
// the code in this file is written, but it's easier for me to think about.
// At some point we may refactor this to fit better with the rest of the
// code.
// screenWidth/Height are the actual width/height of the screen. e.g. 480/800
int screenWidth = getWidth();
int screenHeight = getHeight();
// We might be mid scroll; we want to calculate where we scroll to based on
// where this scroll would end, not where we are now (to allow for people
// bashing 'forwards' very fast.
int remainingX = mScroller.getFinalX() - mScroller.getCurrX();
int remainingY = mScroller.getFinalY() - mScroller.getCurrY();
// right/bottom is in terms of pixels within the scaled document; e.g. 1000
int top = -(v.getTop() + mYScroll + remainingY);
int right = screenWidth -(v.getLeft() + mXScroll + remainingX);
int bottom = screenHeight+top;
// docWidth/Height are the width/height of the scaled document e.g. 2000x3000
int docWidth = v.getMeasuredWidth();
int docHeight = v.getMeasuredHeight();
int xOffset, yOffset;
if (bottom >= docHeight) {
// We are flush with the bottom. Advance to next column.
if (right + screenWidth > docWidth) {
// No room for another column - go to next page
View nv = mChildViews.get(mCurrent+1);
if (nv == null) // No page to advance to
return;
int nextTop = -(nv.getTop() + mYScroll + remainingY);
int nextLeft = -(nv.getLeft() + mXScroll + remainingX);
int nextDocWidth = nv.getMeasuredWidth();
int nextDocHeight = nv.getMeasuredHeight();
// Allow for the next page maybe being shorter than the screen is high
yOffset = (nextDocHeight < screenHeight ? ((nextDocHeight - screenHeight)>>1) : 0);
if (nextDocWidth < screenWidth) {
// Next page is too narrow to fill the screen. Scroll to the top, centred.
xOffset = (nextDocWidth - screenWidth)>>1;
} else {
// Reset X back to the left hand column
xOffset = right % screenWidth;
// Adjust in case the previous page is less wide
if (xOffset + screenWidth > nextDocWidth)
xOffset = nextDocWidth - screenWidth;
}
xOffset -= nextLeft;
yOffset -= nextTop;
} else {
// Move to top of next column
xOffset = screenWidth;
yOffset = screenHeight - bottom;
}
} else {
// Advance by 90% of the screen height downwards (in case lines are partially cut off)
xOffset = 0;
yOffset = smartAdvanceAmount(screenHeight, docHeight - bottom);
}
mScrollerLastX = mScrollerLastY = 0;
mScroller.startScroll(0, 0, remainingX - xOffset, remainingY - yOffset, 400);
mStepper.prod();
}
public void smartMoveBackwards() {
View v = mChildViews.get(mCurrent);
if (v == null)
return;
// The following code works in terms of where the screen is on the views;
// so for example, if the currentView is at (-100,-100), the visible
// region would be at (100,100). If the previous page was (2000, 3000) in
// size, the visible region of the previous page might be (2100 + GAP, 100)
// (i.e. off the previous page). This is different to the way the rest of
// the code in this file is written, but it's easier for me to think about.
// At some point we may refactor this to fit better with the rest of the
// code.
// screenWidth/Height are the actual width/height of the screen. e.g. 480/800
int screenWidth = getWidth();
int screenHeight = getHeight();
// We might be mid scroll; we want to calculate where we scroll to based on
// where this scroll would end, not where we are now (to allow for people
// bashing 'forwards' very fast.
int remainingX = mScroller.getFinalX() - mScroller.getCurrX();
int remainingY = mScroller.getFinalY() - mScroller.getCurrY();
// left/top is in terms of pixels within the scaled document; e.g. 1000
int left = -(v.getLeft() + mXScroll + remainingX);
int top = -(v.getTop() + mYScroll + remainingY);
// docWidth/Height are the width/height of the scaled document e.g. 2000x3000
int docHeight = v.getMeasuredHeight();
int xOffset, yOffset;
if (top <= 0) {
// We are flush with the top. Step back to previous column.
if (left < screenWidth) {
/* No room for previous column - go to previous page */
View pv = mChildViews.get(mCurrent-1);
if (pv == null) /* No page to advance to */
return;
int prevDocWidth = pv.getMeasuredWidth();
int prevDocHeight = pv.getMeasuredHeight();
// Allow for the next page maybe being shorter than the screen is high
yOffset = (prevDocHeight < screenHeight ? ((prevDocHeight - screenHeight)>>1) : 0);
int prevLeft = -(pv.getLeft() + mXScroll);
int prevTop = -(pv.getTop() + mYScroll);
if (prevDocWidth < screenWidth) {
// Previous page is too narrow to fill the screen. Scroll to the bottom, centred.
xOffset = (prevDocWidth - screenWidth)>>1;
} else {
// Reset X back to the right hand column
xOffset = (left > 0 ? left % screenWidth : 0);
if (xOffset + screenWidth > prevDocWidth)
xOffset = prevDocWidth - screenWidth;
while (xOffset + screenWidth*2 < prevDocWidth)
xOffset += screenWidth;
}
xOffset -= prevLeft;
yOffset -= prevTop-prevDocHeight+screenHeight;
} else {
// Move to bottom of previous column
xOffset = -screenWidth;
yOffset = docHeight - screenHeight + top;
}
} else {
// Retreat by 90% of the screen height downwards (in case lines are partially cut off)
xOffset = 0;
yOffset = -smartAdvanceAmount(screenHeight, top);
}
mScrollerLastX = mScrollerLastY = 0;
mScroller.startScroll(0, 0, remainingX - xOffset, remainingY - yOffset, 400);
mStepper.prod();
}
public void resetupChildren() {
for (int i = 0; i < mChildViews.size(); i++)
onChildSetup(mChildViews.keyAt(i), mChildViews.valueAt(i));
}
public void applyToChildren(ViewMapper mapper) {
for (int i = 0; i < mChildViews.size(); i++)
mapper.applyToView(mChildViews.valueAt(i));
}
public void refresh() {
mResetLayout = true;
mScale = 1.0f;
mXScroll = mYScroll = 0;
/* All page views need recreating since both page and screen has changed size,
* invalidating both sizes and bitmaps. */
mAdapter.refresh();
int numChildren = mChildViews.size();
for (int i = 0; i < mChildViews.size(); i++) {
View v = mChildViews.valueAt(i);
onNotInUse(v);
removeViewInLayout(v);
}
mChildViews.clear();
mViewCache.clear();
requestLayout();
}
public View getView(int i) {
return mChildViews.get(i);
}
public View getDisplayedView() {
return mChildViews.get(mCurrent);
}
public void run() {
if (!mScroller.isFinished()) {
mScroller.computeScrollOffset();
int x = mScroller.getCurrX();
int y = mScroller.getCurrY();
mXScroll += x - mScrollerLastX;
mYScroll += y - mScrollerLastY;
mScrollerLastX = x;
mScrollerLastY = y;
requestLayout();
mStepper.prod();
}
else if (!mUserInteracting) {
// End of an inertial scroll and the user is not interacting.
// The layout is stable
View v = mChildViews.get(mCurrent);
if (v != null)
postSettle(v);
}
}
public boolean onDown(MotionEvent arg0) {
mScroller.forceFinished(true);
return true;
}
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
if (mScaling)
return true;
View v = mChildViews.get(mCurrent);
if (v != null) {
Rect bounds = getScrollBounds(v);
switch(directionOfTravel(velocityX, velocityY)) {
case MOVING_LEFT:
if (HORIZONTAL_SCROLLING && bounds.left >= 0) {
// Fling off to the left bring next view onto screen
View vl = mChildViews.get(mCurrent+1);
if (vl != null) {
slideViewOntoScreen(vl);
return true;
}
}
break;
case MOVING_UP:
if (!HORIZONTAL_SCROLLING && bounds.top >= 0) {
// Fling off to the top bring next view onto screen
View vl = mChildViews.get(mCurrent+1);
if (vl != null) {
slideViewOntoScreen(vl);
return true;
}
}
break;
case MOVING_RIGHT:
if (HORIZONTAL_SCROLLING && bounds.right <= 0) {
// Fling off to the right bring previous view onto screen
View vr = mChildViews.get(mCurrent-1);
if (vr != null) {
slideViewOntoScreen(vr);
return true;
}
}
break;
case MOVING_DOWN:
if (!HORIZONTAL_SCROLLING && bounds.bottom <= 0) {
// Fling off to the bottom bring previous view onto screen
View vr = mChildViews.get(mCurrent-1);
if (vr != null) {
slideViewOntoScreen(vr);
return true;
}
}
break;
}
mScrollerLastX = mScrollerLastY = 0;
// If the page has been dragged out of bounds then we want to spring back
// nicely. fling jumps back into bounds instantly, so we don't want to use
// fling in that case. On the other hand, we don't want to forgo a fling
// just because of a slightly off-angle drag taking us out of bounds other
// than in the direction of the drag, so we test for out of bounds only
// in the direction of travel.
//
// Also don't fling if out of bounds in any direction by more than fling
// margin
Rect expandedBounds = new Rect(bounds);
expandedBounds.inset(-FLING_MARGIN, -FLING_MARGIN);
if(withinBoundsInDirectionOfTravel(bounds, velocityX, velocityY)
&& expandedBounds.contains(0, 0)) {
mScroller.fling(0, 0, (int)velocityX, (int)velocityY, bounds.left, bounds.right, bounds.top, bounds.bottom);
mStepper.prod();
}
}
return true;
}
public void onLongPress(MotionEvent e) { }
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
float distanceY) {
PageView pageView = (PageView)getDisplayedView();
if (!tapDisabled)
onDocMotion();
if (!mScaling) {
mXScroll -= distanceX;
mYScroll -= distanceY;
requestLayout();
}
return true;
}
public void onShowPress(MotionEvent e) { }
public boolean onScale(ScaleGestureDetector detector) {
float previousScale = mScale;
mScale = Math.min(Math.max(mScale * detector.getScaleFactor(), MIN_SCALE), MAX_SCALE);
{
float factor = mScale/previousScale;
View v = mChildViews.get(mCurrent);
if (v != null) {
float currentFocusX = detector.getFocusX();
float currentFocusY = detector.getFocusY();
// Work out the focus point relative to the view top left
int viewFocusX = (int)currentFocusX - (v.getLeft() + mXScroll);
int viewFocusY = (int)currentFocusY - (v.getTop() + mYScroll);
// Scroll to maintain the focus point
mXScroll += viewFocusX - viewFocusX * factor;
mYScroll += viewFocusY - viewFocusY * factor;
if (mLastScaleFocusX>=0)
mXScroll+=currentFocusX-mLastScaleFocusX;
if (mLastScaleFocusY>=0)
mYScroll+=currentFocusY-mLastScaleFocusY;
mLastScaleFocusX=currentFocusX;
mLastScaleFocusY=currentFocusY;
requestLayout();
}
}
return true;
}
public boolean onScaleBegin(ScaleGestureDetector detector) {
tapDisabled = true;
mScaling = true;
// Ignore any scroll amounts yet to be accounted for: the
// screen is not showing the effect of them, so they can
// only confuse the user
mXScroll = mYScroll = 0;
mLastScaleFocusX = mLastScaleFocusY = -1;
return true;
}
public void onScaleEnd(ScaleGestureDetector detector) {
mScaling = false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if ((event.getAction() & event.getActionMasked()) == MotionEvent.ACTION_DOWN)
{
tapDisabled = false;
}
mScaleGestureDetector.onTouchEvent(event);
mGestureDetector.onTouchEvent(event);
if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) {
mUserInteracting = true;
}
if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP) {
mUserInteracting = false;
View v = mChildViews.get(mCurrent);
if (v != null) {
if (mScroller.isFinished()) {
// If, at the end of user interaction, there is no
// current inertial scroll in operation then animate
// the view onto screen if necessary
slideViewOntoScreen(v);
}
if (mScroller.isFinished()) {
// If still there is no inertial scroll in operation
// then the layout is stable
postSettle(v);
}
}
}
requestLayout();
return true;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int n = getChildCount();
for (int i = 0; i < n; i++)
measureView(getChildAt(i));
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
try {
onLayout2(changed, left, top, right, bottom);
}
catch (java.lang.OutOfMemoryError e) {
System.out.println("Out of memory during layout");
}
}
private void onLayout2(boolean changed, int left, int top, int right,
int bottom) {
// "Edit mode" means when the View is being displayed in the Android GUI editor. (this class
// is instantiated in the IDE, so we need to be a bit careful what we do).
if (isInEditMode())
return;
View cv = mChildViews.get(mCurrent);
Point cvOffset;
if (!mResetLayout) {
// Move to next or previous if current is sufficiently off center
if (cv != null) {
boolean move;
cvOffset = subScreenSizeOffset(cv);
// cv.getRight() may be out of date with the current scale
// so add left to the measured width for the correct position
if (HORIZONTAL_SCROLLING)
move = cv.getLeft() + cv.getMeasuredWidth() + cvOffset.x + GAP/2 + mXScroll < getWidth()/2;
else
move = cv.getTop() + cv.getMeasuredHeight() + cvOffset.y + GAP/2 + mYScroll < getHeight()/2;
if (move && mCurrent + 1 < mAdapter.getCount()) {
postUnsettle(cv);
// post to invoke test for end of animation
// where we must set hq area for the new current view
mStepper.prod();
onMoveOffChild(mCurrent);
mCurrent++;
onMoveToChild(mCurrent);
}
if (HORIZONTAL_SCROLLING)
move = cv.getLeft() - cvOffset.x - GAP/2 + mXScroll >= getWidth()/2;
else
move = cv.getTop() - cvOffset.y - GAP/2 + mYScroll >= getHeight()/2;
if (move && mCurrent > 0) {
postUnsettle(cv);
// post to invoke test for end of animation
// where we must set hq area for the new current view
mStepper.prod();
onMoveOffChild(mCurrent);
mCurrent--;
onMoveToChild(mCurrent);
}
}
// Remove not needed children and hold them for reuse
int numChildren = mChildViews.size();
int childIndices[] = new int[numChildren];
for (int i = 0; i < numChildren; i++)
childIndices[i] = mChildViews.keyAt(i);
for (int i = 0; i < numChildren; i++) {
int ai = childIndices[i];
if (ai < mCurrent - 1 || ai > mCurrent + 1) {
View v = mChildViews.get(ai);
onNotInUse(v);
mViewCache.add(v);
removeViewInLayout(v);
mChildViews.remove(ai);
}
}
} else {
mResetLayout = false;
mXScroll = mYScroll = 0;
// Remove all children and hold them for reuse
int numChildren = mChildViews.size();
for (int i = 0; i < numChildren; i++) {
View v = mChildViews.valueAt(i);
onNotInUse(v);
mViewCache.add(v);
removeViewInLayout(v);
}
mChildViews.clear();
// post to ensure generation of hq area
mStepper.prod();
}
// Ensure current view is present
int cvLeft, cvRight, cvTop, cvBottom;
boolean notPresent = (mChildViews.get(mCurrent) == null);
cv = getOrCreateChild(mCurrent);
// When the view is sub-screen-size in either dimension we
// offset it to center within the screen area, and to keep
// the views spaced out
cvOffset = subScreenSizeOffset(cv);
if (notPresent) {
// Main item not already present. Just place it top left
cvLeft = cvOffset.x;
cvTop = cvOffset.y;
} else {
// Main item already present. Adjust by scroll offsets
cvLeft = cv.getLeft() + mXScroll;
cvTop = cv.getTop() + mYScroll;
}
// Scroll values have been accounted for
mXScroll = mYScroll = 0;
cvRight = cvLeft + cv.getMeasuredWidth();
cvBottom = cvTop + cv.getMeasuredHeight();
if (!mUserInteracting && mScroller.isFinished()) {
Point corr = getCorrection(getScrollBounds(cvLeft, cvTop, cvRight, cvBottom));
cvRight += corr.x;
cvLeft += corr.x;
cvTop += corr.y;
cvBottom += corr.y;
} else if (HORIZONTAL_SCROLLING && cv.getMeasuredHeight() <= getHeight()) {
// When the current view is as small as the screen in height, clamp
// it vertically
Point corr = getCorrection(getScrollBounds(cvLeft, cvTop, cvRight, cvBottom));
cvTop += corr.y;
cvBottom += corr.y;
} else if (!HORIZONTAL_SCROLLING && cv.getMeasuredWidth() <= getWidth()) {
// When the current view is as small as the screen in width, clamp
// it horizontally
Point corr = getCorrection(getScrollBounds(cvLeft, cvTop, cvRight, cvBottom));
cvRight += corr.x;
cvLeft += corr.x;
}
cv.layout(cvLeft, cvTop, cvRight, cvBottom);
if (mCurrent > 0) {
View lv = getOrCreateChild(mCurrent - 1);
Point leftOffset = subScreenSizeOffset(lv);
if (HORIZONTAL_SCROLLING)
{
int gap = leftOffset.x + GAP + cvOffset.x;
lv.layout(cvLeft - lv.getMeasuredWidth() - gap,
(cvBottom + cvTop - lv.getMeasuredHeight())/2,
cvLeft - gap,
(cvBottom + cvTop + lv.getMeasuredHeight())/2);
} else {
int gap = leftOffset.y + GAP + cvOffset.y;
lv.layout((cvLeft + cvRight - lv.getMeasuredWidth())/2,
cvTop - lv.getMeasuredHeight() - gap,
(cvLeft + cvRight + lv.getMeasuredWidth())/2,
cvTop - gap);
}
}
if (mCurrent + 1 < mAdapter.getCount()) {
View rv = getOrCreateChild(mCurrent + 1);
Point rightOffset = subScreenSizeOffset(rv);
if (HORIZONTAL_SCROLLING)
{
int gap = cvOffset.x + GAP + rightOffset.x;
rv.layout(cvRight + gap,
(cvBottom + cvTop - rv.getMeasuredHeight())/2,
cvRight + rv.getMeasuredWidth() + gap,
(cvBottom + cvTop + rv.getMeasuredHeight())/2);
} else {
int gap = cvOffset.y + GAP + rightOffset.y;
rv.layout((cvLeft + cvRight - rv.getMeasuredWidth())/2,
cvBottom + gap,
(cvLeft + cvRight + rv.getMeasuredWidth())/2,
cvBottom + gap + rv.getMeasuredHeight());
}
}
invalidate();
}
@Override
public Adapter getAdapter() {
return mAdapter;
}
@Override
public View getSelectedView() {
return null;
}
@Override
public void setAdapter(Adapter adapter) {
if (mAdapter != null && mAdapter != adapter)
mAdapter.releaseBitmaps();
mAdapter = (PageAdapter) adapter;
requestLayout();
}
@Override
public void setSelection(int arg0) {
throw new UnsupportedOperationException(getContext().getString(R.string.not_supported));
}
private View getCached() {
if (mViewCache.size() == 0)
return null;
else
return mViewCache.removeFirst();
}
private View getOrCreateChild(int i) {
View v = mChildViews.get(i);
if (v == null) {
v = mAdapter.getView(i, getCached(), this);
addAndMeasureChild(i, v);
onChildSetup(i, v);
}
return v;
}
private void addAndMeasureChild(int i, View v) {
LayoutParams params = v.getLayoutParams();
if (params == null) {
params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
addViewInLayout(v, 0, params, true);
mChildViews.append(i, v); // Record the view against its adapter index
measureView(v);
}
private void measureView(View v) {
// See what size the view wants to be
v.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
// Work out a scale that will fit it to this view
float scale = Math.min((float)getWidth()/(float)v.getMeasuredWidth(),
(float)getHeight()/(float)v.getMeasuredHeight());
// Use the fitting values scaled by our current scale factor
v.measure(View.MeasureSpec.EXACTLY | (int)(v.getMeasuredWidth()*scale*mScale),
View.MeasureSpec.EXACTLY | (int)(v.getMeasuredHeight()*scale*mScale));
}
private Rect getScrollBounds(int left, int top, int right, int bottom) {
int xmin = getWidth() - right;
int xmax = -left;
int ymin = getHeight() - bottom;
int ymax = -top;
// In either dimension, if view smaller than screen then
// constrain it to be central
if (xmin > xmax) xmin = xmax = (xmin + xmax)/2;
if (ymin > ymax) ymin = ymax = (ymin + ymax)/2;
return new Rect(xmin, ymin, xmax, ymax);
}
private Rect getScrollBounds(View v) {
// There can be scroll amounts not yet accounted for in
// onLayout, so add mXScroll and mYScroll to the current
// positions when calculating the bounds.
return getScrollBounds(v.getLeft() + mXScroll,
v.getTop() + mYScroll,
v.getLeft() + v.getMeasuredWidth() + mXScroll,
v.getTop() + v.getMeasuredHeight() + mYScroll);
}
private Point getCorrection(Rect bounds) {
return new Point(Math.min(Math.max(0,bounds.left),bounds.right),
Math.min(Math.max(0,bounds.top),bounds.bottom));
}
private void postSettle(final View v) {
// onSettle and onUnsettle are posted so that the calls
// won't be executed until after the system has performed
// layout.
post (new Runnable() {
public void run () {
onSettle(v);
}
});
}
private void postUnsettle(final View v) {
post (new Runnable() {
public void run () {
onUnsettle(v);
}
});
}
private void slideViewOntoScreen(View v) {
Point corr = getCorrection(getScrollBounds(v));
if (corr.x != 0 || corr.y != 0) {
mScrollerLastX = mScrollerLastY = 0;
mScroller.startScroll(0, 0, corr.x, corr.y, 400);
mStepper.prod();
}
}
private Point subScreenSizeOffset(View v) {
return new Point(Math.max((getWidth() - v.getMeasuredWidth())/2, 0),
Math.max((getHeight() - v.getMeasuredHeight())/2, 0));
}
private static int directionOfTravel(float vx, float vy) {
if (Math.abs(vx) > 2 * Math.abs(vy))
return (vx > 0) ? MOVING_RIGHT : MOVING_LEFT;
else if (Math.abs(vy) > 2 * Math.abs(vx))
return (vy > 0) ? MOVING_DOWN : MOVING_UP;
else
return MOVING_DIAGONALLY;
}
private static boolean withinBoundsInDirectionOfTravel(Rect bounds, float vx, float vy) {
switch (directionOfTravel(vx, vy)) {
case MOVING_DIAGONALLY: return bounds.contains(0, 0);
case MOVING_LEFT: return bounds.left <= 0;
case MOVING_RIGHT: return bounds.right >= 0;
case MOVING_UP: return bounds.top <= 0;
case MOVING_DOWN: return bounds.bottom >= 0;
default: throw new NoSuchElementException();
}
}
protected void onTapMainDocArea() {}
protected void onDocMotion() {}
public void setLinksEnabled(boolean b) {
mLinksEnabled = b;
resetupChildren();
invalidate();
}
public boolean onSingleTapUp(MotionEvent e) {
Link link = null;
if (!tapDisabled) {
PageView pageView = (PageView) getDisplayedView();
if (mLinksEnabled && pageView != null) {
int page = pageView.hitLink(e.getX(), e.getY());
if (page > 0) {
pushHistory();
setDisplayedViewIndex(page);
} else {
onTapMainDocArea();
}
} else if (e.getX() < tapPageMargin) {
smartMoveBackwards();
} else if (e.getX() > super.getWidth() - tapPageMargin) {
smartMoveForwards();
} else if (e.getY() < tapPageMargin) {
smartMoveBackwards();
} else if (e.getY() > super.getHeight() - tapPageMargin) {
smartMoveForwards();
} else {
onTapMainDocArea();
}
}
return true;
}
protected void onChildSetup(int i, View v) {
if (SearchTaskResult.get() != null
&& SearchTaskResult.get().pageNumber == i)
((PageView) v).setSearchBoxes(SearchTaskResult.get().searchBoxes);
else
((PageView) v).setSearchBoxes(null);
((PageView) v).setLinkHighlighting(mLinksEnabled);
}
protected void onMoveToChild(int i) {
if (SearchTaskResult.get() != null
&& SearchTaskResult.get().pageNumber != i) {
SearchTaskResult.set(null);
resetupChildren();
}
}
protected void onMoveOffChild(int i) {
}
protected void onSettle(View v) {
// When the layout has settled ask the page to render
// in HQ
((PageView) v).updateHq(false);
}
protected void onUnsettle(View v) {
// When something changes making the previous settled view
// no longer appropriate, tell the page to remove HQ
((PageView) v).removeHq();
}
protected void onNotInUse(View v) {
((PageView) v).releaseResources();
}
}

View file

@ -0,0 +1,133 @@
package com.artifex.mupdf.viewer;
import com.artifex.mupdf.fitz.Quad;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Handler;
import android.os.AsyncTask;
import android.util.Log;
class ProgressDialogX extends ProgressDialog {
public ProgressDialogX(Context context) {
super(context);
}
private boolean mCancelled = false;
public boolean isCancelled() {
return mCancelled;
}
@Override
public void cancel() {
mCancelled = true;
super.cancel();
}
}
public abstract class SearchTask {
private final String APP = "MuPDF";
private static final int SEARCH_PROGRESS_DELAY = 200;
private final Context mContext;
private final MuPDFCore mCore;
private final Handler mHandler;
private final AlertDialog.Builder mAlertBuilder;
private AsyncTask<Void,Integer,SearchTaskResult> mSearchTask;
public SearchTask(Context context, MuPDFCore core) {
mContext = context;
mCore = core;
mHandler = new Handler();
mAlertBuilder = new AlertDialog.Builder(context);
}
protected abstract void onTextFound(SearchTaskResult result);
public void stop() {
if (mSearchTask != null) {
mSearchTask.cancel(true);
mSearchTask = null;
}
}
public void go(final String text, int direction, int displayPage, int searchPage) {
if (mCore == null)
return;
stop();
final int increment = direction;
final int startIndex = searchPage == -1 ? displayPage : searchPage + increment;
final ProgressDialogX progressDialog = new ProgressDialogX(mContext);
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
progressDialog.setTitle(mContext.getString(R.string.searching_));
progressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
public void onCancel(DialogInterface dialog) {
stop();
}
});
progressDialog.setMax(mCore.countPages());
mSearchTask = new AsyncTask<Void,Integer,SearchTaskResult>() {
@Override
protected SearchTaskResult doInBackground(Void... params) {
int index = startIndex;
while (0 <= index && index < mCore.countPages() && !isCancelled()) {
publishProgress(index);
Quad searchHits[][] = mCore.searchPage(index, text);
if (searchHits != null && searchHits.length > 0)
return new SearchTaskResult(text, index, searchHits);
index += increment;
}
return null;
}
@Override
protected void onPostExecute(SearchTaskResult result) {
progressDialog.cancel();
if (result != null) {
onTextFound(result);
} else {
mAlertBuilder.setTitle(SearchTaskResult.get() == null ? R.string.text_not_found : R.string.no_further_occurrences_found);
AlertDialog alert = mAlertBuilder.create();
alert.setButton(AlertDialog.BUTTON_POSITIVE, mContext.getString(R.string.dismiss),
(DialogInterface.OnClickListener)null);
alert.show();
}
}
@Override
protected void onCancelled() {
progressDialog.cancel();
}
@Override
protected void onProgressUpdate(Integer... values) {
progressDialog.setProgress(values[0].intValue());
}
@Override
protected void onPreExecute() {
super.onPreExecute();
mHandler.postDelayed(new Runnable() {
public void run() {
if (!progressDialog.isCancelled())
{
progressDialog.show();
progressDialog.setProgress(startIndex);
}
}
}, SEARCH_PROGRESS_DELAY);
}
};
mSearchTask.execute();
}
}

View file

@ -0,0 +1,24 @@
package com.artifex.mupdf.viewer;
import com.artifex.mupdf.fitz.Quad;
public class SearchTaskResult {
public final String txt;
public final int pageNumber;
public final Quad searchBoxes[][];
static private SearchTaskResult singleton;
SearchTaskResult(String _txt, int _pageNumber, Quad _searchBoxes[][]) {
txt = _txt;
pageNumber = _pageNumber;
searchBoxes = _searchBoxes;
}
static public SearchTaskResult get() {
return singleton;
}
static public void set(SearchTaskResult r) {
singleton = r;
}
}

View file

@ -0,0 +1,44 @@
package com.artifex.mupdf.viewer;
import android.annotation.SuppressLint;
import android.os.Build;
import android.util.Log;
import android.view.View;
public class Stepper {
private final String APP = "MuPDF";
protected final View mPoster;
protected final Runnable mTask;
protected boolean mPending;
public Stepper(View v, Runnable r) {
mPoster = v;
mTask = r;
mPending = false;
}
@SuppressLint("NewApi")
public void prod() {
if (!mPending) {
mPending = true;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
mPoster.postOnAnimation(new Runnable() {
@Override
public void run() {
mPending = false;
mTask.run();
}
});
} else {
mPoster.post(new Runnable() {
@Override
public void run() {
mPending = false;
mTask.run();
}
});
}
}
}
}

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="oval">
<solid android:color="#a0a0a0" />
<padding android:left="8dp" android:top="8dp" android:right="8dp" android:bottom="8dp" />
</shape>
</item>
<item>
<shape android:shape="oval">
<solid android:color="@android:color/transparent" />
<padding android:left="8dp" android:top="8dp" android:right="8dp" android:bottom="8dp" />
</shape>
</item>
</selector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M15.41,7.41L14,6l-6,6 6,6 1.41,-1.41L10.83,12z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M10,6L8.59,7.41 13.17,12l-4.58,4.59L10,18l6,-6z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
</vector>

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="38.836"
android:viewportHeight="38.836">
<path
android:fillColor="#FFFF0000"
android:fillType="evenOdd"
android:pathData="M38.331,4.315 L34.521,0.505 19.418,15.609 4.315,0.505 0.505,4.315 15.609,19.418 0.505,34.521 4.315,38.331 19.418,23.228 34.521,38.331 38.331,34.521 23.228,19.418Z"
android:strokeColor="#FFFF0000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="0.715"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M9,4v3h5v12h3L17,7h5L22,4L9,4zM3,12h3v7h3v-7h3L12,9L3,9v3z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M3.9,12c0,-1.71 1.39,-3.1 3.1,-3.1h4L11,7L7,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5h4v-1.9L7,15.1c-1.71,0 -3.1,-1.39 -3.1,-3.1zM8,13h8v-2L8,11v2zM17,7h-4v1.9h4c1.71,0 3.1,1.39 3.1,3.1s-1.39,3.1 -3.1,3.1h-4L13,17h4c2.76,0 5,-2.24 5,-5s-2.24,-5 -5,-5z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M3,9h14L17,7L3,7v2zM3,13h14v-2L3,11v2zM3,17h14v-2L3,15v2zM19,17h2v-2h-2v2zM19,7v2h2L21,7h-2zM19,13h2v-2h-2v2z"/>
</vector>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
<solid android:color="@color/page_indicator" />
<padding android:left="12dp" android:top="4dp" android:right="12dp" android:bottom="4dp" />
<corners android:radius="6dp" />
</shape>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="line" >
<stroke android:width="2dp" android:color="@android:color/white" />
</shape>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<size android:width="12dp" android:height="12dp" />
<stroke android:width="2dp" android:color="@android:color/white" />
</shape>

View file

@ -0,0 +1,165 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true"
>
<ViewAnimator
android:id="@+id/switcher"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
>
<LinearLayout
android:id="@+id/mainBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@color/toolbar"
>
<TextView
android:id="@+id/docNameText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center"
android:paddingLeft="16dp"
android:paddingRight="8dp"
android:singleLine="true"
android:ellipsize="end"
android:textSize="16sp"
android:textColor="@android:color/white"
/>
<ImageButton
android:id="@+id/linkButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/button"
android:src="@drawable/ic_link_white_24dp"
/>
<ImageButton
android:id="@+id/searchButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/button"
android:src="@drawable/ic_search_white_24dp"
/>
<ImageButton
android:id="@+id/layoutButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/button"
android:src="@drawable/ic_format_size_white_24dp"
android:visibility="gone"
/>
<ImageButton
android:id="@+id/outlineButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/button"
android:src="@drawable/ic_toc_white_24dp"
/>
</LinearLayout>
<LinearLayout
android:id="@+id/searchBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@color/toolbar"
>
<ImageButton
android:id="@+id/searchClose"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/button"
android:src="@drawable/ic_close_white_24dp"
/>
<EditText
android:id="@+id/searchText"
android:background="@android:color/transparent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center"
android:inputType="text"
android:imeOptions="actionSearch"
android:singleLine="true"
android:hint="@string/search"
android:textSize="16sp"
android:textColor="@android:color/white"
android:textColorHighlight="#a0a0a0"
android:textColorHint="#a0a0a0"
/>
<ImageButton
android:id="@+id/searchBack"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/button"
android:src="@drawable/ic_chevron_left_white_24dp"
/>
<ImageButton
android:id="@+id/searchForward"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/button"
android:src="@drawable/ic_chevron_right_white_24dp"
/>
</LinearLayout>
</ViewAnimator>
<RelativeLayout
android:id="@+id/lowerButtons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
>
<SeekBar
android:id="@+id/pageSlider"
android:layout_width="match_parent"
android:layout_height="36dp"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_margin="0dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="12dp"
android:paddingBottom="8dp"
android:background="@color/toolbar"
android:thumb="@drawable/seek_thumb"
android:progressDrawable="@drawable/seek_line"
/>
<TextView
android:id="@+id/pageNumber"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@+id/pageSlider"
android:layout_centerHorizontal="true"
android:layout_marginBottom="16dp"
android:background="@drawable/page_indicator"
android:textSize="16sp"
android:textColor="@android:color/white"
/>
</RelativeLayout>
</RelativeLayout>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/action_layout_6pt" android:title="6pt" />
<item android:id="@+id/action_layout_7pt" android:title="7pt" />
<item android:id="@+id/action_layout_8pt" android:title="8pt" />
<item android:id="@+id/action_layout_9pt" android:title="9pt" />
<item android:id="@+id/action_layout_10pt" android:title="10pt" />
<item android:id="@+id/action_layout_11pt" android:title="11pt" />
<item android:id="@+id/action_layout_12pt" android:title="12pt" />
<item android:id="@+id/action_layout_13pt" android:title="13pt" />
<item android:id="@+id/action_layout_14pt" android:title="14pt" />
<item android:id="@+id/action_layout_15pt" android:title="15pt" />
<item android:id="@+id/action_layout_16pt" android:title="16pt" />
</menu>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="page_indicator">#C0202020</color>
<color name="toolbar">#C0202020</color>
</resources>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="cancel">Cancel</string>
<string name="cannot_open_document">Cannot open document</string>
<string name="cannot_open_document_Reason">Cannot open document: %1$s</string>
<string name="dismiss">Dismiss</string>
<string name="enter_password">Enter password</string>
<string name="no_further_occurrences_found">No further occurrences found</string>
<string name="not_supported">Not supported</string>
<string name="okay">Okay</string>
<string name="search">Search&#x2026;</string>
<string name="searching_">Searching&#x2026;</string>
<string name="text_not_found">Text not found</string>
</resources>

3
settings.gradle Normal file
View file

@ -0,0 +1,3 @@
include ':jni'
include ':lib'
include ':app'