Repository: viavansi/mupdf-android Branch: master Commit: 9fe7c5277834 Files: 100 Total size: 485.5 KB Directory structure: gitextract_jjhp7xc0/ ├── .gitignore ├── COPYING ├── LICENSE ├── README.md ├── build.gradle ├── gradle.properties ├── proguard-rules.pro └── src/ ├── androidTest/ │ └── java/ │ └── com/ │ └── artifex/ │ └── viafirma/ │ └── mupdf/ │ └── ApplicationTest.java └── main/ ├── AndroidManifest.xml ├── java/ │ └── com/ │ └── artifex/ │ ├── mupdfdemo/ │ │ ├── Annotation.java │ │ ├── ArrayDeque.java │ │ ├── AsyncTask.java │ │ ├── CancellableAsyncTask.java │ │ ├── CancellableTaskDefinition.java │ │ ├── ChoosePDFActivity.java │ │ ├── ChoosePDFAdapter.java │ │ ├── ChoosePDFItem.java │ │ ├── Deque.java │ │ ├── FilePicker.java │ │ ├── LinkInfo.java │ │ ├── LinkInfoExternal.java │ │ ├── LinkInfoInternal.java │ │ ├── LinkInfoRemote.java │ │ ├── LinkInfoVisitor.java │ │ ├── MuPDFActivity.java │ │ ├── MuPDFAlert.java │ │ ├── MuPDFAlertInternal.java │ │ ├── MuPDFCancellableTaskDefinition.java │ │ ├── MuPDFCore.java │ │ ├── MuPDFFragment.java │ │ ├── MuPDFPageAdapter.java │ │ ├── MuPDFPageView.java │ │ ├── MuPDFReaderView.java │ │ ├── MuPDFReflowAdapter.java │ │ ├── MuPDFReflowView.java │ │ ├── MuPDFView.java │ │ ├── OutlineActivity.java │ │ ├── OutlineActivityData.java │ │ ├── OutlineAdapter.java │ │ ├── OutlineItem.java │ │ ├── PageView.java │ │ ├── PrintDialogActivity.java │ │ ├── ReaderView.java │ │ ├── SafeAnimatorInflater.java │ │ ├── SearchTask.java │ │ ├── SearchTaskResult.java │ │ ├── Stepper.java │ │ ├── TextChar.java │ │ ├── TextWord.java │ │ └── WidgetType.java │ └── utils/ │ ├── DigitalizedEventCallback.java │ └── PdfBitmap.java └── res/ ├── animator/ │ └── info.xml ├── drawable/ │ ├── busy.xml │ ├── button.xml │ ├── page_num.xml │ ├── search.xml │ ├── seek_progress.xml │ ├── seek_thumb.xml │ └── tiled_background.xml ├── layout/ │ ├── buttons.xml │ ├── main.xml │ ├── outline_entry.xml │ ├── picker_entry.xml │ ├── print_dialog.xml │ └── textentry.xml ├── values/ │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── values-ar/ │ └── strings.xml ├── values-ca/ │ └── strings.xml ├── values-cs/ │ └── strings.xml ├── values-da/ │ └── strings.xml ├── values-de/ │ └── strings.xml ├── values-el/ │ └── strings.xml ├── values-es/ │ └── strings.xml ├── values-et/ │ └── strings.xml ├── values-fi/ │ └── strings.xml ├── values-fr/ │ └── strings.xml ├── values-hi/ │ └── strings.xml ├── values-hu/ │ └── strings.xml ├── values-in/ │ └── strings.xml ├── values-it/ │ └── strings.xml ├── values-iw/ │ └── strings.xml ├── values-ja/ │ └── strings.xml ├── values-ko/ │ └── strings.xml ├── values-lt/ │ └── strings.xml ├── values-ms/ │ └── strings.xml ├── values-nl/ │ └── strings.xml ├── values-no/ │ └── strings.xml ├── values-pl/ │ └── strings.xml ├── values-pt/ │ └── strings.xml ├── values-ru/ │ └── strings.xml ├── values-sk/ │ └── strings.xml ├── values-sv/ │ └── strings.xml ├── values-th/ │ └── strings.xml ├── values-tl/ │ └── strings.xml ├── values-tr/ │ └── strings.xml ├── values-zh/ │ └── strings.xml └── values-zh-rTW/ └── strings.xml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .gradle /local.properties /.idea/workspace.xml /.idea/libraries .DS_Store /build /*.iml *.iml ================================================ FILE: COPYING ================================================ 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 . ================================================ FILE: LICENSE ================================================ 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 . ================================================ FILE: README.md ================================================ # MuPDF for Android ## Introduction This project is intended to offer an easy integration of MuPDF library (http://www.mupdf.com) on Android, avoiding the building process and adapted to the last version of Android Studio and Gradle as of February 2015 (commit 262a4717a9997c89cac275d24ce6d605ca06284f from http://git.ghostscript.com/mupdf.git) We also added some features: * You can add custom Bitmaps to each page. * You can use the MuPDFActivity as a Fragment (MuPDFFragment), that allows you to include it in your own activity as any other layout. * You can add an interface listener to the page of the pdf, so you can listen when the user taps, double taps or long press any coordinate of the pdf. This version is still on development. ## Installation guide 1. Make sure you have installed the newest NDK from https://developer.android.com/tools/sdk/ndk/index.html#Installing (version 9+ required) ================================================ FILE: build.gradle ================================================ repositories { google() mavenCentral() jcenter() maven { url "https://repositorio.viavansi.com/repo/" } } buildscript { repositories { google() mavenCentral() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:3.4.2' } } apply plugin: 'com.android.library' apply plugin: 'maven-publish' def muPdfversionCode = 22 def muPdfversionName = '1.2.24.1' android { compileSdkVersion 28 lintOptions { abortOnError false } defaultConfig { minSdkVersion 19 targetSdkVersion 31 versionCode muPdfversionCode versionName muPdfversionName } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } configurations { deployerJars } dependencies { deployerJars 'org.apache.maven.wagon:wagon-http:2.2' implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.appcompat:appcompat:1.0.0' } // For Maven Repository submitting : Execute task: UploadArchives. Ex: > gradle uploadArchives // Define repoUsername, repoPassword and repoUrl on "gradle.properties" file in order to use this task. //uploadArchives { // repositories.mavenDeployer { // configuration = configurations.deployerJars // repository(url: repoUrl) { // authentication(userName: repoUsername, password: repoPassword) // } // pom.version = muPdfversionName // pom.artifactId = "mupdf-android" // pom.groupId = "com.viafirma" // } //} ================================================ FILE: gradle.properties ================================================ # Project-wide Gradle settings. # IDE (e.g. Android Studio) users: # Settings specified in this file will override any Gradle settings # configured through the IDE. # For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. # Default value: -Xmx10248m -XX:MaxPermSize=256m # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true repoUrl=http://repositorio.viavansi.com/artifactory/libs-releases-local repoUsername= repoPassword= android.useDeprecatedNdk=true ================================================ FILE: proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in /Applications/android-sdk-macosx/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} ================================================ FILE: src/androidTest/java/com/artifex/viafirma/mupdf/ApplicationTest.java ================================================ package com.artifex.viafirma.mupdf; import android.app.Application; import android.test.ApplicationTestCase; /** * Testing Fundamentals */ public class ApplicationTest extends ApplicationTestCase { public ApplicationTest() { super(Application.class); } } ================================================ FILE: src/main/AndroidManifest.xml ================================================ ================================================ FILE: src/main/java/com/artifex/mupdfdemo/Annotation.java ================================================ package com.artifex.mupdfdemo; import android.graphics.RectF; public class Annotation extends RectF { enum Type { TEXT, LINK, FREETEXT, LINE, SQUARE, CIRCLE, POLYGON, POLYLINE, HIGHLIGHT, UNDERLINE, SQUIGGLY, STRIKEOUT, STAMP, CARET, INK, POPUP, FILEATTACHMENT, SOUND, MOVIE, WIDGET, SCREEN, PRINTERMARK, TRAPNET, WATERMARK, A3D, UNKNOWN } public final Type type; public Annotation(float x0, float y0, float x1, float y1, int _type) { super(x0, y0, x1, y1); type = _type == -1 ? Type.UNKNOWN : Type.values()[_type]; } } ================================================ FILE: src/main/java/com/artifex/mupdfdemo/ArrayDeque.java ================================================ /* * Written by Josh Bloch of Google Inc. and released to the public domain, * as explained at http://creativecommons.org/publicdomain/zero/1.0/. */ package com.artifex.mupdfdemo; import java.util.AbstractCollection; import java.util.Arrays; import java.util.Collection; import java.util.ConcurrentModificationException; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.NoSuchElementException; import java.util.Queue; import java.util.Stack; // BEGIN android-note // removed link to collections framework docs // END android-note /** * Resizable-array implementation of the {@link Deque} interface. Array * deques have no capacity restrictions; they grow as necessary to support * usage. They are not thread-safe; in the absence of external * synchronization, they do not support concurrent access by multiple threads. * Null elements are prohibited. This class is likely to be faster than * {@link java.util.Stack} when used as a stack, and faster than {@link java.util.LinkedList} * when used as a queue. * *

Most ArrayDeque operations run in amortized constant time. * Exceptions include {@link #remove(Object) remove}, {@link * #removeFirstOccurrence removeFirstOccurrence}, {@link #removeLastOccurrence * removeLastOccurrence}, {@link #contains contains}, {@link #iterator * iterator.remove()}, and the bulk operations, all of which run in linear * time. * *

The iterators returned by this class's iterator method are * fail-fast: If the deque is modified at any time after the iterator * is created, in any way except through the iterator's own remove * method, the iterator will generally throw a {@link * java.util.ConcurrentModificationException}. Thus, in the face of concurrent * modification, the iterator fails quickly and cleanly, rather than risking * arbitrary, non-deterministic behavior at an undetermined time in the * future. * *

Note that the fail-fast behavior of an iterator cannot be guaranteed * as it is, generally speaking, impossible to make any hard guarantees in the * presence of unsynchronized concurrent modification. Fail-fast iterators * throw ConcurrentModificationException on a best-effort basis. * Therefore, it would be wrong to write a program that depended on this * exception for its correctness: the fail-fast behavior of iterators * should be used only to detect bugs. * *

This class and its iterator implement all of the * optional methods of the {@link java.util.Collection} and {@link * java.util.Iterator} interfaces. * * @author Josh Bloch and Doug Lea * @since 1.6 * @param the type of elements held in this collection */ public class ArrayDeque extends AbstractCollection implements Deque, Cloneable, java.io.Serializable { /** * The array in which the elements of the deque are stored. * The capacity of the deque is the length of this array, which is * always a power of two. The array is never allowed to become * full, except transiently within an addX method where it is * resized (see doubleCapacity) immediately upon becoming full, * thus avoiding head and tail wrapping around to equal each * other. We also guarantee that all array cells not holding * deque elements are always null. */ private transient Object[] elements; /** * The index of the element at the head of the deque (which is the * element that would be removed by remove() or pop()); or an * arbitrary number equal to tail if the deque is empty. */ private transient int head; /** * The index at which the next element would be added to the tail * of the deque (via addLast(E), add(E), or push(E)). */ private transient int tail; /** * The minimum capacity that we'll use for a newly created deque. * Must be a power of 2. */ private static final int MIN_INITIAL_CAPACITY = 8; // ****** Array allocation and resizing utilities ****** /** * Allocate empty array to hold the given number of elements. * * @param numElements the number of elements to hold */ private void allocateElements(int numElements) { int initialCapacity = MIN_INITIAL_CAPACITY; // Find the best power of two to hold elements. // Tests "<=" because arrays aren't kept full. if (numElements >= initialCapacity) { initialCapacity = numElements; initialCapacity |= (initialCapacity >>> 1); initialCapacity |= (initialCapacity >>> 2); initialCapacity |= (initialCapacity >>> 4); initialCapacity |= (initialCapacity >>> 8); initialCapacity |= (initialCapacity >>> 16); initialCapacity++; if (initialCapacity < 0) // Too many elements, must back off initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements } elements = new Object[initialCapacity]; } /** * Double the capacity of this deque. Call only when full, i.e., * when head and tail have wrapped around to become equal. */ private void doubleCapacity() { // assert head == tail; int p = head; int n = elements.length; int r = n - p; // number of elements to the right of p int newCapacity = n << 1; if (newCapacity < 0) throw new IllegalStateException("Sorry, deque too big"); Object[] a = new Object[newCapacity]; System.arraycopy(elements, p, a, 0, r); System.arraycopy(elements, 0, a, r, p); elements = a; head = 0; tail = n; } /** * Copies the elements from our element array into the specified array, * in order (from first to last element in the deque). It is assumed * that the array is large enough to hold all elements in the deque. * * @return its argument */ private T[] copyElements(T[] a) { if (head < tail) { System.arraycopy(elements, head, a, 0, size()); } else if (head > tail) { int headPortionLen = elements.length - head; System.arraycopy(elements, head, a, 0, headPortionLen); System.arraycopy(elements, 0, a, headPortionLen, tail); } return a; } /** * Constructs an empty array deque with an initial capacity * sufficient to hold 16 elements. */ public ArrayDeque() { elements = new Object[16]; } /** * Constructs an empty array deque with an initial capacity * sufficient to hold the specified number of elements. * * @param numElements lower bound on initial capacity of the deque */ public ArrayDeque(int numElements) { allocateElements(numElements); } /** * Constructs a deque containing the elements of the specified * collection, in the order they are returned by the collection's * iterator. (The first element returned by the collection's * iterator becomes the first element, or front of the * deque.) * * @param c the collection whose elements are to be placed into the deque * @throws NullPointerException if the specified collection is null */ public ArrayDeque(Collection c) { allocateElements(c.size()); addAll(c); } // The main insertion and extraction methods are addFirst, // addLast, pollFirst, pollLast. The other methods are defined in // terms of these. /** * Inserts the specified element at the front of this deque. * * @param e the element to add * @throws NullPointerException if the specified element is null */ public void addFirst(E e) { if (e == null) throw new NullPointerException("e == null"); elements[head = (head - 1) & (elements.length - 1)] = e; if (head == tail) doubleCapacity(); } /** * Inserts the specified element at the end of this deque. * *

This method is equivalent to {@link #add}. * * @param e the element to add * @throws NullPointerException if the specified element is null */ public void addLast(E e) { if (e == null) throw new NullPointerException("e == null"); elements[tail] = e; if ( (tail = (tail + 1) & (elements.length - 1)) == head) doubleCapacity(); } /** * Inserts the specified element at the front of this deque. * * @param e the element to add * @return true (as specified by {@link Deque#offerFirst}) * @throws NullPointerException if the specified element is null */ public boolean offerFirst(E e) { addFirst(e); return true; } /** * Inserts the specified element at the end of this deque. * * @param e the element to add * @return true (as specified by {@link Deque#offerLast}) * @throws NullPointerException if the specified element is null */ public boolean offerLast(E e) { addLast(e); return true; } /** * @throws java.util.NoSuchElementException {@inheritDoc} */ public E removeFirst() { E x = pollFirst(); if (x == null) throw new NoSuchElementException(); return x; } /** * @throws java.util.NoSuchElementException {@inheritDoc} */ public E removeLast() { E x = pollLast(); if (x == null) throw new NoSuchElementException(); return x; } public E pollFirst() { int h = head; @SuppressWarnings("unchecked") E result = (E) elements[h]; // Element is null if deque empty if (result == null) return null; elements[h] = null; // Must null out slot head = (h + 1) & (elements.length - 1); return result; } public E pollLast() { int t = (tail - 1) & (elements.length - 1); @SuppressWarnings("unchecked") E result = (E) elements[t]; if (result == null) return null; elements[t] = null; tail = t; return result; } /** * @throws java.util.NoSuchElementException {@inheritDoc} */ public E getFirst() { @SuppressWarnings("unchecked") E result = (E) elements[head]; if (result == null) throw new NoSuchElementException(); return result; } /** * @throws java.util.NoSuchElementException {@inheritDoc} */ public E getLast() { @SuppressWarnings("unchecked") E result = (E) elements[(tail - 1) & (elements.length - 1)]; if (result == null) throw new NoSuchElementException(); return result; } public E peekFirst() { @SuppressWarnings("unchecked") E result = (E) elements[head]; // elements[head] is null if deque empty return result; } public E peekLast() { @SuppressWarnings("unchecked") E result = (E) elements[(tail - 1) & (elements.length - 1)]; return result; } /** * Removes the first occurrence of the specified element in this * deque (when traversing the deque from head to tail). * If the deque does not contain the element, it is unchanged. * More formally, removes the first element e such that * o.equals(e) (if such an element exists). * Returns true if this deque contained the specified element * (or equivalently, if this deque changed as a result of the call). * * @param o element to be removed from this deque, if present * @return true if the deque contained the specified element */ public boolean removeFirstOccurrence(Object o) { if (o == null) return false; int mask = elements.length - 1; int i = head; Object x; while ( (x = elements[i]) != null) { if (o.equals(x)) { delete(i); return true; } i = (i + 1) & mask; } return false; } /** * Removes the last occurrence of the specified element in this * deque (when traversing the deque from head to tail). * If the deque does not contain the element, it is unchanged. * More formally, removes the last element e such that * o.equals(e) (if such an element exists). * Returns true if this deque contained the specified element * (or equivalently, if this deque changed as a result of the call). * * @param o element to be removed from this deque, if present * @return true if the deque contained the specified element */ public boolean removeLastOccurrence(Object o) { if (o == null) return false; int mask = elements.length - 1; int i = (tail - 1) & mask; Object x; while ( (x = elements[i]) != null) { if (o.equals(x)) { delete(i); return true; } i = (i - 1) & mask; } return false; } // *** Queue methods *** /** * Inserts the specified element at the end of this deque. * *

This method is equivalent to {@link #addLast}. * * @param e the element to add * @return true (as specified by {@link java.util.Collection#add}) * @throws NullPointerException if the specified element is null */ public boolean add(E e) { addLast(e); return true; } /** * Inserts the specified element at the end of this deque. * *

This method is equivalent to {@link #offerLast}. * * @param e the element to add * @return true (as specified by {@link java.util.Queue#offer}) * @throws NullPointerException if the specified element is null */ public boolean offer(E e) { return offerLast(e); } /** * Retrieves and removes the head of the queue represented by this deque. * * This method differs from {@link #poll poll} only in that it throws an * exception if this deque is empty. * *

This method is equivalent to {@link #removeFirst}. * * @return the head of the queue represented by this deque * @throws java.util.NoSuchElementException {@inheritDoc} */ public E remove() { return removeFirst(); } /** * Retrieves and removes the head of the queue represented by this deque * (in other words, the first element of this deque), or returns * null if this deque is empty. * *

This method is equivalent to {@link #pollFirst}. * * @return the head of the queue represented by this deque, or * null if this deque is empty */ public E poll() { return pollFirst(); } /** * Retrieves, but does not remove, the head of the queue represented by * this deque. This method differs from {@link #peek peek} only in * that it throws an exception if this deque is empty. * *

This method is equivalent to {@link #getFirst}. * * @return the head of the queue represented by this deque * @throws java.util.NoSuchElementException {@inheritDoc} */ public E element() { return getFirst(); } /** * Retrieves, but does not remove, the head of the queue represented by * this deque, or returns null if this deque is empty. * *

This method is equivalent to {@link #peekFirst}. * * @return the head of the queue represented by this deque, or * null if this deque is empty */ public E peek() { return peekFirst(); } // *** Stack methods *** /** * Pushes an element onto the stack represented by this deque. In other * words, inserts the element at the front of this deque. * *

This method is equivalent to {@link #addFirst}. * * @param e the element to push * @throws NullPointerException if the specified element is null */ public void push(E e) { addFirst(e); } /** * Pops an element from the stack represented by this deque. In other * words, removes and returns the first element of this deque. * *

This method is equivalent to {@link #removeFirst()}. * * @return the element at the front of this deque (which is the top * of the stack represented by this deque) * @throws java.util.NoSuchElementException {@inheritDoc} */ public E pop() { return removeFirst(); } private void checkInvariants() { // assert elements[tail] == null; // assert head == tail ? elements[head] == null : // (elements[head] != null && // elements[(tail - 1) & (elements.length - 1)] != null); // assert elements[(head - 1) & (elements.length - 1)] == null; } /** * Removes the element at the specified position in the elements array, * adjusting head and tail as necessary. This can result in motion of * elements backwards or forwards in the array. * *

This method is called delete rather than remove to emphasize * that its semantics differ from those of {@link java.util.List#remove(int)}. * * @return true if elements moved backwards */ private boolean delete(int i) { //checkInvariants(); final Object[] elements = this.elements; final int mask = elements.length - 1; final int h = head; final int t = tail; final int front = (i - h) & mask; final int back = (t - i) & mask; // Invariant: head <= i < tail mod circularity if (front >= ((t - h) & mask)) throw new ConcurrentModificationException(); // Optimize for least element motion if (front < back) { if (h <= i) { System.arraycopy(elements, h, elements, h + 1, front); } else { // Wrap around System.arraycopy(elements, 0, elements, 1, i); elements[0] = elements[mask]; System.arraycopy(elements, h, elements, h + 1, mask - h); } elements[h] = null; head = (h + 1) & mask; return false; } else { if (i < t) { // Copy the null tail as well System.arraycopy(elements, i + 1, elements, i, back); tail = t - 1; } else { // Wrap around System.arraycopy(elements, i + 1, elements, i, mask - i); elements[mask] = elements[0]; System.arraycopy(elements, 1, elements, 0, t); tail = (t - 1) & mask; } return true; } } // *** Collection Methods *** /** * Returns the number of elements in this deque. * * @return the number of elements in this deque */ public int size() { return (tail - head) & (elements.length - 1); } /** * Returns true if this deque contains no elements. * * @return true if this deque contains no elements */ public boolean isEmpty() { return head == tail; } /** * Returns an iterator over the elements in this deque. The elements * will be ordered from first (head) to last (tail). This is the same * order that elements would be dequeued (via successive calls to * {@link #remove} or popped (via successive calls to {@link #pop}). * * @return an iterator over the elements in this deque */ public Iterator iterator() { return new DeqIterator(); } public Iterator descendingIterator() { return new DescendingIterator(); } private class DeqIterator implements Iterator { /** * Index of element to be returned by subsequent call to next. */ private int cursor = head; /** * Tail recorded at construction (also in remove), to stop * iterator and also to check for comodification. */ private int fence = tail; /** * Index of element returned by most recent call to next. * Reset to -1 if element is deleted by a call to remove. */ private int lastRet = -1; public boolean hasNext() { return cursor != fence; } public E next() { if (cursor == fence) throw new NoSuchElementException(); @SuppressWarnings("unchecked") E result = (E) elements[cursor]; // This check doesn't catch all possible comodifications, // but does catch the ones that corrupt traversal if (tail != fence || result == null) throw new ConcurrentModificationException(); lastRet = cursor; cursor = (cursor + 1) & (elements.length - 1); return result; } public void remove() { if (lastRet < 0) throw new IllegalStateException(); if (delete(lastRet)) { // if left-shifted, undo increment in next() cursor = (cursor - 1) & (elements.length - 1); fence = tail; } lastRet = -1; } } private class DescendingIterator implements Iterator { /* * This class is nearly a mirror-image of DeqIterator, using * tail instead of head for initial cursor, and head instead of * tail for fence. */ private int cursor = tail; private int fence = head; private int lastRet = -1; public boolean hasNext() { return cursor != fence; } public E next() { if (cursor == fence) throw new NoSuchElementException(); cursor = (cursor - 1) & (elements.length - 1); @SuppressWarnings("unchecked") E result = (E) elements[cursor]; if (head != fence || result == null) throw new ConcurrentModificationException(); lastRet = cursor; return result; } public void remove() { if (lastRet < 0) throw new IllegalStateException(); if (!delete(lastRet)) { cursor = (cursor + 1) & (elements.length - 1); fence = head; } lastRet = -1; } } /** * Returns true if this deque contains the specified element. * More formally, returns true if and only if this deque contains * at least one element e such that o.equals(e). * * @param o object to be checked for containment in this deque * @return true if this deque contains the specified element */ public boolean contains(Object o) { if (o == null) return false; int mask = elements.length - 1; int i = head; Object x; while ( (x = elements[i]) != null) { if (o.equals(x)) return true; i = (i + 1) & mask; } return false; } /** * Removes a single instance of the specified element from this deque. * If the deque does not contain the element, it is unchanged. * More formally, removes the first element e such that * o.equals(e) (if such an element exists). * Returns true if this deque contained the specified element * (or equivalently, if this deque changed as a result of the call). * *

This method is equivalent to {@link #removeFirstOccurrence}. * * @param o element to be removed from this deque, if present * @return true if this deque contained the specified element */ public boolean remove(Object o) { return removeFirstOccurrence(o); } /** * Removes all of the elements from this deque. * The deque will be empty after this call returns. */ public void clear() { int h = head; int t = tail; if (h != t) { // clear all cells head = tail = 0; int i = h; int mask = elements.length - 1; do { elements[i] = null; i = (i + 1) & mask; } while (i != t); } } /** * Returns an array containing all of the elements in this deque * in proper sequence (from first to last element). * *

The returned array will be "safe" in that no references to it are * maintained by this deque. (In other words, this method must allocate * a new array). The caller is thus free to modify the returned array. * *

This method acts as bridge between array-based and collection-based * APIs. * * @return an array containing all of the elements in this deque */ public Object[] toArray() { return copyElements(new Object[size()]); } /** * Returns an array containing all of the elements in this deque in * proper sequence (from first to last element); the runtime type of the * returned array is that of the specified array. If the deque fits in * the specified array, it is returned therein. Otherwise, a new array * is allocated with the runtime type of the specified array and the * size of this deque. * *

If this deque fits in the specified array with room to spare * (i.e., the array has more elements than this deque), the element in * the array immediately following the end of the deque is set to * null. * *

Like the {@link #toArray()} method, this method acts as bridge between * array-based and collection-based APIs. Further, this method allows * precise control over the runtime type of the output array, and may, * under certain circumstances, be used to save allocation costs. * *

Suppose x is a deque known to contain only strings. * The following code can be used to dump the deque into a newly * allocated array of String: * *

 {@code String[] y = x.toArray(new String[0]);}
* * Note that toArray(new Object[0]) is identical in function to * toArray(). * * @param a the array into which the elements of the deque are to * be stored, if it is big enough; otherwise, a new array of the * same runtime type is allocated for this purpose * @return an array containing all of the elements in this deque * @throws ArrayStoreException if the runtime type of the specified array * is not a supertype of the runtime type of every element in * this deque * @throws NullPointerException if the specified array is null */ @SuppressWarnings("unchecked") public T[] toArray(T[] a) { int size = size(); if (a.length < size) a = (T[])java.lang.reflect.Array.newInstance( a.getClass().getComponentType(), size); copyElements(a); if (a.length > size) a[size] = null; return a; } // *** Object methods *** /** * Returns a copy of this deque. * * @return a copy of this deque */ public ArrayDeque clone() { try { @SuppressWarnings("unchecked") ArrayDeque result = (ArrayDeque) super.clone(); result.elements = Arrays.copyOf(elements, elements.length); return result; } catch (CloneNotSupportedException e) { throw new AssertionError(); } } /** * Appease the serialization gods. */ private static final long serialVersionUID = 2340985798034038923L; /** * Serialize this deque. * * @serialData The current size (int) of the deque, * followed by all of its elements (each an object reference) in * first-to-last order. */ private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException { s.defaultWriteObject(); // Write out size s.writeInt(size()); // Write out elements in order. int mask = elements.length - 1; for (int i = head; i != tail; i = (i + 1) & mask) s.writeObject(elements[i]); } /** * Deserialize this deque. */ private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); // Read in size and allocate array int size = s.readInt(); allocateElements(size); head = 0; tail = size; // Read in all elements in the proper order. for (int i = 0; i < size; i++) elements[i] = s.readObject(); } } ================================================ FILE: src/main/java/com/artifex/mupdfdemo/AsyncTask.java ================================================ /* * Copyright (C) 2008 The Android Open Source Project * * 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 * * http://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. */ package com.artifex.mupdfdemo; import java.util.concurrent.BlockingQueue; import java.util.concurrent.Callable; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.FutureTask; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import android.os.Process; import android.os.Handler; import android.os.Message; /** *

AsyncTask enables proper and easy use of the UI thread. This class allows to * perform background operations and publish results on the UI thread without * having to manipulate threads and/or handlers.

* *

AsyncTask is designed to be a helper class around {@link Thread} and {@link android.os.Handler} * and does not constitute a generic threading framework. AsyncTasks should ideally be * used for short operations (a few seconds at the most.) If you need to keep threads * running for long periods of time, it is highly recommended you use the various APIs * provided by the java.util.concurrent pacakge such as {@link java.util.concurrent.Executor}, * {@link java.util.concurrent.ThreadPoolExecutor} and {@link java.util.concurrent.FutureTask}.

* *

An asynchronous task is defined by a computation that runs on a background thread and * whose result is published on the UI thread. An asynchronous task is defined by 3 generic * types, called Params, Progress and Result, * and 4 steps, called onPreExecute, doInBackground, * onProgressUpdate and onPostExecute.

* *
*

Developer Guides

*

For more information about using tasks and threads, read the * Processes and * Threads developer guide.

*
* *

Usage

*

AsyncTask must be subclassed to be used. The subclass will override at least * one method ({@link #doInBackground}), and most often will override a * second one ({@link #onPostExecute}.)

* *

Here is an example of subclassing:

*
 * private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
 *     protected Long doInBackground(URL... urls) {
 *         int count = urls.length;
 *         long totalSize = 0;
 *         for (int i = 0; i < count; i++) {
 *             totalSize += Downloader.downloadFile(urls[i]);
 *             publishProgress((int) ((i / (float) count) * 100));
 *             // Escape early if cancel() is called
 *             if (isCancelled()) break;
 *         }
 *         return totalSize;
 *     }
 *
 *     protected void onProgressUpdate(Integer... progress) {
 *         setProgressPercent(progress[0]);
 *     }
 *
 *     protected void onPostExecute(Long result) {
 *         showDialog("Downloaded " + result + " bytes");
 *     }
 * }
 * 
* *

Once created, a task is executed very simply:

*
 * new DownloadFilesTask().execute(url1, url2, url3);
 * 
* *

AsyncTask's generic types

*

The three types used by an asynchronous task are the following:

*
    *
  1. Params, the type of the parameters sent to the task upon * execution.
  2. *
  3. Progress, the type of the progress units published during * the background computation.
  4. *
  5. Result, the type of the result of the background * computation.
  6. *
*

Not all types are always used by an asynchronous task. To mark a type as unused, * simply use the type {@link Void}:

*
 * private class MyTask extends AsyncTask<Void, Void, Void> { ... }
 * 
* *

The 4 steps

*

When an asynchronous task is executed, the task goes through 4 steps:

*
    *
  1. {@link #onPreExecute()}, invoked on the UI thread before the task * is executed. This step is normally used to setup the task, for instance by * showing a progress bar in the user interface.
  2. *
  3. {@link #doInBackground}, invoked on the background thread * immediately after {@link #onPreExecute()} finishes executing. This step is used * to perform background computation that can take a long time. The parameters * of the asynchronous task are passed to this step. The result of the computation must * be returned by this step and will be passed back to the last step. This step * can also use {@link #publishProgress} to publish one or more units * of progress. These values are published on the UI thread, in the * {@link #onProgressUpdate} step.
  4. *
  5. {@link #onProgressUpdate}, invoked on the UI thread after a * call to {@link #publishProgress}. The timing of the execution is * undefined. This method is used to display any form of progress in the user * interface while the background computation is still executing. For instance, * it can be used to animate a progress bar or show logs in a text field.
  6. *
  7. {@link #onPostExecute}, invoked on the UI thread after the background * computation finishes. The result of the background computation is passed to * this step as a parameter.
  8. *
* *

Cancelling a task

*

A task can be cancelled at any time by invoking {@link #cancel(boolean)}. Invoking * this method will cause subsequent calls to {@link #isCancelled()} to return true. * After invoking this method, {@link #onCancelled(Object)}, instead of * {@link #onPostExecute(Object)} will be invoked after {@link #doInBackground(Object[])} * returns. To ensure that a task is cancelled as quickly as possible, you should always * check the return value of {@link #isCancelled()} periodically from * {@link #doInBackground(Object[])}, if possible (inside a loop for instance.)

* *

Threading rules

*

There are a few threading rules that must be followed for this class to * work properly:

*
    *
  • The AsyncTask class must be loaded on the UI thread. This is done * automatically as of {@link android.os.Build.VERSION_CODES#JELLY_BEAN}.
  • *
  • The task instance must be created on the UI thread.
  • *
  • {@link #execute} must be invoked on the UI thread.
  • *
  • Do not call {@link #onPreExecute()}, {@link #onPostExecute}, * {@link #doInBackground}, {@link #onProgressUpdate} manually.
  • *
  • The task can be executed only once (an exception will be thrown if * a second execution is attempted.)
  • *
* *

Memory observability

*

AsyncTask guarantees that all callback calls are synchronized in such a way that the following * operations are safe without explicit synchronizations.

*
    *
  • Set member fields in the constructor or {@link #onPreExecute}, and refer to them * in {@link #doInBackground}. *
  • Set member fields in {@link #doInBackground}, and refer to them in * {@link #onProgressUpdate} and {@link #onPostExecute}. *
* *

Order of execution

*

When first introduced, AsyncTasks were executed serially on a single background * thread. Starting with {@link android.os.Build.VERSION_CODES#DONUT}, this was changed * to a pool of threads allowing multiple tasks to operate in parallel. Starting with * {@link android.os.Build.VERSION_CODES#HONEYCOMB}, tasks are executed on a single * thread to avoid common application errors caused by parallel execution.

*

If you truly want parallel execution, you can invoke * {@link #executeOnExecutor(java.util.concurrent.Executor, Object[])} with * {@link #THREAD_POOL_EXECUTOR}.

*/ public abstract class AsyncTask { private static final String LOG_TAG = "AsyncTask"; private static final int CORE_POOL_SIZE = 5; private static final int MAXIMUM_POOL_SIZE = 128; private static final int KEEP_ALIVE = 1; private static final ThreadFactory sThreadFactory = new ThreadFactory() { private final AtomicInteger mCount = new AtomicInteger(1); public Thread newThread(Runnable r) { return new Thread(r, "AsyncTask #" + mCount.getAndIncrement()); } }; private static final BlockingQueue sPoolWorkQueue = new LinkedBlockingQueue(10); /** * An {@link java.util.concurrent.Executor} that can be used to execute tasks in parallel. */ public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory); /** * An {@link java.util.concurrent.Executor} that executes tasks one at a time in serial * order. This serialization is global to a particular process. */ public static final Executor SERIAL_EXECUTOR = new SerialExecutor(); private static final int MESSAGE_POST_RESULT = 0x1; private static final int MESSAGE_POST_PROGRESS = 0x2; private static final InternalHandler sHandler = new InternalHandler(); private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR; private final WorkerRunnable mWorker; private final FutureTask mFuture; private volatile Status mStatus = Status.PENDING; private final AtomicBoolean mCancelled = new AtomicBoolean(); private final AtomicBoolean mTaskInvoked = new AtomicBoolean(); private static class SerialExecutor implements Executor { final ArrayDeque mTasks = new ArrayDeque(); Runnable mActive; public synchronized void execute(final Runnable r) { mTasks.offer(new Runnable() { public void run() { try { r.run(); } finally { scheduleNext(); } } }); if (mActive == null) { scheduleNext(); } } protected synchronized void scheduleNext() { if ((mActive = mTasks.poll()) != null) { THREAD_POOL_EXECUTOR.execute(mActive); } } } /** * Indicates the current status of the task. Each status will be set only once * during the lifetime of a task. */ public enum Status { /** * Indicates that the task has not been executed yet. */ PENDING, /** * Indicates that the task is running. */ RUNNING, /** * Indicates that {@link com.artifex.mupdfdemo.AsyncTask#onPostExecute} has finished. */ FINISHED, } /** @hide Used to force static handler to be created. */ public static void init() { sHandler.getLooper(); } /** @hide */ public static void setDefaultExecutor(Executor exec) { sDefaultExecutor = exec; } /** * Creates a new asynchronous task. This constructor must be invoked on the UI thread. */ public AsyncTask() { mWorker = new WorkerRunnable() { public Result call() throws Exception { mTaskInvoked.set(true); Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); //noinspection unchecked return postResult(doInBackground(mParams)); } }; mFuture = new FutureTask(mWorker) { @Override protected void done() { try { postResultIfNotInvoked(get()); } catch (InterruptedException e) { android.util.Log.w(LOG_TAG, e); } catch (ExecutionException e) { throw new RuntimeException("An error occured while executing doInBackground()", e.getCause()); } catch (CancellationException e) { postResultIfNotInvoked(null); } } }; } private void postResultIfNotInvoked(Result result) { final boolean wasTaskInvoked = mTaskInvoked.get(); if (!wasTaskInvoked) { postResult(result); } } private Result postResult(Result result) { @SuppressWarnings("unchecked") Message message = sHandler.obtainMessage(MESSAGE_POST_RESULT, new AsyncTaskResult(this, result)); message.sendToTarget(); return result; } /** * Returns the current status of this task. * * @return The current status. */ public final Status getStatus() { return mStatus; } /** * Override this method to perform a computation on a background thread. The * specified parameters are the parameters passed to {@link #execute} * by the caller of this task. * * This method can call {@link #publishProgress} to publish updates * on the UI thread. * * @param params The parameters of the task. * * @return A result, defined by the subclass of this task. * * @see #onPreExecute() * @see #onPostExecute * @see #publishProgress */ protected abstract Result doInBackground(Params... params); /** * Runs on the UI thread before {@link #doInBackground}. * * @see #onPostExecute * @see #doInBackground */ protected void onPreExecute() { } /** *

Runs on the UI thread after {@link #doInBackground}. The * specified result is the value returned by {@link #doInBackground}.

* *

This method won't be invoked if the task was cancelled.

* * @param result The result of the operation computed by {@link #doInBackground}. * * @see #onPreExecute * @see #doInBackground * @see #onCancelled(Object) */ @SuppressWarnings({"UnusedDeclaration"}) protected void onPostExecute(Result result) { } /** * Runs on the UI thread after {@link #publishProgress} is invoked. * The specified values are the values passed to {@link #publishProgress}. * * @param values The values indicating progress. * * @see #publishProgress * @see #doInBackground */ @SuppressWarnings({"UnusedDeclaration"}) protected void onProgressUpdate(Progress... values) { } /** *

Runs on the UI thread after {@link #cancel(boolean)} is invoked and * {@link #doInBackground(Object[])} has finished.

* *

The default implementation simply invokes {@link #onCancelled()} and * ignores the result. If you write your own implementation, do not call * super.onCancelled(result).

* * @param result The result, if any, computed in * {@link #doInBackground(Object[])}, can be null * * @see #cancel(boolean) * @see #isCancelled() */ @SuppressWarnings({"UnusedParameters"}) protected void onCancelled(Result result) { onCancelled(); } /** *

Applications should preferably override {@link #onCancelled(Object)}. * This method is invoked by the default implementation of * {@link #onCancelled(Object)}.

* *

Runs on the UI thread after {@link #cancel(boolean)} is invoked and * {@link #doInBackground(Object[])} has finished.

* * @see #onCancelled(Object) * @see #cancel(boolean) * @see #isCancelled() */ protected void onCancelled() { } /** * Returns true if this task was cancelled before it completed * normally. If you are calling {@link #cancel(boolean)} on the task, * the value returned by this method should be checked periodically from * {@link #doInBackground(Object[])} to end the task as soon as possible. * * @return true if task was cancelled before it completed * * @see #cancel(boolean) */ public final boolean isCancelled() { return mCancelled.get(); } /** *

Attempts to cancel execution of this task. This attempt will * fail if the task has already completed, already been cancelled, * or could not be cancelled for some other reason. If successful, * and this task has not started when cancel is called, * this task should never run. If the task has already started, * then the mayInterruptIfRunning parameter determines * whether the thread executing this task should be interrupted in * an attempt to stop the task.

* *

Calling this method will result in {@link #onCancelled(Object)} being * invoked on the UI thread after {@link #doInBackground(Object[])} * returns. Calling this method guarantees that {@link #onPostExecute(Object)} * is never invoked. After invoking this method, you should check the * value returned by {@link #isCancelled()} periodically from * {@link #doInBackground(Object[])} to finish the task as early as * possible.

* * @param mayInterruptIfRunning true if the thread executing this * task should be interrupted; otherwise, in-progress tasks are allowed * to complete. * * @return false if the task could not be cancelled, * typically because it has already completed normally; * true otherwise * * @see #isCancelled() * @see #onCancelled(Object) */ public final boolean cancel(boolean mayInterruptIfRunning) { mCancelled.set(true); return mFuture.cancel(mayInterruptIfRunning); } /** * Waits if necessary for the computation to complete, and then * retrieves its result. * * @return The computed result. * * @throws java.util.concurrent.CancellationException If the computation was cancelled. * @throws java.util.concurrent.ExecutionException If the computation threw an exception. * @throws InterruptedException If the current thread was interrupted * while waiting. */ public final Result get() throws InterruptedException, ExecutionException { return mFuture.get(); } /** * Waits if necessary for at most the given time for the computation * to complete, and then retrieves its result. * * @param timeout Time to wait before cancelling the operation. * @param unit The time unit for the timeout. * * @return The computed result. * * @throws java.util.concurrent.CancellationException If the computation was cancelled. * @throws java.util.concurrent.ExecutionException If the computation threw an exception. * @throws InterruptedException If the current thread was interrupted * while waiting. * @throws java.util.concurrent.TimeoutException If the wait timed out. */ public final Result get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { return mFuture.get(timeout, unit); } /** * Executes the task with the specified parameters. The task returns * itself (this) so that the caller can keep a reference to it. * *

Note: this function schedules the task on a queue for a single background * thread or pool of threads depending on the platform version. When first * introduced, AsyncTasks were executed serially on a single background thread. * Starting with {@link android.os.Build.VERSION_CODES#DONUT}, this was changed * to a pool of threads allowing multiple tasks to operate in parallel. Starting * {@link android.os.Build.VERSION_CODES#HONEYCOMB}, tasks are back to being * executed on a single thread to avoid common application errors caused * by parallel execution. If you truly want parallel execution, you can use * the {@link #executeOnExecutor} version of this method * with {@link #THREAD_POOL_EXECUTOR}; however, see commentary there for warnings * on its use. * *

This method must be invoked on the UI thread. * * @param params The parameters of the task. * * @return This instance of AsyncTask. * * @throws IllegalStateException If {@link #getStatus()} returns either * {@link com.artifex.mupdfdemo.AsyncTask.Status#RUNNING} or {@link com.artifex.mupdfdemo.AsyncTask.Status#FINISHED}. * * @see #executeOnExecutor(java.util.concurrent.Executor, Object[]) * @see #execute(Runnable) */ public final AsyncTask execute(Params... params) { return executeOnExecutor(sDefaultExecutor, params); } /** * Executes the task with the specified parameters. The task returns * itself (this) so that the caller can keep a reference to it. * *

This method is typically used with {@link #THREAD_POOL_EXECUTOR} to * allow multiple tasks to run in parallel on a pool of threads managed by * AsyncTask, however you can also use your own {@link java.util.concurrent.Executor} for custom * behavior. * *

Warning: Allowing multiple tasks to run in parallel from * a thread pool is generally not what one wants, because the order * of their operation is not defined. For example, if these tasks are used * to modify any state in common (such as writing a file due to a button click), * there are no guarantees on the order of the modifications. * Without careful work it is possible in rare cases for the newer version * of the data to be over-written by an older one, leading to obscure data * loss and stability issues. Such changes are best * executed in serial; to guarantee such work is serialized regardless of * platform version you can use this function with {@link #SERIAL_EXECUTOR}. * *

This method must be invoked on the UI thread. * * @param exec The executor to use. {@link #THREAD_POOL_EXECUTOR} is available as a * convenient process-wide thread pool for tasks that are loosely coupled. * @param params The parameters of the task. * * @return This instance of AsyncTask. * * @throws IllegalStateException If {@link #getStatus()} returns either * {@link com.artifex.mupdfdemo.AsyncTask.Status#RUNNING} or {@link com.artifex.mupdfdemo.AsyncTask.Status#FINISHED}. * * @see #execute(Object[]) */ public final AsyncTask executeOnExecutor(Executor exec, Params... params) { if (mStatus != Status.PENDING) { switch (mStatus) { case RUNNING: throw new IllegalStateException("Cannot execute task:" + " the task is already running."); case FINISHED: throw new IllegalStateException("Cannot execute task:" + " the task has already been executed " + "(a task can be executed only once)"); } } mStatus = Status.RUNNING; onPreExecute(); mWorker.mParams = params; exec.execute(mFuture); return this; } /** * Convenience version of {@link #execute(Object...)} for use with * a simple Runnable object. See {@link #execute(Object[])} for more * information on the order of execution. * * @see #execute(Object[]) * @see #executeOnExecutor(java.util.concurrent.Executor, Object[]) */ public static void execute(Runnable runnable) { sDefaultExecutor.execute(runnable); } /** * This method can be invoked from {@link #doInBackground} to * publish updates on the UI thread while the background computation is * still running. Each call to this method will trigger the execution of * {@link #onProgressUpdate} on the UI thread. * * {@link #onProgressUpdate} will note be called if the task has been * canceled. * * @param values The progress values to update the UI with. * * @see #onProgressUpdate * @see #doInBackground */ protected final void publishProgress(Progress... values) { if (!isCancelled()) { sHandler.obtainMessage(MESSAGE_POST_PROGRESS, new AsyncTaskResult(this, values)).sendToTarget(); } } private void finish(Result result) { if (isCancelled()) { onCancelled(result); } else { onPostExecute(result); } mStatus = Status.FINISHED; } private static class InternalHandler extends Handler { @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"}) @Override public void handleMessage(Message msg) { AsyncTaskResult result = (AsyncTaskResult) msg.obj; switch (msg.what) { case MESSAGE_POST_RESULT: // There is only one result result.mTask.finish(result.mData[0]); break; case MESSAGE_POST_PROGRESS: result.mTask.onProgressUpdate(result.mData); break; } } } private static abstract class WorkerRunnable implements Callable { Params[] mParams; } @SuppressWarnings({"RawUseOfParameterizedType"}) private static class AsyncTaskResult { final AsyncTask mTask; final Data[] mData; AsyncTaskResult(AsyncTask task, Data... data) { mTask = task; mData = data; } } } ================================================ FILE: src/main/java/com/artifex/mupdfdemo/CancellableAsyncTask.java ================================================ package com.artifex.mupdfdemo; 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 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(); } }; } public void cancelAndWait() { this.asyncTask.cancel(true); ourTask.doCancel(); try { this.asyncTask.get(); } catch (InterruptedException e) { } catch (ExecutionException e) { } catch (CancellationException e) { } ourTask.doCleanup(); } public void execute(Params ... params) { asyncTask.execute(params); } } ================================================ FILE: src/main/java/com/artifex/mupdfdemo/CancellableTaskDefinition.java ================================================ package com.artifex.mupdfdemo; public interface CancellableTaskDefinition { public Result doInBackground(Params... params); public void doCancel(); public void doCleanup(); } ================================================ FILE: src/main/java/com/artifex/mupdfdemo/ChoosePDFActivity.java ================================================ package com.artifex.mupdfdemo; import java.io.File; import java.io.FileFilter; import java.util.Arrays; import java.util.Comparator; import java.util.HashMap; import java.util.Map; import android.app.AlertDialog; import android.app.ListActivity; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.content.res.Resources; import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.os.FileObserver; import android.os.Handler; import android.view.View; import android.widget.ListView; enum Purpose { PickPDF, PickKeyFile } public class ChoosePDFActivity extends ListActivity { static public final String PICK_KEY_FILE = "com.artifex.mupdfdemo.PICK_KEY_FILE"; static private File mDirectory; static private Map mPositions = new HashMap(); private File mParent; private File [] mDirs; private File [] mFiles; private Handler mHandler; private Runnable mUpdateFiles; private ChoosePDFAdapter adapter; private Purpose mPurpose; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mPurpose = PICK_KEY_FILE.equals(getIntent().getAction()) ? Purpose.PickKeyFile : Purpose.PickPDF; String storageState = Environment.getExternalStorageState(); if (!Environment.MEDIA_MOUNTED.equals(storageState) && !Environment.MEDIA_MOUNTED_READ_ONLY.equals(storageState)) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.no_media_warning); builder.setMessage(R.string.no_media_hint); AlertDialog alert = builder.create(); alert.setButton(AlertDialog.BUTTON_POSITIVE,getString(R.string.dismiss), new OnClickListener() { public void onClick(DialogInterface dialog, int which) { finish(); } }); alert.show(); return; } if (mDirectory == null) mDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); // Create a list adapter... adapter = new ChoosePDFAdapter(getLayoutInflater()); setListAdapter(adapter); // ...that is updated dynamically when files are scanned mHandler = new Handler(); mUpdateFiles = new Runnable() { public void run() { Resources res = getResources(); String appName = res.getString(R.string.mupdf_title); String version = res.getString(R.string.version); String title = res.getString(R.string.picker_title_App_Ver_Dir); setTitle(String.format(title, appName, version, mDirectory)); mParent = mDirectory.getParentFile(); mDirs = mDirectory.listFiles(new FileFilter() { public boolean accept(File file) { return file.isDirectory(); } }); if (mDirs == null) mDirs = new File[0]; mFiles = mDirectory.listFiles(new FileFilter() { public boolean accept(File file) { if (file.isDirectory()) return false; String fname = file.getName().toLowerCase(); switch (mPurpose) { case PickPDF: if (fname.endsWith(".pdf")) return true; if (fname.endsWith(".xps")) return true; if (fname.endsWith(".cbz")) return true; if (fname.endsWith(".png")) return true; if (fname.endsWith(".jpe")) return true; if (fname.endsWith(".jpeg")) return true; if (fname.endsWith(".jpg")) return true; if (fname.endsWith(".jfif")) return true; if (fname.endsWith(".jfif-tbnl")) return true; if (fname.endsWith(".tif")) return true; if (fname.endsWith(".tiff")) return true; return false; case PickKeyFile: if (fname.endsWith(".pfx")) return true; return false; default: return false; } } }); if (mFiles == null) mFiles = new File[0]; Arrays.sort(mFiles, new Comparator() { public int compare(File arg0, File arg1) { return arg0.getName().compareToIgnoreCase(arg1.getName()); } }); Arrays.sort(mDirs, new Comparator() { public int compare(File arg0, File arg1) { return arg0.getName().compareToIgnoreCase(arg1.getName()); } }); adapter.clear(); if (mParent != null) adapter.add(new ChoosePDFItem(ChoosePDFItem.Type.PARENT, getString(R.string.parent_directory))); for (File f : mDirs) adapter.add(new ChoosePDFItem(ChoosePDFItem.Type.DIR, f.getName())); for (File f : mFiles) adapter.add(new ChoosePDFItem(ChoosePDFItem.Type.DOC, f.getName())); lastPosition(); } }; // Start initial file scan... mHandler.post(mUpdateFiles); // ...and observe the directory and scan files upon changes. FileObserver observer = new FileObserver(mDirectory.getPath(), FileObserver.CREATE | FileObserver.DELETE) { public void onEvent(int event, String path) { mHandler.post(mUpdateFiles); } }; observer.startWatching(); } private void lastPosition() { String p = mDirectory.getAbsolutePath(); if (mPositions.containsKey(p)) getListView().setSelection(mPositions.get(p)); } @Override protected void onListItemClick(ListView l, View v, int position, long id) { super.onListItemClick(l, v, position, id); mPositions.put(mDirectory.getAbsolutePath(), getListView().getFirstVisiblePosition()); if (position < (mParent == null ? 0 : 1)) { mDirectory = mParent; mHandler.post(mUpdateFiles); return; } position -= (mParent == null ? 0 : 1); if (position < mDirs.length) { mDirectory = mDirs[position]; mHandler.post(mUpdateFiles); return; } position -= mDirs.length; Uri uri = Uri.parse(mFiles[position].getAbsolutePath()); Intent intent = new Intent(this,MuPDFActivity.class); intent.setAction(Intent.ACTION_VIEW); intent.setData(uri); switch (mPurpose) { case PickPDF: // Start an activity to display the PDF file startActivity(intent); break; case PickKeyFile: // Return the uri to the caller setResult(RESULT_OK, intent); finish(); break; } } @Override protected void onPause() { super.onPause(); if (mDirectory != null) mPositions.put(mDirectory.getAbsolutePath(), getListView().getFirstVisiblePosition()); } } ================================================ FILE: src/main/java/com/artifex/mupdfdemo/ChoosePDFAdapter.java ================================================ package com.artifex.mupdfdemo; import java.util.LinkedList; import android.graphics.Color; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.TextView; public class ChoosePDFAdapter extends BaseAdapter { private final LinkedList mItems; private final LayoutInflater mInflater; public ChoosePDFAdapter(LayoutInflater inflater) { mInflater = inflater; mItems = new LinkedList(); } public void clear() { mItems.clear(); } public void add(ChoosePDFItem item) { mItems.add(item); notifyDataSetChanged(); } public int getCount() { return mItems.size(); } public Object getItem(int i) { return null; } public long getItemId(int arg0) { return 0; } private int iconForType(ChoosePDFItem.Type type) { switch (type) { case PARENT: return R.drawable.ic_arrow_up; case DIR: return R.drawable.ic_dir; case DOC: return R.drawable.ic_doc; default: return 0; } } public View getView(int position, View convertView, ViewGroup parent) { View v; if (convertView == null) { v = mInflater.inflate(R.layout.picker_entry, null); } else { v = convertView; } ChoosePDFItem item = mItems.get(position); ((TextView)v.findViewById(R.id.name)).setText(item.name); ((ImageView)v.findViewById(R.id.icon)).setImageResource(iconForType(item.type)); ((ImageView)v.findViewById(R.id.icon)).setColorFilter(Color.argb(255, 0, 0, 0)); return v; } } ================================================ FILE: src/main/java/com/artifex/mupdfdemo/ChoosePDFItem.java ================================================ package com.artifex.mupdfdemo; public class ChoosePDFItem { enum Type { PARENT, DIR, DOC } final public Type type; final public String name; public ChoosePDFItem (Type t, String n) { type = t; name = n; } } ================================================ FILE: src/main/java/com/artifex/mupdfdemo/Deque.java ================================================ /* * Written by Doug Lea and Josh Bloch with assistance from members of * JCP JSR-166 Expert Group and released to the public domain, as explained * at http://creativecommons.org/publicdomain/zero/1.0/ */ package com.artifex.mupdfdemo; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; import java.util.Queue; import java.util.Stack; // BEGIN android-note // removed link to collections framework docs // END android-note /** * A linear collection that supports element insertion and removal at * both ends. The name deque is short for "double ended queue" * and is usually pronounced "deck". Most Deque * implementations place no fixed limits on the number of elements * they may contain, but this interface supports capacity-restricted * deques as well as those with no fixed size limit. * *

This interface defines methods to access the elements at both * ends of the deque. Methods are provided to insert, remove, and * examine the element. Each of these methods exists in two forms: * one throws an exception if the operation fails, the other returns a * special value (either null or false, depending on * the operation). The latter form of the insert operation is * designed specifically for use with capacity-restricted * Deque implementations; in most implementations, insert * operations cannot fail. * *

The twelve methods described above are summarized in the * following table: * *

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
First Element (Head) Last Element (Tail)
Throws exceptionSpecial valueThrows exceptionSpecial value
Insert{@link #addFirst addFirst(e)}{@link #offerFirst offerFirst(e)}{@link #addLast addLast(e)}{@link #offerLast offerLast(e)}
Remove{@link #removeFirst removeFirst()}{@link #pollFirst pollFirst()}{@link #removeLast removeLast()}{@link #pollLast pollLast()}
Examine{@link #getFirst getFirst()}{@link #peekFirst peekFirst()}{@link #getLast getLast()}{@link #peekLast peekLast()}
* *

This interface extends the {@link java.util.Queue} interface. When a deque is * used as a queue, FIFO (First-In-First-Out) behavior results. Elements are * added at the end of the deque and removed from the beginning. The methods * inherited from the Queue interface are precisely equivalent to * Deque methods as indicated in the following table: * *

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
Queue Method Equivalent Deque Method
{@link java.util.Queue#add add(e)}{@link #addLast addLast(e)}
{@link java.util.Queue#offer offer(e)}{@link #offerLast offerLast(e)}
{@link java.util.Queue#remove remove()}{@link #removeFirst removeFirst()}
{@link java.util.Queue#poll poll()}{@link #pollFirst pollFirst()}
{@link java.util.Queue#element element()}{@link #getFirst getFirst()}
{@link java.util.Queue#peek peek()}{@link #peek peekFirst()}
* *

Deques can also be used as LIFO (Last-In-First-Out) stacks. This * interface should be used in preference to the legacy {@link java.util.Stack} class. * When a deque is used as a stack, elements are pushed and popped from the * beginning of the deque. Stack methods are precisely equivalent to * Deque methods as indicated in the table below: * *

* * * * * * * * * * * * * * * * * *
Stack Method Equivalent Deque Method
{@link #push push(e)}{@link #addFirst addFirst(e)}
{@link #pop pop()}{@link #removeFirst removeFirst()}
{@link #peek peek()}{@link #peekFirst peekFirst()}
* *

Note that the {@link #peek peek} method works equally well when * a deque is used as a queue or a stack; in either case, elements are * drawn from the beginning of the deque. * *

This interface provides two methods to remove interior * elements, {@link #removeFirstOccurrence removeFirstOccurrence} and * {@link #removeLastOccurrence removeLastOccurrence}. * *

Unlike the {@link java.util.List} interface, this interface does not * provide support for indexed access to elements. * *

While Deque implementations are not strictly required * to prohibit the insertion of null elements, they are strongly * encouraged to do so. Users of any Deque implementations * that do allow null elements are strongly encouraged not to * take advantage of the ability to insert nulls. This is so because * null is used as a special return value by various methods * to indicated that the deque is empty. * *

Deque implementations generally do not define * element-based versions of the equals and hashCode * methods, but instead inherit the identity-based versions from class * Object. * * @author Doug Lea * @author Josh Bloch * @since 1.6 * @param the type of elements held in this collection */ public interface Deque extends Queue { /** * Inserts the specified element at the front of this deque if it is * possible to do so immediately without violating capacity restrictions. * When using a capacity-restricted deque, it is generally preferable to * use method {@link #offerFirst}. * * @param e the element to add * @throws IllegalStateException if the element cannot be added at this * time due to capacity restrictions * @throws ClassCastException if the class of the specified element * prevents it from being added to this deque * @throws NullPointerException if the specified element is null and this * deque does not permit null elements * @throws IllegalArgumentException if some property of the specified * element prevents it from being added to this deque */ void addFirst(E e); /** * Inserts the specified element at the end of this deque if it is * possible to do so immediately without violating capacity restrictions. * When using a capacity-restricted deque, it is generally preferable to * use method {@link #offerLast}. * *

This method is equivalent to {@link #add}. * * @param e the element to add * @throws IllegalStateException if the element cannot be added at this * time due to capacity restrictions * @throws ClassCastException if the class of the specified element * prevents it from being added to this deque * @throws NullPointerException if the specified element is null and this * deque does not permit null elements * @throws IllegalArgumentException if some property of the specified * element prevents it from being added to this deque */ void addLast(E e); /** * Inserts the specified element at the front of this deque unless it would * violate capacity restrictions. When using a capacity-restricted deque, * this method is generally preferable to the {@link #addFirst} method, * which can fail to insert an element only by throwing an exception. * * @param e the element to add * @return true if the element was added to this deque, else * false * @throws ClassCastException if the class of the specified element * prevents it from being added to this deque * @throws NullPointerException if the specified element is null and this * deque does not permit null elements * @throws IllegalArgumentException if some property of the specified * element prevents it from being added to this deque */ boolean offerFirst(E e); /** * Inserts the specified element at the end of this deque unless it would * violate capacity restrictions. When using a capacity-restricted deque, * this method is generally preferable to the {@link #addLast} method, * which can fail to insert an element only by throwing an exception. * * @param e the element to add * @return true if the element was added to this deque, else * false * @throws ClassCastException if the class of the specified element * prevents it from being added to this deque * @throws NullPointerException if the specified element is null and this * deque does not permit null elements * @throws IllegalArgumentException if some property of the specified * element prevents it from being added to this deque */ boolean offerLast(E e); /** * Retrieves and removes the first element of this deque. This method * differs from {@link #pollFirst pollFirst} only in that it throws an * exception if this deque is empty. * * @return the head of this deque * @throws java.util.NoSuchElementException if this deque is empty */ E removeFirst(); /** * Retrieves and removes the last element of this deque. This method * differs from {@link #pollLast pollLast} only in that it throws an * exception if this deque is empty. * * @return the tail of this deque * @throws java.util.NoSuchElementException if this deque is empty */ E removeLast(); /** * Retrieves and removes the first element of this deque, * or returns null if this deque is empty. * * @return the head of this deque, or null if this deque is empty */ E pollFirst(); /** * Retrieves and removes the last element of this deque, * or returns null if this deque is empty. * * @return the tail of this deque, or null if this deque is empty */ E pollLast(); /** * Retrieves, but does not remove, the first element of this deque. * * This method differs from {@link #peekFirst peekFirst} only in that it * throws an exception if this deque is empty. * * @return the head of this deque * @throws java.util.NoSuchElementException if this deque is empty */ E getFirst(); /** * Retrieves, but does not remove, the last element of this deque. * This method differs from {@link #peekLast peekLast} only in that it * throws an exception if this deque is empty. * * @return the tail of this deque * @throws java.util.NoSuchElementException if this deque is empty */ E getLast(); /** * Retrieves, but does not remove, the first element of this deque, * or returns null if this deque is empty. * * @return the head of this deque, or null if this deque is empty */ E peekFirst(); /** * Retrieves, but does not remove, the last element of this deque, * or returns null if this deque is empty. * * @return the tail of this deque, or null if this deque is empty */ E peekLast(); /** * Removes the first occurrence of the specified element from this deque. * If the deque does not contain the element, it is unchanged. * More formally, removes the first element e such that * (o==null ? e==null : o.equals(e)) * (if such an element exists). * Returns true if this deque contained the specified element * (or equivalently, if this deque changed as a result of the call). * * @param o element to be removed from this deque, if present * @return true if an element was removed as a result of this call * @throws ClassCastException if the class of the specified element * is incompatible with this deque (optional) * @throws NullPointerException if the specified element is null and this * deque does not permit null elements (optional) */ boolean removeFirstOccurrence(Object o); /** * Removes the last occurrence of the specified element from this deque. * If the deque does not contain the element, it is unchanged. * More formally, removes the last element e such that * (o==null ? e==null : o.equals(e)) * (if such an element exists). * Returns true if this deque contained the specified element * (or equivalently, if this deque changed as a result of the call). * * @param o element to be removed from this deque, if present * @return true if an element was removed as a result of this call * @throws ClassCastException if the class of the specified element * is incompatible with this deque (optional) * @throws NullPointerException if the specified element is null and this * deque does not permit null elements (optional) */ boolean removeLastOccurrence(Object o); // *** Queue methods *** /** * Inserts the specified element into the queue represented by this deque * (in other words, at the tail of this deque) if it is possible to do so * immediately without violating capacity restrictions, returning * true upon success and throwing an * IllegalStateException if no space is currently available. * When using a capacity-restricted deque, it is generally preferable to * use {@link #offer(Object) offer}. * *

This method is equivalent to {@link #addLast}. * * @param e the element to add * @return true (as specified by {@link java.util.Collection#add}) * @throws IllegalStateException if the element cannot be added at this * time due to capacity restrictions * @throws ClassCastException if the class of the specified element * prevents it from being added to this deque * @throws NullPointerException if the specified element is null and this * deque does not permit null elements * @throws IllegalArgumentException if some property of the specified * element prevents it from being added to this deque */ boolean add(E e); /** * Inserts the specified element into the queue represented by this deque * (in other words, at the tail of this deque) if it is possible to do so * immediately without violating capacity restrictions, returning * true upon success and false if no space is currently * available. When using a capacity-restricted deque, this method is * generally preferable to the {@link #add} method, which can fail to * insert an element only by throwing an exception. * *

This method is equivalent to {@link #offerLast}. * * @param e the element to add * @return true if the element was added to this deque, else * false * @throws ClassCastException if the class of the specified element * prevents it from being added to this deque * @throws NullPointerException if the specified element is null and this * deque does not permit null elements * @throws IllegalArgumentException if some property of the specified * element prevents it from being added to this deque */ boolean offer(E e); /** * Retrieves and removes the head of the queue represented by this deque * (in other words, the first element of this deque). * This method differs from {@link #poll poll} only in that it throws an * exception if this deque is empty. * *

This method is equivalent to {@link #removeFirst()}. * * @return the head of the queue represented by this deque * @throws java.util.NoSuchElementException if this deque is empty */ E remove(); /** * Retrieves and removes the head of the queue represented by this deque * (in other words, the first element of this deque), or returns * null if this deque is empty. * *

This method is equivalent to {@link #pollFirst()}. * * @return the first element of this deque, or null if * this deque is empty */ E poll(); /** * Retrieves, but does not remove, the head of the queue represented by * this deque (in other words, the first element of this deque). * This method differs from {@link #peek peek} only in that it throws an * exception if this deque is empty. * *

This method is equivalent to {@link #getFirst()}. * * @return the head of the queue represented by this deque * @throws java.util.NoSuchElementException if this deque is empty */ E element(); /** * Retrieves, but does not remove, the head of the queue represented by * this deque (in other words, the first element of this deque), or * returns null if this deque is empty. * *

This method is equivalent to {@link #peekFirst()}. * * @return the head of the queue represented by this deque, or * null if this deque is empty */ E peek(); // *** Stack methods *** /** * Pushes an element onto the stack represented by this deque (in other * words, at the head of this deque) if it is possible to do so * immediately without violating capacity restrictions, returning * true upon success and throwing an * IllegalStateException if no space is currently available. * *

This method is equivalent to {@link #addFirst}. * * @param e the element to push * @throws IllegalStateException if the element cannot be added at this * time due to capacity restrictions * @throws ClassCastException if the class of the specified element * prevents it from being added to this deque * @throws NullPointerException if the specified element is null and this * deque does not permit null elements * @throws IllegalArgumentException if some property of the specified * element prevents it from being added to this deque */ void push(E e); /** * Pops an element from the stack represented by this deque. In other * words, removes and returns the first element of this deque. * *

This method is equivalent to {@link #removeFirst()}. * * @return the element at the front of this deque (which is the top * of the stack represented by this deque) * @throws java.util.NoSuchElementException if this deque is empty */ E pop(); // *** Collection methods *** /** * Removes the first occurrence of the specified element from this deque. * If the deque does not contain the element, it is unchanged. * More formally, removes the first element e such that * (o==null ? e==null : o.equals(e)) * (if such an element exists). * Returns true if this deque contained the specified element * (or equivalently, if this deque changed as a result of the call). * *

This method is equivalent to {@link #removeFirstOccurrence}. * * @param o element to be removed from this deque, if present * @return true if an element was removed as a result of this call * @throws ClassCastException if the class of the specified element * is incompatible with this deque (optional) * @throws NullPointerException if the specified element is null and this * deque does not permit null elements (optional) */ boolean remove(Object o); /** * Returns true if this deque contains the specified element. * More formally, returns true if and only if this deque contains * at least one element e such that * (o==null ? e==null : o.equals(e)). * * @param o element whose presence in this deque is to be tested * @return true if this deque contains the specified element * @throws ClassCastException if the type of the specified element * is incompatible with this deque (optional) * @throws NullPointerException if the specified element is null and this * deque does not permit null elements (optional) */ boolean contains(Object o); /** * Returns the number of elements in this deque. * * @return the number of elements in this deque */ public int size(); /** * Returns an iterator over the elements in this deque in proper sequence. * The elements will be returned in order from first (head) to last (tail). * * @return an iterator over the elements in this deque in proper sequence */ Iterator iterator(); /** * Returns an iterator over the elements in this deque in reverse * sequential order. The elements will be returned in order from * last (tail) to first (head). * * @return an iterator over the elements in this deque in reverse * sequence */ Iterator descendingIterator(); } ================================================ FILE: src/main/java/com/artifex/mupdfdemo/FilePicker.java ================================================ package com.artifex.mupdfdemo; import android.net.Uri; public abstract class FilePicker { public interface FilePickerSupport { void performPickFor(FilePicker picker); } private final FilePickerSupport support; FilePicker(FilePickerSupport _support) { support = _support; } void pick() { support.performPickFor(this); } abstract void onPick(Uri uri); } ================================================ FILE: src/main/java/com/artifex/mupdfdemo/LinkInfo.java ================================================ package com.artifex.mupdfdemo; import android.graphics.RectF; public class LinkInfo { final public RectF rect; public LinkInfo(float l, float t, float r, float b) { rect = new RectF(l, t, r, b); } public void acceptVisitor(LinkInfoVisitor visitor) { } } ================================================ FILE: src/main/java/com/artifex/mupdfdemo/LinkInfoExternal.java ================================================ package com.artifex.mupdfdemo; public class LinkInfoExternal extends LinkInfo { final public String url; public LinkInfoExternal(float l, float t, float r, float b, String u) { super(l, t, r, b); url = u; } public void acceptVisitor(LinkInfoVisitor visitor) { visitor.visitExternal(this); } } ================================================ FILE: src/main/java/com/artifex/mupdfdemo/LinkInfoInternal.java ================================================ package com.artifex.mupdfdemo; public class LinkInfoInternal extends LinkInfo { final public int pageNumber; public LinkInfoInternal(float l, float t, float r, float b, int p) { super(l, t, r, b); pageNumber = p; } public void acceptVisitor(LinkInfoVisitor visitor) { visitor.visitInternal(this); } } ================================================ FILE: src/main/java/com/artifex/mupdfdemo/LinkInfoRemote.java ================================================ package com.artifex.mupdfdemo; public class LinkInfoRemote extends LinkInfo { final public String fileSpec; final public int pageNumber; final public boolean newWindow; public LinkInfoRemote(float l, float t, float r, float b, String f, int p, boolean n) { super(l, t, r, b); fileSpec = f; pageNumber = p; newWindow = n; } public void acceptVisitor(LinkInfoVisitor visitor) { visitor.visitRemote(this); } } ================================================ FILE: src/main/java/com/artifex/mupdfdemo/LinkInfoVisitor.java ================================================ package com.artifex.mupdfdemo; abstract public class LinkInfoVisitor { public abstract void visitInternal(LinkInfoInternal li); public abstract void visitExternal(LinkInfoExternal li); public abstract void visitRemote(LinkInfoRemote li); } ================================================ FILE: src/main/java/com/artifex/mupdfdemo/MuPDFActivity.java ================================================ package com.artifex.mupdfdemo; import java.io.InputStream; import java.util.concurrent.Executor; import com.artifex.mupdfdemo.ReaderView.ViewMapper; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnCancelListener; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Resources; import android.database.Cursor; import android.graphics.Color; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.text.Editable; import android.text.TextWatcher; import android.text.method.PasswordTransformationMethod; import android.view.KeyEvent; import android.view.Menu; import android.view.View; 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.RelativeLayout; import android.widget.SeekBar; import android.widget.TextView; import android.widget.ViewAnimator; class ThreadPerTaskExecutor implements Executor { public void execute(Runnable r) { new Thread(r).start(); } } public class MuPDFActivity extends Activity implements FilePicker.FilePickerSupport { /* The core rendering instance */ enum TopBarMode {Main, Search, Annot, Delete, More, Accept}; enum AcceptMode {Highlight, Underline, StrikeOut, Ink, CopyText}; private final int OUTLINE_REQUEST=0; private final int PRINT_REQUEST=1; private final int FILEPICK_REQUEST=2; private MuPDFCore core; private String mFileName; private MuPDFReaderView mDocView; private View mButtonsView; private boolean mButtonsVisible; private EditText mPasswordView; private TextView mFilenameView; private SeekBar mPageSlider; private int mPageSliderRes; private TextView mPageNumberView; private TextView mInfoView; private ImageButton mSearchButton; private ImageButton mReflowButton; private ImageButton mOutlineButton; private ImageButton mMoreButton; private TextView mAnnotTypeText; private ImageButton mAnnotButton; private ViewAnimator mTopBarSwitcher; private ImageButton mLinkButton; private TopBarMode mTopBarMode = TopBarMode.Main; private AcceptMode mAcceptMode; private ImageButton mSearchBack; private ImageButton mSearchFwd; 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 boolean mReflow = false; private AsyncTask mAlertTask; private AlertDialog mAlertDialog; private FilePicker mFilePicker; public void createAlertWaiter() { mAlertsActive = true; // All mupdf library calls are performed on asynchronous tasks to avoid stalling // the UI. Some calls can lead to javascript-invoked requests to display an // alert dialog and collect a reply from the user. The task has to be blocked // until the user's reply is received. This method creates an asynchronous task, // the purpose of which is to wait of these requests and produce the dialog // in response, while leaving the core blocked. When the dialog receives the // user's response, it is sent to the core via replyToAlert, unblocking it. // Another alert-waiting task is then created to pick up the next alert. if (mAlertTask != null) { mAlertTask.cancel(true); mAlertTask = null; } if (mAlertDialog != null) { mAlertDialog.cancel(); mAlertDialog = null; } mAlertTask = new AsyncTask() { @Override protected MuPDFAlert doInBackground(Void... arg0) { if (!mAlertsActive) return null; return core.waitForAlert(); } @Override protected void onPostExecute(final MuPDFAlert result) { // core.waitForAlert may return null when shutting down if (result == null) return; final MuPDFAlert.ButtonPressed pressed[] = new MuPDFAlert.ButtonPressed[3]; for(int i = 0; i < 3; i++) pressed[i] = MuPDFAlert.ButtonPressed.None; DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { mAlertDialog = null; if (mAlertsActive) { int index = 0; switch (which) { case AlertDialog.BUTTON1: index=0; break; case AlertDialog.BUTTON2: index=1; break; case AlertDialog.BUTTON3: index=2; break; } result.buttonPressed = pressed[index]; // Send the user's response to the core, so that it can // continue processing. core.replyToAlert(result); // Create another alert-waiter to pick up the next alert. createAlertWaiter(); } } }; mAlertDialog = mAlertBuilder.create(); mAlertDialog.setTitle(result.title); mAlertDialog.setMessage(result.message); switch (result.iconType) { case Error: break; case Warning: break; case Question: break; case Status: break; } switch (result.buttonGroupType) { case OkCancel: mAlertDialog.setButton(AlertDialog.BUTTON2, getString(R.string.cancel), listener); pressed[1] = MuPDFAlert.ButtonPressed.Cancel; case Ok: mAlertDialog.setButton(AlertDialog.BUTTON1, getString(R.string.okay), listener); pressed[0] = MuPDFAlert.ButtonPressed.Ok; break; case YesNoCancel: mAlertDialog.setButton(AlertDialog.BUTTON3, getString(R.string.cancel), listener); pressed[2] = MuPDFAlert.ButtonPressed.Cancel; case YesNo: mAlertDialog.setButton(AlertDialog.BUTTON1, getString(R.string.yes), listener); pressed[0] = MuPDFAlert.ButtonPressed.Yes; mAlertDialog.setButton(AlertDialog.BUTTON2, getString(R.string.no), listener); pressed[1] = MuPDFAlert.ButtonPressed.No; break; } mAlertDialog.setOnCancelListener(new OnCancelListener() { public void onCancel(DialogInterface dialog) { mAlertDialog = null; if (mAlertsActive) { result.buttonPressed = MuPDFAlert.ButtonPressed.None; core.replyToAlert(result); createAlertWaiter(); } } }); mAlertDialog.show(); } }; mAlertTask.executeOnExecutor(new ThreadPerTaskExecutor()); } public void destroyAlertWaiter() { mAlertsActive = false; if (mAlertDialog != null) { mAlertDialog.cancel(); mAlertDialog = null; } if (mAlertTask != null) { mAlertTask.cancel(true); mAlertTask = null; } } private MuPDFCore openFile(String path) { int lastSlashPos = path.lastIndexOf('/'); mFileName = new String(lastSlashPos == -1 ? path : path.substring(lastSlashPos+1)); System.out.println("Trying to open "+path); try { core = new MuPDFCore(this, path); // New file: drop the old outline data OutlineActivityData.set(null); } catch (Exception e) { System.out.println(e); return null; } return core; } private MuPDFCore openBuffer(byte buffer[], String magic) { System.out.println("Trying to open byte buffer"); try { core = new MuPDFCore(this, buffer, magic); // New file: drop the old outline data OutlineActivityData.set(null); } catch (Exception e) { System.out.println(e); return null; } return core; } /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mAlertBuilder = new AlertDialog.Builder(this); if (core == null) { core = (MuPDFCore)getLastNonConfigurationInstance(); if (savedInstanceState != null && savedInstanceState.containsKey("FileName")) { mFileName = savedInstanceState.getString("FileName"); } } if (core == null) { Intent intent = getIntent(); byte buffer[] = null; if (Intent.ACTION_VIEW.equals(intent.getAction())) { Uri uri = intent.getData(); System.out.println("URI to open is: " + uri); if (uri.toString().startsWith("content://")) { String reason = null; try { InputStream is = getContentResolver().openInputStream(uri); int len = is.available(); buffer = new byte[len]; is.read(buffer, 0, len); is.close(); } catch (OutOfMemoryError e) { System.out.println("Out of memory during buffer reading"); reason = e.toString(); } catch (Exception e) { System.out.println("Exception reading from stream: " + e); // Handle view requests from the Transformer Prime's file manager // Hopefully other file managers will use this same scheme, if not // using explicit paths. // I'm hoping that this case below is no longer needed...but it's // hard to test as the file manager seems to have changed in 4.x. try { Cursor cursor = getContentResolver().query(uri, new String[]{"_data"}, null, null, null); if (cursor.moveToFirst()) { String str = cursor.getString(0); if (str == null) { reason = "Couldn't parse data in intent"; } else { uri = Uri.parse(str); } } } catch (Exception e2) { System.out.println("Exception in Transformer Prime file manager code: " + e2); reason = e2.toString(); } } if (reason != null) { buffer = null; Resources res = getResources(); AlertDialog alert = mAlertBuilder.create(); setTitle(String.format(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(); return; } } if (buffer != null) { core = openBuffer(buffer, intent.getType()); } else { core = openFile(Uri.decode(uri.getEncodedPath())); } SearchTaskResult.set(null); } 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 createUI(Bundle savedInstanceState) { if (core == null) return; // Now create the UI. // First create the document view mDocView = new MuPDFReaderView(this) { @Override protected void onMoveToChild(int i) { if (core == null) return; mPageNumberView.setText(String.format("%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 protected void onHit(Hit item) { switch (mTopBarMode) { case Annot: if (item == Hit.Annotation) { showButtons(); mTopBarMode = TopBarMode.Delete; mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal()); } break; case Delete: mTopBarMode = TopBarMode.Annot; mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal()); // fall through default: // Not in annotation editing mode, but the pageview will // still select and highlight hit annotations, so // deselect just in case. MuPDFView pageView = (MuPDFView) mDocView.getDisplayedView(); if (pageView != null) pageView.deselectAnnotation(); break; } } }; mDocView.setAdapter(new MuPDFPageAdapter(this, 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 mFilenameView.setText(mFileName); // Activate the seekbar mPageSlider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { public void onStopTrackingTouch(SeekBar seekBar) { 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(); } }); // Activate the reflow button mReflowButton.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { toggleReflow(); } }); if (core.fileFormat().startsWith("PDF") && core.isUnencryptedPDF() && !core.wasOpenedFromBuffer()) { mAnnotButton.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { mTopBarMode = TopBarMode.Annot; mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal()); } }); } else { mAnnotButton.setVisibility(View.GONE); } // 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.hasOutline()) { mOutlineButton.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { OutlineItem outline[] = core.getOutline(); if (outline != null) { OutlineActivityData.get().items = outline; Intent intent = new Intent(MuPDFActivity.this, OutlineActivity.class); 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"+mFileName, 0)); if (savedInstanceState == null || !savedInstanceState.getBoolean("ButtonsHidden", false)) showButtons(); if(savedInstanceState != null && savedInstanceState.getBoolean("SearchMode", false)) searchModeOn(); if(savedInstanceState != null && savedInstanceState.getBoolean("ReflowMode", false)) reflowModeSet(true); // Stick the document view and the buttons overlay into a parent view RelativeLayout layout = new RelativeLayout(this); 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 >= 0) mDocView.setDisplayedViewIndex(resultCode); break; case PRINT_REQUEST: if (resultCode == RESULT_CANCELED) showInfo(getString(R.string.print_failed)); break; case FILEPICK_REQUEST: if (mFilePicker != null && resultCode == RESULT_OK) mFilePicker.onPick(data.getData()); } super.onActivityResult(requestCode, resultCode, data); } public Object onRetainNonConfigurationInstance() { MuPDFCore mycore = core; core = null; return mycore; } private void reflowModeSet(boolean reflow) { mReflow = reflow; mDocView.setAdapter(mReflow ? new MuPDFReflowAdapter(this, core) : new MuPDFPageAdapter(this, this, core)); mReflowButton.setColorFilter(mReflow ? Color.argb(0xFF, 172, 114, 37) : Color.argb(0xFF, 255, 255, 255)); setButtonEnabled(mAnnotButton, !reflow); setButtonEnabled(mSearchButton, !reflow); if (reflow) setLinkHighlight(false); setButtonEnabled(mLinkButton, !reflow); setButtonEnabled(mMoreButton, !reflow); mDocView.refresh(mReflow); } private void toggleReflow() { reflowModeSet(!mReflow); showInfo(mReflow ? getString(R.string.entering_reflow_mode) : getString(R.string.leaving_reflow_mode)); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); if (mFileName != null && mDocView != null) { outState.putString("FileName", mFileName); // 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"+mFileName, mDocView.getDisplayedViewIndex()); edit.commit(); } if (!mButtonsVisible) outState.putBoolean("ButtonsHidden", true); if (mTopBarMode == TopBarMode.Search) outState.putBoolean("SearchMode", true); if (mReflow) outState.putBoolean("ReflowMode", true); } @Override protected void onPause() { super.onPause(); if (mSearchTask != null) mSearchTask.stop(); if (mFileName != null && mDocView != null) { SharedPreferences prefs = getPreferences(Context.MODE_PRIVATE); SharedPreferences.Editor edit = prefs.edit(); edit.putInt("page"+mFileName, mDocView.getDisplayedViewIndex()); edit.commit(); } } public void onDestroy() { if (mDocView != null) { mDocView.applyToChildren(new ViewMapper() { void applyToView(View view) { ((MuPDFView)view).releaseBitmaps(); } }); } if (core != null) core.onDestroy(); if (mAlertTask != null) { mAlertTask.cancel(true); mAlertTask = null; } 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, 172, 114, 37) : 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("%d / %d", index+1, core.countPages())); } private void printDoc() { if (!core.fileFormat().startsWith("PDF")) { showInfo(getString(R.string.format_currently_not_supported)); return; } Intent myIntent = getIntent(); Uri docUri = myIntent != null ? myIntent.getData() : null; if (docUri == null) { showInfo(getString(R.string.print_failed)); } if (docUri.getScheme() == null) docUri = Uri.parse("file://"+docUri.toString()); Intent printIntent = new Intent(this, PrintDialogActivity.class); printIntent.setDataAndType(docUri, "aplication/pdf"); printIntent.putExtra("title", mFileName); startActivityForResult(printIntent, PRINT_REQUEST); } private void showInfo(String message) { mInfoView.setText(message); int currentApiVersion = android.os.Build.VERSION.SDK_INT; if (currentApiVersion >= android.os.Build.VERSION_CODES.HONEYCOMB) { SafeAnimatorInflater safe = new SafeAnimatorInflater((Activity)this, R.animator.info, (View)mInfoView); } else { mInfoView.setVisibility(View.VISIBLE); mHandler.postDelayed(new Runnable() { public void run() { mInfoView.setVisibility(View.INVISIBLE); } }, 500); } } private void makeButtonsView() { mButtonsView = getLayoutInflater().inflate(R.layout.buttons,null); mFilenameView = (TextView)mButtonsView.findViewById(R.id.docNameText); mPageSlider = (SeekBar)mButtonsView.findViewById(R.id.pageSlider); mPageNumberView = (TextView)mButtonsView.findViewById(R.id.pageNumber); mInfoView = (TextView)mButtonsView.findViewById(R.id.info); mSearchButton = (ImageButton)mButtonsView.findViewById(R.id.searchButton); mReflowButton = (ImageButton)mButtonsView.findViewById(R.id.reflowButton); mOutlineButton = (ImageButton)mButtonsView.findViewById(R.id.outlineButton); mAnnotButton = (ImageButton)mButtonsView.findViewById(R.id.editAnnotButton); mAnnotTypeText = (TextView)mButtonsView.findViewById(R.id.annotType); mTopBarSwitcher = (ViewAnimator)mButtonsView.findViewById(R.id.switcher); mSearchBack = (ImageButton)mButtonsView.findViewById(R.id.searchBack); mSearchFwd = (ImageButton)mButtonsView.findViewById(R.id.searchForward); mSearchText = (EditText)mButtonsView.findViewById(R.id.searchText); mLinkButton = (ImageButton)mButtonsView.findViewById(R.id.linkButton); mMoreButton = (ImageButton)mButtonsView.findViewById(R.id.moreButton); mTopBarSwitcher.setVisibility(View.INVISIBLE); mPageNumberView.setVisibility(View.INVISIBLE); mInfoView.setVisibility(View.INVISIBLE); mPageSlider.setVisibility(View.INVISIBLE); } public void OnMoreButtonClick(View v) { mTopBarMode = TopBarMode.More; mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal()); } public void OnCancelMoreButtonClick(View v) { mTopBarMode = TopBarMode.Main; mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal()); } public void OnPrintButtonClick(View v) { printDoc(); } public void OnCopyTextButtonClick(View v) { mTopBarMode = TopBarMode.Accept; mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal()); mAcceptMode = AcceptMode.CopyText; mDocView.setMode(MuPDFReaderView.Mode.Selecting); mAnnotTypeText.setText(getString(R.string.copy_text)); showInfo(getString(R.string.select_text)); } public void OnEditAnnotButtonClick(View v) { mTopBarMode = TopBarMode.Annot; mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal()); } public void OnCancelAnnotButtonClick(View v) { mTopBarMode = TopBarMode.More; mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal()); } public void OnHighlightButtonClick(View v) { mTopBarMode = TopBarMode.Accept; mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal()); mAcceptMode = AcceptMode.Highlight; mDocView.setMode(MuPDFReaderView.Mode.Selecting); mAnnotTypeText.setText(R.string.highlight); showInfo(getString(R.string.select_text)); } public void OnUnderlineButtonClick(View v) { mTopBarMode = TopBarMode.Accept; mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal()); mAcceptMode = AcceptMode.Underline; mDocView.setMode(MuPDFReaderView.Mode.Selecting); mAnnotTypeText.setText(R.string.underline); showInfo(getString(R.string.select_text)); } public void OnStrikeOutButtonClick(View v) { mTopBarMode = TopBarMode.Accept; mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal()); mAcceptMode = AcceptMode.StrikeOut; mDocView.setMode(MuPDFReaderView.Mode.Selecting); mAnnotTypeText.setText(R.string.strike_out); showInfo(getString(R.string.select_text)); } public void OnInkButtonClick(View v) { mTopBarMode = TopBarMode.Accept; mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal()); mAcceptMode = AcceptMode.Ink; mDocView.setMode(MuPDFReaderView.Mode.Drawing); mAnnotTypeText.setText(R.string.ink); showInfo(getString(R.string.draw_annotation)); } public void OnCancelAcceptButtonClick(View v) { MuPDFView pageView = (MuPDFView) mDocView.getDisplayedView(); if (pageView != null) { pageView.deselectText(); pageView.cancelDraw(); } mDocView.setMode(MuPDFReaderView.Mode.Viewing); switch (mAcceptMode) { case CopyText: mTopBarMode = TopBarMode.More; break; default: mTopBarMode = TopBarMode.Annot; break; } mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal()); } public void OnAcceptButtonClick(View v) { MuPDFView pageView = (MuPDFView) mDocView.getDisplayedView(); boolean success = false; switch (mAcceptMode) { case CopyText: if (pageView != null) success = pageView.copySelection(); mTopBarMode = TopBarMode.More; showInfo(success?getString(R.string.copied_to_clipboard):getString(R.string.no_text_selected)); break; case Highlight: if (pageView != null) success = pageView.markupSelection(Annotation.Type.HIGHLIGHT); mTopBarMode = TopBarMode.Annot; if (!success) showInfo(getString(R.string.no_text_selected)); break; case Underline: if (pageView != null) success = pageView.markupSelection(Annotation.Type.UNDERLINE); mTopBarMode = TopBarMode.Annot; if (!success) showInfo(getString(R.string.no_text_selected)); break; case StrikeOut: if (pageView != null) success = pageView.markupSelection(Annotation.Type.STRIKEOUT); mTopBarMode = TopBarMode.Annot; if (!success) showInfo(getString(R.string.no_text_selected)); break; case Ink: if (pageView != null) success = pageView.saveDraw(); mTopBarMode = TopBarMode.Annot; if (!success) showInfo(getString(R.string.nothing_to_save)); break; } mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal()); mDocView.setMode(MuPDFReaderView.Mode.Viewing); } public void OnCancelSearchButtonClick(View v) { searchModeOff(); } public void OnDeleteButtonClick(View v) { MuPDFView pageView = (MuPDFView) mDocView.getDisplayedView(); if (pageView != null) pageView.deleteSelectedAnnotation(); mTopBarMode = TopBarMode.Annot; mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal()); } public void OnCancelDeleteButtonClick(View v) { MuPDFView pageView = (MuPDFView) mDocView.getDisplayedView(); if (pageView != null) pageView.deselectAnnotation(); mTopBarMode = TopBarMode.Annot; mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal()); } 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() { if (core != null) { core.startAlerts(); createAlertWaiter(); } super.onStart(); } @Override protected void onStop() { if (core != null) { destroyAlertWaiter(); core.stopAlerts(); } super.onStop(); } @Override public void onBackPressed() { if (core != null && core.hasChanges()) { DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { if (which == AlertDialog.BUTTON_POSITIVE) core.save(); finish(); } }; AlertDialog alert = mAlertBuilder.create(); alert.setTitle("MuPDF"); alert.setMessage(getString(R.string.document_has_changes_save_them_)); alert.setButton(AlertDialog.BUTTON_POSITIVE, getString(R.string.yes), listener); alert.setButton(AlertDialog.BUTTON_NEGATIVE, getString(R.string.no), listener); alert.show(); } else { super.onBackPressed(); } } @Override public void performPickFor(FilePicker picker) { mFilePicker = picker; Intent intent = new Intent(this, ChoosePDFActivity.class); intent.setAction(ChoosePDFActivity.PICK_KEY_FILE); startActivityForResult(intent, FILEPICK_REQUEST); } } ================================================ FILE: src/main/java/com/artifex/mupdfdemo/MuPDFAlert.java ================================================ package com.artifex.mupdfdemo; public class MuPDFAlert { public enum IconType {Error,Warning,Question,Status}; public enum ButtonPressed {None,Ok,Cancel,No,Yes}; public enum ButtonGroupType {Ok,OkCancel,YesNo,YesNoCancel}; public final String message; public final IconType iconType; public final ButtonGroupType buttonGroupType; public final String title; public ButtonPressed buttonPressed; MuPDFAlert(String aMessage, IconType aIconType, ButtonGroupType aButtonGroupType, String aTitle, ButtonPressed aButtonPressed) { message = aMessage; iconType = aIconType; buttonGroupType = aButtonGroupType; title = aTitle; buttonPressed = aButtonPressed; } } ================================================ FILE: src/main/java/com/artifex/mupdfdemo/MuPDFAlertInternal.java ================================================ package com.artifex.mupdfdemo; // Version of MuPDFAlert without enums to simplify JNI public class MuPDFAlertInternal { public final String message; public final int iconType; public final int buttonGroupType; public final String title; public int buttonPressed; MuPDFAlertInternal(String aMessage, int aIconType, int aButtonGroupType, String aTitle, int aButtonPressed) { message = aMessage; iconType = aIconType; buttonGroupType = aButtonGroupType; title = aTitle; buttonPressed = aButtonPressed; } MuPDFAlertInternal(MuPDFAlert alert) { message = alert.message; iconType = alert.iconType.ordinal(); buttonGroupType = alert.buttonGroupType.ordinal(); title = alert.message; buttonPressed = alert.buttonPressed.ordinal(); } MuPDFAlert toAlert() { return new MuPDFAlert(message, MuPDFAlert.IconType.values()[iconType], MuPDFAlert.ButtonGroupType.values()[buttonGroupType], title, MuPDFAlert.ButtonPressed.values()[buttonPressed]); } } ================================================ FILE: src/main/java/com/artifex/mupdfdemo/MuPDFCancellableTaskDefinition.java ================================================ package com.artifex.mupdfdemo; public abstract class MuPDFCancellableTaskDefinition implements CancellableTaskDefinition { private MuPDFCore.Cookie cookie; public MuPDFCancellableTaskDefinition(MuPDFCore core) { this.cookie = core.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(MuPDFCore.Cookie cookie, Params ... params); } ================================================ FILE: src/main/java/com/artifex/mupdfdemo/MuPDFCore.java ================================================ package com.artifex.mupdfdemo; import java.util.ArrayList; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.PointF; import android.graphics.RectF; public class MuPDFCore { /* load our native library */ static { System.loadLibrary("mupdf"); } /* Readable members */ private int numPages = -1; private float pageWidth; private float pageHeight; private long globals; private byte fileBuffer[]; private String file_format; private boolean isUnencryptedPDF; private final boolean wasOpenedFromBuffer; /* The native functions */ private native long openFile(String filename); private native long openBuffer(String magic); private native String fileFormatInternal(); private native boolean isUnencryptedPDFInternal(); private native int countPagesInternal(); private native void gotoPageInternal(int localActionPageNum); private native float getPageWidth(); private native float getPageHeight(); private native void drawPage(Bitmap bitmap, int pageW, int pageH, int patchX, int patchY, int patchW, int patchH, long cookiePtr); private native void updatePageInternal(Bitmap bitmap, int page, int pageW, int pageH, int patchX, int patchY, int patchW, int patchH, long cookiePtr); private native RectF[] searchPage(String text); private native TextChar[][][][] text(); private native byte[] textAsHtml(); private native void addMarkupAnnotationInternal(PointF[] quadPoints, int type); private native void addInkAnnotationInternal(PointF[][] arcs); private native void deleteAnnotationInternal(int annot_index); private native int passClickEventInternal(int page, float x, float y); private native void setFocusedWidgetChoiceSelectedInternal(String [] selected); private native String [] getFocusedWidgetChoiceSelected(); private native String [] getFocusedWidgetChoiceOptions(); private native int getFocusedWidgetSignatureState(); private native String checkFocusedSignatureInternal(); private native boolean signFocusedSignatureInternal(String keyFile, String password); private native int setFocusedWidgetTextInternal(String text); private native String getFocusedWidgetTextInternal(); private native int getFocusedWidgetTypeInternal(); private native LinkInfo [] getPageLinksInternal(int page); private native RectF[] getWidgetAreasInternal(int page); private native Annotation[] getAnnotationsInternal(int page); private native OutlineItem [] getOutlineInternal(); private native boolean hasOutlineInternal(); private native boolean needsPasswordInternal(); private native boolean authenticatePasswordInternal(String password); private native MuPDFAlertInternal waitForAlertInternal(); private native void replyToAlertInternal(MuPDFAlertInternal alert); private native void startAlertsInternal(); private native void stopAlertsInternal(); private native void destroying(); private native boolean hasChangesInternal(); private native void saveInternal(); private native long createCookie(); private native void destroyCookie(long cookie); private native void abortCookie(long cookie); public native boolean javascriptSupported(); public class Cookie { private final long cookiePtr; public Cookie() { cookiePtr = createCookie(); if (cookiePtr == 0) throw new OutOfMemoryError(); } public void abort() { abortCookie(cookiePtr); } public void destroy() { // We could do this in finalize, but there's no guarantee that // a finalize will occur before the muPDF context occurs. destroyCookie(cookiePtr); } } public MuPDFCore(Context context, String filename) throws Exception { globals = openFile(filename); if (globals == 0) { throw new Exception(String.format(context.getString(R.string.cannot_open_file_Path), filename)); } file_format = fileFormatInternal(); isUnencryptedPDF = isUnencryptedPDFInternal(); wasOpenedFromBuffer = false; } public MuPDFCore(Context context, byte buffer[], String magic) throws Exception { fileBuffer = buffer; globals = openBuffer(magic != null ? magic : ""); if (globals == 0) { throw new Exception(context.getString(R.string.cannot_open_buffer)); } file_format = fileFormatInternal(); isUnencryptedPDF = isUnencryptedPDFInternal(); wasOpenedFromBuffer = true; } public int countPages() { if (numPages < 0) numPages = countPagesSynchronized(); return numPages; } public String fileFormat() { return file_format; } public boolean isUnencryptedPDF() { return isUnencryptedPDF; } public boolean wasOpenedFromBuffer() { return wasOpenedFromBuffer; } private synchronized int countPagesSynchronized() { return countPagesInternal(); } /* Shim function */ private void gotoPage(int page) { if (page > numPages-1) page = numPages-1; else if (page < 0) page = 0; gotoPageInternal(page); this.pageWidth = getPageWidth(); this.pageHeight = getPageHeight(); } public synchronized PointF getPageSize(int page) { gotoPage(page); return new PointF(pageWidth, pageHeight); } public MuPDFAlert waitForAlert() { MuPDFAlertInternal alert = waitForAlertInternal(); return alert != null ? alert.toAlert() : null; } public void replyToAlert(MuPDFAlert alert) { replyToAlertInternal(new MuPDFAlertInternal(alert)); } public void stopAlerts() { stopAlertsInternal(); } public void startAlerts() { startAlertsInternal(); } public synchronized void onDestroy() { destroying(); globals = 0; } public synchronized void drawPage(Bitmap bm, int page, int pageW, int pageH, int patchX, int patchY, int patchW, int patchH, Cookie cookie) { gotoPage(page); drawPage(bm, pageW, pageH, patchX, patchY, patchW, patchH, cookie.cookiePtr); } public synchronized void updatePage(Bitmap bm, int page, int pageW, int pageH, int patchX, int patchY, int patchW, int patchH, Cookie cookie) { updatePageInternal(bm, page, pageW, pageH, patchX, patchY, patchW, patchH, cookie.cookiePtr); } public synchronized PassClickResult passClickEvent(int page, float x, float y) { boolean changed = passClickEventInternal(page, x, y) != 0; switch (WidgetType.values()[getFocusedWidgetTypeInternal()]) { case TEXT: return new PassClickResultText(changed, getFocusedWidgetTextInternal()); case LISTBOX: case COMBOBOX: return new PassClickResultChoice(changed, getFocusedWidgetChoiceOptions(), getFocusedWidgetChoiceSelected()); case SIGNATURE: return new PassClickResultSignature(changed, getFocusedWidgetSignatureState()); default: return new PassClickResult(changed); } } public synchronized boolean setFocusedWidgetText(int page, String text) { boolean success; gotoPage(page); success = setFocusedWidgetTextInternal(text) != 0 ? true : false; return success; } public synchronized void setFocusedWidgetChoiceSelected(String [] selected) { setFocusedWidgetChoiceSelectedInternal(selected); } public synchronized String checkFocusedSignature() { return checkFocusedSignatureInternal(); } public synchronized boolean signFocusedSignature(String keyFile, String password) { return signFocusedSignatureInternal(keyFile, password); } public synchronized LinkInfo [] getPageLinks(int page) { return getPageLinksInternal(page); } public synchronized RectF [] getWidgetAreas(int page) { return getWidgetAreasInternal(page); } public synchronized Annotation [] getAnnoations(int page) { return getAnnotationsInternal(page); } public synchronized RectF [] searchPage(int page, String text) { gotoPage(page); return searchPage(text); } public synchronized byte[] html(int page) { gotoPage(page); return textAsHtml(); } public synchronized TextWord [][] textLines(int page) { gotoPage(page); TextChar[][][][] chars = text(); // The text of the page held in a hierarchy (blocks, lines, spans). // Currently we don't need to distinguish the blocks level or // the spans, and we need to collect the text into words. ArrayList lns = new ArrayList(); for (TextChar[][][] bl: chars) { if (bl == null) continue; for (TextChar[][] ln: bl) { ArrayList wds = new ArrayList(); TextWord wd = new TextWord(); for (TextChar[] sp: ln) { for (TextChar tc: sp) { if (tc.c != ' ') { wd.Add(tc); } else if (wd.w.length() > 0) { wds.add(wd); wd = new TextWord(); } } } if (wd.w.length() > 0) wds.add(wd); if (wds.size() > 0) lns.add(wds.toArray(new TextWord[wds.size()])); } } return lns.toArray(new TextWord[lns.size()][]); } public synchronized void addMarkupAnnotation(int page, PointF[] quadPoints, Annotation.Type type) { gotoPage(page); addMarkupAnnotationInternal(quadPoints, type.ordinal()); } public synchronized void addInkAnnotation(int page, PointF[][] arcs) { gotoPage(page); addInkAnnotationInternal(arcs); } public synchronized void deleteAnnotation(int page, int annot_index) { gotoPage(page); deleteAnnotationInternal(annot_index); } public synchronized boolean hasOutline() { return hasOutlineInternal(); } public synchronized OutlineItem [] getOutline() { return getOutlineInternal(); } public synchronized boolean needsPassword() { return needsPasswordInternal(); } public synchronized boolean authenticatePassword(String password) { return authenticatePasswordInternal(password); } public synchronized boolean hasChanges() { return hasChangesInternal(); } public synchronized void save() { saveInternal(); } } ================================================ FILE: src/main/java/com/artifex/mupdfdemo/MuPDFFragment.java ================================================ package com.artifex.mupdfdemo; import java.io.InputStream; import java.util.Collection; import java.util.HashSet; import java.util.List; import com.artifex.utils.DigitalizedEventCallback; import com.artifex.utils.PdfBitmap; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnCancelListener; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Configuration; import android.content.res.Resources; import android.database.Cursor; import android.graphics.Color; import android.graphics.Point; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.text.Editable; import android.text.TextWatcher; import android.text.method.PasswordTransformationMethod; import android.util.Log; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; 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.RelativeLayout; import android.widget.SeekBar; import android.widget.TextView; import android.widget.ViewAnimator; import androidx.fragment.app.Fragment; public class MuPDFFragment extends Fragment implements FilePicker.FilePickerSupport { private static final String TAG = "MuPDFFragment"; public static final String PARAM_SIGN_BITMAP_PATH = "paramSignBitmapPath"; public static final String PARAM_DIGITALIZED_IMAGE = "paramDigitalizedImage"; public static final String PARAM_PATH_PDF = "paramPathPdf"; public static final String PARAM_SHOW_CONTROLS = "paramShowControls"; public static final String PARAM_MODE_SIGN = "doSign"; public static final String PARAM_PASSWORD_PDF = "paramPasswordPdf"; /* State restoration */ private static final String BUNDLE_FILENAME = "savedFileName"; private static final String BUNDLE_BUTTONS_HIDDEN = "savedButtonsHidden"; /* The core rendering instance */ enum TopBarMode {Main, Search, Annot, Delete, More, Accept}; enum AcceptMode {Highlight, Underline, StrikeOut, Ink, CopyText}; private Context mContext; private final int OUTLINE_REQUEST=0; private final int PRINT_REQUEST=1; private final int FILEPICK_REQUEST=2; private MuPDFCore core; private String mFileName; private MuPDFReaderView mDocView; private View mButtonsView; private boolean mButtonsVisible; private EditText mPasswordView; private TextView mFilenameView; private SeekBar mPageSlider; private int mPageSliderRes; private TextView mPageNumberView; private TextView mInfoView; private ImageButton mSearchButton; private ImageButton mReflowButton; private ImageButton mOutlineButton; private ImageButton mMoreButton; private TextView mAnnotTypeText; private ImageButton mAnnotButton; private ViewAnimator mTopBarSwitcher; private ImageButton mLinkButton; private TopBarMode mTopBarMode = TopBarMode.Main; private AcceptMode mAcceptMode; private ImageButton mSearchBack; private ImageButton mSearchFwd; private EditText mSearchText; private SearchTask mSearchTask; private AlertDialog.Builder mAlertBuilder; private boolean mDoSign; private DigitalizedEventCallback eventCallback; private boolean mLinkHighlight = false; private final Handler mHandler = new Handler(); private boolean mAlertsActive= false; private boolean mReflow = false; private AsyncTask mAlertTask; private AlertDialog mAlertDialog; private FilePicker mFilePicker; private Collection pdfBitmaps; private byte[] byteArrayPdf; private int mPageNumber = 0; public void createAlertWaiter() { mAlertsActive = true; // All mupdf library calls are performed on asynchronous tasks to avoid stalling // the UI. Some calls can lead to javascript-invoked requests to display an // alert dialog and collect a reply from the user. The task has to be blocked // until the user's reply is received. This method creates an asynchronous task, // the purpose of which is to wait of these requests and produce the dialog // in response, while leaving the core blocked. When the dialog receives the // user's response, it is sent to the core via replyToAlert, unblocking it. // Another alert-waiting task is then created to pick up the next alert. if (mAlertTask != null) { mAlertTask.cancel(true); mAlertTask = null; } if (mAlertDialog != null) { mAlertDialog.cancel(); mAlertDialog = null; } mAlertTask = new AsyncTask() { @Override protected MuPDFAlert doInBackground(Void... arg0) { if (!mAlertsActive) return null; return core.waitForAlert(); } @Override protected void onPostExecute(final MuPDFAlert result) { // core.waitForAlert may return null when shutting down if (result == null) return; final MuPDFAlert.ButtonPressed pressed[] = new MuPDFAlert.ButtonPressed[3]; for(int i = 0; i < 3; i++) pressed[i] = MuPDFAlert.ButtonPressed.None; DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { mAlertDialog = null; if (mAlertsActive) { int index = 0; switch (which) { case AlertDialog.BUTTON1: index=0; break; case AlertDialog.BUTTON2: index=1; break; case AlertDialog.BUTTON3: index=2; break; } result.buttonPressed = pressed[index]; // Send the user's response to the core, so that it can // continue processing. core.replyToAlert(result); // Create another alert-waiter to pick up the next alert. createAlertWaiter(); } } }; mAlertDialog = mAlertBuilder.create(); mAlertDialog.setTitle(result.title); mAlertDialog.setMessage(result.message); switch (result.iconType) { case Error: break; case Warning: break; case Question: break; case Status: break; } switch (result.buttonGroupType) { case OkCancel: mAlertDialog.setButton(AlertDialog.BUTTON2, getString(R.string.cancel), listener); pressed[1] = MuPDFAlert.ButtonPressed.Cancel; case Ok: mAlertDialog.setButton(AlertDialog.BUTTON1, getString(R.string.okay), listener); pressed[0] = MuPDFAlert.ButtonPressed.Ok; break; case YesNoCancel: mAlertDialog.setButton(AlertDialog.BUTTON3, getString(R.string.cancel), listener); pressed[2] = MuPDFAlert.ButtonPressed.Cancel; case YesNo: mAlertDialog.setButton(AlertDialog.BUTTON1, getString(R.string.yes), listener); pressed[0] = MuPDFAlert.ButtonPressed.Yes; mAlertDialog.setButton(AlertDialog.BUTTON2, getString(R.string.no), listener); pressed[1] = MuPDFAlert.ButtonPressed.No; break; } mAlertDialog.setOnCancelListener(new OnCancelListener() { public void onCancel(DialogInterface dialog) { mAlertDialog = null; if (mAlertsActive) { result.buttonPressed = MuPDFAlert.ButtonPressed.None; core.replyToAlert(result); createAlertWaiter(); } } }); mAlertDialog.show(); } }; mAlertTask.executeOnExecutor(new ThreadPerTaskExecutor()); } public void destroyAlertWaiter() { mAlertsActive = false; if (mAlertDialog != null) { mAlertDialog.cancel(); mAlertDialog = null; } if (mAlertTask != null) { mAlertTask.cancel(true); mAlertTask = null; } } private MuPDFCore openFile(String path) { int lastSlashPos = path.lastIndexOf('/'); mFileName = new String(lastSlashPos == -1 ? path : path.substring(lastSlashPos+1)); System.out.println("Trying to open " + path); try { core = new MuPDFCore(mContext, path); // New file: drop the old outline data OutlineActivityData.set(null); } catch (Exception e) { System.out.println(e); return null; } return core; } private MuPDFCore openBuffer(byte buffer[], String magic) { System.out.println("Trying to open byte buffer"); try { core = new MuPDFCore(mContext, buffer, magic); // New file: drop the old outline data OutlineActivityData.set(null); } catch (Exception e) { System.out.println(e); return null; } return core; } /** Called when the activity is first created. */ @Override public View onCreateView (LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { super.onCreate(savedInstanceState); //Fuerza la orientacion a landscape //setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); mAlertBuilder = new AlertDialog.Builder(mContext); if (core == null) { core = (MuPDFCore)getActivity().getLastNonConfigurationInstance(); if (savedInstanceState != null && savedInstanceState.containsKey("FileName")) { mFileName = savedInstanceState.getString("FileName"); } } if (core == null) { Intent intent = getActivity().getIntent(); byte buffer[] = null; boolean hasIntent = Intent.ACTION_VIEW.equals(intent.getAction()); boolean hasArguments = getArguments() != null && getArguments().getString(PARAM_PATH_PDF) != null; if (hasIntent || hasArguments) { Uri uri; if (hasArguments) { uri = Uri.parse(getArguments().getString(PARAM_PATH_PDF)); } else { uri = intent.getData(); mDoSign = intent.getBooleanExtra(PARAM_MODE_SIGN, true); } if ((uri != null && uri.toString().startsWith("content://")) || byteArrayPdf != null) { String reason = null; try { if (byteArrayPdf != null) { buffer = byteArrayPdf; } else { InputStream is = mContext.getContentResolver().openInputStream(uri); int len = is.available(); buffer = new byte[len]; is.read(buffer, 0, len); is.close(); } } catch (OutOfMemoryError e) { System.out.println("Out of memory during buffer reading"); reason = e.toString(); } catch (Exception e) { System.out.println("Exception reading from stream: " + e); // Handle view requests from the Transformer Prime's file manager // Hopefully other file managers will use this same scheme, if not // using explicit paths. // I'm hoping that this case below is no longer needed...but it's // hard to test as the file manager seems to have changed in 4.x. try { Cursor cursor = mContext.getContentResolver().query(uri, new String[]{"_data"}, null, null, null); if (cursor.moveToFirst()) { String str = cursor.getString(0); if (str == null) { reason = "Couldn't parse data in intent"; } else { uri = Uri.parse(str); } } } catch (Exception e2) { System.out.println("Exception in Transformer Prime file manager code: " + e2); reason = e2.toString(); } } if (reason != null) { buffer = null; Resources res = getResources(); AlertDialog alert = mAlertBuilder.create(); alert.setTitle(String.format(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) { getActivity().finish(); } }); alert.show(); return null; } } if (buffer != null) { core = openBuffer(buffer, intent.getType()); } else { core = openFile(Uri.decode(uri.getEncodedPath())); } SearchTaskResult.set(null); } if (core != null && core.needsPassword()) { if (getArguments() != null && getArguments().getString(PARAM_PASSWORD_PDF) != null) { String password = getArguments().getString(PARAM_PASSWORD_PDF); core.authenticatePassword(password); } else { requestPassword(savedInstanceState); return null; } } 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) { getActivity().finish(); } }); alert.setOnCancelListener(new OnCancelListener() { @Override public void onCancel(DialogInterface dialog) { getActivity().finish(); } }); alert.show(); return null; } return createUI(savedInstanceState, mContext); } public void requestPassword(final Bundle savedInstanceState) { mPasswordView = new EditText(mContext); 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, mContext); } else { requestPassword(savedInstanceState); } } }); alert.setButton(AlertDialog.BUTTON_NEGATIVE, getString(R.string.cancel), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { getActivity().finish(); } }); alert.show(); } public View createUI(Bundle savedInstanceState, final Context context) { if (core == null) return null; // Now create the UI. // First create the document view mDocView = new MuPDFReaderView(context) { @Override protected void onMoveToChild(int i) { if (core == null) return; mPageNumberView.setText(String.format("%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 protected void onHit(Hit item) { switch (mTopBarMode) { case Annot: if (item == Hit.Annotation) { showButtons(); mTopBarMode = TopBarMode.Delete; mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal()); } break; case Delete: mTopBarMode = TopBarMode.Annot; mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal()); // fall through default: // Not in annotation editing mode, but the pageview will // still select and highlight hit annotations, so // deselect just in case. MuPDFView pageView = (MuPDFView) mDocView.getDisplayedView(); if (pageView != null) pageView.deselectAnnotation(); break; } } }; MuPDFPageAdapter adapter = new MuPDFPageAdapter(context, this, core); mDocView.setAdapter(adapter); mDocView.setEventCallback(eventCallback); mDocView.setPdfBitmapList(pdfBitmaps); mSearchTask = new SearchTask(context, 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 mFilenameView.setText(mFileName); // Activate the seekbar mPageSlider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { public void onStopTrackingTouch(SeekBar seekBar) { mDocView.setDisplayedViewIndex((seekBar.getProgress()+mPageSliderRes/2)/mPageSliderRes); } public void onStartTrackingTouch(SeekBar seekBar) {} public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { int page = (progress+mPageSliderRes/2)/mPageSliderRes; updatePageNumView(page); if (eventCallback != null) { eventCallback.pageChanged(page); } } }); // Activate the search-preparing button mSearchButton.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { searchModeOn(); } }); // Activate the reflow button mReflowButton.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { toggleReflow(); } }); if (core.fileFormat().startsWith("PDF") && core.isUnencryptedPDF() && !core.wasOpenedFromBuffer()) { mAnnotButton.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { mTopBarMode = TopBarMode.Annot; mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal()); } }); } else { mAnnotButton.setVisibility(View.GONE); } // 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.hasOutline()) { mOutlineButton.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { OutlineItem outline[] = core.getOutline(); if (outline != null) { OutlineActivityData.get().items = outline; Intent intent = new Intent(context, OutlineActivity.class); startActivityForResult(intent, OUTLINE_REQUEST); } } }); } else { mOutlineButton.setVisibility(View.GONE); } // Reenstate last state if it was recorded SharedPreferences prefs = getActivity().getPreferences(Context.MODE_PRIVATE); int lastPage = prefs.getInt("page"+mFileName, 0); // mDocView.setDisplayedViewIndex(lastPage); if (mPageNumber < core.countPages()) { mDocView.setDisplayedViewIndex(mPageNumber); } else { mDocView.setDisplayedViewIndex(0); } if (savedInstanceState == null || !savedInstanceState.getBoolean("ButtonsHidden", false)) showButtons(); if(savedInstanceState != null && savedInstanceState.getBoolean("SearchMode", false)) searchModeOn(); if(savedInstanceState != null && savedInstanceState.getBoolean("ReflowMode", false)) reflowModeSet(true); // Stick the document view and the buttons overlay into a parent view RelativeLayout layout = new RelativeLayout(context); layout.addView(mDocView); layout.addView(mButtonsView); if (getArguments() != null && getArguments().getBoolean(PARAM_SHOW_CONTROLS)) { mButtonsView.setVisibility(View.VISIBLE); } else { mButtonsView.setVisibility(View.GONE); } // Performs tap event to refresh view. Handler handler = new Handler(); handler.postDelayed(runnable, msRedraw); return layout; } public Object onRetainNonConfigurationInstance() { MuPDFCore mycore = core; core = null; return mycore; } private void reflowModeSet(boolean reflow) { mReflow = reflow; mDocView.setAdapter(mReflow ? new MuPDFReflowAdapter(mContext, core) : new MuPDFPageAdapter(getActivity(), this, core)); mReflowButton.setColorFilter(mReflow ? Color.argb(0xFF, 172, 114, 37) : Color.argb(0xFF, 255, 255, 255)); setButtonEnabled(mAnnotButton, !reflow); setButtonEnabled(mSearchButton, !reflow); if (reflow) setLinkHighlight(false); setButtonEnabled(mLinkButton, !reflow); setButtonEnabled(mMoreButton, !reflow); mDocView.refresh(mReflow); } private void toggleReflow() { reflowModeSet(!mReflow); showInfo(mReflow ? getString(R.string.entering_reflow_mode) : getString(R.string.leaving_reflow_mode)); } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); if (mFileName != null && mDocView != null) { outState.putString("FileName", mFileName); // 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 = getActivity().getPreferences(Context.MODE_PRIVATE); SharedPreferences.Editor edit = prefs.edit(); edit.putInt("page"+mFileName, mDocView.getDisplayedViewIndex()); edit.commit(); } if (!mButtonsVisible) outState.putBoolean("ButtonsHidden", true); if (mTopBarMode == TopBarMode.Search) outState.putBoolean("SearchMode", true); if (mReflow) outState.putBoolean("ReflowMode", true); } @Override public void onPause() { super.onPause(); if (mSearchTask != null) mSearchTask.stop(); if (mFileName != null && mDocView != null) { SharedPreferences prefs = getActivity().getPreferences(Context.MODE_PRIVATE); SharedPreferences.Editor edit = prefs.edit(); edit.putInt("page"+mFileName, mDocView.getDisplayedViewIndex()); edit.commit(); } } public void onDestroy() { if (mDocView != null) { mDocView.applyToChildren(new ReaderView.ViewMapper() { void applyToView(View view) { ((MuPDFView) view).releaseResources(); } }); mDocView.setEventCallback(null); } if (core != null) core.onDestroy(); if (mAlertTask != null) { mAlertTask.cancel(true); mAlertTask = null; } eventCallback = null; core = null; // Android is not releasing the memory recycled from the bitmaps on certain circumstances, which leads to OutOfMemory errors. // Somehow the gc is not called automatically in those situations... // System.gc(); 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, 172, 114, 37) : 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("%d / %d", index + 1, core.countPages())); } private void printDoc() { if (!core.fileFormat().startsWith("PDF")) { showInfo(getString(R.string.format_currently_not_supported)); return; } Intent myIntent = getActivity().getIntent(); Uri docUri = myIntent != null ? myIntent.getData() : null; if (docUri == null) { showInfo(getString(R.string.print_failed)); } if (docUri.getScheme() == null) docUri = Uri.parse("file://"+docUri.toString()); Intent printIntent = new Intent(mContext, PrintDialogActivity.class); printIntent.setDataAndType(docUri, "aplication/pdf"); printIntent.putExtra("title", mFileName); startActivityForResult(printIntent, PRINT_REQUEST); } private void showInfo(String message) { mInfoView.setText(message); int currentApiVersion = android.os.Build.VERSION.SDK_INT; if (currentApiVersion >= android.os.Build.VERSION_CODES.HONEYCOMB) { SafeAnimatorInflater safe = new SafeAnimatorInflater(getActivity(), R.animator.info, (View)mInfoView); } else { mInfoView.setVisibility(View.VISIBLE); mHandler.postDelayed(new Runnable() { public void run() { mInfoView.setVisibility(View.INVISIBLE); } }, 500); } } private void makeButtonsView() { mButtonsView = getActivity().getLayoutInflater().inflate(R.layout.buttons,null); mFilenameView = (TextView)mButtonsView.findViewById(R.id.docNameText); mPageSlider = (SeekBar)mButtonsView.findViewById(R.id.pageSlider); mPageNumberView = (TextView)mButtonsView.findViewById(R.id.pageNumber); mInfoView = (TextView)mButtonsView.findViewById(R.id.info); mSearchButton = (ImageButton)mButtonsView.findViewById(R.id.searchButton); mReflowButton = (ImageButton)mButtonsView.findViewById(R.id.reflowButton); mOutlineButton = (ImageButton)mButtonsView.findViewById(R.id.outlineButton); mAnnotButton = (ImageButton)mButtonsView.findViewById(R.id.editAnnotButton); mAnnotTypeText = (TextView)mButtonsView.findViewById(R.id.annotType); mTopBarSwitcher = (ViewAnimator)mButtonsView.findViewById(R.id.switcher); mSearchBack = (ImageButton)mButtonsView.findViewById(R.id.searchBack); mSearchFwd = (ImageButton)mButtonsView.findViewById(R.id.searchForward); mSearchText = (EditText)mButtonsView.findViewById(R.id.searchText); mLinkButton = (ImageButton)mButtonsView.findViewById(R.id.linkButton); mMoreButton = (ImageButton)mButtonsView.findViewById(R.id.moreButton); mTopBarSwitcher.setVisibility(View.INVISIBLE); mPageNumberView.setVisibility(View.INVISIBLE); mInfoView.setVisibility(View.INVISIBLE); mPageSlider.setVisibility(View.INVISIBLE); } public void OnMoreButtonClick(View v) { mTopBarMode = TopBarMode.More; mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal()); } public void OnCancelMoreButtonClick(View v) { mTopBarMode = TopBarMode.Main; mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal()); } public void OnPrintButtonClick(View v) { printDoc(); } public void OnCopyTextButtonClick(View v) { mTopBarMode = TopBarMode.Accept; mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal()); mAcceptMode = AcceptMode.CopyText; mDocView.setMode(MuPDFReaderView.Mode.Selecting); mAnnotTypeText.setText(getString(R.string.copy_text)); showInfo(getString(R.string.select_text)); } public void OnEditAnnotButtonClick(View v) { mTopBarMode = TopBarMode.Annot; mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal()); } public void OnCancelAnnotButtonClick(View v) { mTopBarMode = TopBarMode.More; mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal()); } public void OnHighlightButtonClick(View v) { mTopBarMode = TopBarMode.Accept; mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal()); mAcceptMode = AcceptMode.Highlight; mDocView.setMode(MuPDFReaderView.Mode.Selecting); mAnnotTypeText.setText(R.string.highlight); showInfo(getString(R.string.select_text)); } public void OnUnderlineButtonClick(View v) { mTopBarMode = TopBarMode.Accept; mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal()); mAcceptMode = AcceptMode.Underline; mDocView.setMode(MuPDFReaderView.Mode.Selecting); mAnnotTypeText.setText(R.string.underline); showInfo(getString(R.string.select_text)); } public void OnStrikeOutButtonClick(View v) { mTopBarMode = TopBarMode.Accept; mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal()); mAcceptMode = AcceptMode.StrikeOut; mDocView.setMode(MuPDFReaderView.Mode.Selecting); mAnnotTypeText.setText(R.string.strike_out); showInfo(getString(R.string.select_text)); } public void OnInkButtonClick(View v) { mTopBarMode = TopBarMode.Accept; mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal()); mAcceptMode = AcceptMode.Ink; mDocView.setMode(MuPDFReaderView.Mode.Drawing); mAnnotTypeText.setText(R.string.ink); showInfo(getString(R.string.draw_annotation)); } public void OnCancelAcceptButtonClick(View v) { MuPDFView pageView = (MuPDFView) mDocView.getDisplayedView(); if (pageView != null) { pageView.deselectText(); pageView.cancelDraw(); } mDocView.setMode(MuPDFReaderView.Mode.Viewing); switch (mAcceptMode) { case CopyText: mTopBarMode = TopBarMode.More; break; default: mTopBarMode = TopBarMode.Annot; break; } mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal()); } public void OnAcceptButtonClick(View v) { MuPDFView pageView = (MuPDFView) mDocView.getDisplayedView(); boolean success = false; switch (mAcceptMode) { case CopyText: if (pageView != null) success = pageView.copySelection(); mTopBarMode = TopBarMode.More; showInfo(success?getString(R.string.copied_to_clipboard):getString(R.string.no_text_selected)); break; case Highlight: if (pageView != null) success = pageView.markupSelection(Annotation.Type.HIGHLIGHT); mTopBarMode = TopBarMode.Annot; if (!success) showInfo(getString(R.string.no_text_selected)); break; case Underline: if (pageView != null) success = pageView.markupSelection(Annotation.Type.UNDERLINE); mTopBarMode = TopBarMode.Annot; if (!success) showInfo(getString(R.string.no_text_selected)); break; case StrikeOut: if (pageView != null) success = pageView.markupSelection(Annotation.Type.STRIKEOUT); mTopBarMode = TopBarMode.Annot; if (!success) showInfo(getString(R.string.no_text_selected)); break; case Ink: if (pageView != null) success = pageView.saveDraw(); mTopBarMode = TopBarMode.Annot; if (!success) showInfo(getString(R.string.nothing_to_save)); break; } mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal()); mDocView.setMode(MuPDFReaderView.Mode.Viewing); } public void OnCancelSearchButtonClick(View v) { searchModeOff(); } public void OnDeleteButtonClick(View v) { MuPDFView pageView = (MuPDFView) mDocView.getDisplayedView(); if (pageView != null) pageView.deleteSelectedAnnotation(); mTopBarMode = TopBarMode.Annot; mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal()); } public void OnCancelDeleteButtonClick(View v) { MuPDFView pageView = (MuPDFView) mDocView.getDisplayedView(); if (pageView != null) pageView.deselectAnnotation(); mTopBarMode = TopBarMode.Annot; mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal()); } private void showKeyboard() { InputMethodManager imm = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE); if (imm != null) imm.showSoftInput(mSearchText, 0); } private void hideKeyboard() { InputMethodManager imm = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE); if (imm != null) imm.hideSoftInputFromWindow(mSearchText.getWindowToken(), 0); } public 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 void onStart() { if (core != null) { core.startAlerts(); createAlertWaiter(); } super.onStart(); } @Override public void onStop() { if (core != null) { destroyAlertWaiter(); core.stopAlerts(); } super.onStop(); } @Override public void performPickFor(FilePicker picker) { mFilePicker = picker; Intent intent = new Intent(mContext, ChoosePDFActivity.class); intent.setAction(ChoosePDFActivity.PICK_KEY_FILE); startActivityForResult(intent, FILEPICK_REQUEST); } // @viafirma: Custom methods public static MuPDFFragment newInstance (String signBitmapPath, List digitalizedImage, String pathPdf) { return newInstance(signBitmapPath, digitalizedImage, pathPdf, true); } public void setPageNumber(int pageNumber) { this.mPageNumber = pageNumber; if (mDocView != null && core != null) { if (pageNumber < core.countPages()) { mDocView.setDisplayedViewIndex(pageNumber); } } } public int getCurrentPage() { int page = 0; if (mDocView != null) { page = mDocView.getDisplayedViewIndex(); } return page; } public static MuPDFFragment newInstance (String signBitmapPath, List digitalizedImage, String pathPdf, boolean showControls) { MuPDFFragment f = newInstance(signBitmapPath, digitalizedImage, pathPdf, null, showControls); return f; } public static MuPDFFragment newInstance (String signBitmapPath, List digitalizedImage, String pathPdf, String passwordPdf, boolean showControls) { MuPDFFragment f = new MuPDFFragment(); Bundle args = new Bundle(); if (digitalizedImage != null && digitalizedImage.size() > 0) { args.putParcelable(PARAM_DIGITALIZED_IMAGE, digitalizedImage.get(0)); } if (signBitmapPath != null) { args.putString(PARAM_SIGN_BITMAP_PATH, signBitmapPath); } if (pathPdf != null) { args.putString(PARAM_PATH_PDF, pathPdf); } if (passwordPdf != null) { args.putString(PARAM_PASSWORD_PDF, passwordPdf); } args.putBoolean(PARAM_SHOW_CONTROLS, showControls); f.setArguments(args); return f; } public static MuPDFFragment newInstance (byte[] bufferedPdf) { MuPDFFragment f = newInstance(null, null, null, null, false); f.setByteArrayPdf(bufferedPdf); return f; } public static MuPDFFragment newInstance (String pathPdf) { MuPDFFragment f = newInstance(null, null, pathPdf, null, false); return f; } public static MuPDFFragment newInstance (String pathPdf, String passwordPdf) { MuPDFFragment f = newInstance(null, null, pathPdf, passwordPdf, false); return f; } public static MuPDFFragment newInstance (byte[] bufferedPdf, boolean showControls) { MuPDFFragment f = newInstance(null, null, null, null, showControls); f.setByteArrayPdf(bufferedPdf); return f; } public static MuPDFFragment newInstance (String pathPdf, boolean showControls) { MuPDFFragment f = newInstance(null, null, pathPdf, null, showControls); return f; } public void addBitmap(PdfBitmap pdfBitmap) { if (mDocView != null) { mDocView.addBitmap(pdfBitmap); } else { Log.e(TAG, "Couldn't add Bitmap. DocView is NULL."); } } public void setPdfBitmapList(Collection pdfBitmaps) { this.pdfBitmaps = pdfBitmaps; if (mDocView != null) { mDocView.setPdfBitmapList(pdfBitmaps); } } public Collection getBitmapList() { if (mDocView != null) { return mDocView.getBitmapList(); } else { Log.e(TAG, "Couldn't get bitmap list. DocView is NULL."); return new HashSet<>(); } } public boolean removeBitmapOnPosition(float x, float y) { boolean removed = false; if (mDocView != null) { Point point = new Point((int)x, (int)y); removed = mDocView.removeBitmapOnPosition(point); } else { Log.e(TAG, "Couldn't remove Bitmap. DocView is NULL."); } return removed; }; public void setByteArrayPdf(byte[] byteArrayPdf) { this.byteArrayPdf = byteArrayPdf; } @Override public void onAttach(Activity activity) { super.onAttach(activity); this.mContext = activity; } public boolean checkSign(){ MuPDFPageAdapter adapter = (MuPDFPageAdapter)mDocView.getAdapter(); if (adapter.getNumSignature() > 0 || !mDoSign) { return true; } else { AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setTitle(getResources().getString(R.string.noSignOnPdfTitle)); builder.setMessage(R.string.noSignOnPdf); builder.setNegativeButton(R.string.OkKey, null); builder.show(); return false; } } public DigitalizedEventCallback getEventCallback() { return this.eventCallback; } public void setEventCallback(DigitalizedEventCallback eventCallback) { this.eventCallback = eventCallback; if (mDocView != null) { mDocView.setEventCallback(eventCallback); } } @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); Handler handler = new Handler(); // Excalibur line: if you can replace it, please do it. handler.postDelayed(runnable, msRedraw); } public void updateCurrentPage() { if (mDocView != null) { mDocView.updateCurrentPage(); } } public void redrawAll() { if (mDocView != null) { mDocView.redrawAll(); } } private Runnable runnable = new Runnable() { @Override public void run() { redrawTouch(); } }; private final int msRedraw = 500; private void redrawTouch(){ if (mDocView != null) { // Dispatch touch event to view mDocView.refreshView(); } } } ================================================ FILE: src/main/java/com/artifex/mupdfdemo/MuPDFPageAdapter.java ================================================ package com.artifex.mupdfdemo; import android.content.Context; import android.graphics.Point; import android.graphics.PointF; import android.util.SparseArray; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import com.artifex.utils.PdfBitmap; import java.util.Collection; import java.util.HashSet; import java.util.Set; public class MuPDFPageAdapter extends BaseAdapter { private final Context mContext; private final FilePicker.FilePickerSupport mFilePickerSupport; private final MuPDFCore mCore; private final SparseArray mPageSizes = new SparseArray(); private SparseArray pages = new SparseArray(); private Collection pdfBitmapList; // Each signature for each page. private int numSignature; public MuPDFPageAdapter(Context c, FilePicker.FilePickerSupport filePickerSupport, MuPDFCore core) { mContext = c; mFilePickerSupport = filePickerSupport; mCore = core; } public int getCount() { return mCore.countPages(); } public Object getItem(int position) { return pages.get(position); } public long getItemId(int position) { return 0; } public View getView(final int position, View convertView, ViewGroup parent) { final MuPDFPageView pageView; if (pages.get(position) == null) { pageView = new MuPDFPageView(mContext, mFilePickerSupport, mCore, new Point(parent.getWidth(), parent.getHeight()), this); pages.put(position, pageView); } else { pageView = pages.get(position); } //Limit the pages cache to improve memory usage if(pages.size()>3){ if(position>1) { MuPDFPageView previous = pages.get(position - 2); if(previous!=null){ pages.removeAt(pages.indexOfValue(previous)); previous=null; } MuPDFPageView post = pages.get(position + 2); if (post != null) { pages.removeAt(pages.indexOfValue(post)); post = null; } } } 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) { return mCore.getPageSize(position); } @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; } public Collection getPdfBitmapList() { if (pdfBitmapList == null) { pdfBitmapList = new HashSet(); } return pdfBitmapList; } public void setPdfBitmapList(Collection pdfBitmapList) { this.pdfBitmapList = pdfBitmapList; } public int getNumSignature() { return numSignature; } public void setNumSignature(int numSignature) { this.numSignature = numSignature; } public void addBitmaps(Set pdfBitmaps) { if (pdfBitmaps != null) { for (PdfBitmap pdfBitmap : pdfBitmaps) { addBitmap(pdfBitmap); } } } public void addBitmap(PdfBitmap pdfBitmap) { if (pdfBitmap.getType() == PdfBitmap.Type.SIGNATURE) { //mAdapter null ??? numSignature = numSignature + 1; } getPdfBitmapList().add(pdfBitmap); } } ================================================ FILE: src/main/java/com/artifex/mupdfdemo/MuPDFPageView.java ================================================ package com.artifex.mupdfdemo; import java.util.ArrayList; import com.artifex.mupdfdemo.MuPDFCore.Cookie; import android.annotation.TargetApi; import android.app.AlertDialog; import android.content.ClipData; import android.content.Context; import android.content.DialogInterface; import android.graphics.Bitmap; import android.graphics.Point; import android.graphics.PointF; import android.graphics.RectF; import android.net.Uri; import android.os.Build; import android.text.method.PasswordTransformationMethod; import android.view.LayoutInflater; import android.view.WindowManager; import android.view.inputmethod.EditorInfo; import android.widget.EditText; /* This enum should be kept in line with the cooresponding C enum in mupdf.c */ enum SignatureState { NoSupport, Unsigned, Signed } abstract class PassClickResultVisitor { public abstract void visitText(PassClickResultText result); public abstract void visitChoice(PassClickResultChoice result); public abstract void visitSignature(PassClickResultSignature result); } class PassClickResult { public final boolean changed; public PassClickResult(boolean _changed) { changed = _changed; } public void acceptVisitor(PassClickResultVisitor visitor) { } } class PassClickResultText extends PassClickResult { public final String text; public PassClickResultText(boolean _changed, String _text) { super(_changed); text = _text; } public void acceptVisitor(PassClickResultVisitor visitor) { visitor.visitText(this); } } class PassClickResultChoice extends PassClickResult { public final String [] options; public final String [] selected; public PassClickResultChoice(boolean _changed, String [] _options, String [] _selected) { super(_changed); options = _options; selected = _selected; } public void acceptVisitor(PassClickResultVisitor visitor) { visitor.visitChoice(this); } } class PassClickResultSignature extends PassClickResult { public final SignatureState state; public PassClickResultSignature(boolean _changed, int _state) { super(_changed); state = SignatureState.values()[_state]; } public void acceptVisitor(PassClickResultVisitor visitor) { visitor.visitSignature(this); } } public class MuPDFPageView extends PageView implements MuPDFView { final private FilePicker.FilePickerSupport mFilePickerSupport; private final MuPDFCore mCore; private AsyncTask mPassClick; private RectF mWidgetAreas[]; private Annotation mAnnotations[]; private int mSelectedAnnotationIndex = -1; private AsyncTask mLoadWidgetAreas; private AsyncTask mLoadAnnotations; private AlertDialog.Builder mTextEntryBuilder; private AlertDialog.Builder mChoiceEntryBuilder; private AlertDialog.Builder mSigningDialogBuilder; private AlertDialog.Builder mSignatureReportBuilder; private AlertDialog.Builder mPasswordEntryBuilder; private EditText mPasswordText; private AlertDialog mTextEntry; private AlertDialog mPasswordEntry; private EditText mEditText; private AsyncTask mSetWidgetText; private AsyncTask mSetWidgetChoice; private AsyncTask mAddStrikeOut; private AsyncTask mAddInk; private AsyncTask mDeleteAnnotation; private AsyncTask mCheckSignature; private AsyncTask mSign; private Runnable changeReporter; public MuPDFPageView(Context c, FilePicker.FilePickerSupport filePickerSupport, MuPDFCore core, Point parentSize, MuPDFPageAdapter adapter) { super(c, parentSize, adapter); mFilePickerSupport = filePickerSupport; mCore = core; mTextEntryBuilder = new AlertDialog.Builder(c); mTextEntryBuilder.setTitle(getContext().getString(R.string.fill_out_text_field)); LayoutInflater inflater = (LayoutInflater)c.getSystemService(Context.LAYOUT_INFLATER_SERVICE); mEditText = (EditText)inflater.inflate(R.layout.textentry, null); mTextEntryBuilder.setView(mEditText); mTextEntryBuilder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); } }); mTextEntryBuilder.setPositiveButton(R.string.okay, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { mSetWidgetText = new AsyncTask () { @Override protected Boolean doInBackground(String... arg0) { return mCore.setFocusedWidgetText(mPageNumber, arg0[0]); } @Override protected void onPostExecute(Boolean result) { changeReporter.run(); if (!result) invokeTextDialog(mEditText.getText().toString()); } }; mSetWidgetText.execute(mEditText.getText().toString()); } }); mTextEntry = mTextEntryBuilder.create(); mChoiceEntryBuilder = new AlertDialog.Builder(c); mChoiceEntryBuilder.setTitle(getContext().getString(R.string.choose_value)); mSigningDialogBuilder = new AlertDialog.Builder(c); mSigningDialogBuilder.setTitle("Select certificate and sign?"); mSigningDialogBuilder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); } }); mSigningDialogBuilder.setPositiveButton(R.string.okay, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { FilePicker picker = new FilePicker(mFilePickerSupport) { @Override void onPick(Uri uri) { signWithKeyFile(uri); } }; picker.pick(); } }); mSignatureReportBuilder = new AlertDialog.Builder(c); mSignatureReportBuilder.setTitle("Signature checked"); mSignatureReportBuilder.setPositiveButton(R.string.okay, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); } }); mPasswordText = new EditText(c); mPasswordText.setInputType(EditorInfo.TYPE_TEXT_VARIATION_PASSWORD); mPasswordText.setTransformationMethod(new PasswordTransformationMethod()); mPasswordEntryBuilder = new AlertDialog.Builder(c); mPasswordEntryBuilder.setTitle(R.string.enter_password); mPasswordEntryBuilder.setView(mPasswordText); mPasswordEntryBuilder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); } }); mPasswordEntry = mPasswordEntryBuilder.create(); } private void signWithKeyFile(final Uri uri) { mPasswordEntry.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); mPasswordEntry.setButton(AlertDialog.BUTTON_POSITIVE, "Sign", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); signWithKeyFileAndPassword(uri, mPasswordText.getText().toString()); } }); mPasswordEntry.show(); } private void signWithKeyFileAndPassword(final Uri uri, final String password) { mSign = new AsyncTask() { @Override protected Boolean doInBackground(Void... params) { return mCore.signFocusedSignature(Uri.decode(uri.getEncodedPath()), password); } @Override protected void onPostExecute(Boolean result) { if (result) { changeReporter.run(); } else { mPasswordText.setText(""); signWithKeyFile(uri); } } }; mSign.execute(); } public LinkInfo 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; for (LinkInfo l: mLinks) if (l.rect.contains(docRelX, docRelY)) return l; return null; } private void invokeTextDialog(String text) { mEditText.setText(text); mTextEntry.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); mTextEntry.show(); } private void invokeChoiceDialog(final String [] options) { mChoiceEntryBuilder.setItems(options, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { mSetWidgetChoice = new AsyncTask() { @Override protected Void doInBackground(String... params) { String [] sel = {params[0]}; mCore.setFocusedWidgetChoiceSelected(sel); return null; } @Override protected void onPostExecute(Void result) { changeReporter.run(); } }; mSetWidgetChoice.execute(options[which]); } }); AlertDialog dialog = mChoiceEntryBuilder.create(); dialog.show(); } private void invokeSignatureCheckingDialog() { mCheckSignature = new AsyncTask () { @Override protected String doInBackground(Void... params) { return mCore.checkFocusedSignature(); } @Override protected void onPostExecute(String result) { AlertDialog report = mSignatureReportBuilder.create(); report.setMessage(result); report.show(); } }; mCheckSignature.execute(); } private void invokeSigningDialog() { AlertDialog dialog = mSigningDialogBuilder.create(); dialog.show(); } private void warnNoSignatureSupport() { AlertDialog dialog = mSignatureReportBuilder.create(); dialog.setTitle("App built with no signature support"); dialog.show(); } public void setChangeReporter(Runnable reporter) { changeReporter = reporter; } public Hit passClickEvent(float x, float y) { float scale = mSourceScale*(float)getWidth()/(float)mSize.x; final float docRelX = (x - getLeft())/scale; final float docRelY = (y - getTop())/scale; boolean hit = false; int i; if (mAnnotations != null) { for (i = 0; i < mAnnotations.length; i++) if (mAnnotations[i].contains(docRelX, docRelY)) { hit = true; break; } if (hit) { switch (mAnnotations[i].type) { case HIGHLIGHT: case UNDERLINE: case SQUIGGLY: case STRIKEOUT: case INK: mSelectedAnnotationIndex = i; setItemSelectBox(mAnnotations[i]); return Hit.Annotation; } } } mSelectedAnnotationIndex = -1; setItemSelectBox(null); if (!mCore.javascriptSupported()) return Hit.Nothing; if (mWidgetAreas != null) { for (i = 0; i < mWidgetAreas.length && !hit; i++) if (mWidgetAreas[i].contains(docRelX, docRelY)) hit = true; } if (hit) { mPassClick = new AsyncTask() { @Override protected PassClickResult doInBackground(Void... arg0) { return mCore.passClickEvent(mPageNumber, docRelX, docRelY); } @Override protected void onPostExecute(PassClickResult result) { if (result.changed) { changeReporter.run(); } result.acceptVisitor(new PassClickResultVisitor() { @Override public void visitText(PassClickResultText result) { invokeTextDialog(result.text); } @Override public void visitChoice(PassClickResultChoice result) { invokeChoiceDialog(result.options); } @Override public void visitSignature(PassClickResultSignature result) { switch (result.state) { case NoSupport: warnNoSignatureSupport(); break; case Unsigned: invokeSigningDialog(); break; case Signed: invokeSignatureCheckingDialog(); break; } } }); } }; mPassClick.execute(); return Hit.Widget; } return Hit.Nothing; } @TargetApi(11) public boolean copySelection() { final StringBuilder text = new StringBuilder(); processSelectedText(new TextProcessor() { StringBuilder line; public void onStartLine() { line = new StringBuilder(); } public void onWord(TextWord word) { if (line.length() > 0) line.append(' '); line.append(word.w); } public void onEndLine() { if (text.length() > 0) text.append('\n'); text.append(line); } }); if (text.length() == 0) return false; int currentApiVersion = Build.VERSION.SDK_INT; if (currentApiVersion >= Build.VERSION_CODES.HONEYCOMB) { android.content.ClipboardManager cm = (android.content.ClipboardManager)mContext.getSystemService(Context.CLIPBOARD_SERVICE); cm.setPrimaryClip(ClipData.newPlainText("MuPDF", text)); } else { android.text.ClipboardManager cm = (android.text.ClipboardManager)mContext.getSystemService(Context.CLIPBOARD_SERVICE); cm.setText(text); } deselectText(); return true; } public boolean markupSelection(final Annotation.Type type) { final ArrayList quadPoints = new ArrayList(); processSelectedText(new TextProcessor() { RectF rect; public void onStartLine() { rect = new RectF(); } public void onWord(TextWord word) { rect.union(word); } public void onEndLine() { if (!rect.isEmpty()) { quadPoints.add(new PointF(rect.left, rect.bottom)); quadPoints.add(new PointF(rect.right, rect.bottom)); quadPoints.add(new PointF(rect.right, rect.top)); quadPoints.add(new PointF(rect.left, rect.top)); } } }); if (quadPoints.size() == 0) return false; mAddStrikeOut = new AsyncTask() { @Override protected Void doInBackground(PointF[]... params) { addMarkup(params[0], type); return null; } @Override protected void onPostExecute(Void result) { loadAnnotations(); update(); } }; mAddStrikeOut.execute(quadPoints.toArray(new PointF[quadPoints.size()])); deselectText(); return true; } public void deleteSelectedAnnotation() { if (mSelectedAnnotationIndex != -1) { if (mDeleteAnnotation != null) mDeleteAnnotation.cancel(true); mDeleteAnnotation = new AsyncTask() { @Override protected Void doInBackground(Integer... params) { mCore.deleteAnnotation(mPageNumber, params[0]); return null; } @Override protected void onPostExecute(Void result) { loadAnnotations(); update(); } }; mDeleteAnnotation.execute(mSelectedAnnotationIndex); mSelectedAnnotationIndex = -1; setItemSelectBox(null); } } public void deselectAnnotation() { mSelectedAnnotationIndex = -1; setItemSelectBox(null); } public boolean saveDraw() { PointF[][] path = getDraw(); if (path == null) return false; if (mAddInk != null) { mAddInk.cancel(true); mAddInk = null; } mAddInk = new AsyncTask() { @Override protected Void doInBackground(PointF[][]... params) { mCore.addInkAnnotation(mPageNumber, params[0]); return null; } @Override protected void onPostExecute(Void result) { loadAnnotations(); update(); } }; mAddInk.execute(getDraw()); cancelDraw(); return true; } @Override 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(mCore) { @Override public Void doInBackground(Cookie cookie, Void ... params) { // 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); mCore.drawPage(bm, mPageNumber, sizeX, sizeY, patchX, patchY, patchWidth, patchHeight, cookie); return null; } }; } 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(mCore) { @Override public Void doInBackground(Cookie cookie, Void ... params) { // 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); mCore.updatePage(bm, mPageNumber, sizeX, sizeY, patchX, patchY, patchWidth, patchHeight, cookie); return null; } }; } @Override protected LinkInfo[] getLinkInfo() { return mCore.getPageLinks(mPageNumber); } @Override protected TextWord[][] getText() { return mCore.textLines(mPageNumber); } @Override protected void addMarkup(PointF[] quadPoints, Annotation.Type type) { mCore.addMarkupAnnotation(mPageNumber, quadPoints, type); } private void loadAnnotations() { mAnnotations = null; if (mLoadAnnotations != null) mLoadAnnotations.cancel(true); mLoadAnnotations = new AsyncTask () { @Override protected Annotation[] doInBackground(Void... params) { return mCore.getAnnoations(mPageNumber); } @Override protected void onPostExecute(Annotation[] result) { mAnnotations = result; } }; mLoadAnnotations.execute(); } @Override public void setPage(final int page, PointF size) { loadAnnotations(); mLoadWidgetAreas = new AsyncTask () { @Override protected RectF[] doInBackground(Void... arg0) { return mCore.getWidgetAreas(page); } @Override protected void onPostExecute(RectF[] result) { mWidgetAreas = result; } }; mLoadWidgetAreas.execute(); super.setPage(page, size); } public void setScale(float scale) { // This type of view scales automatically to fit the size // determined by the parent view groups during layout } @Override public void releaseResources() { if (mPassClick != null) { mPassClick.cancel(true); mPassClick = null; } if (mLoadWidgetAreas != null) { mLoadWidgetAreas.cancel(true); mLoadWidgetAreas = null; } if (mLoadAnnotations != null) { mLoadAnnotations.cancel(true); mLoadAnnotations = null; } if (mSetWidgetText != null) { mSetWidgetText.cancel(true); mSetWidgetText = null; } if (mSetWidgetChoice != null) { mSetWidgetChoice.cancel(true); mSetWidgetChoice = null; } if (mAddStrikeOut != null) { mAddStrikeOut.cancel(true); mAddStrikeOut = null; } if (mDeleteAnnotation != null) { mDeleteAnnotation.cancel(true); mDeleteAnnotation = null; } super.releaseResources(); } } ================================================ FILE: src/main/java/com/artifex/mupdfdemo/MuPDFReaderView.java ================================================ package com.artifex.mupdfdemo; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.View; import android.view.WindowManager; public class MuPDFReaderView extends ReaderView { enum Mode {Viewing, Selecting, Drawing} private final Context mContext; private boolean mLinksEnabled = false; private Mode mMode = Mode.Viewing; private boolean tapDisabled = false; private int tapPageMargin; private final boolean TAP_PAGING_ENABLED = false; protected void onTapMainDocArea() {} protected void onDocMotion() {} protected void onHit(Hit item) {}; public void setLinksEnabled(boolean b) { mLinksEnabled = b; resetupChildren(); } public void setMode(Mode m) { mMode = m; } private void setup() { // 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 MuPDFReaderView(Context context) { super(context); mContext = context; setup(); } public MuPDFReaderView(Context context, AttributeSet attrs) { super(context, attrs); mContext = context; setup(); } public boolean onSingleTapUp(MotionEvent e) { LinkInfo link = null; if (mMode == Mode.Viewing && !tapDisabled) { MuPDFView pageView = (MuPDFView) getDisplayedView(); Hit item = pageView.passClickEvent(e.getX(), e.getY()); onHit(item); if (item == Hit.Nothing) { if (mLinksEnabled && pageView != null && (link = pageView.hitLink(e.getX(), e.getY())) != null) { link.acceptVisitor(new LinkInfoVisitor() { @Override public void visitInternal(LinkInfoInternal li) { // Clicked on an internal (GoTo) link setDisplayedViewIndex(li.pageNumber); } @Override public void visitExternal(LinkInfoExternal li) { Intent intent = new Intent(Intent.ACTION_VIEW, Uri .parse(li.url)); mContext.startActivity(intent); } @Override public void visitRemote(LinkInfoRemote li) { // Clicked on a remote (GoToR) link } }); } else if (TAP_PAGING_ENABLED && e.getX() < tapPageMargin) { super.smartMoveBackwards(); } else if (TAP_PAGING_ENABLED && e.getX() > super.getWidth() - tapPageMargin) { super.smartMoveForwards(); } else if (TAP_PAGING_ENABLED && e.getY() < tapPageMargin) { super.smartMoveBackwards(); } else if (TAP_PAGING_ENABLED && e.getY() > super.getHeight() - tapPageMargin) { super.smartMoveForwards(); } else { onTapMainDocArea(); } } } return super.onSingleTapUp(e); } @Override public boolean onDown(MotionEvent e) { return super.onDown(e); } public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { MuPDFView pageView = (MuPDFView)getDisplayedView(); switch (mMode) { case Viewing: if (!tapDisabled) onDocMotion(); return super.onScroll(e1, e2, distanceX, distanceY); case Selecting: if (pageView != null) pageView.selectText(e1.getX(), e1.getY(), e2.getX(), e2.getY()); return true; default: return true; } } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { switch (mMode) { case Viewing: return super.onFling(e1, e2, velocityX, velocityY); default: return true; } } public boolean onScaleBegin(ScaleGestureDetector d) { // Disabled showing the buttons until next touch. // Not sure why this is needed, but without it // pinch zoom can make the buttons appear tapDisabled = true; return super.onScaleBegin(d); } public boolean onTouchEvent(MotionEvent event) { if ( mMode == Mode.Drawing ) { float x = event.getX(); float y = event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: touch_start(x, y); break; case MotionEvent.ACTION_MOVE: touch_move(x, y); break; case MotionEvent.ACTION_UP: touch_up(); break; } } if ((event.getAction() & event.getActionMasked()) == MotionEvent.ACTION_DOWN) { tapDisabled = false; } return super.onTouchEvent(event); } private float mX, mY; private static final float TOUCH_TOLERANCE = 2; private void touch_start(float x, float y) { MuPDFView pageView = (MuPDFView)getDisplayedView(); if (pageView != null) { pageView.startDraw(x, y); } mX = x; mY = y; } private void touch_move(float x, float y) { float dx = Math.abs(x - mX); float dy = Math.abs(y - mY); if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) { MuPDFView pageView = (MuPDFView)getDisplayedView(); if (pageView != null) { pageView.continueDraw(x, y); } mX = x; mY = y; } } private void touch_up() { // NOOP } protected void onChildSetup(int i, View v) { if (SearchTaskResult.get() != null && SearchTaskResult.get().pageNumber == i) ((MuPDFView) v).setSearchBoxes(SearchTaskResult.get().searchBoxes); else ((MuPDFView) v).setSearchBoxes(null); ((MuPDFView) v).setLinkHighlighting(mLinksEnabled); ((MuPDFView) v).setChangeReporter(new Runnable() { public void run() { applyToChildren(new ReaderView.ViewMapper() { @Override void applyToView(View view) { ((MuPDFView) view).update(); } }); } }); } protected void onMoveToChild(int i) { if (SearchTaskResult.get() != null && SearchTaskResult.get().pageNumber != i) { SearchTaskResult.set(null); resetupChildren(); } } @Override protected void onMoveOffChild(int i) { View v = getView(i); if (v != null) ((MuPDFView)v).deselectAnnotation(); } protected void onSettle(View v) { // When the layout has settled ask the page to render // in HQ ((MuPDFView) 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 ((MuPDFView) v).removeHq(); } @Override protected void onNotInUse(View v) { ((MuPDFView) v).releaseResources(); } @Override protected void onScaleChild(View v, Float scale) { ((MuPDFView) v).setScale(scale); } } ================================================ FILE: src/main/java/com/artifex/mupdfdemo/MuPDFReflowAdapter.java ================================================ package com.artifex.mupdfdemo; import android.content.Context; import android.graphics.Point; import android.graphics.PointF; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; public class MuPDFReflowAdapter extends BaseAdapter { private final Context mContext; private final MuPDFCore mCore; public MuPDFReflowAdapter(Context c, MuPDFCore core) { mContext = c; mCore = core; } public int getCount() { return mCore.countPages(); } public Object getItem(int arg0) { return null; } public long getItemId(int arg0) { return 0; } public View getView(int position, View convertView, ViewGroup parent) { final MuPDFReflowView reflowView; if (convertView == null) { reflowView = new MuPDFReflowView(mContext, mCore, new Point(parent.getWidth(), parent.getHeight())); } else { reflowView = (MuPDFReflowView) convertView; } reflowView.setPage(position, new PointF()); return reflowView; } } ================================================ FILE: src/main/java/com/artifex/mupdfdemo/MuPDFReflowView.java ================================================ package com.artifex.mupdfdemo; import android.content.Context; import android.graphics.Point; import android.graphics.PointF; import android.graphics.RectF; import android.os.Handler; import android.util.Base64; import android.view.MotionEvent; import android.view.View; import android.webkit.WebView; import android.webkit.WebViewClient; public class MuPDFReflowView extends WebView implements MuPDFView { private final MuPDFCore mCore; private final Handler mHandler; private final Point mParentSize; private int mPage; private float mScale; private int mContentHeight; AsyncTask mLoadHTML; public MuPDFReflowView(Context c, MuPDFCore core, Point parentSize) { super(c); mHandler = new Handler(); mCore = core; mParentSize = parentSize; mScale = 1.0f; mContentHeight = parentSize.y; getSettings().setJavaScriptEnabled(true); addJavascriptInterface(new Object(){ public void reportContentHeight(String value) { mContentHeight = (int)Float.parseFloat(value); mHandler.post(new Runnable() { public void run() { requestLayout(); } }); } }, "HTMLOUT"); setWebViewClient(new WebViewClient() { @Override public void onPageFinished(WebView view, String url) { setScale(mScale); } }); } private void requestHeight() { // Get the webview to report the content height via the interface setup // above. Workaround for getContentHeight not working loadUrl("javascript:elem=document.getElementById('content');window.HTMLOUT.reportContentHeight("+mParentSize.x+"*elem.offsetHeight/elem.offsetWidth)"); } public void setPage(int page, PointF size) { mPage = page; if (mLoadHTML != null) { mLoadHTML.cancel(true); } mLoadHTML = new AsyncTask() { @Override protected byte[] doInBackground(Void... params) { return mCore.html(mPage); } @Override protected void onPostExecute(byte[] result) { String b64 = Base64.encodeToString(result, Base64.DEFAULT); loadData(b64, "text/html; charset=utf-8", "base64"); } }; mLoadHTML.execute(); } public int getPage() { return mPage; } public void setScale(float scale) { mScale = scale; loadUrl("javascript:document.getElementById('content').style.zoom=\""+(int)(mScale*100)+"%\""); requestHeight(); } public void blank(int page) { } public Hit passClickEvent(float x, float y) { return Hit.Nothing; } public LinkInfo hitLink(float x, float y) { return null; } public void selectText(float x0, float y0, float x1, float y1) { } public void deselectText() { } public boolean copySelection() { return false; } public boolean markupSelection(Annotation.Type type) { return false; } public void startDraw(float x, float y) { } public void continueDraw(float x, float y) { } public void cancelDraw() { } public boolean saveDraw() { return false; } public void setSearchBoxes(RectF[] searchBoxes) { } public void setLinkHighlighting(boolean f) { } public void deleteSelectedAnnotation() { } public void deselectAnnotation() { } public void setChangeReporter(Runnable reporter) { } public void update() { } public void updateHq(boolean update) { } public void removeHq() { } public void releaseResources() { if (mLoadHTML != null) { mLoadHTML.cancel(true); mLoadHTML = null; } } public void releaseBitmaps() { } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int x, y; switch(MeasureSpec.getMode(widthMeasureSpec)) { case MeasureSpec.UNSPECIFIED: x = mParentSize.x; break; default: x = MeasureSpec.getSize(widthMeasureSpec); } switch(MeasureSpec.getMode(heightMeasureSpec)) { case MeasureSpec.UNSPECIFIED: y = mContentHeight; break; default: y = MeasureSpec.getSize(heightMeasureSpec); } setMeasuredDimension(x, y); } @Override public boolean onTouchEvent(MotionEvent ev) { // TODO Auto-generated method stub return false; } } ================================================ FILE: src/main/java/com/artifex/mupdfdemo/MuPDFView.java ================================================ package com.artifex.mupdfdemo; import android.graphics.PointF; import android.graphics.RectF; enum Hit {Nothing, Widget, Annotation}; public interface MuPDFView { public void setPage(int page, PointF size); public void setScale(float scale); public int getPage(); public void blank(int page); public Hit passClickEvent(float x, float y); public LinkInfo hitLink(float x, float y); public void selectText(float x0, float y0, float x1, float y1); public void deselectText(); public boolean copySelection(); public boolean markupSelection(Annotation.Type type); public void deleteSelectedAnnotation(); public void setSearchBoxes(RectF searchBoxes[]); public void setLinkHighlighting(boolean f); public void deselectAnnotation(); public void startDraw(float x, float y); public void continueDraw(float x, float y); public void cancelDraw(); public boolean saveDraw(); public void setChangeReporter(Runnable reporter); public void update(); public void updateHq(boolean update); public void removeHq(); public void releaseResources(); public void releaseBitmaps(); } ================================================ FILE: src/main/java/com/artifex/mupdfdemo/OutlineActivity.java ================================================ package com.artifex.mupdfdemo; import android.app.ListActivity; import android.os.Bundle; import android.view.View; import android.widget.ListView; public class OutlineActivity extends ListActivity { OutlineItem mItems[]; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mItems = OutlineActivityData.get().items; setListAdapter(new OutlineAdapter(getLayoutInflater(),mItems)); // Restore the position within the list from last viewing getListView().setSelection(OutlineActivityData.get().position); getListView().setDividerHeight(0); setResult(-1); } @Override protected void onListItemClick(ListView l, View v, int position, long id) { super.onListItemClick(l, v, position, id); OutlineActivityData.get().position = getListView().getFirstVisiblePosition(); setResult(mItems[position].page); finish(); } } ================================================ FILE: src/main/java/com/artifex/mupdfdemo/OutlineActivityData.java ================================================ package com.artifex.mupdfdemo; public class OutlineActivityData { public OutlineItem items[]; public int position; static private OutlineActivityData singleton; static public void set(OutlineActivityData d) { singleton = d; } static public OutlineActivityData get() { if (singleton == null) singleton = new OutlineActivityData(); return singleton; } } ================================================ FILE: src/main/java/com/artifex/mupdfdemo/OutlineAdapter.java ================================================ package com.artifex.mupdfdemo; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.TextView; public class OutlineAdapter extends BaseAdapter { private final OutlineItem mItems[]; private final LayoutInflater mInflater; public OutlineAdapter(LayoutInflater inflater, OutlineItem items[]) { mInflater = inflater; mItems = items; } public int getCount() { return mItems.length; } public Object getItem(int arg0) { return null; } public long getItemId(int arg0) { return 0; } public View getView(int position, View convertView, ViewGroup parent) { View v; if (convertView == null) { v = mInflater.inflate(R.layout.outline_entry, null); } else { v = convertView; } int level = mItems[position].level; if (level > 8) level = 8; String space = ""; for (int i=0; i lines = new ArrayList(); for (TextWord[] line : mText) if (line[0].bottom > mSelectBox.top && line[0].top < mSelectBox.bottom) lines.add(line); Iterator it = lines.iterator(); while (it.hasNext()) { TextWord[] line = it.next(); boolean firstLine = line[0].top < mSelectBox.top; boolean lastLine = line[0].bottom > mSelectBox.bottom; float start = Float.NEGATIVE_INFINITY; float end = Float.POSITIVE_INFINITY; if (firstLine && lastLine) { start = Math.min(mSelectBox.left, mSelectBox.right); end = Math.max(mSelectBox.left, mSelectBox.right); } else if (firstLine) { start = mSelectBox.left; } else if (lastLine) { end = mSelectBox.right; } tp.onStartLine(); for (TextWord word : line) if (word.right > start && word.left < end) tp.onWord(word); tp.onEndLine(); } } } public abstract class PageView extends ViewGroup { private static final int HIGHLIGHT_COLOR = 0x802572AC; private static final int LINK_COLOR = 0x80AC7225; private static final int BOX_COLOR = 0xFF4444FF; private static final int INK_COLOR = 0xFFFF0000; private static final float INK_THICKNESS = 10.0f; private static final int BACKGROUND_COLOR = 0xFFFFFFFF; private static final int PROGRESS_DIALOG_DELAY = 200; private static final String TAG = "PageView"; private static final int SIGN_HEIGHT = 50; private static final int SIGN_WIDTH = 100; protected final Context mContext; protected int mPageNumber; private Point mParentSize; // Size of the view containing the pdf viewer. It could be the same as the screen if this view is full screen. protected Point mSize; // Size of page at minimum zoom protected float mSourceScale; private ImageView mEntire; // Image rendered at minimum zoom private Bitmap mEntireBm; // Bitmap used to draw the entire page at minimum zoom. private Matrix mEntireMat; private AsyncTask mGetText; private AsyncTask mGetLinkInfo; private CancellableAsyncTask mDrawEntire; private Point mPatchViewSize; // View size on the basis of which the patch was created. After zoom. private Rect mPatchArea; // Area of the screen zoomed. private ImageView mPatch; // Image rendered at zoom resolution. private Bitmap mPatchBm; // Bitmap used to draw the zoomed image. private CancellableAsyncTask mDrawPatch; private RectF mSearchBoxes[]; protected LinkInfo mLinks[]; private RectF mSelectBox; private TextWord mText[][]; private RectF mItemSelectBox; protected ArrayList> mDrawing; private View mSearchView; private boolean mIsBlank; private boolean mHighlightLinks; private ProgressBar mBusyIndicator; private final Handler mHandler = new Handler(); private static boolean flagPositions = true; // Concurrency flag to avoid entering twice onDoubleTap method. private Bitmap signBitmap; // Bitmap for signature at higher resolution. // *BACKWARD COMPATIBILITY* private Point signBitmapSize; // Bitmap size, scaled to screen size and pdf. private static DigitalizedEventCallback eventCallback; // Callback for the app. The library fires an event when the user touched longPress or doubleTap, and the app can manage the behaviour. private Paint mBitmapPaint; private MuPDFPageAdapter mAdapter; private PointF pdfSize; private PdfBitmap picturePdfBitmap; // *BACKWARD COMPATIBILITY* private MuPDFCore core; public PageView(Context c, Point parentSize, MuPDFPageAdapter adapter) { super(c); mContext = c; flagPositions = true; mParentSize = parentSize; setBackgroundColor(BACKGROUND_COLOR); mEntireMat = new Matrix(); mAdapter = adapter; } protected abstract CancellableTaskDefinition getDrawPageTask(Bitmap bm, int sizeX, int sizeY, int patchX, int patchY, int patchWidth, int patchHeight); protected abstract CancellableTaskDefinition getUpdatePageTask(Bitmap bm, int sizeX, int sizeY, int patchX, int patchY, int patchWidth, int patchHeight); protected abstract LinkInfo[] getLinkInfo(); protected abstract TextWord[][] getText(); protected abstract void addMarkup(PointF[] quadPoints, Annotation.Type type); private void reinit() { // Cancel pending render task if (mDrawEntire != null) { mDrawEntire.cancelAndWait(); mDrawEntire = null; } if (mDrawPatch != null) { mDrawPatch.cancelAndWait(); mDrawPatch = null; } if (mGetLinkInfo != null) { mGetLinkInfo.cancel(true); mGetLinkInfo = null; } if (mGetText != null) { mGetText.cancel(true); mGetText = 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; mSelectBox = null; mText = null; mItemSelectBox = null; } public void releaseResources() { releaseBitmaps(); reinit(); if (mBusyIndicator != null) { removeView(mBusyIndicator); mBusyIndicator = null; } } public void releaseBitmaps() { if (mEntire != null) { mEntire.setImageBitmap(null); mEntire.invalidate(); } if (mPatch != null) { mPatch.setImageBitmap(null); mPatch.invalidate(); } Log.i(TAG, "Recycle mEntire on releaseBitmaps: " + mEntireBm); recycleBitmap(mEntireBm); mEntireBm = null; Log.i(TAG, "Recycle mPathBm on releaseBitmaps: " + mPatchBm); recycleBitmap(mPatchBm); mPatchBm = null; } public void blank(int page) { reinit(); mPageNumber = page; if (mBusyIndicator == null) { mBusyIndicator = new ProgressBar(mContext); mBusyIndicator.setIndeterminate(true); mBusyIndicator.setBackgroundResource(R.drawable.busy); addView(mBusyIndicator); } setBackgroundColor(BACKGROUND_COLOR); } public void setPage(int page, PointF size) { pdfSize = correctBugMuPdf(size); if (mEntireBm == null) { try { mEntireBm = Bitmap.createBitmap(mParentSize.x, mParentSize.y, Config.ARGB_8888); } catch (OutOfMemoryError e) { e.printStackTrace(); } } // Cancel pending render task if (mDrawEntire != null) { mDrawEntire.cancelAndWait(); mDrawEntire = null; } mIsBlank = false; // Highlights may be missing because mIsBlank was true on last draw if (mSearchView != null) mSearchView.invalidate(); mPageNumber = page; if (mEntire == null) { mEntire = new OpaqueImageView(mContext); mEntire.setScaleType(ImageView.ScaleType.MATRIX); addView(mEntire); } // 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; mEntire.setImageBitmap(null); mEntire.invalidate(); // Get the link info in the background mGetLinkInfo = new AsyncTask() { protected LinkInfo[] doInBackground(Void... v) { return getLinkInfo(); } protected void onPostExecute(LinkInfo[] v) { mLinks = v; if (mSearchView != null) mSearchView.invalidate(); } }; mGetLinkInfo.execute(); updateEntireCanvas(false); 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 (RectF rect : mSearchBoxes) canvas.drawRect(rect.left * scale, rect.top * scale, rect.right * scale, rect.bottom * scale, paint); } if (!mIsBlank && mLinks != null && mHighlightLinks) { paint.setColor(LINK_COLOR); for (LinkInfo link : mLinks) canvas.drawRect(link.rect.left * scale, link.rect.top * scale, link.rect.right * scale, link.rect.bottom * scale, paint); } if (mSelectBox != null && mText != null) { paint.setColor(HIGHLIGHT_COLOR); processSelectedText(new TextProcessor() { RectF rect; public void onStartLine() { rect = new RectF(); } public void onWord(TextWord word) { rect.union(word); } public void onEndLine() { if (!rect.isEmpty()) canvas.drawRect(rect.left * scale, rect.top * scale, rect.right * scale, rect.bottom * scale, paint); } }); } if (mItemSelectBox != null) { paint.setStyle(Paint.Style.STROKE); paint.setColor(BOX_COLOR); canvas.drawRect(mItemSelectBox.left * scale, mItemSelectBox.top * scale, mItemSelectBox.right * scale, mItemSelectBox.bottom * scale, paint); } if (mDrawing != null) { Path path = new Path(); PointF p; paint.setAntiAlias(true); paint.setDither(true); paint.setStrokeJoin(Paint.Join.ROUND); paint.setStrokeCap(Paint.Cap.ROUND); paint.setStyle(Paint.Style.FILL); paint.setStrokeWidth(INK_THICKNESS * scale); paint.setColor(INK_COLOR); Iterator> it = mDrawing.iterator(); while (it.hasNext()) { ArrayList arc = it.next(); if (arc.size() >= 2) { Iterator iit = arc.iterator(); p = iit.next(); float mX = p.x * scale; float mY = p.y * scale; path.moveTo(mX, mY); while (iit.hasNext()) { p = iit.next(); float x = p.x * scale; float y = p.y * scale; path.quadTo(mX, mY, (x + mX) / 2, (y + mY) / 2); mX = x; mY = y; } path.lineTo(mX, mY); } else { p = arc.get(0); canvas.drawCircle(p.x * scale, p.y * scale, INK_THICKNESS * scale / 2, paint); } } paint.setStyle(Paint.Style.STROKE); canvas.drawPath(path, paint); } } }; addView(mSearchView); } requestLayout(); } public void updateEntireCanvas(final boolean updateZoomed) { // Render the page in the background mDrawEntire = new CancellableAsyncTask(getDrawPageTask(mEntireBm, mSize.x, mSize.y, 0, 0, mSize.x, mSize.y)) { @Override public void cancelAndWait() { super.cancelAndWait(); flagHQ = false; } @Override public void onPreExecute() { setBackgroundColor(BACKGROUND_COLOR); mEntire.setImageBitmap(null); mEntire.invalidate(); if (mBusyIndicator == null) { mBusyIndicator = new ProgressBar(mContext); mBusyIndicator.setIndeterminate(true); mBusyIndicator.setBackgroundResource(R.drawable.busy); 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(Void result) { removeView(mBusyIndicator); mBusyIndicator = null; mEntire.setImageBitmap(mEntireBm); // Draws the signatures on EntireCanvas after changing pages (post loading). if (mEntireBm != null && !mEntireBm.isRecycled()) { Canvas entireCanvas = new Canvas(mEntireBm); drawBitmaps(entireCanvas, null, null); } if (updateZoomed && (mPatchBm != null) && !mPatchBm.isRecycled()) { Canvas zoomedCanvas = new Canvas(mPatchBm); drawBitmaps(zoomedCanvas, mPatchViewSize, mPatchArea); } flagHQ = false; mEntire.invalidate(); setBackgroundColor(Color.TRANSPARENT); } }; mDrawEntire.execute(); } public void setSearchBoxes(RectF searchBoxes[]) { mSearchBoxes = searchBoxes; if (mSearchView != null) mSearchView.invalidate(); } public void setLinkHighlighting(boolean f) { mHighlightLinks = f; if (mSearchView != null) mSearchView.invalidate(); } public void deselectText() { mSelectBox = null; mSearchView.invalidate(); } public void selectText(float x0, float y0, float x1, float y1) { float scale = mSourceScale * (float) getWidth() / (float) mSize.x; float docRelX0 = (x0 - getLeft()) / scale; float docRelY0 = (y0 - getTop()) / scale; float docRelX1 = (x1 - getLeft()) / scale; float docRelY1 = (y1 - getTop()) / scale; // Order on Y but maintain the point grouping if (docRelY0 <= docRelY1) mSelectBox = new RectF(docRelX0, docRelY0, docRelX1, docRelY1); else mSelectBox = new RectF(docRelX1, docRelY1, docRelX0, docRelY0); if (mSearchView != null) mSearchView.invalidate(); if (mGetText == null) { mGetText = new AsyncTask() { @Override protected TextWord[][] doInBackground(Void... params) { return getText(); } @Override protected void onPostExecute(TextWord[][] result) { mText = result; if (mSearchView != null) mSearchView.invalidate(); } }; mGetText.execute(); } } public void startDraw(float x, float y) { float scale = mSourceScale * (float) getWidth() / (float) mSize.x; float docRelX = (x - getLeft()) / scale; float docRelY = (y - getTop()) / scale; if (mDrawing == null) mDrawing = new ArrayList>(); ArrayList arc = new ArrayList(); arc.add(new PointF(docRelX, docRelY)); mDrawing.add(arc); if (mSearchView != null) mSearchView.invalidate(); } public void continueDraw(float x, float y) { float scale = mSourceScale * (float) getWidth() / (float) mSize.x; float docRelX = (x - getLeft()) / scale; float docRelY = (y - getTop()) / scale; if (mDrawing != null && mDrawing.size() > 0) { ArrayList arc = mDrawing.get(mDrawing.size() - 1); arc.add(new PointF(docRelX, docRelY)); if (mSearchView != null) mSearchView.invalidate(); } } public void cancelDraw() { mDrawing = null; if (mSearchView != null) mSearchView.invalidate(); } protected PointF[][] getDraw() { if (mDrawing == null) return null; PointF[][] path = new PointF[mDrawing.size()][]; for (int i = 0; i < mDrawing.size(); i++) { ArrayList arc = mDrawing.get(i); path[i] = arc.toArray(new PointF[arc.size()]); } return path; } protected void processSelectedText(TextProcessor tp) { (new TextSelector(mText, mSelectBox)).select(tp); } public void setItemSelectBox(RectF rect) { mItemSelectBox = rect; if (mSearchView != null) mSearchView.invalidate(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int x, y; switch (MeasureSpec.getMode(widthMeasureSpec)) { case MeasureSpec.UNSPECIFIED: x = mSize.x; break; default: x = MeasureSpec.getSize(widthMeasureSpec); } switch (MeasureSpec.getMode(heightMeasureSpec)) { case MeasureSpec.UNSPECIFIED: y = mSize.y; break; default: y = MeasureSpec.getSize(heightMeasureSpec); } setMeasuredDimension(x, y); if (mBusyIndicator != null) { int limit = Math.min(mParentSize.x, mParentSize.y) / 2; mBusyIndicator.measure(MeasureSpec.AT_MOST | limit, 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); } } private boolean flagHQ = false; public void updateHq(boolean update) { if(!flagHQ) { flagHQ = true; 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(); } flagHQ = false; } 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)) { flagHQ = false; 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); boolean completeRedraw = !area_unchanged || update; // Stop the drawing of previous patch if still going if (mDrawPatch != null) { mDrawPatch.cancelAndWait(); 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; final Bitmap oldPatchBm = mPatchBm; try { int mPatchAreaHeight = patchArea.bottom - patchArea.top; int mPatchAreaWidth = patchArea.right - patchArea.left; mPatchBm = Bitmap.createBitmap(mPatchAreaWidth, mPatchAreaHeight, Bitmap.Config.ARGB_8888); Log.i(TAG, "Recycle oldPatchBm on updateHQ: " + oldPatchBm); cancelDraw(); } catch (OutOfMemoryError e) { Log.e(TAG, e.getMessage(), e); flagHQ = false; } 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) { @Override public void cancelAndWait() { super.cancelAndWait(); flagHQ = false; } public void onPostExecute(Void result) { mPatchViewSize = patchViewSize; mPatchArea = patchArea; if (mPatchBm != null && !mPatchBm.isRecycled()) { Canvas zoomedCanvas = new Canvas(mPatchBm); drawBitmaps(zoomedCanvas, mPatchViewSize, mPatchArea); 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); if (mPatchBm != null && !mPatchBm.equals(oldPatchBm)) { recycleBitmap(oldPatchBm); } flagHQ = false; } }; mDrawPatch.execute(); } } } public void update() { // Cancel pending render task if (mDrawEntire != null) { mDrawEntire.cancelAndWait(); mDrawEntire = null; } if (mDrawPatch != null) { mDrawPatch.cancelAndWait(); mDrawPatch = null; } mDrawEntire = new CancellableAsyncTask(getUpdatePageTask(mEntireBm, mSize.x, mSize.y, 0, 0, mSize.x, mSize.y)) { @Override public void cancelAndWait() { super.cancelAndWait(); flagHQ = false; } public void onPostExecute(Void result) { if (mEntireBm != null && !mEntireBm.isRecycled()) { Canvas entireCanvas = new Canvas(mEntireBm); drawBitmaps(entireCanvas, null, null); mEntire.setImageBitmap(mEntireBm); mEntire.invalidate(); flagHQ=false; } } }; mDrawEntire.execute(); updateHq(true); } public void removeHq() { // Stop the drawing of the patch if still going if (mDrawPatch != null) { mDrawPatch.cancelAndWait(); mDrawPatch = null; } // And get rid of it mPatchViewSize = null; mPatchArea = null; if (mPatch != null) { mPatch.setImageBitmap(null); mPatch.invalidate(); } flagHQ = false; } public int getPage() { return mPageNumber; } @Override public boolean isOpaque() { return true; } protected void redrawEntireBitmaps() { if (mEntireBm != null && !mEntireBm.isRecycled()) { Canvas entireCanvas = new Canvas(mEntireBm); drawBitmaps(entireCanvas, null, null); mEntire.setImageBitmap(mEntireBm); mEntire.invalidate(); } } private void redrawZoomedBitmaps() { if (mPatchBm != null && !mPatchBm.isRecycled()) { Canvas zoomedCanvas = new Canvas(mPatchBm); drawBitmaps(zoomedCanvas, mPatchViewSize, mPatchArea); mPatch.setImageBitmap(mPatchBm); mPatch.invalidate(); } } public boolean removeBitmapOnPosition(Point point) { boolean removed = false; switch (removeIfExistSign(point)) { case -1: removed = false; break; case 0: removed = true; break; case 1: removed = true; break; } //Forzamos pintado de pantalla invalidate(); return removed; } public void onLongPress(MotionEvent e, float mScale) { if (eventCallback != null) { float x = e.getX(); float y = e.getY(); //Comprobamos si ha picado dentro o fuera del espacio del pdf if (x < getLeft() || x > getRight()) { eventCallback.error(DigitalizedEventCallback.ERROR_OUTSIDE_HORIZONTAL); } if (y < getTop() || y > getBottom()) { eventCallback.error(DigitalizedEventCallback.ERROR_OUTSIDE_VERTICAL); } float[] coords = translateCoords(mScale, x, y); if (coords != null) { eventCallback.longPressOnPdfPosition(mPageNumber, coords[0], coords[1], coords[2], coords[3]); } } } public void onSingleTap(MotionEvent e, float mScale) { if (eventCallback != null) { float x = e.getX(); float y = e.getY(); //Comprobamos si ha picado dentro o fuera del espacio del pdf if (x < getLeft() || x > getRight()) { eventCallback.error(DigitalizedEventCallback.ERROR_OUTSIDE_HORIZONTAL); } if (y < getTop() || y > getBottom()) { eventCallback.error(DigitalizedEventCallback.ERROR_OUTSIDE_VERTICAL); } float[] coords = translateCoords(mScale, x, y); if (coords != null) { eventCallback.singleTapOnPdfPosition(mPageNumber, coords[0], coords[1], coords[2], coords[3]); } } } private float[] translateCoords(float mScale, float x, float y) { float screenX, screenY, percentX, percentY; if (pdfSize != null && mSize != null) { //Factor de corrección por si se gira float factorRotationX = ((float) mSize.x / (float) getWidth()) * mScale; float factorRotationY = ((float) mSize.y / (float) getHeight()) * mScale; //Posicion en la pantalla respecto a las coordenadas del pdf (el 0.0 es la esquina arriba izquierda del pdf). Usado para poder dibujar las firmas encima del PDF. En esta representación, el PDF tendría de alto valores similares al alto de la pantalla en la que se muestra. screenX = ((x - getLeft()) / mScale) * factorRotationX; screenY = ((y - getTop()) / mScale) * factorRotationY; // Calculamos posicion en el pdf. No se usa en la visualización, pero es necesario para conocer la posición. En esta representación, el alto del pdf será de unos 900 píxeles, y no variará se muestre donde se muestre. percentX = (x - getLeft()) / getWidth(); percentY = (y - getTop()) / getHeight(); //Se coge la posicion en porcentaje float pdfX = percentX * pdfSize.x; //Se calcula X el punto en el pdf float pdfY = (1 - percentY) * pdfSize.y;//Se calcula Y // Proportions: screenX / mSize.x == pdfX / pdfSize.x !!! return new float[]{screenX, screenY, pdfX, pdfY}; } else { return null; } } private float[] pdfCoordsToScreen(float pdfX, float pdfY) { float screenX = (pdfX * mSize.x) / pdfSize.x; float screenY = ((pdfSize.y - pdfY) * mSize.y) / pdfSize.y; return new float[]{screenX, screenY}; } public boolean onDoubleTap(MotionEvent e, float mScale) { if (flagPositions) { flagPositions = false; float x = e.getX(); float y = e.getY(); //Comprobamos si ha picado dentro o fuera del espacio del pdf if (x < getLeft() || x > getRight()) { flagPositions = true; if (eventCallback != null) { eventCallback.error(DigitalizedEventCallback.ERROR_OUTSIDE_HORIZONTAL); } return true; } if (y < getTop() || y > getBottom()) { flagPositions = true; if (eventCallback != null) { eventCallback.error(DigitalizedEventCallback.ERROR_OUTSIDE_VERTICAL); } return true; } float[] coords = translateCoords(mScale, x, y); if (coords != null) { float screenX = coords[0]; float screenY = coords[1]; float pdfX = coords[2]; float pdfY = coords[3]; if (eventCallback != null) { flagPositions = true; eventCallback.doubleTapOnPdfPosition(mPageNumber, screenX, screenY, pdfX, pdfY); return true; } //Salvamos la posicion donde se ha elegido estampar la firma Point point = new Point((int) screenX, (int) screenY); boolean removed = removeBitmapOnPosition(point); if (signBitmap != null && signBitmapSize != null && !removed) { PdfBitmap newPdfBitmap = new PdfBitmap(signBitmap, SIGN_WIDTH, SIGN_HEIGHT, (int) screenX, (int) screenY, mPageNumber, PdfBitmap.Type.SIGNATURE); mAdapter.getPdfBitmapList().add(newPdfBitmap); mAdapter.setNumSignature(mAdapter.getNumSignature() + 1); } } flagPositions = true; } return true; } /** * Check if a Bitmap exists in the point coordinates, and remove it. * * @param screenPoint Point for the pdf to check * @return */ private int removeIfExistSign(Point screenPoint) { PdfBitmap toRemove = null; for (PdfBitmap pdfBitmap : mAdapter.getPdfBitmapList()) { if (pdfBitmap.getPageNumber() == mPageNumber) { float[] scaledSize = scaledSize(pdfBitmap.getWidth(), pdfBitmap.getHeight()); int originalW = (int) scaledSize[0]; int originalH = (int) scaledSize[1]; float[] screenCoords = pdfCoordsToScreen(pdfBitmap.getPdfX(), pdfBitmap.getPdfY()); int screenX = (int) screenCoords[0]; int screenY = (int) screenCoords[1]; Rect r = new Rect(screenX - (originalW / 2), screenY + (originalH / 2), screenX + (originalW / 2), screenY - (originalH / 2)); if (screenPoint.x > r.left && screenPoint.x < r.right && screenPoint.y < r.top && screenPoint.y > r.bottom) { toRemove = pdfBitmap; boolean indexOf = mAdapter.getPdfBitmapList().contains(toRemove); if (indexOf && toRemove.isRemovable()) { mAdapter.getPdfBitmapList().remove(toRemove); mAdapter.setNumSignature(mAdapter.getNumSignature() - 1); // We need to remove the previous entireBm (with the bitmaps added), and create a new one empty (the bitmaps will be added on update) Log.i(TAG, "Recycle mEntire on removeIfExistSign: " + mEntireBm); final Bitmap oldEntireBm = mEntireBm; try { mEntireBm = Bitmap.createBitmap(mParentSize.x, mParentSize.y, Bitmap.Config.ARGB_8888); updateEntireCanvas(true); updateHq(true); } catch (OutOfMemoryError e) { Log.e(TAG, e.getMessage(), e); } if(oldEntireBm!=null && !oldEntireBm.equals(mEntireBm)) { recycleBitmap(oldEntireBm); } // Bitmap removed return 0; } } } } // No bitmap removed return -1; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); } /** * Por defecto la medida de pagina que devuelve MuPdf parece ser dos veces superior al correcto * * @param size * @return */ private PointF correctBugMuPdf(PointF size) { return new PointF(size.x / 2, size.y / 2); } public DigitalizedEventCallback getEventCallback() { return eventCallback; } public void setEventCallback(DigitalizedEventCallback eventCallback) { this.eventCallback = eventCallback; } private void drawBitmaps(Canvas canvas, Point patchViewSize, Rect patchArea) { // Sólo ejecutamos este código en caso de que tengamos un Bitmap de firma: for (PdfBitmap pdfBitmap : mAdapter.getPdfBitmapList()) { float[] scaledSize = scaledSize(pdfBitmap.getWidth(), pdfBitmap.getHeight()); float originalW = scaledSize[0]; float originalH = scaledSize[1]; float zoomRatio = patchViewSize != null ? (float) patchViewSize.y / (float) mSize.y : 1.0f; float newWidth = originalW * zoomRatio; float newHeight = originalH * zoomRatio; if (pdfBitmap.getPageNumber() == getPage()) { float[] screenCoords = pdfCoordsToScreen(pdfBitmap.getPdfX(), pdfBitmap.getPdfY()); float newGlobalPosX = (screenCoords[0] * zoomRatio); float newGlobalPosY = (screenCoords[1] * zoomRatio); float newZoomPosX = patchArea != null ? newGlobalPosX - patchArea.left : newGlobalPosX; float newZoomPosY = patchArea != null ? newGlobalPosY - patchArea.top : newGlobalPosY; float leftGlobalMargin = newGlobalPosX - newWidth / 2; float rightGlobalMargin = newGlobalPosX + newWidth / 2; float topGlobalMargin = newGlobalPosY - newHeight / 2; float bottomGlobalMargin = newGlobalPosY + newHeight / 2; Rect signZoomedRect = new Rect( (int) newZoomPosX - (int) newWidth / 2, (int) newZoomPosY - (int) newHeight / 2, (int) newZoomPosX + (int) newWidth / 2, (int) newZoomPosY + (int) newHeight / 2); boolean outside; if (patchArea == null) { outside = false; } else { outside = (rightGlobalMargin <= patchArea.left || leftGlobalMargin >= patchArea.right || topGlobalMargin >= patchArea.bottom || bottomGlobalMargin <= patchArea.top); } if (!outside) { Bitmap bitmap = pdfBitmap.getBitmapImage(); try { if (!isBitmapRecycled(bitmap)) { canvas.drawBitmap(bitmap, new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()), signZoomedRect, mBitmapPaint); canvas.save(); } else { Log.i(TAG, "Avoided using recycled bitmap"); } } catch (RuntimeException e) { Log.e(TAG, e.getLocalizedMessage(), e); } } } } } private float[] scaledSize(int width, int height) { float x = 0, y = 0; if (pdfSize != null && mSize != null) { x = (width * mSize.x) / pdfSize.x; y = (height * mSize.y) / pdfSize.y; } return new float[]{x, y}; } public void setParentSize(Point parentSize) { this.mParentSize = parentSize; } public boolean isBitmapRecycled(Bitmap bitmap) { if (android.os.Build.VERSION.SDK_INT < 17) { return bitmap.isRecycled(); } else { return bitmap.isRecycled() || (!bitmap.isPremultiplied() && bitmap.getConfig() == Bitmap.Config.ARGB_8888 && bitmap.hasAlpha()); } } public void recycleBitmap(Bitmap bitmap) { if (bitmap != null) { Log.d(TAG, "Recycling bitmap " + bitmap.toString()); bitmap.recycle(); if(!bitmap.isRecycled()){ Log.e(TAG, "NOT Recycled bitmap " + bitmap.toString()); } } } } ================================================ FILE: src/main/java/com/artifex/mupdfdemo/PrintDialogActivity.java ================================================ package com.artifex.mupdfdemo; import java.io.ByteArrayOutputStream; import java.io.InputStream; import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.ContentResolver; import android.content.Intent; import android.os.Bundle; import android.util.Base64; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; public class PrintDialogActivity extends Activity { private static final String PRINT_DIALOG_URL = "https://www.google.com/cloudprint/dialog.html"; private static final String JS_INTERFACE = "AndroidPrintDialog"; private static final String CONTENT_TRANSFER_ENCODING = "base64"; private static final String ZXING_URL = "http://zxing.appspot.com"; private static final int ZXING_SCAN_REQUEST = 65743; /** * Post message that is sent by Print Dialog web page when the printing dialog * needs to be closed. */ private static final String CLOSE_POST_MESSAGE_NAME = "cp-dialog-on-close"; /** * Web view element to show the printing dialog in. */ private WebView dialogWebView; /** * Intent that started the action. */ Intent cloudPrintIntent; private int resultCode; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); resultCode = RESULT_OK; setContentView(R.layout.print_dialog); dialogWebView = (WebView) findViewById(R.id.webview); cloudPrintIntent = this.getIntent(); WebSettings settings = dialogWebView.getSettings(); settings.setJavaScriptEnabled(true); dialogWebView.setWebViewClient(new PrintDialogWebClient()); dialogWebView.addJavascriptInterface( new PrintDialogJavaScriptInterface(), JS_INTERFACE); dialogWebView.loadUrl(PRINT_DIALOG_URL); } @Override public void onActivityResult(int requestCode, int resultCode, Intent intent) { if (requestCode == ZXING_SCAN_REQUEST && resultCode == RESULT_OK) { dialogWebView.loadUrl(intent.getStringExtra("SCAN_RESULT")); } } final class PrintDialogJavaScriptInterface { public String getType() { return cloudPrintIntent.getType(); } public String getTitle() { return cloudPrintIntent.getExtras().getString("title"); } public String getContent() { try { ContentResolver contentResolver = getContentResolver(); InputStream is = contentResolver.openInputStream(cloudPrintIntent.getData()); ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] buffer = new byte[4096]; int n = is.read(buffer); while (n >= 0) { baos.write(buffer, 0, n); n = is.read(buffer); } is.close(); baos.flush(); return Base64.encodeToString(baos.toByteArray(), Base64.DEFAULT); } catch (Throwable e) { resultCode = RESULT_CANCELED; setResult(resultCode); finish(); e.printStackTrace(); } return ""; } public String getEncoding() { return CONTENT_TRANSFER_ENCODING; } public void onPostMessage(String message) { if (message.startsWith(CLOSE_POST_MESSAGE_NAME)) { setResult(resultCode); finish(); } } } private final class PrintDialogWebClient extends WebViewClient { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { if (url.startsWith(ZXING_URL)) { Intent intentScan = new Intent("com.google.zxing.client.android.SCAN"); intentScan.putExtra("SCAN_MODE", "QR_CODE_MODE"); try { startActivityForResult(intentScan, ZXING_SCAN_REQUEST); } catch (ActivityNotFoundException error) { view.loadUrl(url); } } else { view.loadUrl(url); } return false; } @Override public void onPageFinished(WebView view, String url) { if (PRINT_DIALOG_URL.equals(url)) { // Submit print document. view.loadUrl("javascript:printDialog.setPrintDocument(printDialog.createPrintDocument(" + "window." + JS_INTERFACE + ".getType(),window." + JS_INTERFACE + ".getTitle()," + "window." + JS_INTERFACE + ".getContent(),window." + JS_INTERFACE + ".getEncoding()))"); // Add post messages listener. view.loadUrl("javascript:window.addEventListener('message'," + "function(evt){window." + JS_INTERFACE + ".onPostMessage(evt.data)}, false)"); } } } } ================================================ FILE: src/main/java/com/artifex/mupdfdemo/ReaderView.java ================================================ package com.artifex.mupdfdemo; import android.content.Context; import android.graphics.Point; import android.graphics.Rect; import android.os.SystemClock; import androidx.core.view.MotionEventCompat; import android.util.AttributeSet; import android.util.SparseArray; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.View; import android.widget.Adapter; import android.widget.AdapterView; import android.widget.Scroller; import com.artifex.utils.DigitalizedEventCallback; import com.artifex.utils.PdfBitmap; import java.util.Collection; import java.util.HashSet; import java.util.LinkedList; import java.util.NoSuchElementException; public class ReaderView extends AdapterView implements GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener, ScaleGestureDetector.OnScaleGestureListener, Runnable { 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 = 3.0f; private static final float REFLOW_SCALE_FACTOR = 0.5f; private static final boolean HORIZONTAL_SCROLLING = true; private Adapter mAdapter; private 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 boolean mReflow = false; private boolean mReflowChanged = false; private final GestureDetector mGestureDetector; private final ScaleGestureDetector mScaleGestureDetector; private final Scroller mScroller; private final Stepper mStepper; private int mScrollerLastX; private int mScrollerLastY; private PageView currentPage; private DigitalizedEventCallback eventCallback; private float mLastTouchX; private float mLastTouchY; private Collection pdfBitmaps; static abstract class ViewMapper { abstract void applyToView(View view); } public ReaderView(Context context) { super(context); mGestureDetector = new GestureDetector(this); mScaleGestureDetector = new ScaleGestureDetector(context, this); mScroller = new Scroller(context); mStepper = new Stepper(this, this); } public ReaderView(Context context, AttributeSet attrs) { super(context, attrs); // "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()) { mGestureDetector = null; mScaleGestureDetector = null; mScroller = null; mStepper = null; } else { mGestureDetector = new GestureDetector(this); mScaleGestureDetector = new ScaleGestureDetector(context, this); mScroller = new Scroller(context); mStepper = new Stepper(this, this); } } public ReaderView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mGestureDetector = new GestureDetector(this); mScaleGestureDetector = new ScaleGestureDetector(context, this); mScroller = new Scroller(context); mStepper = new Stepper(this, this); } 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(boolean reflow) { mReflow = reflow; mReflowChanged = true; mResetLayout = true; mScale = 1.0f; mXScroll = mYScroll = 0; requestLayout(); } protected void onChildSetup(int i, View v) {} protected void onMoveToChild(int i) {} protected void onMoveOffChild(int i) {} protected void onSettle(View v) {}; protected void onUnsettle(View v) {}; protected void onNotInUse(View v) { ((PageView)v).releaseResources(); }; protected void onScaleChild(View v, Float scale) {}; 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 boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { if (!mScaling) { mXScroll -= distanceX; mYScroll -= distanceY; requestLayout(); } return true; } public void onShowPress(MotionEvent e) { } public boolean onSingleTapUp(MotionEvent e) { return false; } public boolean onScale(ScaleGestureDetector detector) { float previousScale = mScale; float scale_factor = mReflow ? REFLOW_SCALE_FACTOR : 1.0f; float min_scale = MIN_SCALE * scale_factor; float max_scale = MAX_SCALE * scale_factor; mScale = Math.min(Math.max(mScale * detector.getScaleFactor(), min_scale), max_scale); if (mReflow) { View v = mChildViews.get(mCurrent); if (v != null) onScaleChild(v, mScale); } else { float factor = mScale/previousScale; View v = mChildViews.get(mCurrent); if (v != null) { // Work out the focus point relative to the view top left int viewFocusX = (int)detector.getFocusX() - (v.getLeft() + mXScroll); int viewFocusY = (int)detector.getFocusY() - (v.getTop() + mYScroll); // Scroll to maintain the focus point mXScroll += viewFocusX - viewFocusX * factor; mYScroll += viewFocusY - viewFocusY * factor; requestLayout(); } } return true; } public boolean onScaleBegin(ScaleGestureDetector detector) { 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; return true; } public void onScaleEnd(ScaleGestureDetector detector) { if (mReflow) { applyToChildren(new ViewMapper() { @Override void applyToView(View view) { onScaleChild(view, mScale); } }); } mScaling = false; } @Override public boolean onTouchEvent(MotionEvent event) { boolean movementEnd = false; // We need this check to avoid refreshing the screen after a "tap" or "double tap". We only want to refresh the PDF after a pan, pinch or drag. int ident = MotionEventCompat.getActionIndex(event); if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { mLastTouchX = MotionEventCompat.getX(event, ident); mLastTouchY = MotionEventCompat.getY(event, ident); } if (event.getActionMasked() == MotionEvent.ACTION_UP) { float upX = MotionEventCompat.getX(event, ident); float upY = MotionEventCompat.getY(event, ident); int displacementX = (int) Math.abs(mLastTouchX - upX); int displacementY = (int) Math.abs(mLastTouchY - upY); movementEnd = (displacementX > 10) || (displacementY > 10); } processTouchEvent(event, movementEnd); return true; } private void processTouchEvent(MotionEvent event, boolean withRefresh) { 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 && withRefresh) { 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(); } @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); // "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(); // Don't reuse cached views if the adapter has changed if (mReflowChanged) { mReflowChanged = false; mViewCache.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); currentPage = (PageView) cv; currentPage.setEventCallback(eventCallback); currentPage.setParentSize(new Point(right-left, bottom-top)); // 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) { mAdapter = 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); onScaleChild(v, mScale); } 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 it's adapter index measureView(v); } private void measureView(View v) { // See what size the view wants to be v.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); if (!mReflow) { // 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()); float scale = (float)getWidth()/(float)v.getMeasuredWidth(); // Use the fitting values scaled by our current scale factor v.measure(MeasureSpec.EXACTLY | (int)(v.getMeasuredWidth()*scale*mScale), MeasureSpec.EXACTLY | (int)(v.getMeasuredHeight()*scale*mScale)); } else { v.measure(MeasureSpec.EXACTLY | (int)(v.getMeasuredWidth()), MeasureSpec.EXACTLY | (int)(v.getMeasuredHeight())); } } 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 // wont 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(); } } // Viafirma Code: public void addBitmap(PdfBitmap pdfBitmap) { if (mAdapter instanceof MuPDFPageAdapter) { // Add the bitmap to the adapter. ((MuPDFPageAdapter)mAdapter).addBitmap(pdfBitmap); // Update the view to see the added bitmap. updateCurrentPage(); } } public void setPdfBitmapList(Collection pdfBitmaps) { this.pdfBitmaps = pdfBitmaps; if (mAdapter instanceof MuPDFPageAdapter) { ((MuPDFPageAdapter) mAdapter).setPdfBitmapList(pdfBitmaps); } } public Collection getBitmapList() { if (mAdapter instanceof MuPDFPageAdapter) { return ((MuPDFPageAdapter)mAdapter).getPdfBitmapList(); } else { return new HashSet<>(); } } public boolean removeBitmapOnPosition(Point point) { if (currentPage != null) { return currentPage.removeBitmapOnPosition(point); } else { return false; } } public void refreshView(){ long downTime = SystemClock.uptimeMillis()+200; long eventTime = SystemClock.uptimeMillis() + 210; float x = 1.0f; float y = 1.0f; int metaState = 0; MotionEvent motionEvent = MotionEvent.obtain( downTime, eventTime, MotionEvent.ACTION_UP, x, y, metaState ); processTouchEvent(motionEvent, true); } public void updateCurrentPage() { if (currentPage != null) { //setDisplayedViewIndex(currentPage.getPage()); currentPage.redrawEntireBitmaps(); // No repinta el zoomed si ya estoy zoomed. currentPage.updateHq(true); } } public void redrawAll() { redrawPage(currentPage); if (mCurrent-1 >= 0) { PageView prevPage = (PageView) mChildViews.get(mCurrent - 1); redrawPage(prevPage); } if (mCurrent+1 < mChildViews.size()) { PageView posPage = (PageView) mChildViews.get(mCurrent + 1); redrawPage(posPage); } } private void redrawPage(PageView pageView) { if (pageView != null) { pageView.updateEntireCanvas(false); pageView.updateHq(true); } } @Override public boolean onDoubleTap(MotionEvent e) { if (currentPage != null) { return currentPage.onDoubleTap(e, mScale); } else { return false; } } @Override public void onLongPress(MotionEvent e) { if (currentPage != null) { currentPage.onLongPress(e, mScale); } } @Override public boolean onDoubleTapEvent(MotionEvent e) { return false; } @Override public boolean onSingleTapConfirmed(MotionEvent e) { if (currentPage != null) { currentPage.onSingleTap(e, mScale); } return false; } public DigitalizedEventCallback getEventCallback() { DigitalizedEventCallback result = null; if (currentPage != null) { result = currentPage.getEventCallback(); } return result; } public void setEventCallback(DigitalizedEventCallback eventCallback) { this.eventCallback = eventCallback; if (currentPage != null) { currentPage.setEventCallback(eventCallback); } } } ================================================ FILE: src/main/java/com/artifex/mupdfdemo/SafeAnimatorInflater.java ================================================ package com.artifex.mupdfdemo; import android.animation.Animator; import android.animation.AnimatorInflater; import android.animation.AnimatorSet; import android.app.Activity; import android.view.View; public class SafeAnimatorInflater { private View mView; public SafeAnimatorInflater(Activity activity, int animation, View view) { AnimatorSet set = (AnimatorSet) AnimatorInflater.loadAnimator(activity, R.animator.info); mView = view; set.setTarget(view); set.addListener(new Animator.AnimatorListener() { public void onAnimationStart(Animator animation) { mView.setVisibility(View.VISIBLE); } public void onAnimationRepeat(Animator animation) { } public void onAnimationEnd(Animator animation) { mView.setVisibility(View.INVISIBLE); } public void onAnimationCancel(Animator animation) { } }); set.start(); } } ================================================ FILE: src/main/java/com/artifex/mupdfdemo/SearchTask.java ================================================ package com.artifex.mupdfdemo; import android.app.AlertDialog; import android.app.ProgressDialog; import android.content.Context; import android.content.DialogInterface; import android.graphics.RectF; import android.os.Handler; 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 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); RectF 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(); } } ================================================ FILE: src/main/java/com/artifex/mupdfdemo/SearchTaskResult.java ================================================ package com.artifex.mupdfdemo; import android.graphics.RectF; public class SearchTaskResult { public final String txt; public final int pageNumber; public final RectF searchBoxes[]; static private SearchTaskResult singleton; SearchTaskResult(String _txt, int _pageNumber, RectF _searchBoxes[]) { txt = _txt; pageNumber = _pageNumber; searchBoxes = _searchBoxes; } static public SearchTaskResult get() { return singleton; } static public void set(SearchTaskResult r) { singleton = r; } } ================================================ FILE: src/main/java/com/artifex/mupdfdemo/Stepper.java ================================================ package com.artifex.mupdfdemo; import android.annotation.SuppressLint; import android.os.Build; import android.view.View; public class Stepper { 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(); } }); } } } } ================================================ FILE: src/main/java/com/artifex/mupdfdemo/TextChar.java ================================================ package com.artifex.mupdfdemo; import android.graphics.RectF; public class TextChar extends RectF { public char c; public TextChar(float x0, float y0, float x1, float y1, char _c) { super(x0, y0, x1, y1); c = _c; } } ================================================ FILE: src/main/java/com/artifex/mupdfdemo/TextWord.java ================================================ package com.artifex.mupdfdemo; import android.graphics.RectF; public class TextWord extends RectF { public String w; public TextWord() { super(); w = new String(); } public void Add(TextChar tc) { super.union(tc); w = w.concat(new String(new char[]{tc.c})); } } ================================================ FILE: src/main/java/com/artifex/mupdfdemo/WidgetType.java ================================================ package com.artifex.mupdfdemo; public enum WidgetType { NONE, TEXT, LISTBOX, COMBOBOX, SIGNATURE } ================================================ FILE: src/main/java/com/artifex/utils/DigitalizedEventCallback.java ================================================ package com.artifex.utils; /** * Created by @elage on 6/2/15. */ public interface DigitalizedEventCallback { public static final String ERROR_OUTSIDE_VERTICAL = "ERROR_OUTSIDE_VERTICAL"; public static final String ERROR_OUTSIDE_HORIZONTAL = "ERROR_OUTSIDE_HORIZONTAL"; public void longPressOnPdfPosition(int page, float viewX, float viewY, float pdfX, float pdfY); public void doubleTapOnPdfPosition(int page, float viewX, float viewY, float pdfX, float pdfY); public void singleTapOnPdfPosition(int page, float viewX, float viewY, float pdfX, float pdfY); public void pageChanged(int page); public void error(String message); } ================================================ FILE: src/main/java/com/artifex/utils/PdfBitmap.java ================================================ package com.artifex.utils; import android.graphics.Bitmap; import android.os.Parcel; import android.os.Parcelable; import android.util.Log; import java.io.Serializable; import java.util.HashMap; public class PdfBitmap implements Parcelable { public enum Type { SIGNATURE, // Signature used to sign the document SIGNATURE_USER_IMAGE, // User image of some older versions on Viafirma, where we sent the image to the server for it to process that along with the signature. IMAGE // All generic images shown }; private Bitmap image; private int height; private int width; private int pageNumber; private int pdfX; private int pdfY; private Type type; private boolean isRemovable; private HashMap metadata; /** * This class is used to store the information of each stamp and annotation on the PDF. * @param image The bitmap in charge of storing the stamp or annotation * @param height The height defined for the drawing * @param width The width defined for the drawing * @param pdfX The X coordinate position defined for the drawing * @param pdfY The Y coordinate position defined for the drawing * @param page The page of the PDF where the bitmap is added */ public PdfBitmap(Bitmap image, int width, int height, int pdfX, int pdfY, int page, Type type) { this.image = image; this.height = height; this.width = width; this.pdfX = pdfX; this.pdfY = pdfY; this.pageNumber = page;// first page is 0 this.type = type; this.isRemovable = true; this.metadata = new HashMap<>(); } public PdfBitmap(Parcel in) { // We just need to read back each // field in the order that it was image = in.readParcelable(Bitmap.class.getClassLoader()); height = in.readInt(); width = in.readInt(); pdfX = in.readInt(); pdfY = in.readInt(); pageNumber = in.readInt(); String typeString = in.readString(); if (typeString != null) { type = Type.valueOf(typeString); } isRemovable = in.readByte() != 0; in.readMap(metadata, HashMap.class.getClassLoader()); } public Bitmap getBitmapImage() { return image; } public int getWidth() { return width; } public int getHeight() { return height; } public int getPageNumber() { return pageNumber; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeParcelable(image, flags); dest.writeInt(height); dest.writeInt(width); dest.writeInt(pdfX); dest.writeInt(pdfY); dest.writeInt(pageNumber); dest.writeString(type.name()); dest.writeByte((byte)(isRemovable ? 1 : 0)); dest.writeMap(metadata); } public static final Creator CREATOR = new Creator() { public PdfBitmap createFromParcel(Parcel in) { return new PdfBitmap(in); } public PdfBitmap[] newArray(int size) { return new PdfBitmap[size]; } }; public int getPdfX() { return pdfX; } public int getPdfY() { return pdfY; } public Type getType() { return type; } public void setType(Type type) { this.type = type; } public boolean isRemovable() { return isRemovable; } public void setIsRemovable(boolean isRemovable) { this.isRemovable = isRemovable; } public HashMap getMetadata() { return metadata; } public void setMetadata(HashMap metadata) { this.metadata = metadata; } @Override public String toString() { String result = "page:"+pageNumber+", x:"+pdfX+", y:"+pdfY+", width:"+width+", height:"+height+", type:"+type.name(); return result; } @Override public boolean equals(Object o) { boolean result = false; try { if (o == this) { result = true; } else if (o instanceof PdfBitmap) { PdfBitmap that = (PdfBitmap) o; boolean sameBitmaps = that.getBitmapImage().sameAs(image); result = (that.getPdfX() == pdfX) && (that.getPdfY() == pdfY) && (that.getHeight() == height) && (that.getWidth() == width) && (that.getPageNumber() == pageNumber) && sameBitmaps; } } catch (Exception e) { Log.e("PdfBitmap", e.getLocalizedMessage(), e); } return result; } } ================================================ FILE: src/main/res/animator/info.xml ================================================ ================================================ FILE: src/main/res/drawable/busy.xml ================================================ ================================================ FILE: src/main/res/drawable/button.xml ================================================ ================================================ FILE: src/main/res/drawable/page_num.xml ================================================ ================================================ FILE: src/main/res/drawable/search.xml ================================================ ================================================ FILE: src/main/res/drawable/seek_progress.xml ================================================ ================================================ FILE: src/main/res/drawable/seek_thumb.xml ================================================ ================================================ FILE: src/main/res/drawable/tiled_background.xml ================================================ ================================================ FILE: src/main/res/layout/buttons.xml ================================================ ================================================ FILE: src/main/res/layout/main.xml ================================================ ================================================ FILE: src/main/res/layout/outline_entry.xml ================================================ ================================================ FILE: src/main/res/layout/picker_entry.xml ================================================ ================================================ FILE: src/main/res/layout/print_dialog.xml ================================================ ================================================ FILE: src/main/res/layout/textentry.xml ================================================ ================================================ FILE: src/main/res/values/colors.xml ================================================ #404040 #C0000000 #C0202020 #C0202020 #00000000 #FF2572AC #FFFFFF #FFFFFF #000000 #2572AC #000000 #2572AC #FFFFFF ================================================ FILE: src/main/res/values/strings.xml ================================================ MuPDF 1.6 (git build) Storage media not present Sharing the storage media with a PC can make it inaccessible Cancel Search backwards Search forwards Search document %1$s %2$s: %3$s Table of Contents Enter password Text not found Searching… Highlight and enable links No further occurrences found Select Search Copy Strike-out Delete Highlight Underline Edit annotations Ink Save Print Dismiss [Up one level] Yes No Entering reflow mode Leaving reflow mode Print failed Select text Copied to clipboard No text selected Draw annotation Nothing to save Document has changes. Save them? Cannot open document Cannot open document: %1$s Cannot open file: %1$s Cannot open buffer Fill out text field Okay Choose value Not supported Copy text to the clipboard More Accept copy text Format currently not supported Toggle reflow mode Unable to open the document Double-tap where you want to place your signature Warning The document has not been signed. Please, double-tap where you want to place your signature Ok ================================================ FILE: src/main/res/values/styles.xml ================================================ ================================================ FILE: src/main/res/values-ar/strings.xml ================================================ قبول MuPDF إلغاء تعذر فتح المخزن المؤقت تعذر فتح المستند تعذر فتح المستند: %1$s تعذر فتح الملف: %1$s اختر قيمة تم النسخ إلى الحافظة نسخ نسخ النص نسخ النص إلى الحافظة حذف تجاهل يحتوي المستند على تغييرات. هل تريد حفظها؟ سحب تعليق توضيحي تعديل التعليقات التوضيحية أدخل كلمة المرور دخول إلى وضع إعادة التدفق تعبئة حقل النص التنسيق غير مدعوم حاليًا تظليل حبر خروج من وضع إعادة التدفق المزيد لا لم يتم العثور على متكررات أخرى مشاركة وسائط التخزين مع حاسوب شخصي قد يمنع الوصول إليها وسائط التخزين غير موجودة لم يتم تحديد نص غير مدعوم لا يوجد شيء لحفظه موافق جدول المحتويات [أعلى مستوى واحد] %1$s %2$s: %3$s طباعة فشلت الطباعة حفظ بحث بحث إلى الخلف بحث في المستند بحث إلى الأمام جاري البحث في&#8230; تحديد تحديد النص شطب لم يتم العثور على النص تظليل وتمكين الروابط تسطير نعم ================================================ FILE: src/main/res/values-ca/strings.xml ================================================ Acceptar MuPDF Cancel·lar No es pot obrir el buffer No es pot obrir el document No es pot obrir el document: %1$s No es pot obrir l\'arxiu: %1$s Tria el valor Copiat al portapapers Copiar copiar text Copiar text al portapapers Esborrar Descartar El document té canvis. Desar? Dibuixar anotació Editar anotacions Introduir contrasenya Entrant en modo de reflux Emplena el camp de text Format no suportat actualment Destacar Tinta Abandonant modo de reflux Més No No hi ha més coincidències Compartir el mitjà d\'emmagatzematge amb un PC pot fer que sigui inaccessible Mitjà d\'emmagatzematge no present No s\'ha seleccionat text No compatible No hi ha gens que guardar Acceptar Índex [Pujar un nivell] %1$s %2$s: %3$s Imprimir Fallada al imprimir Desar Buscar Buscar cap a enrere Buscar document Buscar cap a davant Buscant… Seleccionar Seleccionar text Ratllat Text no trobat Ressaltar i habilitar enllaços Subratllat ================================================ FILE: src/main/res/values-cs/strings.xml ================================================ Přijmout MuPDF Zrušit Nelze otevřít vyrovnávací paměť Nelze otevřít dokument Nelze otevřít dokument: %1$s Nelze otevřít soubor: %1$s Zvolte hodnotu Kopírováno do schránky Kopírovat kopírovat text Kopírovat text do schránky Smazat Odmítnout Dokument byl změněn. Uložit? Vložit anotaci Upravit anotace Zadat heslo Vstup do režimu přeformátování řádků Vyplnit textové pole Formát aktuálně nepodporován Zvýraznit Inkoust Odchod z režimu přeformátování řádků Více Ne Nenalezeny další výskyty Při sdílení s PC může být paměťové médium nedostupné Paměťové médim nenalezeno Nevybrán žádný text Nepodporováno Nic k uložení OK Obsah [Nahoru o jednu úroveň] %1$s %2$s: %3$s Tisk Tisk selhal Uložit Hledat Hledat zpět Prohledat dokument Hledat vpřed Hledání&#8230; Vybrat Vybrat text Přeškrtnout Text nenalezen Zvýraznit a aktivovat odkazy Podtrhnout Ano ================================================ FILE: src/main/res/values-da/strings.xml ================================================ Accepter MuPDF Annuller Buffer kan ikke åbnes Dokument kan ikke åbnes Kan ikke åbne dokumentet: %1$s Kan ikke åbne filen: %1$s Vælg værdi Kopieret til udklipsholder Kopier kopier tekst kopier tekst til udklipsholder Slet Afvis Dokumentet er ændret. Gem ændringer? Lav anmærkning Rediger anmærkninger Indtast adgangskode Går over til konverteringstilstand Udfyld tekstfelt Format ikke understøttet i øjeblikket Fremhæv Ink Forlader konverteringstilstand Mere Nej Der blev ikke fundet flere tilfælde Deles lagermediet med en PC, kan det gøre det utilgængeligt Lagermedie ikke fundet Ingen tekst valgt Ikke understøttet Intet at gemme Okay Indholdsfortegnelse [Et niveau op] %1$s %2$s: %3$s Udskriv Udskrivning mislykket Gem Søg Søg bagud Søg i dokument Søg fremad Søger&#8230; Vælg Vælg tekst Gennemstreget Tekst ikke fundet Fremhæv og aktiver links Understreg Ja ================================================ FILE: src/main/res/values-de/strings.xml ================================================ Akzeptieren MuPDF Abbrechen Zwischenspeicher kann nicht geöffnet werden Dokument kann nicht geöffnet werden Dokument kann nicht geöffnet werden: %1$s Datei kann nicht geöffnet werden: %1$s Wert auswählen In die Zwischenanlage kopiert Kopieren Text kopieren Text in Zwischenablage kopieren Entfernen Verwerfen Das Dokument wurde verändert. Sollen die Änderungen gespeichert werden? Kommentar einfügen Kommentar bearbeiten Passwort eingeben Anpassungsmodus wird gestartet Textfeld ausfüllen Format wird momentan nicht unterstützt Markieren Farbe Anpassungsmodus wird beendet Mehr Nein Keine weiteren Treffer Die Freigabe des Speichermediums für einen PC kann es unzugänglich machen Speichermedium nicht vorhanden Kein Text ausgewählt Nicht unterstützt Nichts zum Speichern OK Inhaltsverzeichnis [Eine Ebene nach oben] %1$s %2$s: %3$s Drucken Fehler beim Drucken Speichern Suchen Rückwärts suchen Dokument durchsuchen Vorwärts suchen Suche… Auswählen Text auswählen Durchstreichen Text konnte nicht gefunden werden Markiere und aktiviere Verknüpfungen Unterstreichen Ja ================================================ FILE: src/main/res/values-el/strings.xml ================================================ Αποδοχή MuPDF Ακύρωση Αδυναμία ανοίγματος buffer Αδυναμία ανοίγματος εγγράφου Αδυναμία ανοίγματος εγγράφου: %1$s Αδυναμία ανοίγματος αρχείου: %1$s Επιλογή τιμής Αντιγράφηκε στο πρόχειρο Αντιγραφή αντιγραφή κειμένου Αντιγραφή κειμένου στο πρόχειρο Διαγραφή Ματαίωση Το έγγραφο έχει αλλαγές. Να αποθηκευτούν; Σχεδίαση σχολίου Επεξεργασία σχολίων Πληκτρολογήστε κωδικό πρόσβασης Είσοδος σε λειτουργία δυναμικής προσαρμογής Συμπλήρωση πεδίου κειμένου Αυτή η μορφή δεν υποστηρίζεται τη δεδομένη στιγμή Επισήμανση Γραφή Έξοδος από λειτουργία δυναμικής προσαρμογής Περισσότερο Όχι Δεν βρέθηκαν άλλες εμφανίσεις Η κοινή χρήση του αποθηκευτικού μέσου με έναν υπολογιστή μπορεί να το καταστήσει μη προσβάσιμο Δεν υπάρχει αποθηκευτικό μέσο Δεν έχει επιλεγεί κείμενο Δεν υποστηρίζεται Δεν υπάρχει περιεχόμενο για αποθήκευση ΟΚ Πίνακας περιεχομένων [Ένα επίπεδο επάνω] %1$s %2$s: %3$s Εκτύπωση Η εκτύπωση απέτυχε Αποθήκευση Αναζήτηση Αναζήτηση προς τα πίσω Αναζήτηση εγγράφου Αναζήτηση προς τα μπροστά Αναζήτηση&#8230; Επιλογή Επιλογή κειμένου Διακριτή διαγραφή Δεν βρέθηκε το κείμενο Επισήμανση και ενεργοποίηση συνδέσεων Υπογράμμιση Ναι ================================================ FILE: src/main/res/values-es/strings.xml ================================================ Aceptar MuPDF Cancelar No se puede abrir el búfer No se puede abrir el documento No se puede abrir el documento:%1$s No se puede abrir el archivo: %1$s Elegir valor Copiado al portapapeles Copiar copiar texto Copiar texto al portapapeles Borrar Ignorar El documento tiene cambios. ¿Guardar? Dibujar anotación Editar anotaicones Introducir contraseña Entrando en el modo de redistribución Rellenar el campo de texto Formato actualmente no soportado Resaltar Tinta Saliendo del modo de redistribución Más No No se han encontrado más casos Compartir el medio de almacenamiento con un PC puede hacerlo inaccesible Medio de almacenimiento no presente Texto no seleccionado No aceptado Nada que guardar OK Tabla de contenidos [Subir un nivel] %1$s %2$s: %3$s Imprimir No se ha imprimido Guardar Buscar Buscar hacia atrás Buscar documento Buscar hacia adelante Buscando&#8230; Seleccionar Seleccionar texto Tachar Texto no encontrado Resaltar y activar Subrayar No ha sido posible abrir el documento Toque dos veces seguidas donde desee ubicar su firma Atención El documento no ha sido firmado.Por favor, toque dos veces seguidas donde desee ubicar su firma. ================================================ FILE: src/main/res/values-et/strings.xml ================================================ Nõustu MuPDF Tühista Ei saa avada puhvrit Ei saa avada dokumenti Ei saa avada dokumenti: %1$s Ei saa avada faili: %1$s Vali väärtus Kopeeritud lõikelauale Kopeeri kopeeri tekst Kopeeri tekst lõikelauale Kustuta Lõpeta Dokumendis on tehtud muudatusi. Kas salvestada need? Tee marginaal Redigeeri marginaale Sisesta salasõna Sisenen ümberpaigutamise režiimi Täida tekstiväli Vormingul puudub hetkel tugi Tõsta esile Tint Lahkun ümberpaigutamise režiimist Veel Ei Ei leitud rohkem juhtumeid Salvestuskandja jagamine arvutiga võib selle juurdepääsmatuks muuta Salvestuskandja puudub Teksti ei ole valitud Puudub tugi Ei ole midagi salvestada OK Sisukord [Taseme võrra üles] %1$s%2$s%3$s Prindi Printimine ebaõnnestus Salvesta Otsi Otsi tagasisuunas Otsi dokumendist Otsi edasisuunas Otsin&#8230; Vali Vali tekst Läbikriipsutus Teksti ei leitud Tõsta lingid esile ja luba need Jooni alla Jah ================================================ FILE: src/main/res/values-fi/strings.xml ================================================ Hyväksy MuPDF Peruuta Puskuria ei voi avata Tiedostoa ei voi avata Ei voi avata tiedostoa: %1$s Ei voi avata tiedostoa: %1$s Valitse arvo Kopioitu leikepöydälle Kopioi kopio teksti Kopioi teksti leikepöydälle Poista Hylkää Tiedostossa on muutoksia. Haluatko tallentaa muutokset? Piirrä huomautus Muokkaa huomautuksia Anna salasana Siirrytään takaisinmuuntotilaan Täytä tekstikenttä Muotoa ei tällä hetkellä tueta Korosta Muste Poistutaan takaisinmuuntotilasta Lisää Ei Muita esiintymiä ei löydy Tallennustietovälineen jakaminen tietokoneen kanssa voi estää sen käyttämisen Tallennustietoväline ei ole käytössä Ei valittua tekstiä Ei tuettu Ei mitään tallennettavaa OK Sisällys [Yksi taso ylöspäin] %1$s %2$s: %3$s Tulosta Tulostus ei onnistunut Tallenna Haku Hae taaksepäin Hae tiedostosta Hae eteenpäin Haetaan &#8230; Valitse Valitse teksti Yliviivaa Tekstiä ei löydy Korosta ja ota käyttöön linkit Alleviivaa Kyllä ================================================ FILE: src/main/res/values-fr/strings.xml ================================================ Accepter MuPDF Annuler Impossible d\'ouvrir le buffer Impossible d\'ouvrir le document Impossible d\'ouvrir le document : %1$s Impossible d\'ouvrir le fichier : %1$s Choisir la valeur Copié dans le presse-papier Copier copier le texte Copier le texte sur le presse-papier Supprimer Ignorer Des modifications ont été effectuées au document. Les sauvegarder ? Dessiner une note Éditer une note Introduire le mot de passe Entrer en mode refusion Remplir le champ du texte Format non compatible pour l\'instant Surligner Encre Quitter le mode refusion Plus Non Aucune occurrence trouvée Sauvegarder le support de stockage avec un PC peut le rendre inaccessible Support de stockage absent Aucun texte sélectionné Non compatible Rien à sauvegarder OK Table des matières [Niveau supérieur] %1$s%2$s : %3$s Imprimer L\'impression a échoué Sauvegarder Rechercher Rechercher en arrière Rechercher document Rechercher en avant Chercher&#8230 ; Sélectionner Sélectionner le texte Rayer Texte introuvable Surligner et autoriser les liens Souligner Oui ================================================ FILE: src/main/res/values-hi/strings.xml ================================================ स्वीकार करें MuPDF रद्द करें बफ़र खोल नहीं सके दस्तावेज़ खोल नहीं सके दस्तावेज़ नहीं खोल सके: %1$s फ़ाइल खोल नहीं सके: %1$s मान चुनें क्लिपबोर्ड में कॉप कर दिया गया कॉपी करें पाठ कॉपी करें पाठ को क्लिपबोर्ड में कॉपी करें हटाएँ खारिज करें दस्तावेज़ में परिवर्तन हैं। उन्हें सहेजें? एनोटेशन बनाएँ एनोटेशनों को संपादित करें पासवर्ड दर्ज करें रीफ़्लो मोड में प्रवेश कर रहे हैं पाठ फ़ील्ड को भरें इस समय इस फ़ॉर्मेट को समर्थन नहीं प्राप्त है हाइलाइट करें स्याही रीफ़्लो मोड को छोड़ रहे हैं और भी नहीं यह और कहीं नहीं मिला संग्रह माध्यम को पीसी के साथ साझा करने से उस तक पहुँचना मुश्किल हो सकता है संग्रह माध्यम मौजूद नहीं है कोई भी पाठ नहीं चुना गया है असमर्थित सहेजने के लिए कुछ नहीं है ठीक है विषय सूची [एक स्तर ऊपर] %1$s%2$s:%3$s मुद्रित करें मुद्रण विफल हुआ सहेजें खोजें पीछे की ओर खोजें दस्तावेज़ में खोजें आगे की ओर खोजें &#8230 को खोज रहे हैं; चुनें पाठ चुनें काटें पाठ नहीं मिला लिंकों को हाइलाइट और सक्षम करें रेखांकित करें हाँ ================================================ FILE: src/main/res/values-hu/strings.xml ================================================ Elfogadás MuPDF Mégse A puffert nem lehet megnyitni A dokumentumot nem lehet megnyitni A dokumentumot nem lehet megnyitni: %1$s A fájlt nem lehet megnyitni: %1$s Érték kiválasztása A vágólapra másolva Másolás szöveg másolása Szöveg másolása a vágólapra Törlés Bezárás A dokumentum módosítva lett. Menti a változtatásokat? Jegyzet rajzolása Jegyzetek szerkesztése Jelszó megadása Belépés az újrarendezési módba Szövegmező kitöltése A formátum jelenleg nem támogatott Kiemelés Kézírás Kilépés az újrarendezési módból Több Nem Nincsenek további találatok Az adathordozó a PC-vel való megosztás esetén elérhetetlenné válhat Nincs jelen adathordozó Nincs kijelölt szöveg Nem támogatott Nem kell semmit menteni OK Tartalomjegyzék [Egy szinttel feljebb] %1$s %2$s: %3$s Nyomtatás Nyomtatás sikertelen Mentés Keresés Keresés visszafelé Dokumentum keresése Keresés előrefelé Keresés:&#8230; Kijelölés Szöveg kijelölése Áthúzás Szöveg nem található Kiemelés és linkek engedélyezése Aláhúzás Igen ================================================ FILE: src/main/res/values-in/strings.xml ================================================ Terima MuPDF Batal Tidak bisa membuka penyangga Tidak bisa membuka dokumen Tidak bisa membuka dokumen: %1$s Tidak bisa membuka berkas: %1$s Pilih nilai Disalin ke papan klip Salin Salin teks Salin teks ke papan klip Hapus Hilangkan Dokumen telah berubah. Simpan perubahan? Gambar anotasi Sunting anotasi Masukkan kata sandi Masuk mode alir-ulang Isi bidang teks Format ini tidak didukung Sorotan Tinta Tinggalkan mode alir-ulang Selengkapnya Tidak Tidak ditemukan kejadian lain Berbagi media penyimpanan dengan PC dapat membuatnya tidak bisa diakses Media penyimpanan tidak ada Tidak ada teks yang dipilih Tidak didukung Tidak ada yang disimpan Oke Daftar Isi [Naik satu tingkat] %1$s %2$s: %3$s Cetak Pencetakan gagal Simpan Cari Cari mundur Cari dokumen Cari maju Mencari… Pilih Pilih teks Gagal Teks tidak ditemukan Sorot dan aktifkan tautan Garis bawah Ya ================================================ FILE: src/main/res/values-it/strings.xml ================================================ Accetta MuPDF Annulla Impossibile aprire buffering Impossibile aprire documento Impossibile aprire documento: %1$s Impossibile aprire file: %1$s Scegli valore Copiato negli appunti Copia copia testo Copia testo negli appunti Elimina Ignora Il documento contiene modifiche. Salvare? Disegna annotazione Modifica annotazione Inserisci password Inserimento modalità di adattamento dinamico del contenuto Riempi il campo di testo Formato attualmente non supportato Evidenzia Inchiostro Abbandono della modalità di adattamento dinamico del contenuto Altro No Nessun\'altra occorrenza trovata La condivisione del supporto di archiviazione con un PC può renderlo inaccessibile Supporto di archiviazione non presente Nessun testo selezionato Non supportato Niente da salvare Ok Sommario [Su di un livello] %1$s %2$s: %3$s Stampa Stampa non riuscita Salva Cerca Cerca indietro Cerca documento Cerca avanti Ricerca... Seleziona Seleziona testo Barrato Testo non trovato Evidenzia e abilita link Sottolinea ================================================ FILE: src/main/res/values-iw/strings.xml ================================================ קבל MuPDF בטל אין אפשרות לפתוח מאגר אין אפשרות לפתוח מסמך אין אפשרות לפתוח מסמך: %1$s אין אפשרות לפתוח קובץ: %1$s בחר ערך הועתק ללוח העתק העתק טקסט העתק טקסט ללוח מחק התעלם קיימים שינויים במסמך. לשמור אותם? רשום ביאור ערוך ביאורים הזן סיסמה כניסה למצב הזרמה מחדש מלא את שדה הטקסט תבנית לא נתמכת כעת הבלטה דיו יציאה ממצב הזרמה מחדש עוד לא לא עוד שיתוף מדיית האחסון עם מחשב עשויה להפוך אותה לבלתי נגישה מדיית אחסון לא קיימת לא נבחר טקסט לא נתמך אין מה לשמור בסדר תוכן העניינים [למעלה ברמה אחת] %1$s %2$s: %3$s הדפס ההדפסה נכשלה שמור חפש חפש אחורה חפש במסמך חפש קדימה מחפש&#8230; בחר ערך בחר טקסט הדגש לא נמצא טקסט הבלט ואפשר קישורים קו תחתון כן ================================================ FILE: src/main/res/values-ja/strings.xml ================================================ 承諾する MuPDF キャンセル バッファーを開けません ドキュメントを開けません 次のドキュメントを開けません:%1$s 次のファイルを開けません: %1$s バリューを選択してください クリップボードにコピーされました コピー テキストをコピー テキストをクリップボードにコピー 削除 却下 ドキュメントは変更されました。保存しますか? 注釈を挿入する 注釈を編集する パスワードを入力する リフローモードを開始する テキストフィールドに書き込む このフォーマットは現在サポートされていません ハイライト インク リフローモードを終了する もっと いいえ 他にオカレンスは見つかりませんでした 記憶媒体をPCとシェアするとアクセスできなくなる可能性があります 記憶媒体が見つかりません テキストが選択されていません サポートされていません 保存するものがありません 了解 目次 [一つ上位のレベル] %1$s %2$s: %3$s 印刷 印刷に失敗しました 保存 検索 逆方向検索 ドキュメントを検索する 順方向検索 検索中 選択 テキストを選択する 取り消し線を引く テキストが見つかりません ハイライトしてリンクを有効にする 下線を引く はい ================================================ FILE: src/main/res/values-ko/strings.xml ================================================ 수락 MuPDF 취소 버퍼 열 수 없음 문서 열 수 없음 문서 열 수 없음: %1$s 파일 열 수 없음: %1$s 값 선택 클립보드로 복사됨 복사 텍스트 복사 클립보드로 텍스트 복사 삭제 무시 문서에 변경사항이 있습니다. 저장? 주석달기 주석 편집 패스워드 입력 리플로우 모드 시작 텍스트 입력란에 기입하십시오. 현재 지원되지 않는 포맷 주요기능 잉크 리플로우 모드 해제 기타 아니오 발견된 추가 발생 없음 PC와 스토리지 미디어를 공유하면 액세스할 수 없습니다. 스토리지 미디어 없음 선택된 텍스트 없음 지원 안됨 저장 대상 없음 확인 목차 [레벨 한 단계 상승] %1$s %2$s: %3$s 인쇄 인쇄 실패 저장 검색 뒤로 검색 문서 검색 앞으로 검색 검색 중&#8230; 선택 텍스트 선택 삭제 발견된 텍스트 없음 하이라이트 및 링크 활성화 밑줄 ================================================ FILE: src/main/res/values-lt/strings.xml ================================================ Priimti „MuPDF“ Atšaukti Nepavyksta atverti buferinės atmintinės Nepavyksta atverti dokumento Nepavyksta atverti dokumento: %1$s Nepavyksta atverti failo: %1$s Pasirinkti vertę Nukopijuota į iškarpinę Kopijuoti kopijuoti tekstą Kopijuoti tekstą į iškarpinę Naikinti Atmesti Dokumente yra pakeitimų. Ar juos įrašyti? Braižyti anotaciją Redaguoti anotacijas Įvesti slaptažodį Pereinama į pertvarkymo režimą Užpildyti teksto lauką Formatas šiuo metu nedera Pažymėti Rašalas Išeinama iš pertvarkymo režimo Daugiau Ne Daugiau įrašų nerasta Pabendrinus laikmeną su kompiuteriu, ji gali tapti nebepasiekiama Laikmenos nėra Neparinktas tekstas Nedera Nėra ką įrašyti Gerai Turinys [Vienu lygiu aukštyn] %1$s %2$s: %3$s Spausdinti Išspausdinti nepavyko Įrašyti Ieškoti Ieškoti atgal Ieškoti dokumente Ieškoti pirmyn Ieškoma&#8230; Pasirinkti Pasirinkti tekstą Išbraukti Teksto nerasta Pažymėti ir įjungti nuorodas Pabraukti Taip ================================================ FILE: src/main/res/values-ms/strings.xml ================================================ Terima MuPDF Batal Tidak boleh membuka penimbal Tidak boleh membuka dokumen Tidak boleh membuka dokumen: %1$s Tidak boleh membuka fail: %1$s Pilih nilai Disalin ke papan klip Salin salin teks Salin teks ke papan klip Padam Singkir Dokumen mempunyai perubahan. Simpankannya? Lakarkan catatan Suntingkan catatan Masukkan kata laluan Memasuki mod penyusunan semula Mengisi medan teks Format buat masa ini tidak disokong Serlahkan Dakwat Meninggalkan mod penyusunan semula Lagi Tidak Tiada kejadian lanjut ditemui Berkongsi media storan dengan PC boleh menjadikannya tidak dapat dicapai Media storan tidak wujud Tiada teks dipilih Tidak disokong Tiada apa untuk disimpan Okey Jadual Kandungan [Naik satu tahap] %1$s %2$s: %3$s Cetak Gagal dicetak Simpan Carian Carian ke belakang Carian dokumen Carian ke depan Mencari&#8230; Pilih Pilih teks Mansuhkan Teks tidak ditemui Serlahkan dan dayakan pautan Gariskan Ya ================================================ FILE: src/main/res/values-nl/strings.xml ================================================ Accepteren MuPDF Annuleren Buffer kan niet geopend worden Document kan niet geopend worden Document kan niet geopend worden: %1$s Bestand kan niet geopend worden : %1$s Kies waarde Gekopieerd naar klembord Kopiëren tekst kopiëren Tekst kopiëren naar klembord Verwijderen Afwijzen Het document is gewijzigd. Opslaan? Opmerking tekenen Opmerkingen bewerken Voer wachtwoord in Conversiemodus wordt geopend Vul het tekstveld in Formaat wordt momenteel niet ondersteund Markeren Inkten Conversiemodus wordt beëindigd Meer Nee Geen andere resultaten gevonden Het opslagmedium kan ontoegankelijk worden als het met een pc wordt gedeeld Geen opslagmedium aanwezig Geen tekst geselecteerd Niet ondersteund Niets om op te slaan Oké Inhoudsopgave [Een niveau hoger] %1$s %2$s: %3$s Afdrukken Afdrukken mislukt Opslaan Zoeken Achterstevoren zoeken Document doorzoeken Vooruit zoeken Aan het zoeken … Selecteren Tekst selecteren Doorhalen Tekst niet gevonden Markeren en koppelingen inschakelen Onderstrepen Ja ================================================ FILE: src/main/res/values-no/strings.xml ================================================ Aksepter MuPDF Avbryt Kan ikke åpne buffer Kan ikke åpne dukumentet Kan ikke åpne dokumentet: %1$s Kan ikke åpne filen: %1$s Velg verdi Kopiert til utklippstavlen Kopier kopier tekst Kopier teksten til utklippstavlen Slett Avvis Det er endringer i dokumentet. Lagre dem? Lag merknad Rediger merknader Skriv inn passord Bytter til konverteringsmodus Fyll ut tekstfeltet Formatet er ikke støttet for øyeblikket Uthev Håndskrift Går ut av konverteringsmodus Mer Nei Ingen flere hendelser funnet Deling av lagringsmedia med en PC kan gjøre det utilgjengelig Lagringsmedia ikke til stede Ingen tekst er valgt Ikke støttet Ingenting å lagre Ok Innholdsfortegnelse [OPP ett nivå] %1$s%2$s%3$s Skriv ut Kunne ikke skrive ut Lagre Søk Søk bakover Søk i dokument Søk framover Søker&#8230; Velg Valgt tekst Gjennomstreking Teksten ble ikke funnet Uthev og aktiver koblinger Understrek Ja ================================================ FILE: src/main/res/values-pl/strings.xml ================================================ Zaakceptuj MuPDF Anuluj Nie można otworzyć bufora Nie można otworzyć dokumentu Nie można otworzyć dokumentu: %1$s Nie można otworzyć pliku: %1$s Wybierz wartość Skopiowano do schowka Kopiuj kopiuj tekst Kopiuj tekst do schowka Usuń Odrzuć W dokumencie dokonano zmian. Czy chcesz je zapisać? Sporządź notatkę Edytuj notatki Wprowadź hasło Włączanie trybu zawijania tekstu Wypełnij pole tekstowe Format obecnie nieobsługiwany Podświetl Atrament Wyłączanie trybu zawijania tekstu Więcej Nie Nie znaleziono więcej wystąpień Współdzielenie nośnika danych z komputerem PC może sprawić, że będzie niedostępny Nośnik danych niedostępny Nie wybrano tekstu Nieobsługiwany Nie ma nic do zapisania OK Spis treści [W górę o jeden poziom] %1$s %2$s: %3$s Drukuj Drukowanie nieudane Zapisz Szukaj Szukaj z tyłu Szukaj w dokumencie Szukaj z przodu Wyszukiwanie&#8230; Wybierz Wybierz tekst Przekreślenie Nie znaleziono tekstu Podświetl i aktywuj linki Podkreślenie Tak ================================================ FILE: src/main/res/values-pt/strings.xml ================================================ Aceitar MuPDF Cancelar Não é possível abrir a memória intermédia Não é possível abrir o documento Não é possível abrir o documento: %1$s Não é possível abrir o ficheiro: %1$s Escolha um valor Copiado para a área de transferência Copiar copiar o texto Copiar o texto para a área de transferência Eliminar Desistir Há alterações ao documento. Deseja guardá-las? Adicionar anotação Editar anotações Escrever a palavra-passe A entrar no modo de refluxo Preencher o campo de texto Esse formato não é atualmente suportado Destacar Tinta A sair do modo de refluxo Mais Não Não foram encontradas mais ocorrências Partilhar o dispositivo de armazenamento com um PC poderá torná-lo inacessível O dispositivo de armazenamento não está presente Não há texto selecionado Não suportado Não há nada para guardar Okay Índice [subir um nível] %1$s%2$s: %3$s Imprimir Falha na Impressão Guardar Pesquisar Pesquisar para trás Pesquisar no documento Pesquisar para a frente A pesquisar&#8230; Selecionar Selecionar o texto Rasurado Texto não encontrado Destacar e permitir links Sublinhado Sim ================================================ FILE: src/main/res/values-ru/strings.xml ================================================ Принять MuPDF Отмена Невозможно открыть буфер Невозможно открыть документ Невозможно открыть документ: %1$s Невозможно открыть файл: %1$s Выберите значение Скопировано в буфер Копировать копировать текст Копировать текст в буфер Удалить Пропустить Документ был изменен. Сохранить изменения? Создать аннтоацию Редактировать аннотации Введите пароль Переход в режим Reflow Заполните текстовое поле Формат не поддерживается Выделить Чернила Выход из режима Reflow Еще Нет Других ошибок не зафиксировано Подключение компьютеров к хранилищу данных может привести к потере доступа к хранилищу Хранилище данных отсутствует Текст не выбран Не поддерживается Не выбраны файлы для сохранения ОК Содержание [Вверх на один уровень] %1$s %2$s: %3$s Печать Печать не выполнена Сохранить Поиск Искать в предыдущей части документа Искать в документе Искать в остальной части документа Поиск&#8230; Выбор Выбрать текст Зачеркнуть Текст не найден Выделить и включить ссылки Подчеркнуть Да ================================================ FILE: src/main/res/values-sk/strings.xml ================================================ Prijať MuPDF Zrušiť Buffer sa nedá otvoriť Dokument sa nedá otvoriť Nedá sa otvoriť dokument: %1$s Nedá sa otvoriť súbor: %1$s Vyberte si hodnotu Skopírované do vyrovnávacej pamäti Kopírovať kopírovať text Kopírovať text do vyrovnávacej pamäti Zmazať Zrušiť Dokument bol zmený. Uložiť zmeny? Zostaviť anotáciu Upraviť anotácie Zadať heslo Vstupujem do režimu opätovného nalievania Vyplniť textové pole Tento formát momentálne nepodporujem Zvýrazniť Atrament Vystupujem z režimu opätovného nalievania Viac Nie Viac príkladov sa nenašlo Zdieľanie úložného média s PC môže znemožniť prístup Nie je tu úložné médium Žiadny text nie je vybraný Nepodporované Niet čo uložiť Dobre Obsah [O úroveň vyššie] %1$s %2$s: %3$s Tlačiť Tlačenie zlyhalo Uložiť Hľadať Hľadať spätne Hľadať v dokumente Hľadať dopredu Hľadám&#8230; Vybrať Vybrať text Preškrtnúť Text sa nenašiel Zvýrazniť a zapnúť linky Podčiarknúť Áno ================================================ FILE: src/main/res/values-sv/strings.xml ================================================ Acceptera MuPDF Avbryt Kan inte öppna buffer Kan inte öppna dokument Kan inte öppna dokument: %1$s Kan inte öppna fil: %1$s Välj värde Kopierat till klippbordet Kopiera kopiera text Kopiera text till klippbordet Ta bort Avfärda Dokumentet har ändrats. Spara ändringar? Rita annotation Ändra annotation Fyll i lösenord Aktiverar reflow-läge Fyll i textfält Formatat stöds inte för närvarande Markera Bläck Lämnar reflow-läge Mer Nej Inga flera förekomster hittades Att dela lagringsmediet med en PC kan göra den oåtkomlig Lagringsmedia finns inte Ingen text har valts Stöds ej Inget att spara OK Innehållsförteckning [Upp en nivå] %1$s %2$s: %3$s Skriv ut Utskrift misslyckades Spara Sök Sök bakåt Sök dokument Sök framåt Letar&#8230; Välj Välj text Stryk Text hittades ej Markera och aktivera länkar Understryk Ja ================================================ FILE: src/main/res/values-th/strings.xml ================================================ ยอมรับ MuPDF ยกเลิก ไม่สามารถเปิดบัฟเฟอร์ ไม่สามารถเปิดเอกสาร ไม่สามารถเปิดเอกสาร: %1$s ไม่สามารถเปิดไฟล์: %1$s เลือกค่า คัดลอกไปที่คลิปบอร์ดแล้ว คัดลอก คัดลอกข้อความ คัดลอกข้อความไปที่คลิปบอร์ด ลบ เลิกใช้ เอกสารมีการเปลี่ยนแปลง ต้องการบันทึกหรือไม่ เขียนคำอธิบายประกอบ แก้ไขคำอธิบายประกอบ ป้อนรหัสผ่าน เข้าสู่โหมดเรียงหน้ากระดาษใหม่ เติมในช่องข้อความ ไม่รองรับรูปแบบในขณะนี้ ไฮไลท์ หมึก ออกจากโหมดเรียงหน้ากระดาษใหม่ เพิ่มเติม ไม่ ไม่พบเหตุการณ์ที่เกิดขึ้นเพิ่มเติม การแบ่งปันสื่อจัดเก็บข้อมูลกับพีซีสามารถทำให้สื่อจัดเก็บข้อมูลไม่สามารถเข้าถึงได้ สื่อเก็บข้อมูลไม่ปรากฏ ไม่มีข้อความที่เลือก ไม่รองรับ ไม่มีอะไรให้บันทึก ตกลง สารบัญ [ขึ้นหนึ่งระดับ] %1$s %2$s: %3$s พิมพ์ พิมพ์ล้มเหลว บันทึก ค้นหา ค้นหาย้อนกลับ ค้นหาเอกสาร ค้นหาไปข้างหน้า กำลังค้นหา&#8230; เลือก เลือกข้อความ ขีดทับ ไม่พบข้อความ ไฮไลท์และเปิดใช้งานลิงก์ ขีดเส้นใต้ ใช่ ================================================ FILE: src/main/res/values-tl/strings.xml ================================================ Tanggapin MuPDF Kanselahin Hindi mabuksan ang buffer Hindi mabuksan ang dokumento Hindi mabuksan ang dokumentong: %1$s Hindi mabuksan ang file na: %1$s Pumili ng halaga Kinopya sa clipboard Kopyahin kopyahin ang teksto Kopyahin ang teksto sa clipboard Alisin Umalis May mga pagbabago sa dokumento. I-save ang mga ito? Gumuhit ng anotasyon I-edit ang mga anotasyon Ilagay ang password Pumapasok sa reflow mode Punan ang puwang para sa teksto Ang format ay kasalukuyang hindi gumagana dito I-highlight Lagdaan (Ink) Umaalis sa reflow mode Higit pa Hindi Walang nahanap na karagdagang paglitaw Ang pagbabahagi ng storage media sa isang PC ay gagawin itong hindi magagamit Walang storage media Walang piniling teksto Hindi gumagana dito Walang ise-save Okay Talaan ng Nilalaman [Umakyat ng isang antas] %1$s %2$s: %3$s I-print Hindi nai-print I-save Maghanap Maghanap pabalik Maghanap sa dokumento Maghanap nang pasulong Hinahanap ang&#8230; Piliin Piliin ang teksto Guhitan ang teksto (strike-out) Hindi nahanap ang teksto I-highlight at paganahin ang mga link Guhitan Oo ================================================ FILE: src/main/res/values-tr/strings.xml ================================================ Kabul et MuPDF İptal et Arabellek açılamıyor Belge açılamıyor Belge açılamıyor: %1$s Dosya açılamıyor: %1$s Değeri seç Panoya kopyalandı Kopyala metni kopyala Metni panoya kopyala Sil Bırak Belgede değişiklikler var. Kaydetmek istiyor musunuz? Ek açıklama çiz Ek açıklamalar düzenle Şifreyi gir Yeniden akma moduna giriyor Metin alanını doldurun Bu format şu an için desteklenmiyor Vurgula Mürekkep Yeniden akma modundan çıkılıyor Daha fazla Hayır Daha fazla öğe bulunamadı Depolama ortamının bilgisayar ile paylaşımı onu erişilmez yapabilir Depolama ortamı bulunmuyor Seçili metin bulunmuyor Desteklenmiyor Kaydedecek bir şey yok Tamam İçindekiler Tablosu [Bir seviye üste çık] %1$s %2$s: %3$s Yazdır Yazdırma başarısız oldu Kaydet Ara Geriye doğru ara Belge ara İleriye doğru ara Aranıyor&#8230; Seç Metin seç Üstünü çiz Metin bulunamadı Bağlantıları vurgula ve etkinleştir Altını çiz Evet ================================================ FILE: src/main/res/values-zh/strings.xml ================================================ 接受 MuPDF 取消 无法打开缓冲器 无法打开文档 无法打开文档: %1$s 无法打开文件:%1$s 选择值 已复制到剪贴板 复制 复制文本 将文本复制到剪贴板 删除 解除 文档已变更,保存变更吗? 作批注 编辑批注 输入密码 输入重排模式 填充文本字段 当前不支持此格式 高亮 墨迹 正在离开重排模式 更多 未发现更多实例。 存储介质在设备和 PC 上共同使用,会导致该存储介质在设备上无法被访问 没有存储介质 未选择文本 不被支持 没有要保存的内容 确定 目录 [向上一级] %1$s%2$s:%3$s 打印 未能打印 保存 搜索 向后搜索 搜索文档 向前搜索 正在搜索… 选择 选择文本 删除线 未发现文本 高亮并启用墨迹 下划线 ================================================ FILE: src/main/res/values-zh-rTW/strings.xml ================================================ 同意 MuPDF 取消 未能開啟緩衝 未能開啟文件 未能開啟文件: %1$s 未能開啟檔案%1$s 選擇數值 複製至剪貼簿 複製 複製文字 複製文字至剪貼簿 刪除 取消 你需要儲存已編輯的文件嗎? 繪畫註釋 編輯註釋 輸入密碼 根據螢幕大小顯示 填寫文字欄 暫時不支援此格式 標示重點 墨水 不根據螢幕大小顯示 更多 沒有 沒有相符項目 未能與電腦分享存放裝置 沒有存放裝置 沒有選擇文字 不支援 沒有資料儲存 完成 目錄 [升一級] %1$s%2$s%3$s 列印 列印失敗 儲存 搜尋 往後搜尋 搜尋文件 往前搜尋 搜尋中&#8230; 選擇 選擇文字 刪除線 未能找到文字 標示及允許連結 在下面劃線