diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ee8bdf4
--- /dev/null
+++ b/.gitignore
@@ -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
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..cf17dfc
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "jni"]
+ path = jni
+ url = ../mupdf-android-fitz.git
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..dba13ed
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,661 @@
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ 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.
+
+
+ Copyright (C)
+
+ 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 .
+
+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
+.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..8d875d1
--- /dev/null
+++ b/Makefile
@@ -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
diff --git a/README b/README
new file mode 100644
index 0000000..051acb6
--- /dev/null
+++ b/README
@@ -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 .
+
+## 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=
+ release_keyAlias=MyKey
+ release_keyPassword=
+
+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!
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..16da65d
--- /dev/null
+++ b/app/build.gradle
@@ -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'
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..9da588a
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/com/artifex/mupdf/viewer/app/LibraryActivity.java b/app/src/main/java/com/artifex/mupdf/viewer/app/LibraryActivity.java
new file mode 100644
index 0000000..a90043c
--- /dev/null
+++ b/app/src/main/java/com/artifex/mupdf/viewer/app/LibraryActivity.java
@@ -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;
+ }
+}
diff --git a/app/src/main/res/drawable/ic_mupdf.xml b/app/src/main/res/drawable/ic_mupdf.xml
new file mode 100644
index 0000000..fd5379d
--- /dev/null
+++ b/app/src/main/res/drawable/ic_mupdf.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..8770582
--- /dev/null
+++ b/build.gradle
@@ -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()
+ }
+}
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..5bac8ac
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1 @@
+android.useAndroidX=true
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..5c2d1cf
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..48c0a02
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -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
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..83f2acf
--- /dev/null
+++ b/gradlew
@@ -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" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..24467a1
--- /dev/null
+++ b/gradlew.bat
@@ -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
diff --git a/lib/build.gradle b/lib/build.gradle
new file mode 100644
index 0000000..df10462
--- /dev/null
+++ b/lib/build.gradle
@@ -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"
+ }
+ }
+ }
+ }
+}
diff --git a/lib/src/main/AndroidManifest.xml b/lib/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..2078e9d
--- /dev/null
+++ b/lib/src/main/AndroidManifest.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lib/src/main/java/com/artifex/mupdf/viewer/CancellableAsyncTask.java b/lib/src/main/java/com/artifex/mupdf/viewer/CancellableAsyncTask.java
new file mode 100644
index 0000000..e808242
--- /dev/null
+++ b/lib/src/main/java/com/artifex/mupdf/viewer/CancellableAsyncTask.java
@@ -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
+{
+ private final String APP = "MuPDF";
+
+ private final AsyncTask asyncTask;
+ private final CancellableTaskDefinition ourTask;
+
+ public void onPreExecute()
+ {
+
+ }
+
+ public void onPostExecute(Result result)
+ {
+
+ }
+
+ public CancellableAsyncTask(final CancellableTaskDefinition task)
+ {
+ if (task == null)
+ throw new IllegalArgumentException();
+
+ this.ourTask = task;
+ asyncTask = new AsyncTask()
+ {
+ @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);
+ }
+
+}
diff --git a/lib/src/main/java/com/artifex/mupdf/viewer/CancellableTaskDefinition.java b/lib/src/main/java/com/artifex/mupdf/viewer/CancellableTaskDefinition.java
new file mode 100644
index 0000000..2833969
--- /dev/null
+++ b/lib/src/main/java/com/artifex/mupdf/viewer/CancellableTaskDefinition.java
@@ -0,0 +1,8 @@
+package com.artifex.mupdf.viewer;
+
+public interface CancellableTaskDefinition
+{
+ public Result doInBackground(Params ... params);
+ public void doCancel();
+ public void doCleanup();
+}
diff --git a/lib/src/main/java/com/artifex/mupdf/viewer/ContentInputStream.java b/lib/src/main/java/com/artifex/mupdf/viewer/ContentInputStream.java
new file mode 100644
index 0000000..97b4332
--- /dev/null
+++ b/lib/src/main/java/com/artifex/mupdf/viewer/ContentInputStream.java
@@ -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;
+ }
+
+}
diff --git a/lib/src/main/java/com/artifex/mupdf/viewer/DocumentActivity.java b/lib/src/main/java/com/artifex/mupdf/viewer/DocumentActivity.java
new file mode 100644
index 0000000..0ab8bab
--- /dev/null
+++ b/lib/src/main/java/com/artifex/mupdf/viewer/DocumentActivity.java
@@ -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 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);
+ }
+ }
+ }
+}
diff --git a/lib/src/main/java/com/artifex/mupdf/viewer/MuPDFCancellableTaskDefinition.java b/lib/src/main/java/com/artifex/mupdf/viewer/MuPDFCancellableTaskDefinition.java
new file mode 100644
index 0000000..b586eb8
--- /dev/null
+++ b/lib/src/main/java/com/artifex/mupdf/viewer/MuPDFCancellableTaskDefinition.java
@@ -0,0 +1,40 @@
+package com.artifex.mupdf.viewer;
+
+import com.artifex.mupdf.fitz.Cookie;
+
+public abstract class MuPDFCancellableTaskDefinition implements CancellableTaskDefinition
+{
+ 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);
+}
diff --git a/lib/src/main/java/com/artifex/mupdf/viewer/MuPDFCore.java b/lib/src/main/java/com/artifex/mupdf/viewer/MuPDFCore.java
new file mode 100644
index 0000000..c61ebba
--- /dev/null
+++ b/lib/src/main/java/com/artifex/mupdf/viewer/MuPDFCore.java
@@ -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 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 getOutline() {
+ ArrayList result = new ArrayList();
+ 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;
+ }
+}
diff --git a/lib/src/main/java/com/artifex/mupdf/viewer/OutlineActivity.java b/lib/src/main/java/com/artifex/mupdf/viewer/OutlineActivity.java
new file mode 100644
index 0000000..6281ae4
--- /dev/null
+++ b/lib/src/main/java/com/artifex/mupdf/viewer/OutlineActivity.java
@@ -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- adapter;
+
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+
+ adapter = new ArrayAdapter
- (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
- outline = (ArrayList
- )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();
+ }
+}
diff --git a/lib/src/main/java/com/artifex/mupdf/viewer/PageAdapter.java b/lib/src/main/java/com/artifex/mupdf/viewer/PageAdapter.java
new file mode 100644
index 0000000..7639aac
--- /dev/null
+++ b/lib/src/main/java/com/artifex/mupdf/viewer/PageAdapter.java
@@ -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 mPageSizes = new SparseArray();
+ 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 sizingTask = new AsyncTask() {
+ @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;
+ }
+}
diff --git a/lib/src/main/java/com/artifex/mupdf/viewer/PageView.java b/lib/src/main/java/com/artifex/mupdf/viewer/PageView.java
new file mode 100644
index 0000000..bc1113b
--- /dev/null
+++ b/lib/src/main/java/com/artifex/mupdf/viewer/PageView.java
@@ -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 mGetLinkInfo;
+ private CancellableAsyncTask 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 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() {
+ 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(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 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(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(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 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() {
+ @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 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() {
+ @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;
+ }
+ }
+}
diff --git a/lib/src/main/java/com/artifex/mupdf/viewer/Pallet.java b/lib/src/main/java/com/artifex/mupdf/viewer/Pallet.java
new file mode 100644
index 0000000..3baf352
--- /dev/null
+++ b/lib/src/main/java/com/artifex/mupdf/viewer/Pallet.java
@@ -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 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));
+ }
+}
diff --git a/lib/src/main/java/com/artifex/mupdf/viewer/ReaderView.java b/lib/src/main/java/com/artifex/mupdf/viewer/ReaderView.java
new file mode 100644
index 0000000..88d7364
--- /dev/null
+++ b/lib/src/main/java/com/artifex/mupdf/viewer/ReaderView.java
@@ -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
+ 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
+ mChildViews = new SparseArray(3);
+ // Shadows the children of the adapter view
+ // but with more sensible indexing
+ private final LinkedList
+ mViewCache = new LinkedList();
+ 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 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();
+
+ // 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();
+ }
+}
diff --git a/lib/src/main/java/com/artifex/mupdf/viewer/SearchTask.java b/lib/src/main/java/com/artifex/mupdf/viewer/SearchTask.java
new file mode 100644
index 0000000..bc67fc5
--- /dev/null
+++ b/lib/src/main/java/com/artifex/mupdf/viewer/SearchTask.java
@@ -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 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() {
+ @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();
+ }
+}
diff --git a/lib/src/main/java/com/artifex/mupdf/viewer/SearchTaskResult.java b/lib/src/main/java/com/artifex/mupdf/viewer/SearchTaskResult.java
new file mode 100644
index 0000000..09f83af
--- /dev/null
+++ b/lib/src/main/java/com/artifex/mupdf/viewer/SearchTaskResult.java
@@ -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;
+ }
+}
diff --git a/lib/src/main/java/com/artifex/mupdf/viewer/Stepper.java b/lib/src/main/java/com/artifex/mupdf/viewer/Stepper.java
new file mode 100644
index 0000000..a7d2720
--- /dev/null
+++ b/lib/src/main/java/com/artifex/mupdf/viewer/Stepper.java
@@ -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();
+ }
+ });
+
+ }
+ }
+ }
+}
diff --git a/lib/src/main/res/drawable/button.xml b/lib/src/main/res/drawable/button.xml
new file mode 100644
index 0000000..162ab93
--- /dev/null
+++ b/lib/src/main/res/drawable/button.xml
@@ -0,0 +1,15 @@
+
+
+
-
+
+
+
+
+
+ -
+
+
+
+
+
+
diff --git a/lib/src/main/res/drawable/ic_chevron_left_white_24dp.xml b/lib/src/main/res/drawable/ic_chevron_left_white_24dp.xml
new file mode 100644
index 0000000..7428907
--- /dev/null
+++ b/lib/src/main/res/drawable/ic_chevron_left_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/lib/src/main/res/drawable/ic_chevron_right_white_24dp.xml b/lib/src/main/res/drawable/ic_chevron_right_white_24dp.xml
new file mode 100644
index 0000000..36b411a
--- /dev/null
+++ b/lib/src/main/res/drawable/ic_chevron_right_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/lib/src/main/res/drawable/ic_close_white_24dp.xml b/lib/src/main/res/drawable/ic_close_white_24dp.xml
new file mode 100644
index 0000000..d11cc5c
--- /dev/null
+++ b/lib/src/main/res/drawable/ic_close_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/lib/src/main/res/drawable/ic_error_red_24dp.xml b/lib/src/main/res/drawable/ic_error_red_24dp.xml
new file mode 100644
index 0000000..3365b1a
--- /dev/null
+++ b/lib/src/main/res/drawable/ic_error_red_24dp.xml
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/lib/src/main/res/drawable/ic_format_size_white_24dp.xml b/lib/src/main/res/drawable/ic_format_size_white_24dp.xml
new file mode 100644
index 0000000..23f2f33
--- /dev/null
+++ b/lib/src/main/res/drawable/ic_format_size_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/lib/src/main/res/drawable/ic_link_white_24dp.xml b/lib/src/main/res/drawable/ic_link_white_24dp.xml
new file mode 100644
index 0000000..d9f3fe3
--- /dev/null
+++ b/lib/src/main/res/drawable/ic_link_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/lib/src/main/res/drawable/ic_search_white_24dp.xml b/lib/src/main/res/drawable/ic_search_white_24dp.xml
new file mode 100644
index 0000000..47432c1
--- /dev/null
+++ b/lib/src/main/res/drawable/ic_search_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/lib/src/main/res/drawable/ic_toc_white_24dp.xml b/lib/src/main/res/drawable/ic_toc_white_24dp.xml
new file mode 100644
index 0000000..9f8420c
--- /dev/null
+++ b/lib/src/main/res/drawable/ic_toc_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/lib/src/main/res/drawable/page_indicator.xml b/lib/src/main/res/drawable/page_indicator.xml
new file mode 100644
index 0000000..f2ca93c
--- /dev/null
+++ b/lib/src/main/res/drawable/page_indicator.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/lib/src/main/res/drawable/seek_line.xml b/lib/src/main/res/drawable/seek_line.xml
new file mode 100644
index 0000000..ee67d29
--- /dev/null
+++ b/lib/src/main/res/drawable/seek_line.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/lib/src/main/res/drawable/seek_thumb.xml b/lib/src/main/res/drawable/seek_thumb.xml
new file mode 100644
index 0000000..2a0745c
--- /dev/null
+++ b/lib/src/main/res/drawable/seek_thumb.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/lib/src/main/res/layout/document_activity.xml b/lib/src/main/res/layout/document_activity.xml
new file mode 100644
index 0000000..e7e1354
--- /dev/null
+++ b/lib/src/main/res/layout/document_activity.xml
@@ -0,0 +1,165 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lib/src/main/res/menu/layout_menu.xml b/lib/src/main/res/menu/layout_menu.xml
new file mode 100644
index 0000000..4bc834a
--- /dev/null
+++ b/lib/src/main/res/menu/layout_menu.xml
@@ -0,0 +1,14 @@
+
+
diff --git a/lib/src/main/res/values/colors.xml b/lib/src/main/res/values/colors.xml
new file mode 100644
index 0000000..5c23909
--- /dev/null
+++ b/lib/src/main/res/values/colors.xml
@@ -0,0 +1,5 @@
+
+
+ #C0202020
+ #C0202020
+
diff --git a/lib/src/main/res/values/strings.xml b/lib/src/main/res/values/strings.xml
new file mode 100644
index 0000000..292ba78
--- /dev/null
+++ b/lib/src/main/res/values/strings.xml
@@ -0,0 +1,14 @@
+
+
+ Cancel
+ Cannot open document
+ Cannot open document: %1$s
+ Dismiss
+ Enter password
+ No further occurrences found
+ Not supported
+ Okay
+ Search…
+ Searching…
+ Text not found
+
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..1fc37bd
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,3 @@
+include ':jni'
+include ':lib'
+include ':app'