Repo created
This commit is contained in:
parent
e09986deae
commit
fa69fd81a1
48 changed files with 5156 additions and 0 deletions
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal 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
3
.gitmodules
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[submodule "jni"]
|
||||
path = jni
|
||||
url = ../mupdf-android-fitz.git
|
||||
661
COPYING
Normal file
661
COPYING
Normal 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
47
Makefile
Normal 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
143
README
Normal 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
54
app/build.gradle
Normal 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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
app/src/main/AndroidManifest.xml
Normal file
19
app/src/main/AndroidManifest.xml
Normal 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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
52
app/src/main/res/drawable/ic_mupdf.xml
Normal file
52
app/src/main/res/drawable/ic_mupdf.xml
Normal 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
27
build.gradle
Normal 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
1
gradle.properties
Normal file
|
|
@ -0,0 +1 @@
|
|||
android.useAndroidX=true
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
5
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
5
gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
188
gradlew
vendored
Executable 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
100
gradlew.bat
vendored
Normal 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
68
lib/build.gradle
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
33
lib/src/main/AndroidManifest.xml
Normal file
33
lib/src/main/AndroidManifest.xml
Normal 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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
844
lib/src/main/java/com/artifex/mupdf/viewer/DocumentActivity.java
Normal file
844
lib/src/main/java/com/artifex/mupdf/viewer/DocumentActivity.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
232
lib/src/main/java/com/artifex/mupdf/viewer/MuPDFCore.java
Normal file
232
lib/src/main/java/com/artifex/mupdf/viewer/MuPDFCore.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
105
lib/src/main/java/com/artifex/mupdf/viewer/PageAdapter.java
Normal file
105
lib/src/main/java/com/artifex/mupdf/viewer/PageAdapter.java
Normal 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;
|
||||
}
|
||||
}
|
||||
672
lib/src/main/java/com/artifex/mupdf/viewer/PageView.java
Normal file
672
lib/src/main/java/com/artifex/mupdf/viewer/PageView.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
39
lib/src/main/java/com/artifex/mupdf/viewer/Pallet.java
Normal file
39
lib/src/main/java/com/artifex/mupdf/viewer/Pallet.java
Normal 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));
|
||||
}
|
||||
}
|
||||
980
lib/src/main/java/com/artifex/mupdf/viewer/ReaderView.java
Normal file
980
lib/src/main/java/com/artifex/mupdf/viewer/ReaderView.java
Normal 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();
|
||||
}
|
||||
}
|
||||
133
lib/src/main/java/com/artifex/mupdf/viewer/SearchTask.java
Normal file
133
lib/src/main/java/com/artifex/mupdf/viewer/SearchTask.java
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
44
lib/src/main/java/com/artifex/mupdf/viewer/Stepper.java
Normal file
44
lib/src/main/java/com/artifex/mupdf/viewer/Stepper.java
Normal 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();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
lib/src/main/res/drawable/button.xml
Normal file
15
lib/src/main/res/drawable/button.xml
Normal 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>
|
||||
9
lib/src/main/res/drawable/ic_chevron_left_white_24dp.xml
Normal file
9
lib/src/main/res/drawable/ic_chevron_left_white_24dp.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
9
lib/src/main/res/drawable/ic_close_white_24dp.xml
Normal file
9
lib/src/main/res/drawable/ic_close_white_24dp.xml
Normal 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>
|
||||
15
lib/src/main/res/drawable/ic_error_red_24dp.xml
Normal file
15
lib/src/main/res/drawable/ic_error_red_24dp.xml
Normal 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>
|
||||
|
||||
9
lib/src/main/res/drawable/ic_format_size_white_24dp.xml
Normal file
9
lib/src/main/res/drawable/ic_format_size_white_24dp.xml
Normal 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>
|
||||
9
lib/src/main/res/drawable/ic_link_white_24dp.xml
Normal file
9
lib/src/main/res/drawable/ic_link_white_24dp.xml
Normal 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>
|
||||
9
lib/src/main/res/drawable/ic_search_white_24dp.xml
Normal file
9
lib/src/main/res/drawable/ic_search_white_24dp.xml
Normal 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>
|
||||
9
lib/src/main/res/drawable/ic_toc_white_24dp.xml
Normal file
9
lib/src/main/res/drawable/ic_toc_white_24dp.xml
Normal 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>
|
||||
6
lib/src/main/res/drawable/page_indicator.xml
Normal file
6
lib/src/main/res/drawable/page_indicator.xml
Normal 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>
|
||||
4
lib/src/main/res/drawable/seek_line.xml
Normal file
4
lib/src/main/res/drawable/seek_line.xml
Normal 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>
|
||||
5
lib/src/main/res/drawable/seek_thumb.xml
Normal file
5
lib/src/main/res/drawable/seek_thumb.xml
Normal 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>
|
||||
165
lib/src/main/res/layout/document_activity.xml
Normal file
165
lib/src/main/res/layout/document_activity.xml
Normal 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>
|
||||
14
lib/src/main/res/menu/layout_menu.xml
Normal file
14
lib/src/main/res/menu/layout_menu.xml
Normal 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>
|
||||
5
lib/src/main/res/values/colors.xml
Normal file
5
lib/src/main/res/values/colors.xml
Normal 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>
|
||||
14
lib/src/main/res/values/strings.xml
Normal file
14
lib/src/main/res/values/strings.xml
Normal 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…</string>
|
||||
<string name="searching_">Searching…</string>
|
||||
<string name="text_not_found">Text not found</string>
|
||||
</resources>
|
||||
3
settings.gradle
Normal file
3
settings.gradle
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
include ':jni'
|
||||
include ':lib'
|
||||
include ':app'
|
||||
Loading…
Add table
Add a link
Reference in a new issue