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'