commit 47ef315796a844eee2bf0ca10c73d749035f4723 Author: Garfield Tan Date: Mon Jun 19 13:55:49 2017 -0700 Initial commit. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f703dfb --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.externalNativeBuild +app/src/main/jniLibs/** +app/src/main/cpp/samba_includes/** diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml new file mode 100644 index 0000000..403bcb5 --- /dev/null +++ b/.idea/codeStyleSettings.xml @@ -0,0 +1,228 @@ + + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..96cc43e --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/copyright/GPLv3.xml b/.idea/copyright/GPLv3.xml new file mode 100644 index 0000000..5f1df5c --- /dev/null +++ b/.idea/copyright/GPLv3.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml new file mode 100644 index 0000000..fcaae30 --- /dev/null +++ b/.idea/copyright/profiles_settings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..0e23f8e --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/.idea/markdown-navigator.xml b/.idea/markdown-navigator.xml new file mode 100644 index 0000000..1f63c27 --- /dev/null +++ b/.idea/markdown-navigator.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/markdown-navigator/profiles_settings.xml b/.idea/markdown-navigator/profiles_settings.xml new file mode 100644 index 0000000..57927c5 --- /dev/null +++ b/.idea/markdown-navigator/profiles_settings.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..5d19981 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..71c1baa --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..6ec97c9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,24 @@ +# How to contribute + +We'd love to accept your patches and contributions to this project. There are +just a few small guidelines you need to follow. + +## Contributor License Agreement + +Contributions to this project must be accompanied by a Contributor License +Agreement. You (or your employer) retain the copyright to your contribution, +this simply gives us permission to use and redistribute your contributions as +part of the project. Head over to to see +your current agreements on file or to sign a new one. + +You generally only need to submit a CLA once, so if you've already submitted one +(even if it was for a different project), you probably don't need to do it +again. + +## Code reviews + +All submissions, including submissions by project members, require review. We +use GitHub pull requests for this purpose. Consult [GitHub Help] for more +information on using pull requests. + +[GitHub Help]: https://help.github.com/articles/about-pull-requests/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 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 General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is 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. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + 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. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + 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 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. Use with the GNU Affero General Public License. + + 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 Affero 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 special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU 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 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 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 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 General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + 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 GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..128c224 --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# Samba Documents Provider +## Overview +This is an Android app that extends the built in File Manager to support connecting to SMB +file shares. + +This app is built on top of Samba 4.5.1. + +## Setup +### Prerequisite +Android SDK and NDK r13b or above are required to build this app. Android Studio is highly +recommended. + +Check out Samba Documents Provider code into a folder called `SambaDocumentsProvider` to +avoid Android Studio module name change. + +### Build Steps +1. Download and unarchive Samba 4.5.1 [source code][samba-source]. +2. Change directory to the root of Samba source code. +3. Create a git repository. +4. Run `git apply /sambapatch.diff`. +5. Modify configure.sh to change $NDK to point to your NDK folder. +6. Uncomment corresponding flags in configure.sh to compile for different architecture. + Uncomment flags for ARMv7 in addition to 32-bit ARM to compile it for ARMv7. +7. Run `configure.sh` to configure Samba project. +8. Run `compile.sh` to compile libsmbclient.so. +9. Run `install.sh /app/src/main/jniLibs/`. +10. Change directory to SambaDocumentsProvider source code. +11. Run `mv app/src/main/jniLibs//includes app/src/main/cpp/samba_includes`. +12. Change directory to the root of Samba source code and run `make distclean`. +13. Repeat step 6-12 for all desired ABI's. +14. Make sure to change app's `build.gradle` to include only ABI's that Samba was built + for in previous steps. +15. Compile SambaDocumentsProvider. + +## Discussion +Please go to our [Google group][discussion] to discuss any issues. + + +[samba-source]: https://download.samba.org/pub/samba/stable/samba-4.5.1.tar.gz +[discussion]: https://groups.google.com/forum/#!forum/samba-documents-provider diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt new file mode 100644 index 0000000..d126899 --- /dev/null +++ b/app/CMakeLists.txt @@ -0,0 +1,51 @@ +# Sets the minimum version of CMake required to build the native +# library. You should either keep the default value or only pass a +# value of 3.4.0 or lower. + +cmake_minimum_required(VERSION 3.4.1) + +# Creates and names a library, sets it as either STATIC +# or SHARED, and provides the relative paths to its source code. +# You can define multiple libraries, and CMake builds it for you. +# Gradle automatically packages shared libraries with your APK. + +add_library(samba_client + SHARED + src/main/cpp/jni_helper/JniHelper.cc + src/main/cpp/jni_helper/JavaClassCache.cc + src/main/cpp/samba_client/SambaClient.cc + src/main/cpp/credential_cache/CredentialCache.cc + ) + +include_directories(src/main/cpp) + +set(CMAKE_CXX_FLAGS + "${CMAKE_CXX_FLAGS} -std=c++0x -O2 -D_FORTIFY_SOURCE=2 -fstack-protector-all -fPIE") +set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -pie") + +# Searches for a specified prebuilt library and stores the path as a +# variable. Because system libraries are included in the search path by +# default, you only need to specify the name of the public NDK library +# you want to add. CMake verifies that the library exists before +# completing its build. + +find_library( # Sets the name of the path variable. + log-lib + + # Specifies the name of the NDK library that + # you want CMake to locate. + log ) + +# Specifies libraries CMake should link to your target library. You +# can link multiple libraries, such as libraries you define in the +# build script, prebuilt third-party libraries, or system libraries. + +set(libfolder "${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}") +add_library(libsmbclient + SHARED + IMPORTED) +set_target_properties(libsmbclient + PROPERTIES IMPORTED_LOCATION + ${libfolder}/libsmbclient.so) + +target_link_libraries(samba_client ${log-lib} libsmbclient) diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..41488be --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,51 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 25 + buildToolsVersion "25.0.2" + defaultConfig { + applicationId "com.google.android.sambadocumentsprovider" + minSdkVersion 21 + targetSdkVersion 25 + versionCode 102 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + + externalNativeBuild { + cmake { + cppFlags "" + } + } + + ndk { + abiFilters 'armeabi-v7a' + abiFilters 'arm64-v8a' + abiFilters 'x86' + abiFilters 'x86_64' + } + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + debug { + jniDebuggable true + } + } + externalNativeBuild { + cmake { + path "CMakeLists.txt" + } + } +} + +dependencies { + compile fileTree(include: ['*.jar'], dir: 'libs') + androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { + exclude group: 'com.android.support', module: 'support-annotations' + }) + compile 'com.android.support:appcompat-v7:25.1.0' + compile 'com.android.support:design:25.1.0' + testCompile 'junit:junit:4.12' +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..c7e2a4b --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /usr/local/google/home/xutan/android-sdk/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 *; +#} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..4e885c4 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/cpp/base/Callback.h b/app/src/main/cpp/base/Callback.h new file mode 100644 index 0000000..b4ecf70 --- /dev/null +++ b/app/src/main/cpp/base/Callback.h @@ -0,0 +1,31 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef SAMBADOCUMENTSPROVIDER_CALLBACK_H +#define SAMBADOCUMENTSPROVIDER_CALLBACK_H + +namespace SambaClient { + +template +class Callback { + public: + virtual int operator()(Ts... args) const = 0; +}; + +} + +#endif //SAMBADOCUMENTSPROVIDER_CALLBACK_H diff --git a/app/src/main/cpp/credential_cache/CredentialCache.cc b/app/src/main/cpp/credential_cache/CredentialCache.cc new file mode 100644 index 0000000..0b1de9b --- /dev/null +++ b/app/src/main/cpp/credential_cache/CredentialCache.cc @@ -0,0 +1,40 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "CredentialCache.h" + +namespace SambaClient { + +struct CredentialTuple emptyTuple_; + +struct CredentialTuple CredentialCache::get(const std::string &key) const { + if (credentialMap_.find(key) != credentialMap_.end()) { + return credentialMap_.at(key); + } else { + return emptyTuple_; + } +} + +void CredentialCache::put(const char *key, const struct CredentialTuple &tuple) { + credentialMap_[key] = tuple; +} + +void CredentialCache::remove(const char *key_) { + std::string key(key_); + credentialMap_.erase(key); +} +} diff --git a/app/src/main/cpp/credential_cache/CredentialCache.h b/app/src/main/cpp/credential_cache/CredentialCache.h new file mode 100644 index 0000000..dae4e8e --- /dev/null +++ b/app/src/main/cpp/credential_cache/CredentialCache.h @@ -0,0 +1,40 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef SAMBADOCUMENTSPROVIDER_SERVERCACHE_H +#define SAMBADOCUMENTSPROVIDER_SERVERCACHE_H + +#include + +namespace SambaClient { +struct CredentialTuple { + std::string workgroup; + std::string username; + std::string password; +}; + +class CredentialCache { + public: + struct CredentialTuple get(const std::string &key) const; + void put(const char *key, const struct CredentialTuple &tuple); + void remove(const char *key); + private: + std::unordered_map credentialMap_; +}; +} + +#endif //SAMBADOCUMENTSPROVIDER_SERVERCACHE_H diff --git a/app/src/main/cpp/jni_helper/JavaClassCache.cc b/app/src/main/cpp/jni_helper/JavaClassCache.cc new file mode 100644 index 0000000..9f3633e --- /dev/null +++ b/app/src/main/cpp/jni_helper/JavaClassCache.cc @@ -0,0 +1,33 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "JavaClassCache.h" + +namespace SambaClient { +jclass JavaClassCache::get(JNIEnv *env, const char *name_) { + std::string name(name_); + jclass &value = cache_[name]; + if (value == NULL) { + jclass localRef = env->FindClass(name_); + if (localRef == NULL) { + return NULL; + } + value = reinterpret_cast(env->NewGlobalRef(localRef)); + } + return value; +} +} diff --git a/app/src/main/cpp/jni_helper/JavaClassCache.h b/app/src/main/cpp/jni_helper/JavaClassCache.h new file mode 100644 index 0000000..04d190a --- /dev/null +++ b/app/src/main/cpp/jni_helper/JavaClassCache.h @@ -0,0 +1,33 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef SAMBADOCUMENTSPROVIDER_JAVACLASSCACHE_H +#define SAMBADOCUMENTSPROVIDER_JAVACLASSCACHE_H + +#include +#include + +namespace SambaClient { +class JavaClassCache { + public: + jclass get(JNIEnv *env, const char *name); + private: + std::unordered_map cache_; +}; +} + +#endif //SAMBADOCUMENTSPROVIDER_JAVACLASSCACHE_H diff --git a/app/src/main/cpp/jni_helper/JniCallback.h b/app/src/main/cpp/jni_helper/JniCallback.h new file mode 100644 index 0000000..0e57d54 --- /dev/null +++ b/app/src/main/cpp/jni_helper/JniCallback.h @@ -0,0 +1,52 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef SAMBADOCUMENTSPROIVDER_JNICALLBACK_H +#define SAMBADOCUMENTSPROIVDER_JNICALLBACK_H + +#include "base/Callback.h" + +#include +#include + +template +struct JniContext { + JNIEnv * const env; + const T &instance; + JniContext(JNIEnv * const env, T &obj) + : env(env), instance(obj) {} +}; + +template +class JniCallback : public SambaClient::Callback { + public: + JniCallback( + const JniContext &context, std::function, Us...)> callback) + : context(context), callback(callback) {} + + JniCallback(JniCallback &) = delete; + JniCallback(JniCallback &&) = delete; + + int operator()(Us... args) const { + return callback(context, args...); + } + private: + const JniContext context; + const std::function, Us...)> callback; +}; + +#endif //SAMBADOCUMENTSPROIVDER_JNICALLBACK_H diff --git a/app/src/main/cpp/jni_helper/JniHelper.cc b/app/src/main/cpp/jni_helper/JniHelper.cc new file mode 100644 index 0000000..04ea121 --- /dev/null +++ b/app/src/main/cpp/jni_helper/JniHelper.cc @@ -0,0 +1,658 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include + +#include "JniHelper.h" + +#include "JavaClassCache.h" +#include "JniCallback.h" +#include "logger/logger.h" +#include "samba_client/SambaClient.h" +#include "credential_cache/CredentialCache.h" + +#include + +#define CLASS_PREFIX "com/google/android/sambadocumentsprovider" + +#define TAG "JniHelper" + +namespace { +SambaClient::JavaClassCache classCache_; +} + +jlong +Java_com_google_android_sambadocumentsprovider_nativefacade_NativeSambaFacade_nativeInit( + JNIEnv *env, jobject instance, jboolean debug, jlong cachePointer) { + SambaClient::SambaClient + *client = new SambaClient::SambaClient(); + + const SambaClient::CredentialCache *cache = + reinterpret_cast(cachePointer); + if (client->Init(debug, cache)) { + // Successfully initialized. + return reinterpret_cast(client); + } else { + // Something wrong with the initialization. + LOGE(TAG, "Native Samba client failed to initialize."); + delete client; + return 0L; + } +} + +void +Java_com_google_android_sambadocumentsprovider_nativefacade_NativeSambaFacade_nativeDestroy( + JNIEnv *env, jobject instance, jlong pointer) { + SambaClient::SambaClient *client= + reinterpret_cast(pointer); + + delete client; +} + +static jobject +create_directory_entry(JNIEnv* env, const struct smbc_dirent &ent) { + // Only initialize these variables once to avoid costly calls into JNIEnv. + static const jclass dirEntryClass = + classCache_.get(env, CLASS_PREFIX "/base/DirectoryEntry"); + static const jmethodID dirEntryConstructor = + env->GetMethodID(dirEntryClass, + "", + "(ILjava/lang/String;Ljava/lang/String;)V"); + + jobject entry = NULL; + + const jstring comment = env->NewStringUTF(ent.comment); + if (comment == NULL) { + return NULL; + } + const jstring name = env->NewStringUTF(ent.name); + if (name == NULL) { + goto bail; + } + + entry = env->NewObject(dirEntryClass, + dirEntryConstructor, + ent.smbc_type, + comment, + name); + + env->DeleteLocalRef(name); + bail: + env->DeleteLocalRef(comment); + + return entry; +} + +static jobject create_array_list(JNIEnv* env) { + static const jclass arrayListClass = + classCache_.get(env, "java/util/ArrayList"); + static const jmethodID arrayListConstructor = + env->GetMethodID(arrayListClass, "", "()V"); + + return env->NewObject(arrayListClass, arrayListConstructor); +} + +static void +add_object_to_array_list(JNIEnv *env, jobject arrayList, jobject obj) { + static const jclass arrayListClass = + classCache_.get(env, "java/util/ArrayList"); + static const jmethodID addMethod = + env->GetMethodID(arrayListClass, "add", "(Ljava/lang/Object;)Z"); + + env->CallBooleanMethod(arrayList, addMethod, obj); +} + +static int add_dir_entry_to_array_list( + JniContext context, struct smbc_dirent* dirent) { + jobject entry = create_directory_entry(context.env, *dirent); + if (entry == NULL) { + return -1; + } + add_object_to_array_list(context.env, context.instance, entry); + if (context.env->ExceptionCheck()) { + return -1; + } + // We're done with this entry, remove local ref to avoid leak. + context.env->DeleteLocalRef(entry); + return 0; +} + +static void +throw_new_file_not_found_exception(JNIEnv *env, const char *fmt, ...) { + char message[256]; + va_list args; + va_start(args, fmt); + vsnprintf(message, sizeof(message), fmt, args); + va_end(args); + + static const jclass fileNotFoundExceptionClass = + classCache_.get(env, "java/io/FileNotFoundException"); + env->ThrowNew(fileNotFoundExceptionClass, message); +} + +static void +throw_new_errno_exception(JNIEnv *env, const char *functionName_, int err) { + static const jclass errnoExceptionClass = + classCache_.get(env, "android/system/ErrnoException"); + static const jmethodID errnoExceptionConstructor = + env->GetMethodID(errnoExceptionClass, "", "(Ljava/lang/String;I)V"); + + jstring functionName = env->NewStringUTF(functionName_); + if (functionName == NULL) { + return; + } + jthrowable errnoException = + reinterpret_cast( + env->NewObject(errnoExceptionClass, errnoExceptionConstructor, + functionName, err)); + if (errnoException == NULL) { + return; + } + env->Throw(errnoException); +} + +static void +throw_new_auth_failed_exception(JNIEnv* env) { + static const jclass authFailedExceptionClass = + classCache_.get(env, CLASS_PREFIX "/base/AuthFailedException"); + static const jmethodID authFailedExceptionConstructor = + env->GetMethodID( + authFailedExceptionClass, "", "()V"); + + jthrowable authFailedException = + reinterpret_cast( + env->NewObject( + authFailedExceptionClass, + authFailedExceptionConstructor)); + if (authFailedException == NULL) { + return; + } + env->Throw(authFailedException); +} + +jobject +Java_com_google_android_sambadocumentsprovider_nativefacade_NativeSambaFacade_readDir( + JNIEnv *env, jobject instance, jlong pointer, jstring uri_) { + const char *uri = env->GetStringUTFChars(uri_, 0); + if (uri == NULL) { + return NULL; + } + + jobject arrayList = create_array_list(env); + if (arrayList == NULL) { + // Java exception happened. + env->ReleaseStringUTFChars(uri_, uri); + return NULL; + } + + JniContext context(env, arrayList); + SambaClient::SambaClient *client = + reinterpret_cast(pointer); + int result = client->ReadDir( + uri, + JniCallback( + context, add_dir_entry_to_array_list)); + + if (env->ExceptionCheck()) { + // Java exception happened. + goto bail; + } + + if (result < 0) { + int err = -result; + switch (err) { + case ENODEV: + case ENOENT: + throw_new_file_not_found_exception( + env, "Directory at %s can't be found.", uri); + break; + case EACCES: + case EPERM: + LOGW(TAG, "No access to directory at %s.", uri); + throw_new_auth_failed_exception(env); + break; + default: + throw_new_errno_exception(env, "readDir", err); + } + } + + bail: + env->ReleaseStringUTFChars(uri_, uri); + + return arrayList; +} + +static jobject create_structstat(JNIEnv *env, const struct stat &st) { + static const jclass structStatClass = + classCache_.get(env, "android/system/StructStat"); + static const jmethodID structStatConstructor = + env->GetMethodID(structStatClass, "", "(JJIJIIJJJJJJJ)V"); + + return env->NewObject( + structStatClass, + structStatConstructor, + static_cast(st.st_dev), + static_cast(st.st_ino), + static_cast(st.st_mode), + static_cast(st.st_nlink), + static_cast(st.st_uid), + static_cast(st.st_gid), + static_cast(st.st_rdev), + static_cast(st.st_size), + static_cast(st.st_atime), + static_cast(st.st_mtime), + static_cast(st.st_ctime), + static_cast(st.st_blksize), + static_cast(st.st_blocks)); +} + +jobject +Java_com_google_android_sambadocumentsprovider_nativefacade_NativeSambaFacade_stat( + JNIEnv *env, jobject instance, jlong pointer, jstring uri_) { + const char *uri = env->GetStringUTFChars(uri_, 0); + if (uri == NULL) { + return NULL; + } + + SambaClient::SambaClient *client = + reinterpret_cast(pointer); + + jobject stat = NULL; + + struct stat st; + int result = client->Stat(uri, &st); + if (result < 0) { + int err = -result; + switch (err) { + case ENODEV: + case ENOENT: + throw_new_file_not_found_exception(env, "Can't find %s", uri); + break; + case EACCES: + LOGW(TAG, "No access to %s.", uri); + throw_new_auth_failed_exception(env); + break; + default: + throw_new_errno_exception(env, "stat", err); + } + + goto bail; + } + + stat = create_structstat(env, st); + + bail: + env->ReleaseStringUTFChars(uri_, uri); + + return stat; +} + +void +Java_com_google_android_sambadocumentsprovider_nativefacade_NativeSambaFacade_createFile( + JNIEnv *env, jobject instance, jlong pointer, jstring uri_) { + const char *uri = env->GetStringUTFChars(uri_, 0); + if (uri == NULL) { + return; + } + + SambaClient::SambaClient *client = + reinterpret_cast(pointer); + int result = client->CreateFile(uri); + if (result < 0) { + int err = -result; + switch(err) { + case ENODEV: + case ENOENT: + throw_new_file_not_found_exception( + env, "Missing parent folders of %s", uri); + break; + case EACCES: + LOGW(TAG, "No access to %s.", uri); + throw_new_auth_failed_exception(env); + break; + default: + throw_new_errno_exception(env, "createFile", err); + } + } + + env->ReleaseStringUTFChars(uri_, uri); +} + +void +Java_com_google_android_sambadocumentsprovider_nativefacade_NativeSambaFacade_mkdir( + JNIEnv *env, jobject instance, jlong pointer, jstring uri_) { + const char *uri = env->GetStringUTFChars(uri_, 0); + if (uri == NULL) { + return; + } + + SambaClient::SambaClient *client = + reinterpret_cast(pointer); + int result = client->Mkdir(uri); + if (result < 0) { + int err = -result; + switch(err) { + case ENODEV: + case ENOENT: + throw_new_file_not_found_exception( + env, "Missing parent folders of %s", uri); + break; + case EACCES: + // TODO: Add authentication callback here. + LOGD(TAG, "No access to %s.", uri); + throw_new_auth_failed_exception(env); + break; + default: + throw_new_errno_exception(env, "mkdir", err); + } + } + + env->ReleaseStringUTFChars(uri_, uri); +} + +void +Java_com_google_android_sambadocumentsprovider_nativefacade_NativeSambaFacade_rename( + JNIEnv *env, jobject instance, jlong pointer, jstring uri_, jstring nuri_) { + const char *uri = env->GetStringUTFChars(uri_, 0); + if (uri == NULL) { + return; + } + const char *nuri = env->GetStringUTFChars(nuri_, 0); + if (nuri == NULL) { + env->ReleaseStringUTFChars(uri_, uri); + return; + } + + SambaClient::SambaClient *client = + reinterpret_cast(pointer); + int result = client->Rename(uri, nuri); + if (result < 0) { + int err = -result; + switch(err) { + case ENODEV: + case ENOENT: + throw_new_file_not_found_exception( + env, "%s is not found, or parent of %s is not found.", uri, nuri); + break; + case EACCES: + case EPERM: + LOGD(TAG, "No access to either %s or %s.", uri, nuri); + throw_new_auth_failed_exception(env); + break; + default: + throw_new_errno_exception(env, "rename", err); + } + } + + env->ReleaseStringUTFChars(nuri_, nuri); + env->ReleaseStringUTFChars(uri_, uri); +} + +void +Java_com_google_android_sambadocumentsprovider_nativefacade_NativeSambaFacade_unlink( + JNIEnv *env, jobject instance, jlong pointer, jstring uri_) { + const char *uri = env->GetStringUTFChars(uri_, 0); + if (uri == NULL) { + return; + } + + SambaClient::SambaClient *client = + reinterpret_cast(pointer); + int result = client->Unlink(uri); + if (result < 0) { + int err = -result; + switch (err) { + case ENODEV: + case ENOENT: + throw_new_file_not_found_exception(env, "%s is not found.", uri); + break; + case EACCES: + case EPERM: + LOGD(TAG, "No access to %s.", uri); + throw_new_auth_failed_exception(env); + break; + default: + throw_new_errno_exception(env, "unlink", err); + } + } + + env->ReleaseStringUTFChars(uri_, uri); +} + +void +Java_com_google_android_sambadocumentsprovider_nativefacade_NativeSambaFacade_rmdir( + JNIEnv *env, jobject instance, jlong pointer, jstring uri_) { + const char *uri = env->GetStringUTFChars(uri_, 0); + if (uri == NULL) { + return; + } + + SambaClient::SambaClient *client = + reinterpret_cast(pointer); + int result = client->Rmdir(uri); + if (result < 0) { + int err = -result; + switch (err) { + case ENODEV: + case ENOENT: + throw_new_file_not_found_exception(env, "%s is not found.", uri); + break; + case EACCES: + case EPERM: + LOGD(TAG, "No access to %s.", uri); + throw_new_auth_failed_exception(env); + break; + default: + throw_new_errno_exception(env, "rmdir", err); + } + } + + env->ReleaseStringUTFChars(uri_, uri); +} + +jint +Java_com_google_android_sambadocumentsprovider_nativefacade_NativeSambaFacade_openFile( + JNIEnv *env, jobject instance, jlong pointer, jstring uri_, jstring mode_) { + int fd = -1; + + const char *uri = env->GetStringUTFChars(uri_, 0); + if (uri == NULL) { + return fd; + } + const char *mode = env->GetStringUTFChars(mode_, 0); + if (mode == NULL) { + env->ReleaseStringUTFChars(uri_, uri); + return fd; + } + + int flag = -1; + if (mode[0] == 'r') { + if (mode[1] == '\0') { + flag = O_RDONLY; + } else if (mode[1] == 'w') { + flag = O_RDWR; + if (mode[2] == 't' && mode[3] == '\0') { + flag |= O_TRUNC; + } + } + } else if (mode[0] == 'w') { + flag = O_WRONLY; + if (mode[1] == 'a') { + flag |= O_APPEND; + } else if (mode[1] =='\0') { + flag |= O_TRUNC; + } + } + + if (flag >= 0) { + SambaClient::SambaClient *client = + reinterpret_cast(pointer); + fd = client->OpenFile(uri, flag, 0); + } + + if (fd < 0) { + int err = -fd; + switch (err) { + case ENODEV: + case ENOENT: + throw_new_file_not_found_exception( + env, "File at %s can't be found.", uri); + break; + case EACCES: + LOGW(TAG, "No access to file at %s.", uri); + throw_new_auth_failed_exception(env); + break; + default: + throw_new_errno_exception(env, "openFile", err); + } + } + + env->ReleaseStringUTFChars(uri_, uri); + env->ReleaseStringUTFChars(mode_, mode); + + return fd; +} + +jlong Java_com_google_android_sambadocumentsprovider_nativefacade_SambaFile_read( + JNIEnv *env, + jobject instance, + jlong pointer, + jint fd, + jobject buffer_, + jint maxlen) { + void *buffer = env->GetDirectBufferAddress(buffer_); + + SambaClient::SambaClient *client = + reinterpret_cast(pointer); + + ssize_t size = client->ReadFile( + fd, buffer, static_cast(maxlen)); + if (size < 0) { + throw_new_errno_exception(env, "read", static_cast(-size)); + } + + return size; +} + +jlong Java_com_google_android_sambadocumentsprovider_nativefacade_SambaFile_write( + JNIEnv *env, + jobject instance, + jlong pointer, + jint fd, + jobject buffer_, + jint length) { + void *buffer = env->GetDirectBufferAddress(buffer_); + + SambaClient::SambaClient *client = + reinterpret_cast(pointer); + + ssize_t size = client->WriteFile( + fd, buffer, static_cast(length)); + if (size < 0) { + throw_new_errno_exception(env, "write", static_cast(-size)); + } + + return size; +} + +void Java_com_google_android_sambadocumentsprovider_nativefacade_SambaFile_close( + JNIEnv *env, jobject instance, jlong pointer, jint fd) { + SambaClient::SambaClient *client = + reinterpret_cast(pointer); + + int result = client->CloseFile(fd); + if (result < 0) { + throw_new_errno_exception(env, "close", -result); + } +} + +jlong Java_com_google_android_sambadocumentsprovider_nativefacade_NativeCredentialCache_nativeInit( + JNIEnv *env, jobject instance) { + return reinterpret_cast(new SambaClient::CredentialCache()); +} + +void Java_com_google_android_sambadocumentsprovider_nativefacade_NativeCredentialCache_putCredential( + JNIEnv *env, jobject instance, jlong pointer, + jstring uri_, jstring workgroup_, jstring username_, jstring password_) { + + const char *uri; + const char *workgroup; + const char *username; + const char *password; + uri = env->GetStringUTFChars(uri_, 0); + if (uri == NULL) { + return; + } + workgroup = env->GetStringUTFChars(workgroup_, 0); + if (workgroup == NULL) { + goto bail_workgroup; + } + username = env->GetStringUTFChars(username_, 0); + if (username == NULL) { + goto bail_username; + } + password = env->GetStringUTFChars(password_, 0); + if (password == NULL) { + goto bail_password; + } + + { + SambaClient::CredentialCache *cache = + reinterpret_cast(pointer); + SambaClient::CredentialTuple tuple = + {std::string(workgroup), std::string(username), std::string(password)}; + cache->put(uri, tuple); + } + + env->ReleaseStringUTFChars(password_, password); + bail_password: + env->ReleaseStringUTFChars(username_, username); + bail_username: + env->ReleaseStringUTFChars(workgroup_, workgroup); + bail_workgroup: + env->ReleaseStringUTFChars(uri_, uri); +} + +void Java_com_google_android_sambadocumentsprovider_nativefacade_NativeCredentialCache_removeCredential( + JNIEnv *env, jobject instance, jlong pointer, jstring uri_) { + const char *uri = env->GetStringUTFChars(uri_, 0); + if (uri == NULL) { + return; + } + SambaClient::CredentialCache *cache = + reinterpret_cast(pointer); +} + +void Java_com_google_android_sambadocumentsprovider_SambaConfiguration_setEnv( + JNIEnv *env, jobject instance, jstring var_, jstring value_) { + const char *var = env->GetStringUTFChars(var_, 0); + if (var == NULL) { + return; + } + + const char *value = env->GetStringUTFChars(value_, 0); + if (value == NULL) { + goto bail; + } + + if (setenv(var, value, true) < 0) { + throw_new_errno_exception(env, "setEnv", errno); + } + + bail: + env->ReleaseStringUTFChars(var_, var); +} diff --git a/app/src/main/cpp/jni_helper/JniHelper.h b/app/src/main/cpp/jni_helper/JniHelper.h new file mode 100644 index 0000000..4fbff95 --- /dev/null +++ b/app/src/main/cpp/jni_helper/JniHelper.h @@ -0,0 +1,100 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef SAMBADOCUMENTSPROVIDER_JNI_HELPER_H +#define SAMBADOCUMENTSPROVIDER_JNI_HELPER_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +JNIEXPORT jlong JNICALL + Java_com_google_android_sambadocumentsprovider_nativefacade_NativeSambaFacade_nativeInit( + JNIEnv *env, jobject instance, jboolean debug, jlong cachePointer); + +JNIEXPORT void JNICALL + Java_com_google_android_sambadocumentsprovider_nativefacade_NativeSambaFacade_nativeDestroy( + JNIEnv *env, jobject instance, jlong pointer); + +JNIEXPORT jobject JNICALL + Java_com_google_android_sambadocumentsprovider_nativefacade_NativeSambaFacade_readDir( + JNIEnv *env, jobject instance, jlong pointer, jstring uri_); + +JNIEXPORT jobject JNICALL + Java_com_google_android_sambadocumentsprovider_nativefacade_NativeSambaFacade_stat( + JNIEnv *env, jobject instance, jlong pointer, jstring uri_); + +JNIEXPORT void JNICALL + Java_com_google_android_sambadocumentsprovider_nativefacade_NativeSambaFacade_createFile( + JNIEnv *env, jobject instance, jlong pointer, jstring uri_); + +JNIEXPORT void JNICALL + Java_com_google_android_sambadocumentsprovider_nativefacade_NativeSambaFacade_mkdir( + JNIEnv *env, jobject instance, jlong pointer, jstring uri_); + +JNIEXPORT void JNICALL + Java_com_google_android_sambadocumentsprovider_nativefacade_NativeSambaFacade_rename( + JNIEnv *env, jobject instance, jlong pointer, jstring uri_, jstring nuri_); + +JNIEXPORT void JNICALL + Java_com_google_android_sambadocumentsprovider_nativefacade_NativeSambaFacade_unlink( + JNIEnv *env, jobject instance, jlong pointer, jstring uri_); + +JNIEXPORT void JNICALL + Java_com_google_android_sambadocumentsprovider_nativefacade_NativeSambaFacade_rmdir( + JNIEnv *env, jobject instance, jlong pointer, jstring uri_); + +JNIEXPORT jint JNICALL + Java_com_google_android_sambadocumentsprovider_nativefacade_NativeSambaFacade_openFile( + JNIEnv *env, jobject instance, jlong pointer, jstring uri_, jstring mode_); + +JNIEXPORT jlong JNICALL + Java_com_google_android_sambadocumentsprovider_nativefacade_SambaFile_read( + JNIEnv *env, jobject instance, jlong pointer, jint fd, jobject buffer, jint maxlen); + +JNIEXPORT jlong JNICALL + Java_com_google_android_sambadocumentsprovider_nativefacade_SambaFile_write( + JNIEnv *env, jobject instance, jlong pointer, jint fd, jobject buffer, jint length); + +JNIEXPORT void JNICALL + Java_com_google_android_sambadocumentsprovider_nativefacade_SambaFile_close( + JNIEnv *env, jobject instance, jlong pointer, jint fd); + +JNIEXPORT jlong JNICALL + Java_com_google_android_sambadocumentsprovider_nativefacade_NativeCredentialCache_nativeInit( + JNIEnv *env, jobject instance); + +JNIEXPORT void JNICALL + Java_com_google_android_sambadocumentsprovider_nativefacade_NativeCredentialCache_putCredential( + JNIEnv *env, jobject instance, jlong pointer, + jstring uri_, jstring workgroup_, jstring username_, jstring password_); + +JNIEXPORT void JNICALL + Java_com_google_android_sambadocumentsprovider_nativefacade_NativeCredentialCache_removeCredential( + JNIEnv *env, jobject instance, jlong pointer, jstring uri_); + +JNIEXPORT void JNICALL + Java_com_google_android_sambadocumentsprovider_SambaConfiguration_setEnv( + JNIEnv *env, jobject instance, jstring var_, jstring value_); + +#ifdef __cplusplus +} +#endif + +#endif //SAMBADOCUMENTSPROVIDER_JNI_HELPER_H diff --git a/app/src/main/cpp/logger/logger.h b/app/src/main/cpp/logger/logger.h new file mode 100644 index 0000000..b16a4c7 --- /dev/null +++ b/app/src/main/cpp/logger/logger.h @@ -0,0 +1,38 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef MYAPPLICATION_LOGGER_H +#define MYAPPLICATION_LOGGER_H + +#include + +#define LOG(level, tag, args...) \ + __android_log_print((level), (tag), args) + +#define LOGV(tag, args...) LOG(ANDROID_LOG_VERBOSE, tag, args) + +#define LOGD(tag, args...) LOG(ANDROID_LOG_DEBUG, tag, args) + +#define LOGI(tag, args...) LOG(ANDROID_LOG_INFO, tag, args) + +#define LOGW(tag, args...) LOG(ANDROID_LOG_WARN, tag, args) + +#define LOGE(tag, args...) LOG(ANDROID_LOG_ERROR, tag, args) + +#define LOGF(tag, args...) LOG(ANDROID_LOG_FATAL, tag, args) + +#endif //MYAPPLICATION_LOGGER_H diff --git a/app/src/main/cpp/samba_client/SambaClient.cc b/app/src/main/cpp/samba_client/SambaClient.cc new file mode 100644 index 0000000..190f80e --- /dev/null +++ b/app/src/main/cpp/samba_client/SambaClient.cc @@ -0,0 +1,293 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "logger/logger.h" +#include "SambaClient.h" +#include "credential_cache/CredentialCache.h" + +#include +#include + +#define TAG "NativeSambaClient" + +namespace SambaClient { + +const CredentialCache *credentialCache_; + +SambaClient::~SambaClient() { + LOGD(TAG, "Destroying SambaClient."); + if (sambaContext && smbc_free_context(sambaContext, true)) { + LOGE(TAG, "Failed to free Samba context. Exit to avoid leaking."); + exit(1); + } + sambaContext = NULL; + LOGD(TAG, "Destroyed SambaClient."); +} + +bool SambaClient::Init(const bool debug, const CredentialCache *credentialCache) { + LOGD(TAG, "Initializing SambaClient. Debug: %d CredentialCache: %x HOME: %s", + debug, credentialCache, getenv("HOME")); + + sambaContext = smbc_new_context(); + if (!sambaContext) { + LOGE(TAG, "Failed to create a Samba context."); + return false; + } + + LOGD(TAG, "Setting debug level to %d.", debug); + smbc_setDebug(sambaContext, debug); + + LOGD(TAG, "Setting up auth callback."); + smbc_setFunctionAuthData(sambaContext, GetAuthData); + smbc_setOptionUseKerberos(sambaContext, true); + smbc_setOptionFallbackAfterKerberos(sambaContext, true); + + LOGD(TAG, "Initializing Samba context."); + if (!smbc_init_context(sambaContext)) { + LOGE(TAG, "Failed to initialize Samba context."); + smbc_free_context(sambaContext, 0); + return false; + } + + LOGD(TAG, "Setting Samba context."); + smbc_set_context(sambaContext); + + LOGD(TAG, "Set up Samba context."); + + credentialCache_ = credentialCache; + return true; +} + +void SambaClient::GetAuthData(const char *server, + const char *share, + char *workgroup, + int maxLenWorkgroup, + char *username, + int maxLenUsername, + char *password, + int maxLenPassword) { + + LOGV(TAG, "Requesting authentication data for server: %s and share: %s.", + server, share); + const std::string key = "smb://" + std::string(server) + "/" + share; + const struct CredentialTuple tuple = credentialCache_->get(key); + + if ((tuple.workgroup.length() + 1 > maxLenWorkgroup) + || (tuple.username.length() + 1 > maxLenUsername) + || (tuple.password.length() + 1 > maxLenPassword)) { + LOGE(TAG, "Credential buffer is too small for input." + "Ignore auth request for server %s and share %s.", server, share); + return; + } + + strncpy(workgroup, tuple.workgroup.c_str(), tuple.workgroup.length()); + workgroup[tuple.workgroup.length()] = '\0'; + + strncpy(username, tuple.username.c_str(), tuple.username.length()); + username[tuple.username.length()] = '\0'; + + strncpy(password, tuple.password.c_str(), tuple.password.length()); + password[tuple.password.length()] = '\0'; +} + +static const char* getTypeName(unsigned int smbc_type) { + switch (smbc_type) { + case SMBC_WORKGROUP: + return "WORKGROUP"; + case SMBC_SERVER: + return "SERVER"; + case SMBC_FILE_SHARE: + return "FILE_SHARE"; + case SMBC_PRINTER_SHARE: + return "PRINTER_SHARE"; + case SMBC_COMMS_SHARE: + return "PRINTER_SHARE"; + case SMBC_IPC_SHARE: + return "IPC_SHARE"; + case SMBC_DIR: + return "DIR"; + case SMBC_FILE: + return "FILE"; + case SMBC_LINK: + return "LINK"; + default: + return "UNKNOWN"; + } +} + +int +SambaClient::ReadDir( + const char *url, const Callback &entryHandler) { + LOGD(TAG, "Reading dir at %s.", url); + const int dir = smbc_opendir(url); + if (dir < 0) { + int err = errno; + LOGE(TAG, "Failed to open dir at %s. Errno: %x", url, err); + return -err; + } + + struct smbc_dirent *dirent = NULL; + while ((dirent = smbc_readdir(dir)) != NULL) { + LOGV(TAG, "Found entry name: %s, comment: %s, type: %s.", + dirent->name, dirent->comment, getTypeName(dirent->smbc_type)); + if (entryHandler(dirent) < 0) { + // Java exceptions. + smbc_closedir(dir); + return -1; + } + } + + const int ret = smbc_closedir(dir); + if (ret) { + int err = errno; + LOGW(TAG, "Failed to close dir %d at %s. Errno: %x.", dir, url, err); + } + + return 0; +} + +int +SambaClient::Stat(const char *url, struct stat * const st) { + LOGD(TAG, "Getting stat for %s.", url); + int result = smbc_stat(url, st); + if (result < 0) { + int err = errno; + LOGE(TAG, "Failed to obtain stat for %s. Errno: %x.", url, err); + return -err; + } + LOGV(TAG, "Got stat for %s.", url); + return 0; +} + +int +SambaClient::CreateFile(const char *url) { + LOGD(TAG, "Creating a file at %s.", url); + int fd = smbc_creat(url, 0755); + if (fd < 0) { + int err = errno; + LOGE(TAG, "Failed to create a file at %s. Errno: %x.", url, err); + return -err; + } + + if (smbc_close(fd) < 0) { + LOGW(TAG, "Failed to close the created file at %s.", url); + } + return 0; +} + +int +SambaClient::Mkdir(const char *url) { + LOGD(TAG, "Making dir at %s.", url); + int result = smbc_mkdir(url, 0755); + if (result < 0) { + int err = errno; + LOGE(TAG, "Failed to make dir at %s. Errno: %x.", url, err); + return -err; + } + + return result; +} + +int +SambaClient::Rename(const char *url, const char *nurl) { + LOGD(TAG, "Renaming %s to %s.", url, nurl); + int result = smbc_rename(url, nurl); + if (result < 0) { + int err = errno; + LOGE(TAG, "Failed to rename %s to %s. Errno: %x.", url, nurl, err); + return -err; + } + return result; +} + +int +SambaClient::Unlink(const char *url) { + LOGD(TAG, "Unlinking %s.", url); + int result = smbc_unlink(url); + if (result < 0) { + int err = errno; + LOGE(TAG, "Failed to unlink %s. Errno: %x.", url, err); + return -err; + } + return result; +} + +int +SambaClient::Rmdir(const char *url) { + LOGD(TAG, "Removing dir at %s.", url); + int result = smbc_rmdir(url); + if (result < 0) { + int err = errno; + LOGE(TAG, "Failed to remove dir at %s. Errno: %x.", url, err); + return -err; + } + return result; +} + +int SambaClient::OpenFile(const char *url, const int flag, const mode_t mode) { + LOGD(TAG, "Opening file at %s with flag %x.", url, flag); + int fd = smbc_open(url, flag, mode); + if (fd < 0) { + int err = errno; + LOGE(TAG, "Failed to open file at %s. Errno: %x", url, err); + return -err; + } else { + LOGV(TAG, "Opened file at %s with fd %x.", url, fd); + } + return fd; +} + +ssize_t +SambaClient::ReadFile(const int fd, void *buffer, const size_t maxlen) { + LOGV(TAG, "Reading max %lu bytes from file with fd %x", maxlen, fd); + const ssize_t size = smbc_read(fd, buffer, maxlen); + if (size < 0) { + int err = errno; + LOGE(TAG, "Failed to read file with fd %x. Errno: %x", fd, err); + return -err; + } else { + LOGV(TAG, "Read %ld bytes.", size); + } + return size; +} + +ssize_t +SambaClient::WriteFile(const int fd, void *buffer, const size_t length) { + LOGV(TAG, "Writing %lu bytes to file with fd %x.", length, fd); + const ssize_t size = smbc_write(fd, buffer, length); + if (size < 0) { + int err = errno; + LOGE(TAG, "Failed to write file with fd %x. Errno: %x", fd, err); + return -err; + } else { + LOGV(TAG, "Wrote %ld bytes.", size); + } + return size; +} + +int SambaClient::CloseFile(const int fd) { + LOGD(TAG, "Closing file with fd: %x", fd); + int result = smbc_close(fd); + if (result < 0) { + int err = errno; + LOGE(TAG, "Failed to close file with fd: %x with errno: %x", fd, err); + return -err; + } + return 0; +} + +} \ No newline at end of file diff --git a/app/src/main/cpp/samba_client/SambaClient.h b/app/src/main/cpp/samba_client/SambaClient.h new file mode 100644 index 0000000..f1c99e5 --- /dev/null +++ b/app/src/main/cpp/samba_client/SambaClient.h @@ -0,0 +1,70 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef SAMBADOCUMENTSPROVIDER_SAMBAPROVIDER_H +#define SAMBADOCUMENTSPROVIDER_SAMBAPROVIDER_H + +#include "base/Callback.h" +#include "jni_helper/JniHelper.h" +#include "samba_includes/libsmbclient.h" + +#include + +namespace SambaClient { + +struct CredentialCache; + +class SambaClient { + public: + ~SambaClient(); + + bool Init(const bool debug, const CredentialCache *credentialCache); + + int ReadDir(const char *url, const Callback &entryHandler); + + int Stat(const char *url, struct stat *st); + + int CreateFile(const char *url); + + int Mkdir(const char *url); + + int Rename(const char *url, const char *nurl); + + int Unlink(const char *url); + + int Rmdir(const char *url); + + int OpenFile(const char *url, const int flag, const mode_t mode); + + ssize_t ReadFile(const int fd, void *buffer, const size_t maxlen); + + ssize_t WriteFile(const int fd, void *buffer, const size_t length); + + int CloseFile(const int fd); + private: + ::SMBCCTX *sambaContext = NULL; + + static void GetAuthData(const char *server, + const char *share, + char *workgroup, int maxLenWorkgroup, + char *username, int maxLenUsername, + char *password, int maxLenPassword); +}; + +} + +#endif //SAMBADOCUMENTSPROVIDER_SAMBAPROVIDER_H diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/SambaConfiguration.java b/app/src/main/java/com/google/android/sambadocumentsprovider/SambaConfiguration.java new file mode 100644 index 0000000..6f525d4 --- /dev/null +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/SambaConfiguration.java @@ -0,0 +1,167 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.google.android.sambadocumentsprovider; + +import android.system.ErrnoException; +import android.util.Log; +import com.google.android.sambadocumentsprovider.base.BiResultTask; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.PrintStream; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; + +class SambaConfiguration implements Iterable> { + + private static final String TAG = "SambaConfiguration"; + + private static final String HOME_VAR = "HOME"; + private static final String SMB_FOLDER_NAME = ".smb"; + private static final String SMB_CONF_FILE = "smb.conf"; + private static final String CONF_KEY_VALUE_SEPARATOR = " = "; + + private final File mHomeFolder; + private final Map mConfigurations = new HashMap<>(); + + SambaConfiguration(File homeFolder) { + mHomeFolder = homeFolder; + + setHomeEnv(homeFolder.getAbsolutePath()); + } + + public void flushAsDefault(OnConfigurationChangedListener listener) { + File smbFile = getSmbFile(mHomeFolder); + if (!smbFile.exists()) { + flush(listener); + } + } + + public synchronized SambaConfiguration addConfiguration(String key, String value) { + mConfigurations.put(key, value); + return this; + } + + public synchronized SambaConfiguration removeConfiguration(String key) { + mConfigurations.remove(key); + return this; + } + + public void load(OnConfigurationChangedListener listener) { + new LoadTask(listener).execute(); + } + + public void flush(OnConfigurationChangedListener listener) { + new FlushTask(listener).execute(); + } + + private synchronized void read() throws IOException { + mConfigurations.clear(); + try (BufferedReader reader = new BufferedReader(new FileReader(getSmbFile(mHomeFolder)))) { + String line; + while ((line = reader.readLine()) != null) { + String[] conf = line.split(CONF_KEY_VALUE_SEPARATOR); + if (conf.length == 2) { + mConfigurations.put(conf[0], conf[1]); + } + } + } + } + + private synchronized void write() throws IOException { + try (PrintStream fs = new PrintStream(getSmbFile(mHomeFolder))) { + for (Map.Entry entry : mConfigurations.entrySet()) { + fs.print(entry.getKey()); + fs.print(CONF_KEY_VALUE_SEPARATOR); + fs.print(entry.getValue()); + fs.println(); + } + + fs.flush(); + } + } + + private static File getSmbFile(File homeFolder) { + File smbFolder = new File(homeFolder, SMB_FOLDER_NAME); + if (!smbFolder.isDirectory() && !smbFolder.mkdir()) { + Log.e(TAG, "Failed to obtain .smb folder."); + } + + return new File(smbFolder, SMB_CONF_FILE); + } + + private void setHomeEnv(String absoluteFolder) { + try { + setEnv(HOME_VAR, absoluteFolder); + } catch(ErrnoException e) { + Log.e(TAG, "Failed to set HOME environment variable.", e); + } + } + + private native void setEnv(String var, String value) throws ErrnoException; + + @Override + public synchronized Iterator> iterator() { + return mConfigurations.entrySet().iterator(); + } + + private class LoadTask extends BiResultTask { + private final OnConfigurationChangedListener mListener; + + private LoadTask(OnConfigurationChangedListener listener) { + mListener = listener; + } + + @Override + public Void run(Void... params) throws IOException { + read(); + return null; + } + + @Override + public void onSucceeded(Void result) { + mListener.onConfigurationChanged(); + } + } + + private class FlushTask extends BiResultTask { + private final OnConfigurationChangedListener mListener; + + private FlushTask(OnConfigurationChangedListener listener) { + mListener = listener; + } + + @Override + public Void run(Void... params) throws IOException { + write(); + return null; + } + + @Override + public void onSucceeded(Void result) { + mListener.onConfigurationChanged(); + } + } + + interface OnConfigurationChangedListener { + void onConfigurationChanged(); + } +} diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/SambaProviderApplication.java b/app/src/main/java/com/google/android/sambadocumentsprovider/SambaProviderApplication.java new file mode 100644 index 0000000..88c7035 --- /dev/null +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/SambaProviderApplication.java @@ -0,0 +1,112 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.google.android.sambadocumentsprovider; + +import android.app.Application; +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.ConnectivityManager.NetworkCallback; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import com.google.android.sambadocumentsprovider.SambaConfiguration.OnConfigurationChangedListener; +import com.google.android.sambadocumentsprovider.cache.DocumentCache; +import com.google.android.sambadocumentsprovider.nativefacade.CredentialCache; +import com.google.android.sambadocumentsprovider.nativefacade.SambaMessageLooper; +import com.google.android.sambadocumentsprovider.nativefacade.SmbClient; + +public class SambaProviderApplication extends Application { + + private final DocumentCache mCache = new DocumentCache(); + private final TaskManager mTaskManager = new TaskManager(); + private final SmbClient mSambaClient; + private final CredentialCache mCredentialCache; + + private SambaConfiguration mSambaConf; + private ShareManager mShareManager; + + public SambaProviderApplication() { + final SambaMessageLooper looper = new SambaMessageLooper(); + mCredentialCache = looper.getCredentialCache(); + mSambaClient = looper.getClient(); + } + + @Override + public void onCreate() { + final ConnectivityManager manager = + (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE); + manager.registerNetworkCallback( + new NetworkRequest.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET) + .build(), + new NetworkCallback() { + @Override + public void onAvailable(Network network) { + mSambaClient.reset(); + } + }); + + initializeSambaConf(); + } + + private void initializeSambaConf() { + mSambaConf = new SambaConfiguration(getDir("home", MODE_PRIVATE)); + + // lmhosts are not used in SambaDocumentsProvider and prioritize bcast because sometimes in home + // settings DNS will resolve unknown domain name to a specific IP for advertisement. + // + // lmhosts -- lmhosts file if existed side by side to smb.conf + // wins -- Windows Internet Name Service + // hosts -- hosts file and DNS resolution + // bcast -- NetBIOS broadcast + mSambaConf.addConfiguration("name resolve order", "wins bcast hosts"); + mSambaConf.flushAsDefault(new OnConfigurationChangedListener() { + @Override + public void onConfigurationChanged() { + mSambaClient.reset(); + } + }); + } + + public static ShareManager getServerManager(Context context) { + final SambaProviderApplication application = getApplication(context); + synchronized (application) { + if (application.mShareManager == null) { + application.mShareManager = new ShareManager(context, application.mCredentialCache); + } + return application.mShareManager; + } + } + + public static SmbClient getSambaClient(Context context) { + return getApplication(context).mSambaClient; + } + + public static DocumentCache getDocumentCache(Context context) { + return getApplication(context).mCache; + } + + public static TaskManager getTaskManager(Context context) { + return getApplication(context).mTaskManager; + } + + private static SambaProviderApplication getApplication(Context context) { + return ((SambaProviderApplication) context.getApplicationContext()); + } +} diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/ShareManager.java b/app/src/main/java/com/google/android/sambadocumentsprovider/ShareManager.java new file mode 100644 index 0000000..7343689 --- /dev/null +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/ShareManager.java @@ -0,0 +1,293 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.google.android.sambadocumentsprovider; + +import android.content.Context; +import android.content.SharedPreferences; +import android.text.TextUtils; +import android.util.JsonReader; +import android.util.JsonToken; +import android.util.JsonWriter; +import android.util.Log; +import com.google.android.sambadocumentsprovider.nativefacade.CredentialCache; +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class ShareManager implements Iterable { + private static final String TAG = "ShareManager"; + + private static final String SERVER_CACHE_PREF_KEY = "ServerCachePref"; + private static final String SERVER_STRING_SET_KEY = "ServerStringSet"; + + // JSON value + private static final String URI_KEY = "uri"; + private static final String CREDENTIAL_TUPLE_KEY = "credentialTuple"; + private static final String WORKGROUP_KEY = "workgroup"; + private static final String USERNAME_KEY = "username"; + private static final String PASSWORD_KEY = "password"; + + private final SharedPreferences mPref; + private final Set mServerStringSet; + private final Map mServerStringMap = new HashMap<>(); + private final CredentialCache mCredentialCache; + + private final List mListeners = new ArrayList<>(); + + ShareManager(Context context, CredentialCache credentialCache) { + mCredentialCache = credentialCache; + + mPref = context.getSharedPreferences(SERVER_CACHE_PREF_KEY, Context.MODE_PRIVATE); + // Loading mounted servers + final Set serverStringSet = + mPref.getStringSet(SERVER_STRING_SET_KEY, Collections. emptySet()); + final Map credentialMap = new HashMap<>(serverStringSet.size()); + for (String serverString : serverStringSet) { + // TODO: Add decryption + String uri = decode(serverString, credentialMap); + if (uri != null) { + mServerStringMap.put(uri, serverString); + } + } + + mServerStringSet = new HashSet<>(serverStringSet); + + for (Map.Entry server : credentialMap.entrySet()) { + final CredentialTuple tuple = server.getValue(); + mCredentialCache.putCredential( + server.getKey(), tuple.mWorkgroup, tuple.mUsername, tuple.mPassword); + } + } + + public synchronized void mountServer( + String uri, String workgroup, String username, String password, ShareMountChecker checker) + throws IOException { + if (mServerStringMap.containsKey(uri)) { + throw new IllegalStateException("Uri " + uri + " is already mounted."); + } + + final boolean hasPassword = !TextUtils.isEmpty(username) && !TextUtils.isEmpty(password); + if (hasPassword) { + mCredentialCache.putCredential(uri, workgroup, username, password); + } + try { + checker.checkShareMounting(); + } catch (Exception e) { + Log.i(TAG, "Failed to mount server.", e); + mCredentialCache.removeCredential(uri); + throw e; + } + + final CredentialTuple tuple = hasPassword + ? new CredentialTuple(workgroup, username, password) + : CredentialTuple.EMPTY_TUPLE; + final String serverString = encode(uri, tuple); + if (serverString == null) { + throw new IllegalStateException("Failed to encode credential tuple."); + } + // TODO: Add encryption + mServerStringSet.add(serverString); + mPref.edit().putStringSet(SERVER_STRING_SET_KEY, mServerStringSet).apply(); + + mServerStringMap.put(uri, serverString); + notifyServerChange(); + } + + public synchronized boolean unmountServer(String uri) { + if (!mServerStringMap.containsKey(uri)) { + return true; + } + + if (!mServerStringSet.remove(mServerStringMap.get(uri))) { + Log.e(TAG, "Failed to remove server " + uri); + return false; + } + + mServerStringMap.remove(uri); + + mCredentialCache.removeCredential(uri); + + notifyServerChange(); + + return true; + } + + @Override + public synchronized Iterator iterator() { + // Create a deep copy of current set to avoid modification on iteration. + return new ArrayList<>(mServerStringMap.keySet()).iterator(); + } + + public synchronized int size() { + return mServerStringMap.size(); + } + + public synchronized boolean containsShare(String uri) { + return mServerStringMap.containsKey(uri); + } + + private void notifyServerChange() { + for (int i = mListeners.size() - 1; i >= 0; --i) { + mListeners.get(i).onMountedServerChange(); + } + } + + public void addListener(MountedShareChangeListener listener) { + mListeners.add(listener); + } + + public void removeListener(MountedShareChangeListener listener) { + mListeners.remove(listener); + } + + private static String encode(String uri, CredentialTuple tuple) { + final StringWriter stringWriter = new StringWriter(); + try (final JsonWriter jsonWriter = new JsonWriter(stringWriter)) { + jsonWriter.beginObject(); + jsonWriter.name(URI_KEY).value(uri); + + jsonWriter.name(CREDENTIAL_TUPLE_KEY); + encodeTuple(jsonWriter, tuple); + jsonWriter.endObject(); + } catch (IOException e) { + Log.e(TAG, "Failed to encode credential for " + uri); + return null; + } + + return stringWriter.toString(); + } + + private static void encodeTuple(JsonWriter writer, CredentialTuple tuple) throws IOException { + if (tuple == CredentialTuple.EMPTY_TUPLE) { + writer.nullValue(); + } else { + writer.beginObject(); + writer.name(WORKGROUP_KEY).value(tuple.mWorkgroup); + writer.name(USERNAME_KEY).value(tuple.mUsername); + writer.name(PASSWORD_KEY).value(tuple.mPassword); + writer.endObject(); + } + } + + private static String decode(String content, Map credentialMap) { + final StringReader stringReader = new StringReader(content); + try (final JsonReader jsonReader = new JsonReader(stringReader)) { + jsonReader.beginObject(); + + String uri = null; + CredentialTuple tuple = null; + while (jsonReader.hasNext()) { + final String name = jsonReader.nextName(); + switch (name) { + case URI_KEY: + uri = jsonReader.nextString(); + break; + case CREDENTIAL_TUPLE_KEY: + tuple = decodeTuple(jsonReader); + break; + default: + Log.w(TAG, "Ignoring unknown key " + name); + } + } + jsonReader.endObject(); + + if (uri == null || tuple == null) { + throw new IllegalStateException("Either uri or tuple is null."); + } + credentialMap.put(uri, tuple); + + return uri; + } catch (IOException e) { + Log.e(TAG, "Failed to load credential."); + return null; + } + } + + private static CredentialTuple decodeTuple(JsonReader reader) throws IOException { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull(); + return CredentialTuple.EMPTY_TUPLE; + } + + String workgroup = null; + String username = null; + String password = null; + + reader.beginObject(); + while (reader.hasNext()) { + String name = reader.nextName(); + String value = reader.nextString(); + + switch (name) { + case WORKGROUP_KEY: + workgroup = value; + break; + case USERNAME_KEY: + username = value; + break; + case PASSWORD_KEY: + password = value; + break; + default: + Log.w(TAG, "Ignoring unknown key " + name); + } + } + reader.endObject(); + + return new CredentialTuple(workgroup, username, password); + } + + private static class CredentialTuple { + private static final CredentialTuple EMPTY_TUPLE = new CredentialTuple("", "", ""); + + private final String mWorkgroup; + private final String mUsername; + private final String mPassword; + + private CredentialTuple(String workgroup, String username, String password) { + if (workgroup == null) { + throw new IllegalArgumentException("workgroup is null."); + } + if (username == null) { + throw new IllegalArgumentException("username is null."); + } + if (password == null) { + throw new IllegalArgumentException("password is null."); + } + mWorkgroup = workgroup; + mUsername = username; + mPassword = password; + } + } + + public interface ShareMountChecker { + void checkShareMounting() throws IOException; + } + + public interface MountedShareChangeListener { + void onMountedServerChange(); + } +} diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/TaskManager.java b/app/src/main/java/com/google/android/sambadocumentsprovider/TaskManager.java new file mode 100644 index 0000000..e53176e --- /dev/null +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/TaskManager.java @@ -0,0 +1,55 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.google.android.sambadocumentsprovider; + +import android.net.Uri; +import android.os.AsyncTask; +import android.os.AsyncTask.Status; +import android.util.Log; +import com.google.android.sambadocumentsprovider.provider.ReadFileTask; +import com.google.android.sambadocumentsprovider.provider.WriteFileTask; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +public class TaskManager { + + private static final String TAG = "TaskManager"; + + private final Map mTasks = new HashMap<>(); + + private final Executor mExecutor = Executors.newCachedThreadPool(); + + public void runTask(Uri uri, AsyncTask task, T... args) { + synchronized (mTasks) { + if (!mTasks.containsKey(uri) || mTasks.get(uri).getStatus() == Status.FINISHED) { + mTasks.put(uri, task); + // TODO: Use different executor for different servers. + task.executeOnExecutor(mExecutor, args); + } else { + Log.i(TAG, + "Ignore this task for " + uri + " to avoid running multiple updates at the same time."); + } + } + } + + public void runIoTask(AsyncTask task) { + task.executeOnExecutor(mExecutor); + } +} diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/base/AuthFailedException.java b/app/src/main/java/com/google/android/sambadocumentsprovider/base/AuthFailedException.java new file mode 100644 index 0000000..609171d --- /dev/null +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/base/AuthFailedException.java @@ -0,0 +1,22 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.google.android.sambadocumentsprovider.base; + +public class AuthFailedException extends SecurityException { + +} diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/base/BiResultTask.java b/app/src/main/java/com/google/android/sambadocumentsprovider/base/BiResultTask.java new file mode 100644 index 0000000..922b0b6 --- /dev/null +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/base/BiResultTask.java @@ -0,0 +1,54 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.google.android.sambadocumentsprovider.base; + +import android.os.AsyncTask; +import android.support.annotation.IntDef; +import android.support.annotation.Nullable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +public abstract class BiResultTask + extends AsyncTask { + + private volatile Exception mException; + + public abstract Result run(Param... params) throws Exception; + + @Override + public final @Nullable Result doInBackground(Param... params) { + try { + return run(params); + } catch (Exception e) { + mException = e; + return null; + } + } + + public abstract void onSucceeded(Result result); + public void onFailed(Exception exception) {} + + @Override + public final void onPostExecute(@Nullable Result result) { + if (mException == null) { + onSucceeded(result); + } else { + onFailed(mException); + } + } +} diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/base/DirectoryEntry.java b/app/src/main/java/com/google/android/sambadocumentsprovider/base/DirectoryEntry.java new file mode 100644 index 0000000..5c0b03a --- /dev/null +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/base/DirectoryEntry.java @@ -0,0 +1,68 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.google.android.sambadocumentsprovider.base; + +import android.support.annotation.IntDef; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Denotes a directory entry in Samba. This can represent not only a directory in Samba, but also + * a workgroup, a share or a file. + */ +public class DirectoryEntry { + + @IntDef({WORKGROUP, SERVER, FILE_SHARE, PRINTER_SHARE, COMMS_SHARE, IPC_SHARE, DIR, FILE, LINK}) + @Retention(RetentionPolicy.SOURCE) + @interface Type {} + public static final int WORKGROUP = 1; + public static final int SERVER = 2; + public static final int FILE_SHARE = 3; + public static final int PRINTER_SHARE = 4; + public static final int COMMS_SHARE = 5; + public static final int IPC_SHARE = 6; + public static final int DIR = 7; + public static final int FILE = 8; + public static final int LINK = 9; + + private final int mType; + private final String mComment; + private String mName; + + public DirectoryEntry(@Type int type, String comment, String name) { + mType = type; + mComment = comment; + mName = name; + } + + public @Type int getType() { + return mType; + } + + public String getComment() { + return mComment; + } + + public String getName() { + return mName; + } + + public void setName(String newName) { + mName = newName; + } +} diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/base/DocumentCursor.java b/app/src/main/java/com/google/android/sambadocumentsprovider/base/DocumentCursor.java new file mode 100644 index 0000000..8e7d3e2 --- /dev/null +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/base/DocumentCursor.java @@ -0,0 +1,67 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.google.android.sambadocumentsprovider.base; + +import android.database.Cursor; +import android.database.MatrixCursor; +import android.os.AsyncTask; +import android.os.AsyncTask.Status; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.util.Log; + +/** + * Use this class to avoid using {@link Cursor#setExtras(Bundle)} on API level < 23. + */ +public class DocumentCursor extends MatrixCursor { + + private static final String TAG = "DocumentCursor"; + + private Bundle mExtra; + private AsyncTask mLoadingTask; + + public DocumentCursor(String[] projection) { + super(projection); + } + + public void setLoadingTask(AsyncTask task) { + mLoadingTask = task; + } + + @Override + public void setExtras(Bundle extras) { + mExtra = extras; + } + + @Override + public Bundle getExtras() { + return mExtra; + } + + @Override + public void close() { + super.close(); + if (mLoadingTask != null && mLoadingTask.getStatus() != Status.FINISHED) { + Log.d(TAG, "Cursor is closed. Cancel the loading task " + mLoadingTask); + // Interrupting the task is not a good choice as it's waiting for the Samba client thread + // returning the result. Interrupting the task only frees the task from waiting for the + // result, rather than freeing the Samba client thread doing the hard work. + mLoadingTask.cancel(false); + } + } +} diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/base/DocumentIdHelper.java b/app/src/main/java/com/google/android/sambadocumentsprovider/base/DocumentIdHelper.java new file mode 100644 index 0000000..83357af --- /dev/null +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/base/DocumentIdHelper.java @@ -0,0 +1,48 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.google.android.sambadocumentsprovider.base; + +import android.net.Uri; +import com.google.android.sambadocumentsprovider.BuildConfig; +import com.google.android.sambadocumentsprovider.document.DocumentMetadata; + +public class DocumentIdHelper { + + private DocumentIdHelper() {} + + public static String toRootId(DocumentMetadata metadata) { + if (BuildConfig.DEBUG && !metadata.isFileShare()) { + throw new RuntimeException(metadata + " is not a file share."); + } + + return metadata.getUri().toString(); + } + + public static String toDocumentId(Uri smbUri) { + // TODO: Change document ID to infer root. + return smbUri.toString(); + } + + public static Uri toUri(String documentId) { + return Uri.parse(toUriString(documentId)); + } + + public static String toUriString(String documentId) { + return documentId; + } +} diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/base/OnTaskFinishedCallback.java b/app/src/main/java/com/google/android/sambadocumentsprovider/base/OnTaskFinishedCallback.java new file mode 100644 index 0000000..04ee1b8 --- /dev/null +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/base/OnTaskFinishedCallback.java @@ -0,0 +1,36 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.google.android.sambadocumentsprovider.base; + +import android.support.annotation.IntDef; +import android.support.annotation.Nullable; +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +public interface OnTaskFinishedCallback { + + @IntDef({ SUCCEEDED, FAILED, CANCELLED }) + @Retention(RetentionPolicy.SOURCE) + @interface Status {} + int SUCCEEDED = 0; + int FAILED = 1; + int CANCELLED = 2; + + void onTaskFinished(@Status int status, @Nullable T item, @Nullable Exception exception); +} diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/cache/CacheResult.java b/app/src/main/java/com/google/android/sambadocumentsprovider/cache/CacheResult.java new file mode 100644 index 0000000..1704bec --- /dev/null +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/cache/CacheResult.java @@ -0,0 +1,77 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.google.android.sambadocumentsprovider.cache; + +import android.os.Build; +import android.support.annotation.IntDef; +import android.support.annotation.Nullable; +import android.support.v4.util.Pools.Pool; +import android.support.v4.util.Pools.SimplePool; +import android.support.v4.util.Pools.SynchronizedPool; +import com.google.android.sambadocumentsprovider.BuildConfig; +import com.google.android.sambadocumentsprovider.document.DocumentMetadata; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +public class CacheResult implements AutoCloseable { + + @IntDef({CACHE_MISS, CACHE_HIT, CACHE_EXPIRED}) + @Retention(RetentionPolicy.SOURCE) + @interface State {} + public static final int CACHE_MISS = 0; + public static final int CACHE_HIT = 1; + public static final int CACHE_EXPIRED = 2; + + private static final Pool POOL = new SynchronizedPool<>(10); + + private @State int mState; + private @Nullable DocumentMetadata mItem; + + private CacheResult() {} + + public @State int getState() { + return mState; + } + + public DocumentMetadata getItem() { + return mItem; + } + + static CacheResult obtain(int state, @Nullable DocumentMetadata item) { + CacheResult result = POOL.acquire(); + if (result == null) { + result = new CacheResult(); + } + result.mState = state; + result.mItem = item; + + return result; + } + + public void recycle() { + mState = CACHE_MISS; + mItem = null; + boolean recycled = POOL.release(this); + if (BuildConfig.DEBUG && !recycled) throw new IllegalStateException("One item is not enough!"); + } + + @Override + public void close() { + recycle(); + } +} diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/cache/DocumentCache.java b/app/src/main/java/com/google/android/sambadocumentsprovider/cache/DocumentCache.java new file mode 100644 index 0000000..fa6beaf --- /dev/null +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/cache/DocumentCache.java @@ -0,0 +1,78 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.google.android.sambadocumentsprovider.cache; + +import android.net.Uri; +import com.google.android.sambadocumentsprovider.document.DocumentMetadata; +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +public class DocumentCache { + + private static final long CACHE_EXPIRATION = TimeUnit.MILLISECONDS.convert(1, TimeUnit.MINUTES); + private final Map mCache = new ConcurrentHashMap<>(); + private final Map mExceptionCache = new ConcurrentHashMap<>(); + + public CacheResult get(Uri uri) { + DocumentMetadata metadata = mCache.get(uri); + if (metadata == null) { + return CacheResult.obtain(CacheResult.CACHE_MISS, null); + } + + if (metadata.getTimeStamp() + CACHE_EXPIRATION < System.currentTimeMillis()) { + return CacheResult.obtain(CacheResult.CACHE_EXPIRED, metadata); + } + + return CacheResult.obtain(CacheResult.CACHE_HIT, metadata); + } + + public void throwLastExceptionIfAny(Uri uri) throws Exception { + if (mExceptionCache.containsKey(uri)) { + Exception e = mExceptionCache.get(uri); + mExceptionCache.remove(uri); + throw e; + } + } + + public void put(DocumentMetadata metadata) { + mCache.put(metadata.getUri(), metadata); + + final Uri parentUri = DocumentMetadata.buildParentUri(metadata.getUri()); + final DocumentMetadata parentMetadata = mCache.get(parentUri); + if (parentMetadata != null) { + parentMetadata.putChild(metadata); + } + } + + public void put(Uri uri, Exception e) { + mExceptionCache.put(uri, e); + } + + public void remove(Uri uri) { + mCache.remove(uri); + mExceptionCache.remove(uri); + + final Uri parentUri = DocumentMetadata.buildParentUri(uri); + final DocumentMetadata parentMetadata = mCache.get(parentUri); + if (parentMetadata != null && parentMetadata.getChildren() != null) { + parentMetadata.getChildren().remove(uri); + } + } +} diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/document/DocumentMetadata.java b/app/src/main/java/com/google/android/sambadocumentsprovider/document/DocumentMetadata.java new file mode 100644 index 0000000..d46f5fa --- /dev/null +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/document/DocumentMetadata.java @@ -0,0 +1,322 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.google.android.sambadocumentsprovider.document; + +import android.net.Uri; +import android.provider.DocumentsContract.Document; +import android.support.annotation.Nullable; +import android.system.OsConstants; +import android.system.StructStat; +import android.text.TextUtils; +import android.util.Log; +import android.webkit.MimeTypeMap; +import com.google.android.sambadocumentsprovider.base.DirectoryEntry; +import com.google.android.sambadocumentsprovider.nativefacade.SmbClient; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +/** + * This is a snapshot of the metadata of a seen document. It contains its SMB URI, display name, + * access/create/modified time, size, its children etc sometime in the past. It also contains + * the last exception thrown when querying its children. + * + * The metadata inside this class may be fetched at different time due to Samba client API. + */ +public class DocumentMetadata { + + private static final String TAG = "DocumentMetadata"; + private static final String GENERIC_MIME_TYPE = "application/octet-stream"; + private static final Uri SMB_BASE_URI = Uri.parse("smb://"); + + private final DirectoryEntry mEntry; + private Uri mUri; + + private final AtomicReference mStat = new AtomicReference<>(null); + private final AtomicReference> mChildren = new AtomicReference<>(null); + + private final AtomicReference mLastChildUpdateException = new AtomicReference<>(null); + private final AtomicReference mLastStatException = new AtomicReference<>(null); + private long mTimeStamp; + + public DocumentMetadata(Uri uri, DirectoryEntry entry) { + mUri = uri; + mEntry = entry; + + mTimeStamp = System.currentTimeMillis(); + } + + public Uri getUri() { + return mUri; + } + + public boolean isFileShare() { + return mEntry.getType() == DirectoryEntry.FILE_SHARE; + } + + public Long getLastModified() { + final StructStat stat = mStat.get(); + return (stat == null) ? null : TimeUnit.MILLISECONDS.convert(stat.st_mtime, TimeUnit.SECONDS); + } + + public String getDisplayName() { + return mEntry.getName(); + } + + public String getComment() { + return mEntry.getComment(); + } + + public boolean needsStat() { + return hasStat() && mStat.get() == null; + } + + private boolean hasStat() { + switch (mEntry.getType()) { + case DirectoryEntry.FILE: + return true; + case DirectoryEntry.WORKGROUP: + case DirectoryEntry.SERVER: + case DirectoryEntry.FILE_SHARE: + case DirectoryEntry.DIR: + // Everything is writable so no need to fetch stats for them. + return false; + default: + throw new UnsupportedOperationException( + "Unsupported type of Samba directory entry: " + mEntry.getType()); + } + } + + public Long getSize() { + final StructStat stat = mStat.get(); + return (stat == null) ? null : stat.st_size; + } + + public boolean canCreateDocument() { + switch (mEntry.getType()) { + case DirectoryEntry.DIR: + case DirectoryEntry.FILE_SHARE: + return true; + case DirectoryEntry.WORKGROUP: + case DirectoryEntry.SERVER: + case DirectoryEntry.FILE: + return false; + default: + throw new UnsupportedOperationException( + "Unsupported type of Samba directory entry " + mEntry.getType()); + } + } + + public String getMimeType() { + switch (mEntry.getType()) { + case DirectoryEntry.FILE_SHARE: + case DirectoryEntry.WORKGROUP: + case DirectoryEntry.SERVER: + case DirectoryEntry.DIR: + return Document.MIME_TYPE_DIR; + + case DirectoryEntry.LINK: + case DirectoryEntry.COMMS_SHARE: + case DirectoryEntry.IPC_SHARE: + case DirectoryEntry.PRINTER_SHARE: + throw new UnsupportedOperationException( + "Unsupported type of Samba directory entry " + mEntry.getType()); + + case DirectoryEntry.FILE: + final String ext = getExtension(mEntry.getName()); + if (ext == null) { + return GENERIC_MIME_TYPE; + } + + final String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext); + return (mimeType == null) ? GENERIC_MIME_TYPE : mimeType; + } + + throw new IllegalStateException("Should never reach here."); + } + + private String getExtension(String name) { + if (TextUtils.isEmpty(name)) { + return null; + } + + final int idxOfDot = name.lastIndexOf('.', name.length() - 1); + if (idxOfDot <= 0) { + return null; + } + + return name.substring(idxOfDot + 1).toLowerCase(); + } + + public void reset() { + mStat.set(null); + mChildren.set(null); + } + + public long getTimeStamp() { + return mTimeStamp; + } + + public void throwLastChildUpdateExceptionIfAny() throws Exception { + final Exception e = mLastChildUpdateException.get(); + if (e != null) { + mLastChildUpdateException.set(null); + throw e; + } + } + + public boolean hasLoadingStatFailed() { + final Exception e = mLastStatException.getAndSet(null); + return e != null; + } + + public void rename(Uri newUri) { + mEntry.setName(newUri.getLastPathSegment()); + mUri = newUri; + } + + /** + * Gets children of this document. + * @return the list of children or {@code null} if it's not fetched yet. + */ + public @Nullable Map getChildren() { + return mChildren.get(); + } + + public void loadChildren(SmbClient client) throws IOException { + try { + List entries = client.readDir(mUri.toString()); + + Map children = new HashMap<>(entries.size()); + for (DirectoryEntry entry : entries) { + Uri childUri = DocumentMetadata.buildChildUri(mUri, entry); + if (childUri != null) { + children.put(childUri, new DocumentMetadata(childUri, entry)); + } + } + + mChildren.set(children); + mTimeStamp = System.currentTimeMillis(); + + } catch (Exception e) { + Log.e(TAG, "Failed to load children.", e); + mLastChildUpdateException.set(e); + throw e; + } + } + + public void putChild(DocumentMetadata child) { + Map children = mChildren.get(); + if (children != null) { + children.put(child.getUri(), child); + } + } + + void loadStat(SmbClient client) throws IOException { + try { + mStat.set(client.stat(mUri.toString())); + + mTimeStamp = System.currentTimeMillis(); + } catch (Exception e) { + Log.e(TAG, "Failed to get stat.", e); + mLastStatException.set(e); + throw e; + } + } + + public static Uri buildChildUri(Uri parentUri, DirectoryEntry entry) { + switch (entry.getType()) { + // TODO: Support LINK type? + case DirectoryEntry.LINK: + case DirectoryEntry.COMMS_SHARE: + case DirectoryEntry.IPC_SHARE: + case DirectoryEntry.PRINTER_SHARE: + Log.i(TAG, "Found unsupported type: " + entry.getType() + + " name: " + entry.getName() + + " comment: " + entry.getComment()); + return null; + case DirectoryEntry.WORKGROUP: + case DirectoryEntry.SERVER: + return SMB_BASE_URI.buildUpon().authority(entry.getName()).build(); + case DirectoryEntry.FILE_SHARE: + case DirectoryEntry.DIR: + case DirectoryEntry.FILE: + return buildChildUri(parentUri, entry.getName()); + } + + Log.w(TAG, "Unknown type: " + entry.getType() + + " name: " + entry.getName() + + " comment: " + entry.getComment()); + return null; + } + + public static Uri buildChildUri(Uri parentUri, String displayName) { + if (".".equals(displayName) || "..".equals(displayName)) { + return null; + } else { + return parentUri.buildUpon().appendPath(displayName).build(); + } + } + + public static Uri buildParentUri(Uri childUri) { + final List segments = childUri.getPathSegments(); + if (segments.isEmpty()) { + // This is possibly a server or a workgroup. We don't know its exact parent, so just return + // "smb://". + return SMB_BASE_URI; + } + + Uri.Builder builder = SMB_BASE_URI.buildUpon().authority(childUri.getAuthority()); + for (int i = 0; i < segments.size() - 1; ++i) { + builder.appendPath(segments.get(i)); + } + return builder.build(); + } + + public static DocumentMetadata fromUri(Uri uri, SmbClient client) throws IOException { + final List pathSegments = uri.getPathSegments(); + if (pathSegments.isEmpty()) { + throw new UnsupportedOperationException("Can't load metadata for workgroup or server."); + } + + final StructStat stat = client.stat(uri.toString()); + final DirectoryEntry entry = new DirectoryEntry( + OsConstants.S_ISDIR(stat.st_mode) ? DirectoryEntry.DIR : DirectoryEntry.FILE, + "", + uri.getLastPathSegment()); + final DocumentMetadata metadata = new DocumentMetadata(uri, entry); + metadata.mStat.set(stat); + + return metadata; + } + + public static DocumentMetadata createShare(String host, String share) { + final Uri uri = SMB_BASE_URI.buildUpon().authority(host).encodedPath(share).build(); + return createShare(uri); + } + + public static DocumentMetadata createShare(Uri uri) { + final DirectoryEntry entry = + new DirectoryEntry(DirectoryEntry.FILE_SHARE, "", uri.getLastPathSegment()); + return new DocumentMetadata(uri, entry); + } +} diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/document/LoadChildrenTask.java b/app/src/main/java/com/google/android/sambadocumentsprovider/document/LoadChildrenTask.java new file mode 100644 index 0000000..0ef628b --- /dev/null +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/document/LoadChildrenTask.java @@ -0,0 +1,74 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.google.android.sambadocumentsprovider.document; + +import android.net.Uri; +import com.google.android.sambadocumentsprovider.cache.DocumentCache; +import com.google.android.sambadocumentsprovider.base.BiResultTask; +import com.google.android.sambadocumentsprovider.base.OnTaskFinishedCallback; +import com.google.android.sambadocumentsprovider.nativefacade.SmbClient; +import java.io.IOException; +import java.util.Map; + +public class LoadChildrenTask extends BiResultTask> { + + private final DocumentMetadata mMetadata; + private final DocumentCache mCache; + private final SmbClient mClient; + private final OnTaskFinishedCallback mCallback; + + public LoadChildrenTask(DocumentMetadata metadata, SmbClient client, + DocumentCache cache, OnTaskFinishedCallback callback) { + mMetadata = metadata; + mCache = cache; + mClient = client; + mCallback = callback; + } + + @Override + public Map run(Void... args) throws IOException { + mMetadata.loadChildren(mClient); + + return mMetadata.getChildren(); + } + + private void onFinish(Map children) { + for (DocumentMetadata metadata : children.values()) { + mCache.put(metadata); + } + } + + @Override + public void onSucceeded(Map children) { + onFinish(children); + mCallback.onTaskFinished(OnTaskFinishedCallback.SUCCEEDED, mMetadata, null); + } + + @Override + public void onFailed(Exception e) { + mCallback.onTaskFinished(OnTaskFinishedCallback.FAILED, mMetadata, e); + } + + @Override + public void onCancelled(Map children) { + if (children != null) { + onFinish(children); + mCallback.onTaskFinished(OnTaskFinishedCallback.CANCELLED, mMetadata, null); + } + } +} diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/document/LoadDocumentTask.java b/app/src/main/java/com/google/android/sambadocumentsprovider/document/LoadDocumentTask.java new file mode 100644 index 0000000..51555e2 --- /dev/null +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/document/LoadDocumentTask.java @@ -0,0 +1,69 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.google.android.sambadocumentsprovider.document; + +import android.net.Uri; +import com.google.android.sambadocumentsprovider.base.BiResultTask; +import com.google.android.sambadocumentsprovider.base.OnTaskFinishedCallback; +import com.google.android.sambadocumentsprovider.cache.DocumentCache; +import com.google.android.sambadocumentsprovider.nativefacade.SmbClient; + +public class LoadDocumentTask extends BiResultTask { + + private final Uri mUri; + private final SmbClient mClient; + private final DocumentCache mCache; + private final OnTaskFinishedCallback mCallback; + + public LoadDocumentTask(Uri uri, SmbClient client, DocumentCache cache, + OnTaskFinishedCallback callback) { + mUri = uri; + mClient = client; + mCache = cache; + mCallback = callback; + } + + @Override + public DocumentMetadata run(Void... args) throws Exception { + return DocumentMetadata.fromUri(mUri, mClient); + } + + @Override + public void onSucceeded(DocumentMetadata documentMetadata) { + mCache.put(documentMetadata); + + mCallback.onTaskFinished(OnTaskFinishedCallback.SUCCEEDED, mUri, null); + } + + @Override + public void onFailed(Exception e) { + mCache.put(mUri, e); + + mCallback.onTaskFinished(OnTaskFinishedCallback.FAILED, mUri, e); + } + + @Override + public void onCancelled(DocumentMetadata metadata) { + if (metadata != null) { + // This is still valid result. Don't waste the hard work. + mCache.put(metadata); + + mCallback.onTaskFinished(OnTaskFinishedCallback.FAILED, mUri, null); + } + } +} diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/document/LoadStatTask.java b/app/src/main/java/com/google/android/sambadocumentsprovider/document/LoadStatTask.java new file mode 100644 index 0000000..cc395c7 --- /dev/null +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/document/LoadStatTask.java @@ -0,0 +1,73 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.google.android.sambadocumentsprovider.document; + +import android.net.Uri; +import android.os.AsyncTask; +import android.system.StructStat; +import android.util.Log; +import com.google.android.sambadocumentsprovider.base.OnTaskFinishedCallback; +import com.google.android.sambadocumentsprovider.nativefacade.SmbClient; +import java.util.HashMap; +import java.util.Map; + +public class LoadStatTask extends AsyncTask> { + + private static final String TAG = "LoadStatTask"; + + private final Map mMetadataMap; + private final SmbClient mClient; + private final OnTaskFinishedCallback> mCallback; + + public LoadStatTask( + Map metadataMap, + SmbClient client, + OnTaskFinishedCallback> callback) { + mMetadataMap = metadataMap; + mClient = client; + mCallback = callback; + } + + @Override + public Map doInBackground(Void... args) { + Map stats = new HashMap<>(mMetadataMap.size()); + for (DocumentMetadata metadata : mMetadataMap.values()) { + try { + metadata.loadStat(mClient); + if (isCancelled()) { + return stats; + } + } catch(Exception e) { + // Failed to load a stat for a child... Just eat this exception, the only consequence it may + // have is constantly retrying to fetch the stat. + Log.e(TAG, "Failed to load stat for " + metadata.getUri()); + } + } + return stats; + } + + @Override + public void onPostExecute(Map stats) { + mCallback.onTaskFinished(OnTaskFinishedCallback.SUCCEEDED, mMetadataMap, null); + } + + @Override + public void onCancelled(Map stats) { + mCallback.onTaskFinished(OnTaskFinishedCallback.CANCELLED, mMetadataMap, null); + } +} diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/mount/MountServerActivity.java b/app/src/main/java/com/google/android/sambadocumentsprovider/mount/MountServerActivity.java new file mode 100644 index 0000000..1b692f6 --- /dev/null +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/mount/MountServerActivity.java @@ -0,0 +1,349 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.google.android.sambadocumentsprovider.mount; + +import static com.google.android.sambadocumentsprovider.base.DocumentIdHelper.toRootId; + +import android.app.ProgressDialog; +import android.content.ActivityNotFoundException; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.Uri; +import android.provider.DocumentsContract; +import android.support.annotation.Nullable; +import android.support.annotation.StringRes; +import android.support.design.widget.Snackbar; +import android.support.v7.app.AppCompatActivity; +import android.os.Bundle; +import android.text.TextUtils; +import android.text.method.LinkMovementMethod; +import android.util.Log; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnKeyListener; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; +import com.google.android.sambadocumentsprovider.R; +import com.google.android.sambadocumentsprovider.SambaProviderApplication; +import com.google.android.sambadocumentsprovider.ShareManager; +import com.google.android.sambadocumentsprovider.TaskManager; +import com.google.android.sambadocumentsprovider.base.AuthFailedException; +import com.google.android.sambadocumentsprovider.base.OnTaskFinishedCallback; +import com.google.android.sambadocumentsprovider.cache.DocumentCache; +import com.google.android.sambadocumentsprovider.document.DocumentMetadata; +import com.google.android.sambadocumentsprovider.nativefacade.SmbClient; +import com.google.android.sambadocumentsprovider.provider.SambaDocumentsProvider; +import java.util.List; + +public class MountServerActivity extends AppCompatActivity { + + private static final String TAG = "MountServerActivity"; + + private static final String ACTION_BROWSE = "android.provider.action.BROWSE"; + + private static final String SHARE_PATH_KEY = "sharePath"; + private static final String NEEDS_PASSWORD_KEY = "needsPassword"; + private static final String DOMAIN_KEY = "domain"; + private static final String USERNAME_KEY = "username"; + private static final String PASSWORD_KEY = "password"; + + private final OnClickListener mPasswordStateChangeListener = new OnClickListener() { + @Override + public void onClick(View view) { + final boolean isChecked = mNeedPasswordCheckbox.isChecked(); + setNeedsPasswordState(isChecked); + } + }; + + private final OnClickListener mMountListener = new OnClickListener() { + @Override + public void onClick(View view) { + tryMount(); + } + }; + + private final OnKeyListener mMountKeyListener = new OnKeyListener() { + @Override + public boolean onKey(View view, int i, KeyEvent keyEvent) { + if (keyEvent.getAction() == KeyEvent.ACTION_UP + && keyEvent.getKeyCode() == KeyEvent.KEYCODE_ENTER) { + tryMount(); + return true; + } + return false; + } + }; + + private DocumentCache mCache; + private TaskManager mTaskManager; + private ShareManager mShareManager; + private SmbClient mClient; + + private CheckBox mNeedPasswordCheckbox; + private View mPasswordHideGroup; + + private EditText mSharePathEditText; + private EditText mDomainEditText; + private EditText mUsernameEditText; + private EditText mPasswordEditText; + + private ConnectivityManager mConnectivityManager; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + mCache = SambaProviderApplication.getDocumentCache(this); + mTaskManager = SambaProviderApplication.getTaskManager(this); + mShareManager = SambaProviderApplication.getServerManager(this); + mClient = SambaProviderApplication.getSambaClient(this); + + mNeedPasswordCheckbox = (CheckBox) findViewById(R.id.needs_password); + mNeedPasswordCheckbox.setOnClickListener(mPasswordStateChangeListener); + + mPasswordHideGroup = findViewById(R.id.password_hide_group); + + mSharePathEditText = (EditText) findViewById(R.id.share_path); + mSharePathEditText.setOnKeyListener(mMountKeyListener); + + mUsernameEditText = (EditText) findViewById(R.id.username); + mDomainEditText = (EditText) findViewById(R.id.domain); + mPasswordEditText = (EditText) findViewById(R.id.password); + mPasswordEditText.setOnKeyListener(mMountKeyListener); + + final Button mount = (Button) findViewById(R.id.mount); + mount.setOnClickListener(mMountListener); + + final Button cancel = (Button) findViewById(R.id.cancel); + cancel.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + finish(); + } + }); + + // Set MovementMethod to make it respond to clicks on hyperlinks + final TextView gplv3Link = (TextView) findViewById(R.id.gplv3_link); + gplv3Link.setMovementMethod(LinkMovementMethod.getInstance()); + + mConnectivityManager = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE); + + restoreSavedInstanceState(savedInstanceState); + } + + private void restoreSavedInstanceState(@Nullable Bundle savedInstanceState) { + if (savedInstanceState == null) { + return; + } + + mSharePathEditText.setText(savedInstanceState.getString(SHARE_PATH_KEY, "")); + final boolean needsPassword = savedInstanceState.getBoolean(NEEDS_PASSWORD_KEY); + setNeedsPasswordState(needsPassword); + if (needsPassword) { + mDomainEditText.setText(savedInstanceState.getString(DOMAIN_KEY, "")); + mUsernameEditText.setText(savedInstanceState.getString(USERNAME_KEY, "")); + mPasswordEditText.setText(savedInstanceState.getString(PASSWORD_KEY, "")); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + outState.putString(SHARE_PATH_KEY, mSharePathEditText.getText().toString()); + final boolean needsPassword = mNeedPasswordCheckbox.isChecked(); + outState.putBoolean(NEEDS_PASSWORD_KEY, needsPassword); + if (needsPassword) { + outState.putString(DOMAIN_KEY, mDomainEditText.getText().toString()); + outState.putString(USERNAME_KEY, mUsernameEditText.getText().toString()); + outState.putString(PASSWORD_KEY, mPasswordEditText.getText().toString()); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.activity, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem menuItem) { + switch (menuItem.getItemId()) { + case R.id.send_feedback: + sendFeedback(); + return true; + } + + return false; + } + + private void sendFeedback() { + final String url = getString(R.string.feedback_link); + + try { + final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + } catch (ActivityNotFoundException e) { + Toast.makeText(this, R.string.no_web_browser, Toast.LENGTH_LONG).show(); + } + } + + private void tryMount() { + final NetworkInfo info = mConnectivityManager.getActiveNetworkInfo(); + if (info == null || !info.isConnected()) { + showMessage(R.string.no_active_network); + return; + } + + final String[] path = parseSharePath(); + if (path == null) { + showMessage(R.string.share_path_malformed); + return; + } + final String host = path[0]; + final String share = path[1]; + + final String domain = mDomainEditText.getText().toString(); + final String username = mUsernameEditText.getText().toString(); + final String password = mPasswordEditText.getText().toString(); + + final DocumentMetadata metadata = DocumentMetadata.createShare(host, share); + + if (mShareManager.containsShare(metadata.getUri().toString())) { + showMessage(R.string.share_already_mounted); + return; + } + + mCache.put(metadata); + + final ProgressDialog dialog = + ProgressDialog.show(this, null, getString(R.string.mounting_share), true); + final OnTaskFinishedCallback callback = new OnTaskFinishedCallback() { + @Override + public void onTaskFinished(@Status int status, @Nullable Void item, Exception exception) { + dialog.dismiss(); + switch (status) { + case SUCCEEDED: + clearInputs(); + launchFileManager(metadata); + showMessage(R.string.share_mounted); + break; + case FAILED: + mCache.remove(metadata.getUri()); + if ((exception instanceof AuthFailedException)) { + showMessage(R.string.credential_error); + } else { + showMessage(R.string.failed_mounting); + } + break; + } + } + }; + final MountServerTask task = new MountServerTask( + metadata, domain, username, password, mClient, mCache, mShareManager, callback); + mTaskManager.runTask(metadata.getUri(), task); + } + + private void showMessage(@StringRes int id) { + Snackbar.make(mNeedPasswordCheckbox, id, Snackbar.LENGTH_SHORT).show(); + } + + private void launchFileManager(DocumentMetadata metadata) { + final Uri rootUri = DocumentsContract.buildRootUri( + SambaDocumentsProvider.AUTHORITY, toRootId(metadata)); + + if (launchFileManager(Intent.ACTION_VIEW, rootUri)) { + return; + } + + if (launchFileManager(ACTION_BROWSE, rootUri)) { + return; + } + + Log.w(TAG, "Failed to find an activity to show mounted root."); + } + + private boolean launchFileManager(String action, Uri data) { + try { + final Intent intent = new Intent(action); + intent.addCategory(Intent.CATEGORY_DEFAULT); + intent.setData(data); + startActivity(intent); + return true; + } catch (ActivityNotFoundException e) { + return false; + } + } + + private void clearInputs() { + mSharePathEditText.setText(""); + clearCredentials(); + } + + private void clearCredentials() { + mDomainEditText.setText(""); + mUsernameEditText.setText(""); + mPasswordEditText.setText(""); + } + + private String[] parseSharePath() { + final String path = mSharePathEditText.getText().toString(); + if (path.startsWith("\\")) { + // Possibly Windows share path + final int endCharacter = path.endsWith("\\") ? path.length() - 1 : path.length(); + final String[] components = path.substring(2, endCharacter).split("\\\\"); + return components.length == 2 ? components : null; + } else { + // Try SMB URI + final Uri smbUri = Uri.parse(path); + + final String host = smbUri.getAuthority(); + if (TextUtils.isEmpty(host)) { + return null; + } + + final List pathSegments = smbUri.getPathSegments(); + if (pathSegments.size() != 1) { + return null; + } + final String share = pathSegments.get(0); + return new String[] { host, share }; + } + } + + private void setNeedsPasswordState(boolean needsPassword) { + mNeedPasswordCheckbox.setChecked(needsPassword); + + // TODO: Add animation + mPasswordHideGroup.setVisibility(needsPassword ? View.VISIBLE : View.GONE); + if (!needsPassword) { + clearCredentials(); + } + } +} diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/mount/MountServerTask.java b/app/src/main/java/com/google/android/sambadocumentsprovider/mount/MountServerTask.java new file mode 100644 index 0000000..3c2eadb --- /dev/null +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/mount/MountServerTask.java @@ -0,0 +1,99 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.google.android.sambadocumentsprovider.mount; + +import android.net.Uri; +import android.util.Log; +import com.google.android.sambadocumentsprovider.ShareManager; +import com.google.android.sambadocumentsprovider.ShareManager.ShareMountChecker; +import com.google.android.sambadocumentsprovider.base.BiResultTask; +import com.google.android.sambadocumentsprovider.base.OnTaskFinishedCallback; +import com.google.android.sambadocumentsprovider.cache.DocumentCache; +import com.google.android.sambadocumentsprovider.document.DocumentMetadata; +import com.google.android.sambadocumentsprovider.nativefacade.SmbClient; +import java.io.IOException; +import java.util.Map; + +class MountServerTask extends BiResultTask { + + private static final String TAG = "MountServerTask"; + + private final DocumentMetadata mMetadata; + private final String mDomain; + private final String mUsername; + private final String mPassword; + private final SmbClient mClient; + private final DocumentCache mCache; + private final ShareManager mShareManager; + private final OnTaskFinishedCallback mCallback; + + private final ShareMountChecker mChecker = new ShareMountChecker() { + @Override + public void checkShareMounting() throws IOException { + mMetadata.loadChildren(mClient); + } + }; + + MountServerTask( + DocumentMetadata metadata, + String domain, + String username, + String password, + SmbClient client, + DocumentCache cache, + ShareManager shareManager, + OnTaskFinishedCallback callback) { + mMetadata = metadata; + mDomain = domain; + mUsername = username; + mPassword = password; + mClient = client; + mCache = cache; + mShareManager = shareManager; + mCallback = callback; + } + + @Override + public Void run(Void... args) throws IOException { + mShareManager.mountServer( + mMetadata.getUri().toString(), mDomain, mUsername, mPassword, mChecker); + return null; + } + + @Override + public void onSucceeded(Void arg) { + final Map children = mMetadata.getChildren(); + for (DocumentMetadata metadata : children.values()) { + mCache.put(metadata); + } + + mCallback.onTaskFinished(OnTaskFinishedCallback.SUCCEEDED, null, null); + } + + @Override + public void onFailed(Exception e) { + Log.e(TAG, "Failed to mount share.", e); + mCallback.onTaskFinished(OnTaskFinishedCallback.FAILED, null, e); + } + + @Override + public void onCancelled(Void arg) { + // User cancelled the task, unmount it regardless of its result. + mShareManager.unmountServer(mMetadata.getUri().toString()); + } +} diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/nativefacade/BaseClient.java b/app/src/main/java/com/google/android/sambadocumentsprovider/nativefacade/BaseClient.java new file mode 100644 index 0000000..9157344 --- /dev/null +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/nativefacade/BaseClient.java @@ -0,0 +1,56 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.google.android.sambadocumentsprovider.nativefacade; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; + +abstract class BaseClient { + + BaseHandler mHandler; + + void enqueue(Message msg) { + try { + synchronized (msg.obj) { + mHandler.sendMessage(msg); + msg.obj.wait(); + } + } catch(InterruptedException e) { + // It should never happen. + throw new RuntimeException("Unexpected interruption.", e); + } + } + + abstract static class BaseHandler extends Handler { + + BaseHandler(Looper looper) { + super(looper); + } + + abstract void processMessage(Message msg); + + @Override + public void handleMessage(Message msg) { + synchronized (msg.obj) { + processMessage(msg); + msg.obj.notify(); + } + } + } +} diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/nativefacade/CredentialCache.java b/app/src/main/java/com/google/android/sambadocumentsprovider/nativefacade/CredentialCache.java new file mode 100644 index 0000000..d292b46 --- /dev/null +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/nativefacade/CredentialCache.java @@ -0,0 +1,23 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.google.android.sambadocumentsprovider.nativefacade; + +public interface CredentialCache { + void putCredential(String uri, String workgroup, String username, String password); + void removeCredential(String uri); +} diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/nativefacade/CredentialCacheClient.java b/app/src/main/java/com/google/android/sambadocumentsprovider/nativefacade/CredentialCacheClient.java new file mode 100644 index 0000000..53129ea --- /dev/null +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/nativefacade/CredentialCacheClient.java @@ -0,0 +1,100 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.google.android.sambadocumentsprovider.nativefacade; + +import android.os.Bundle; +import android.os.Looper; +import android.os.Message; +import android.support.annotation.IntDef; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +class CredentialCacheClient extends BaseClient implements CredentialCache { + + @IntDef( {PUT_CREDENTIAL, REMOVE_CREDENTIAL }) + @Retention(RetentionPolicy.SOURCE) + @interface Operation {} + private static final int PUT_CREDENTIAL = 1; + private static final int REMOVE_CREDENTIAL = 2; + + private static final String URI_KEY = "URI"; + private static final String WORKGROUP_KEY = "WORKGROUP"; + private static final String USERNAME_KEY = "USERNAME"; + private static final String PASSWORD_KEY = "PASSWORD"; + + CredentialCacheClient(Looper looper, CredentialCache credentialCacheImpl) { + mHandler = new CredentialCacheHandler(looper, credentialCacheImpl); + } + + @Override + public void putCredential(String uri, String workgroup, String username, String password) { + try (final MessageValues messageValues = MessageValues.obtain()) { + final Message msg = mHandler.obtainMessage(PUT_CREDENTIAL, messageValues); + + final Bundle args = msg.getData(); + args.putString(URI_KEY, uri); + args.putString(WORKGROUP_KEY, workgroup); + args.putString(USERNAME_KEY, username); + args.putString(PASSWORD_KEY, password); + + enqueue(msg); + } + } + + @Override + public void removeCredential(String uri) { + try (final MessageValues messageValues = MessageValues.obtain()) { + final Message msg = mHandler.obtainMessage(REMOVE_CREDENTIAL, messageValues); + + final Bundle args = msg.getData(); + args.putString(URI_KEY, uri); + + enqueue(msg); + } + } + + private static class CredentialCacheHandler extends BaseHandler { + + private CredentialCache mCredentialCacheImpl; + private CredentialCacheHandler(Looper looper, CredentialCache credentialCacheImpl) { + super(looper); + mCredentialCacheImpl = credentialCacheImpl; + } + + @Override + void processMessage(Message msg) { + final Bundle args = msg.peekData(); + final String uri = args.getString(URI_KEY); + switch (msg.what) { + case PUT_CREDENTIAL: { + final String workgroup = args.getString(WORKGROUP_KEY); + final String username = args.getString(USERNAME_KEY); + final String password = args.getString(PASSWORD_KEY); + mCredentialCacheImpl.putCredential(uri, workgroup, username, password); + break; + } + case REMOVE_CREDENTIAL: { + mCredentialCacheImpl.removeCredential(uri); + break; + } + default: + throw new UnsupportedOperationException("Unknown operation " + msg.what); + } + } + } +} diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/nativefacade/MessageValues.java b/app/src/main/java/com/google/android/sambadocumentsprovider/nativefacade/MessageValues.java new file mode 100644 index 0000000..4b2a865 --- /dev/null +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/nativefacade/MessageValues.java @@ -0,0 +1,94 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.google.android.sambadocumentsprovider.nativefacade; + +import android.support.v4.util.Pools.Pool; +import android.support.v4.util.Pools.SynchronizedPool; +import java.io.IOException; + +/** + * A class used for pass values between two sides of {@link SambaMessageLooper}. + * + * If it were C/C++, this would be a union type. + * + * @param A convenient parameterized type to avoid casting everywhere. + */ +class MessageValues implements AutoCloseable { + + private static final Pool> POOL = new SynchronizedPool<>(20); + + private volatile T mObj; + private volatile int mInt; + private volatile IOException mException; + private volatile RuntimeException mRuntimeException; + + private MessageValues() {} + + void checkException() throws IOException { + if (mException != null) { + throw mException; + } + if (mRuntimeException != null) { + throw mRuntimeException; + } + } + + T getObj() throws IOException { + checkException(); + return mObj; + } + + void setObj(T obj) { + mObj = obj; + } + + int getInt() throws IOException { + checkException(); + return mInt; + } + + void setInt(int value) { + mInt = value; + } + + void setException(IOException exception) { + mException = exception; + } + + void setRuntimeException(RuntimeException exception) { + mRuntimeException = exception; + } + + @SuppressWarnings("unchecked") + static MessageValues obtain() { + MessageValues response = POOL.acquire(); + if (response == null) { + response = new MessageValues<>(); + } + return (MessageValues) response; + } + + @Override + public void close() { + mObj = null; + mInt = 0; + mException = null; + mRuntimeException = null; + POOL.release(this); + } +} diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/nativefacade/NativeCredentialCache.java b/app/src/main/java/com/google/android/sambadocumentsprovider/nativefacade/NativeCredentialCache.java new file mode 100644 index 0000000..cd73e2f --- /dev/null +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/nativefacade/NativeCredentialCache.java @@ -0,0 +1,52 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.google.android.sambadocumentsprovider.nativefacade; + +class NativeCredentialCache implements CredentialCache { + + static { + System.loadLibrary("samba_client"); + } + + private long mNativeHandler; + + NativeCredentialCache() { + mNativeHandler = nativeInit(); + } + + long getNativeHandler() { + return mNativeHandler; + } + + @Override + public void putCredential(String uri, String workgroup, String username, String password) { + putCredential(mNativeHandler, uri, workgroup, username, password); + } + + @Override + public void removeCredential(String uri) { + removeCredential(mNativeHandler, uri); + } + + private native long nativeInit(); + + private native void putCredential( + long handler, String uri, String workgroup, String username, String password); + + private native void removeCredential(long handler, String uri); +} diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/nativefacade/NativeSambaFacade.java b/app/src/main/java/com/google/android/sambadocumentsprovider/nativefacade/NativeSambaFacade.java new file mode 100644 index 0000000..5b364a5 --- /dev/null +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/nativefacade/NativeSambaFacade.java @@ -0,0 +1,162 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.google.android.sambadocumentsprovider.nativefacade; + +import android.system.ErrnoException; +import android.system.StructStat; +import com.google.android.sambadocumentsprovider.BuildConfig; +import com.google.android.sambadocumentsprovider.base.DirectoryEntry; +import java.io.IOException; +import java.util.List; + +/** + * Java facade for libsmbclient native library. + * + * This class is not thread safe. + */ +class NativeSambaFacade implements SmbClient { + + private final long mCredentialCacheHandler; + private long mNativeHandler; + + static { + System.loadLibrary("samba_client"); + } + + NativeSambaFacade(NativeCredentialCache cache) { + mCredentialCacheHandler = cache.getNativeHandler(); + } + + private boolean isInitialized() { + return mNativeHandler != 0; + } + + @Override + public void reset() { + if (isInitialized()) { + nativeDestroy(mNativeHandler); + } + mNativeHandler = nativeInit(BuildConfig.DEBUG, mCredentialCacheHandler); + } + + @Override + public List readDir(String uri) throws IOException { + try { + checkNativeHandler(); + return readDir(mNativeHandler, uri); + } catch (ErrnoException e) { + throw new IOException("Failed to read directory " + uri, e); + } + } + + @Override + public StructStat stat(String uri) throws IOException { + try { + checkNativeHandler(); + return stat(mNativeHandler, uri); + } catch (ErrnoException e) { + throw new IOException("Failed to get stat of " + uri, e); + } + } + + @Override + public void createFile(String uri) throws IOException { + try { + checkNativeHandler(); + createFile(mNativeHandler, uri); + } catch(ErrnoException e) { + throw new IOException("Failed to create file at " + uri, e); + } + } + + @Override + public void mkdir(String uri) throws IOException { + try { + checkNativeHandler(); + mkdir(mNativeHandler, uri); + } catch(ErrnoException e) { + throw new IOException("Failed to make directory at " + uri, e); + } + } + + @Override + public void rename(String uri, String newUri) throws IOException { + try { + checkNativeHandler(); + rename(mNativeHandler, uri, newUri); + } catch(ErrnoException e) { + throw new IOException("Failed to rename " + uri + " to " + newUri, e); + } + } + + @Override + public void unlink(String uri) throws IOException { + try { + checkNativeHandler(); + unlink(mNativeHandler, uri); + } catch(ErrnoException e) { + throw new IOException("Failed to unlink " + uri, e); + } + } + + @Override + public void rmdir(String uri) throws IOException { + try { + checkNativeHandler(); + rmdir(mNativeHandler, uri); + } catch(ErrnoException e) { + throw new IOException("Failed to rmdir " + uri, e); + } + } + + @Override + public SambaFile openFile(String uri, String mode) throws IOException { + try { + checkNativeHandler(); + return new SambaFile(mNativeHandler, openFile(mNativeHandler, uri, mode)); + } catch(ErrnoException e) { + throw new IOException("Failed to open " + uri, e); + } + } + + private void checkNativeHandler() { + if (!isInitialized()) { + throw new IllegalStateException("Samba client is not initialized."); + } + } + + private native long nativeInit(boolean debug, long cacheHandler); + + private native void nativeDestroy(long handler); + + private native List readDir(long handler, String uri) throws ErrnoException; + + private native StructStat stat(long handler, String uri) throws ErrnoException; + + private native void createFile(long handler, String uri) throws ErrnoException; + + private native void mkdir(long handler, String uri) throws ErrnoException; + + private native void rmdir(long handler, String uri) throws ErrnoException; + + private native void rename(long handler, String uri, String newUri) throws ErrnoException; + + private native void unlink(long handler, String uri) throws ErrnoException; + + private native int openFile(long handler, String uri, String mode) throws ErrnoException; +} diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/nativefacade/SambaFacadeClient.java b/app/src/main/java/com/google/android/sambadocumentsprovider/nativefacade/SambaFacadeClient.java new file mode 100644 index 0000000..2e508bd --- /dev/null +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/nativefacade/SambaFacadeClient.java @@ -0,0 +1,205 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.google.android.sambadocumentsprovider.nativefacade; + +import android.os.Bundle; +import android.os.Looper; +import android.os.Message; +import android.support.annotation.IntDef; +import android.system.StructStat; +import com.google.android.sambadocumentsprovider.base.DirectoryEntry; +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.List; + +class SambaFacadeClient extends BaseClient implements SmbClient { + + @IntDef({ RESET, READ_DIR, STAT, MKDIR, RENAME, UNLINK, RMDIR, OPEN_FILE }) + @Retention(RetentionPolicy.SOURCE) + @interface Operation {} + static final int RESET = 1; + static final int READ_DIR = RESET + 1; + static final int STAT = READ_DIR + 1; + static final int CREATE_FILE = STAT + 1; + static final int MKDIR = CREATE_FILE + 1; + static final int RENAME = MKDIR + 1; + static final int UNLINK = RENAME + 1; + static final int RMDIR = UNLINK + 1; + static final int OPEN_FILE = RMDIR + 1; + + private static final String URI = "URI"; + private static final String NEW_URI = "NEW_URI"; + private static final String MODE = "MODE"; + + SambaFacadeClient(Looper looper, SmbClient clientImpl) { + mHandler = new SambaServiceHandler(looper, clientImpl); + } + + private Message obtainMessage(int what, MessageValues messageValues, String uri) { + final Message msg = mHandler.obtainMessage(what, messageValues); + + final Bundle args = msg.getData(); + args.putString(URI, uri); + + return msg; + } + + @Override + public void reset() { + try (MessageValues messageValues = MessageValues.obtain()) { + final Message msg = obtainMessage(RESET, messageValues, null); + enqueue(msg); + } + } + + @Override + public List readDir(String uri) throws IOException { + try (final MessageValues> messageValues = MessageValues.obtain()) { + final Message msg = obtainMessage(READ_DIR, messageValues, uri); + enqueue(msg); + return messageValues.getObj(); + } + } + + @Override + public StructStat stat(String uri) throws IOException { + try (final MessageValues messageValues = MessageValues.obtain()) { + final Message msg = obtainMessage(STAT, messageValues, uri); + enqueue(msg); + return messageValues.getObj(); + } + } + + @Override + public void createFile(String uri) throws IOException { + try (final MessageValues messageValues = MessageValues.obtain()) { + final Message msg = obtainMessage(CREATE_FILE, messageValues, uri); + enqueue(msg); + messageValues.checkException(); + } + } + + @Override + public void mkdir(String uri) throws IOException { + try (final MessageValues messageValues = MessageValues.obtain()) { + final Message msg = obtainMessage(MKDIR, messageValues, uri); + enqueue(msg); + messageValues.checkException(); + } + } + + @Override + public void rename(String uri, String newUri) throws IOException { + try (final MessageValues messageValues = MessageValues.obtain()) { + final Message msg = obtainMessage(RENAME, messageValues, uri); + msg.peekData().putString(NEW_URI, newUri); + enqueue(msg); + messageValues.checkException(); + } + } + + @Override + public void unlink(String uri) throws IOException { + try (final MessageValues messageValues = MessageValues.obtain()) { + final Message msg = obtainMessage(UNLINK, messageValues, uri); + enqueue(msg); + messageValues.checkException(); + } + } + + @Override + public void rmdir(String uri) throws IOException { + try (final MessageValues messageValues = MessageValues.obtain()) { + final Message msg = obtainMessage(RMDIR, messageValues, uri); + enqueue(msg); + messageValues.checkException(); + } + } + + @Override + public SmbFile openFile(String uri, String mode) throws IOException { + try (final MessageValues messageValues = MessageValues.obtain()) { + final Message msg = obtainMessage(OPEN_FILE, messageValues, uri); + msg.peekData().putString(MODE, mode); + enqueue(msg); + return new SambaFileClient(mHandler.getLooper(), messageValues.getObj()); + } + } + + private static class SambaServiceHandler extends BaseHandler { + + private final SmbClient mClientImpl; + + private SambaServiceHandler(Looper looper, SmbClient clientImpl) { + super(looper); + mClientImpl = clientImpl; + } + + @Override + @SuppressWarnings("unchecked") + public void processMessage(Message msg) { + final Bundle args = msg.peekData(); + final String uri = args.getString(URI); + final MessageValues messageValues = (MessageValues) msg.obj; + + try { + switch (msg.what) { + case RESET: + mClientImpl.reset(); + break; + case READ_DIR: + messageValues.setObj(mClientImpl.readDir(uri)); + break; + case STAT: + messageValues.setObj(mClientImpl.stat(uri)); + break; + case CREATE_FILE: + mClientImpl.createFile(uri); + break; + case MKDIR: + mClientImpl.mkdir(uri); + break; + case RENAME: { + final String newUri = args.getString(NEW_URI); + mClientImpl.rename(uri, newUri); + break; + } + case UNLINK: + mClientImpl.unlink(uri); + break; + case RMDIR: + mClientImpl.rmdir(uri); + break; + case OPEN_FILE: { + final String mode = args.getString(MODE); + messageValues.setObj(mClientImpl.openFile(uri, mode)); + break; + } + default: + throw new UnsupportedOperationException("Unknown operation " + msg.what); + } + } catch (IOException e) { + messageValues.setException(e); + } catch (RuntimeException e) { + messageValues.setRuntimeException(e); + } + } + } + +} diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/nativefacade/SambaFile.java b/app/src/main/java/com/google/android/sambadocumentsprovider/nativefacade/SambaFile.java new file mode 100644 index 0000000..b16158d --- /dev/null +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/nativefacade/SambaFile.java @@ -0,0 +1,67 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.google.android.sambadocumentsprovider.nativefacade; + +import android.system.ErrnoException; +import java.io.IOException; +import java.nio.ByteBuffer; + +class SambaFile implements SmbFile { + + private final long mNativeHandler; + private int mNativeFd; + + SambaFile(long nativeHandler, int nativeFd) { + mNativeHandler = nativeHandler; + mNativeFd = nativeFd; + } + public int read(ByteBuffer buffer) throws IOException { + try { + return read(mNativeHandler, mNativeFd, buffer, buffer.capacity()); + } catch(ErrnoException e) { + throw new IOException("Failed to read file. Fd: " + mNativeFd, e); + } + } + + public int write(ByteBuffer buffer, int length) throws IOException { + try { + return write(mNativeHandler, mNativeFd, buffer, length); + } catch(ErrnoException e) { + throw new IOException("Failed to write file. Fd: " + mNativeFd, e); + } + } + + @Override + public void close() throws IOException { + try { + int fd = mNativeFd; + mNativeFd = -1; + close(mNativeHandler, fd); + } catch(ErrnoException e) { + throw new IOException("Failed to close file. Fd: " + mNativeFd, e); + } + } + + private native int read(long handler, int fd, ByteBuffer buffer, int capacity) + throws ErrnoException; + + private native int write(long handler, int fd, ByteBuffer buffer, int length) + throws ErrnoException; + + private native void close(long handler, int fd) throws ErrnoException; +} diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/nativefacade/SambaFileClient.java b/app/src/main/java/com/google/android/sambadocumentsprovider/nativefacade/SambaFileClient.java new file mode 100644 index 0000000..f83bcc3 --- /dev/null +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/nativefacade/SambaFileClient.java @@ -0,0 +1,107 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.google.android.sambadocumentsprovider.nativefacade; + +import android.os.Looper; +import android.os.Message; +import android.support.annotation.IntDef; +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.nio.ByteBuffer; + +class SambaFileClient extends BaseClient implements SmbFile { + + @IntDef({ READ, WRITE, CLOSE }) + @Retention(RetentionPolicy.SOURCE) + @interface Operation {} + private static final int READ = 1; + private static final int WRITE = 2; + private static final int CLOSE = 3; + + SambaFileClient(Looper looper, SmbFile smbFileImpl) { + mHandler = new SambaFileHandler(looper, smbFileImpl); + } + + @Override + public int read(ByteBuffer buffer) throws IOException { + final MessageValues messageValues = MessageValues.obtain(); + messageValues.setObj(buffer); + final Message msg = mHandler.obtainMessage(READ, messageValues); + enqueue(msg); + return messageValues.getInt(); + } + + @Override + public int write(ByteBuffer buffer, int length) throws IOException { + final MessageValues messageValues = MessageValues.obtain(); + messageValues.setObj(buffer); + final Message msg = mHandler.obtainMessage(WRITE, messageValues); + msg.arg1 = length; + enqueue(msg); + return messageValues.getInt(); + } + + @Override + public void close() throws IOException { + final MessageValues messageValues = MessageValues.obtain(); + final Message msg = mHandler.obtainMessage(CLOSE, messageValues); + enqueue(msg); + messageValues.checkException(); + } + + private static class SambaFileHandler extends BaseHandler { + + private SmbFile mSmbFileImpl; + + private SambaFileHandler(Looper looper, SmbFile smbFileImpl) { + super(looper); + + mSmbFileImpl = smbFileImpl; + } + + @Override + @SuppressWarnings("unchecked") + public void processMessage(Message msg) { + final MessageValues messageValues = (MessageValues) msg.obj; + try { + final ByteBuffer buffer = messageValues.getObj(); + + switch (msg.what) { + case READ: + messageValues.setInt(mSmbFileImpl.read(buffer)); + break; + case WRITE: { + final int length = msg.arg1; + messageValues.setInt(mSmbFileImpl.write(buffer, length)); + break; + } + case CLOSE: + mSmbFileImpl.close(); + break; + default: + throw new UnsupportedOperationException("Unknown operation " + msg.what); + } + } catch (IOException e) { + messageValues.setException(e); + } catch (RuntimeException e) { + messageValues.setRuntimeException(e); + } + } + } +} diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/nativefacade/SambaMessageLooper.java b/app/src/main/java/com/google/android/sambadocumentsprovider/nativefacade/SambaMessageLooper.java new file mode 100644 index 0000000..3b5608f --- /dev/null +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/nativefacade/SambaMessageLooper.java @@ -0,0 +1,75 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.google.android.sambadocumentsprovider.nativefacade; + +import android.os.Looper; +import java.util.concurrent.CountDownLatch; + +public class SambaMessageLooper { + + private final Thread mLooperThread = new Thread(new Runnable() { + @Override + public void run() { + prepare(); + } + }); + private final CountDownLatch mLatch = new CountDownLatch(1); + + private volatile Looper mLooper; + private volatile NativeSambaFacade mClientImpl; + private volatile NativeCredentialCache mCredentialCacheImpl; + + private SambaFacadeClient mServiceClient; + private CredentialCacheClient mCredentialCacheClient; + + public SambaMessageLooper() { + init(); + } + + public SmbClient getClient() { + return mServiceClient; + } + + public CredentialCache getCredentialCache() { + return mCredentialCacheClient; + } + + private void init() { + try { + mLooperThread.start(); + mLatch.await(); + + mCredentialCacheClient = new CredentialCacheClient(mLooper, mCredentialCacheImpl); + mServiceClient = new SambaFacadeClient(mLooper, mClientImpl); + } catch(InterruptedException e) { + // Should never happen + throw new RuntimeException(e); + } + } + + private void prepare() { + Looper.prepare(); + mLooper = Looper.myLooper(); + + mCredentialCacheImpl = new NativeCredentialCache(); + mClientImpl = new NativeSambaFacade(mCredentialCacheImpl); + mLatch.countDown(); + + Looper.loop(); + } +} diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/nativefacade/SmbClient.java b/app/src/main/java/com/google/android/sambadocumentsprovider/nativefacade/SmbClient.java new file mode 100644 index 0000000..efa04fb --- /dev/null +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/nativefacade/SmbClient.java @@ -0,0 +1,44 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.google.android.sambadocumentsprovider.nativefacade; + +import android.system.StructStat; +import com.google.android.sambadocumentsprovider.base.DirectoryEntry; +import java.io.IOException; +import java.util.List; + +public interface SmbClient { + + void reset(); + + List readDir(String uri) throws IOException; + + StructStat stat(String uri) throws IOException; + + void createFile(String uri) throws IOException; + + void mkdir(String uri) throws IOException; + + void rename(String uri, String newUri) throws IOException; + + void unlink(String uri) throws IOException; + + void rmdir(String uri) throws IOException; + + SmbFile openFile(String uri, String mode) throws IOException; +} diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/nativefacade/SmbFile.java b/app/src/main/java/com/google/android/sambadocumentsprovider/nativefacade/SmbFile.java new file mode 100644 index 0000000..3231838 --- /dev/null +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/nativefacade/SmbFile.java @@ -0,0 +1,29 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.google.android.sambadocumentsprovider.nativefacade; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.ByteBuffer; + +public interface SmbFile extends Closeable { + + int read(ByteBuffer buffer) throws IOException; + int write(ByteBuffer buffer, int length) throws IOException; + +} diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/provider/ByteBufferPool.java b/app/src/main/java/com/google/android/sambadocumentsprovider/provider/ByteBufferPool.java new file mode 100644 index 0000000..afa31b3 --- /dev/null +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/provider/ByteBufferPool.java @@ -0,0 +1,42 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.google.android.sambadocumentsprovider.provider; + +import android.support.v4.util.Pools; +import java.nio.ByteBuffer; + +class ByteBufferPool { + + private static final int BUFFER_CAPACITY = 128 * 1024; + private final Pools.Pool mBufferPool = new Pools.SynchronizedPool<>(16); + + ByteBuffer obtainBuffer() { + ByteBuffer buffer = mBufferPool.acquire(); + + if (buffer == null) { + buffer = ByteBuffer.allocateDirect(BUFFER_CAPACITY); + } + + return buffer; + } + + void recycleBuffer(ByteBuffer buffer) { + buffer.clear(); + mBufferPool.release(buffer); + } +} diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/provider/ReadFileTask.java b/app/src/main/java/com/google/android/sambadocumentsprovider/provider/ReadFileTask.java new file mode 100644 index 0000000..4c966f2 --- /dev/null +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/provider/ReadFileTask.java @@ -0,0 +1,87 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.google.android.sambadocumentsprovider.provider; + +import android.os.AsyncTask; +import android.os.CancellationSignal; +import android.os.ParcelFileDescriptor; +import android.os.ParcelFileDescriptor.AutoCloseOutputStream; +import android.support.annotation.Nullable; +import android.util.Log; +import com.google.android.sambadocumentsprovider.nativefacade.SmbFile; +import com.google.android.sambadocumentsprovider.nativefacade.SmbClient; +import java.io.IOException; +import java.nio.ByteBuffer; + +public class ReadFileTask extends AsyncTask { + + private static final String TAG = "ReadFileTask"; + + private final String mUri; + private final SmbClient mClient; + private final ParcelFileDescriptor mPfd; + private final CancellationSignal mSignal; + private final ByteBufferPool mBufferPool; + + private ByteBuffer mBuffer; + + ReadFileTask(String uri, SmbClient client, ParcelFileDescriptor pfd, + ByteBufferPool bufferPool, @Nullable CancellationSignal signal) { + mUri = uri; + mClient = client; + mPfd = pfd; + mSignal = signal; + mBufferPool = bufferPool; + } + + @Override + public void onPreExecute() { + mBuffer = mBufferPool.obtainBuffer(); + } + + @Override + public Void doInBackground(Void... args) { + try (final AutoCloseOutputStream os = new AutoCloseOutputStream(mPfd); + final SmbFile file = mClient.openFile(mUri, "r")) { + int size; + byte[] buf = new byte[mBuffer.capacity()]; + while ((mSignal == null || !mSignal.isCanceled()) + && (size = file.read(mBuffer)) > 0) { + mBuffer.get(buf, 0, size); + os.write(buf, 0, size); + mBuffer.clear(); + } + } catch (IOException e) { + Log.e(TAG, "Failed to read file.", e); + + try { + mPfd.closeWithError(e.getMessage()); + } catch (IOException exc) { + Log.e(TAG, "Can't even close PFD with error.", exc); + } + } + + return null; + } + + @Override + public void onPostExecute(Void arg) { + mBufferPool.recycleBuffer(mBuffer); + mBuffer = null; + } +} diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/provider/SambaDocumentsProvider.java b/app/src/main/java/com/google/android/sambadocumentsprovider/provider/SambaDocumentsProvider.java new file mode 100644 index 0000000..a4e6827 --- /dev/null +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/provider/SambaDocumentsProvider.java @@ -0,0 +1,586 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.google.android.sambadocumentsprovider.provider; + +import static com.google.android.sambadocumentsprovider.base.DocumentIdHelper.toDocumentId; +import static com.google.android.sambadocumentsprovider.base.DocumentIdHelper.toRootId; +import static com.google.android.sambadocumentsprovider.base.DocumentIdHelper.toUri; +import static com.google.android.sambadocumentsprovider.base.DocumentIdHelper.toUriString; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.CancellationSignal; +import android.os.ParcelFileDescriptor; +import android.provider.DocumentsContract; +import android.provider.DocumentsContract.Document; +import android.provider.DocumentsContract.Root; +import android.provider.DocumentsProvider; +import android.support.annotation.Nullable; +import android.support.annotation.StringRes; +import android.util.Log; +import com.google.android.sambadocumentsprovider.R; +import com.google.android.sambadocumentsprovider.SambaProviderApplication; +import com.google.android.sambadocumentsprovider.ShareManager; +import com.google.android.sambadocumentsprovider.ShareManager.MountedShareChangeListener; +import com.google.android.sambadocumentsprovider.TaskManager; +import com.google.android.sambadocumentsprovider.base.AuthFailedException; +import com.google.android.sambadocumentsprovider.base.DirectoryEntry; +import com.google.android.sambadocumentsprovider.base.DocumentCursor; +import com.google.android.sambadocumentsprovider.cache.CacheResult; +import com.google.android.sambadocumentsprovider.cache.DocumentCache; +import com.google.android.sambadocumentsprovider.document.DocumentMetadata; +import com.google.android.sambadocumentsprovider.document.LoadChildrenTask; +import com.google.android.sambadocumentsprovider.base.OnTaskFinishedCallback; +import com.google.android.sambadocumentsprovider.document.LoadDocumentTask; +import com.google.android.sambadocumentsprovider.document.LoadStatTask; +import com.google.android.sambadocumentsprovider.nativefacade.SmbClient; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class SambaDocumentsProvider extends DocumentsProvider { + + public static final String AUTHORITY = "com.google.android.sambadocumentsprovider"; + + private static final String TAG = "SambaDocumentsProvider"; + + private static final String[] DEFAULT_ROOT_PROJECTION = { + Root.COLUMN_ROOT_ID, + Root.COLUMN_DOCUMENT_ID, + Root.COLUMN_TITLE, + Root.COLUMN_FLAGS, + Root.COLUMN_ICON + }; + + private static final String[] DEFAULT_DOCUMENT_PROJECTION = { + Document.COLUMN_DOCUMENT_ID, + Document.COLUMN_DISPLAY_NAME, + Document.COLUMN_FLAGS, + Document.COLUMN_MIME_TYPE, + Document.COLUMN_SIZE, + Document.COLUMN_LAST_MODIFIED + }; + + private final OnTaskFinishedCallback mLoadDocumentCallback = + new OnTaskFinishedCallback() { + @Override + public void onTaskFinished(@Status int status, @Nullable Uri uri, Exception exception) { + getContext().getContentResolver().notifyChange(toNotifyUri(uri), null, false); + } + }; + + private final OnTaskFinishedCallback mLoadChildrenCallback = + new OnTaskFinishedCallback() { + @Override + public void onTaskFinished(@Status int status, DocumentMetadata metadata, + Exception exception) { + // Notify remote side that we get the list even though we don't have the stat yet. + // If it failed we still should notify the remote side that the loading failed. + getContext().getContentResolver().notifyChange( + toNotifyUri(metadata.getUri()), null, false); + } + }; + + private final OnTaskFinishedCallback mWriteFinishedCallback = + new OnTaskFinishedCallback() { + @Override + public void onTaskFinished( + @Status int status, @Nullable String item, Exception exception) { + final Uri uri = toUri(item); + try (final CacheResult result = mCache.get(uri)) { + if (result.getState() != CacheResult.CACHE_MISS) { + result.getItem().reset(); + } + } + + final Uri parentUri = DocumentMetadata.buildParentUri(uri); + getContext().getContentResolver().notifyChange(toNotifyUri(parentUri), null, false); + } + }; + + private final MountedShareChangeListener mShareChangeListener = new MountedShareChangeListener() { + @Override + public void onMountedServerChange() { + final Uri rootsUri = DocumentsContract.buildRootsUri(AUTHORITY); + final ContentResolver resolver = getContext().getContentResolver(); + resolver.notifyChange(rootsUri, null, false); + } + }; + + private ShareManager mShareManager; + private SmbClient mClient; + private ByteBufferPool mBufferPool; + private DocumentCache mCache; + private TaskManager mTaskManager; + + @Override + public boolean onCreate() { + final Context context = getContext(); + mClient = SambaProviderApplication.getSambaClient(context); + mCache = SambaProviderApplication.getDocumentCache(context); + mTaskManager = SambaProviderApplication.getTaskManager(context); + mBufferPool = new ByteBufferPool(); + mShareManager = SambaProviderApplication.getServerManager(context); + mShareManager.addListener(mShareChangeListener); + + return mClient != null; + } + + @Override + public Cursor queryRoots(String[] projection) throws FileNotFoundException { + Log.d(TAG, "Querying roots."); + projection = (projection == null) ? DEFAULT_ROOT_PROJECTION : projection; + + MatrixCursor cursor = new MatrixCursor(projection, mShareManager.size()); + for (String uri : mShareManager) { + final String name; + final Uri parsedUri = Uri.parse(uri); + try(CacheResult result = mCache.get(parsedUri)) { + final DocumentMetadata metadata; + if (result.getState() == CacheResult.CACHE_MISS) { + metadata = DocumentMetadata.createShare(parsedUri); + mCache.put(metadata); + } else { + metadata = result.getItem(); + } + + name = metadata.getDisplayName(); + + cursor.addRow(new Object[] { + toRootId(metadata), + toDocumentId(parsedUri), + name, + Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_IS_CHILD, + R.drawable.ic_folder_shared + }); + } + + } + return cursor; + } + + @Override + public boolean isChildDocument(String parentDocumentId, String documentId) { + final String parentUri = toUriString(parentDocumentId); + final String childUri = toUriString(documentId); + return childUri.startsWith(parentUri); + } + + @Override + public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException { + Log.d(TAG, "Querying document: " + documentId); + projection = (projection == null) ? DEFAULT_DOCUMENT_PROJECTION : projection; + + final Uri uri = toUri(documentId); + try { + try (CacheResult result = mCache.get(uri)) { + + final DocumentMetadata metadata; + if (result.getState() == CacheResult.CACHE_MISS) { + if (mShareManager.containsShare(uri.toString())) { + metadata = DocumentMetadata.createShare(uri); + } else { + // There is no cache for this URI. Fetch it from remote side. + metadata = DocumentMetadata.fromUri(uri, mClient); + } + mCache.put(metadata); + } else { + metadata = result.getItem(); + } + + final MatrixCursor cursor = new MatrixCursor(projection); + cursor.addRow(getDocumentValues(projection, metadata)); + + return cursor; + } + } catch (FileNotFoundException|RuntimeException e) { + throw e; + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + @Override + public Cursor queryChildDocuments(String documentId, String[] projection, String sortOrder) + throws FileNotFoundException { + Log.d(TAG, "Querying children documents under " + documentId); + projection = (projection == null) ? DEFAULT_DOCUMENT_PROJECTION : projection; + + final Uri uri = toUri(documentId); + try { + try (final CacheResult result = mCache.get(uri)) { + boolean isLoading = false; + final DocumentCursor cursor = new DocumentCursor(projection); + final Bundle extra = new Bundle(); + final Uri notifyUri = toNotifyUri(uri); + + if (result.getState() == CacheResult.CACHE_MISS) { + // Last loading failed... Just feed the bitter fruit. + mCache.throwLastExceptionIfAny(uri); + + final LoadDocumentTask task = + new LoadDocumentTask(uri, mClient, mCache, mLoadDocumentCallback); + mTaskManager.runTask(uri, task); + cursor.setLoadingTask(task); + + isLoading = true; + } else { // At least we have something in cache. + final DocumentMetadata metadata = result.getItem(); + + if (!Document.MIME_TYPE_DIR.equals(metadata.getMimeType())) { + throw new IllegalArgumentException(documentId + " is not a folder."); + } + + metadata.throwLastChildUpdateExceptionIfAny(); + + final Map childrenMap = metadata.getChildren(); + if (childrenMap == null || result.getState() == CacheResult.CACHE_EXPIRED) { + final LoadChildrenTask task = + new LoadChildrenTask(metadata, mClient, mCache, mLoadChildrenCallback); + mTaskManager.runTask(uri, task); + cursor.setLoadingTask(task); + + isLoading = true; + } + + // Still return something even if the cache expired. + if (childrenMap != null) { + final Collection children = childrenMap.values(); + final Map docMap = new HashMap<>(); + for (DocumentMetadata child : children) { + if (child.needsStat() && !child.hasLoadingStatFailed()) { + docMap.put(child.getUri(), child); + } + cursor.addRow(getDocumentValues(projection, child)); + } + if (!isLoading && !docMap.isEmpty()) { + LoadStatTask task = new LoadStatTask(docMap, mClient, + new OnTaskFinishedCallback>() { + @Override + public void onTaskFinished( + @Status int status, Map item, Exception exception) { + getContext().getContentResolver().notifyChange(notifyUri, null, false); + } + }); + mTaskManager.runTask(uri, task); + cursor.setLoadingTask(task); + + isLoading = true; + } + } + } + extra.putBoolean(DocumentsContract.EXTRA_LOADING, isLoading); + cursor.setExtras(extra); + cursor.setNotificationUri(getContext().getContentResolver(), notifyUri); + return cursor; + } + } catch (AuthFailedException e) { + return buildErrorCursor(projection, R.string.view_folder_denied); + } catch (FileNotFoundException|RuntimeException e) { + throw e; + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + private Cursor buildErrorCursor(String[] projection, @StringRes int resId) { + final String message = getContext().getString(resId); + + final Bundle extra = new Bundle(); + extra.putString(DocumentsContract.EXTRA_ERROR, message); + + final DocumentCursor cursor = new DocumentCursor(projection); + cursor.setExtras(extra); + + return cursor; + } + + private Object[] getDocumentValues( + String[] projection, DocumentMetadata metadata) { + Object[] row = new Object[projection.length]; + for (int i = 0; i < projection.length; ++i) { + switch (projection[i]) { + case Document.COLUMN_DOCUMENT_ID: + row[i] = toDocumentId(metadata.getUri()); + break; + case Document.COLUMN_DISPLAY_NAME: + row[i] = metadata.getDisplayName(); + break; + case Document.COLUMN_FLAGS: + // Always assume it can write to it until the file operation fails. Windows 10 also does + // the same thing. + int flag = metadata.canCreateDocument() ? Document.FLAG_DIR_SUPPORTS_CREATE : 0; + flag |= Document.FLAG_SUPPORTS_WRITE; + flag |= Document.FLAG_SUPPORTS_DELETE; + flag |= Document.FLAG_SUPPORTS_RENAME; + flag |= Document.FLAG_SUPPORTS_REMOVE; + flag |= Document.FLAG_SUPPORTS_MOVE; + row[i] = flag; + break; + case Document.COLUMN_MIME_TYPE: + row[i] = metadata.getMimeType(); + break; + case Document.COLUMN_SIZE: + row[i] = metadata.getSize(); + break; + case Document.COLUMN_LAST_MODIFIED: + row[i] = metadata.getLastModified(); + break; + } + } + return row; + } + + @Override + public String createDocument(String parentDocumentId, String mimeType, String displayName) + throws FileNotFoundException { + try { + final Uri parentUri = toUri(parentDocumentId); + + boolean isDir = Document.MIME_TYPE_DIR.equals(mimeType); + final DirectoryEntry entry = new DirectoryEntry( + isDir ? DirectoryEntry.DIR : DirectoryEntry.FILE, + "", // comment + displayName); + final Uri uri = DocumentMetadata.buildChildUri(parentUri, entry); + + if (isDir) { + mClient.mkdir(uri.toString()); + } else { + mClient.createFile(uri.toString()); + } + + // Notify anyone who's listening on the parent folder. + getContext().getContentResolver().notifyChange(toNotifyUri(parentUri), null, false); + + try (CacheResult result = mCache.get(uri)) { + if (result.getState() != CacheResult.CACHE_MISS) { + // It must be a file, and the file is truncated... Reset its cache. + result.getItem().reset(); + + // No need to update the cache anymore. + return toDocumentId(uri); + } + } + + // Put it to cache without stat, newly created stuff is likely to be changed soon. + DocumentMetadata metadata = new DocumentMetadata(uri, entry); + mCache.put(metadata); + + return toDocumentId(uri); + } catch (FileNotFoundException e) { + throw e; + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + @Override + public String renameDocument(String documentId, String displayName) throws FileNotFoundException { + try { + final Uri uri = toUri(documentId); + final Uri parentUri = DocumentMetadata.buildParentUri(uri); + if (parentUri.getPathSegments().isEmpty()) { + throw new UnsupportedOperationException("Not support renaming a share/workgroup/server."); + } + final Uri newUri = DocumentMetadata.buildChildUri(parentUri, displayName); + + if (newUri == null) { + throw new UnsupportedOperationException(displayName + " is not a valid name."); + } + + mClient.rename(uri.toString(), newUri.toString()); + + revokeDocumentPermission(documentId); + + getContext().getContentResolver().notifyChange(toNotifyUri(parentUri), null, false); + + // Update cache + try (CacheResult result = mCache.get(uri)) { + if (result.getState() != CacheResult.CACHE_MISS) { + DocumentMetadata metadata = result.getItem(); + metadata.rename(newUri); + mCache.remove(uri); + mCache.put(metadata); + } + } + + return toDocumentId(newUri); + } catch (FileNotFoundException e) { + throw e; + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + @Override + public void deleteDocument(String documentId) throws FileNotFoundException { + final Uri uri = toUri(documentId); + + try { + // Obtain metadata first to determine whether it's a file or a folder. We need to do + // different things on them. Ignore our cache since it might be out of date. + final DocumentMetadata metadata = DocumentMetadata.fromUri(uri, mClient); + if (Document.MIME_TYPE_DIR.equals(metadata.getMimeType())) { + recursiveDeleteFolder(metadata); + } else { + deleteFile(metadata); + } + + final Uri notifyUri = toNotifyUri(DocumentMetadata.buildParentUri(uri)); + getContext().getContentResolver().notifyChange(notifyUri, null, false); + + } catch(FileNotFoundException e) { + Log.w(TAG, documentId + " is not found. No need to delete it.", e); + mCache.remove(uri); + final Uri notifyUri = toNotifyUri(DocumentMetadata.buildParentUri(uri)); + getContext().getContentResolver().notifyChange(notifyUri, null, false); + } catch(IOException e) { + throw new IllegalStateException(e); + } + } + + private void recursiveDeleteFolder(DocumentMetadata metadata) throws IOException { + // Fetch the latest children just in case our cache is out of date. + metadata.loadChildren(mClient); + for (DocumentMetadata child : metadata.getChildren().values()) { + if (Document.MIME_TYPE_DIR.equals(child.getMimeType())) { + recursiveDeleteFolder(child); + } else { + deleteFile(child); + } + } + + final Uri uri = metadata.getUri(); + mClient.rmdir(uri.toString()); + mCache.remove(uri); + + revokeDocumentPermission(toDocumentId(uri)); + } + + private void deleteFile(DocumentMetadata metadata) throws IOException { + final Uri uri = metadata.getUri(); + mClient.unlink(uri.toString()); + mCache.remove(uri); + revokeDocumentPermission(toDocumentId(uri)); + } + + @Override + public void removeDocument(String documentId, String parentDocumentId) + throws FileNotFoundException { + // documentId is hierarchical. It can only have one parent. + deleteDocument(documentId); + } + + @Override + public String moveDocument( + String sourceDocumentId, String sourceParentDocumentId, String targetParentDocumentId) + throws FileNotFoundException { + try { + final Uri uri = toUri(sourceDocumentId); + final Uri targetParentUri = toUri(targetParentDocumentId); + + if (!Objects.equals(uri.getAuthority(), targetParentUri.getAuthority())) { + throw new UnsupportedOperationException("Instant move across services are not supported."); + } + + final List pathSegmentsOfSource = uri.getPathSegments(); + final List pathSegmentsOfTargetParent = targetParentUri.getPathSegments(); + if (pathSegmentsOfSource.isEmpty() || + pathSegmentsOfTargetParent.isEmpty() || + !Objects.equals(pathSegmentsOfSource.get(0), pathSegmentsOfTargetParent.get(0))) { + throw new UnsupportedOperationException("Instance move across shares are not supported."); + } + + final Uri targetUri = DocumentMetadata + .buildChildUri(targetParentUri, uri.getLastPathSegment()); + mClient.rename(uri.toString(), targetUri.toString()); + + revokeDocumentPermission(sourceDocumentId); + + getContext().getContentResolver() + .notifyChange(toNotifyUri(DocumentMetadata.buildParentUri(uri)), null, false); + getContext().getContentResolver().notifyChange(toNotifyUri(targetParentUri), null, false); + + try (CacheResult result = mCache.get(uri)) { + if (result.getState() != CacheResult.CACHE_MISS) { + final DocumentMetadata metadata = result.getItem(); + metadata.rename(targetUri); + + mCache.remove(uri); + mCache.put(metadata); + } + } + + return toDocumentId(targetUri); + } catch(FileNotFoundException e) { + throw e; + } catch(IOException e) { + throw new IllegalStateException(e); + } + } + + @Override + public ParcelFileDescriptor openDocument(String documentId, String mode, + CancellationSignal cancellationSignal) throws FileNotFoundException { + Log.d(TAG, "Opening document " + documentId + " with mode " + mode); + try { + if (!"r".equals(mode) && !"w".equals(mode)) { + throw new UnsupportedOperationException("Mode " + mode + " is not supported"); + } + + final String uri = toUriString(documentId); + ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createReliablePipe(); + switch (mode) { + case "r": { + final ReadFileTask task = new ReadFileTask( + uri, mClient, pipe[1], mBufferPool, cancellationSignal); + mTaskManager.runIoTask(task); + } + return pipe[0]; + case "w": { + final WriteFileTask task = new WriteFileTask(uri, mClient, pipe[0], mBufferPool, + cancellationSignal, mWriteFinishedCallback); + mTaskManager.runIoTask(task); + return pipe[1]; + } + default: + // Should never happen. + pipe[0].close(); + pipe[1].close(); + throw new UnsupportedOperationException("Mode " + mode + " is not supported."); + } + } catch(FileNotFoundException e) { + throw e; + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + private Uri toNotifyUri(Uri uri) { + return DocumentsContract.buildDocumentUri(AUTHORITY, toDocumentId(uri)); + } +} diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/provider/WriteFileTask.java b/app/src/main/java/com/google/android/sambadocumentsprovider/provider/WriteFileTask.java new file mode 100644 index 0000000..e706482 --- /dev/null +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/provider/WriteFileTask.java @@ -0,0 +1,91 @@ +/* + * Copyright 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.google.android.sambadocumentsprovider.provider; + +import android.os.AsyncTask; +import android.os.CancellationSignal; +import android.os.ParcelFileDescriptor; +import android.os.ParcelFileDescriptor.AutoCloseInputStream; +import android.support.annotation.Nullable; +import android.util.Log; +import com.google.android.sambadocumentsprovider.base.OnTaskFinishedCallback; +import com.google.android.sambadocumentsprovider.nativefacade.SmbFile; +import com.google.android.sambadocumentsprovider.nativefacade.SmbClient; +import java.io.IOException; +import java.nio.ByteBuffer; + +public class WriteFileTask extends AsyncTask { + + private static final String TAG = "WriteFileTask"; + + private final String mUri; + private final SmbClient mClient; + private final ParcelFileDescriptor mPfd; + private final CancellationSignal mSignal; + private final OnTaskFinishedCallback mCallback; + private final ByteBufferPool mBufferPool; + private final ByteBuffer mBuffer; + + WriteFileTask(String uri, + SmbClient service, + ParcelFileDescriptor pfd, + ByteBufferPool bufferPool, + @Nullable CancellationSignal signal, + OnTaskFinishedCallback callback) { + mUri = uri; + mClient = service; + mPfd = pfd; + mSignal = signal; + mCallback = callback; + + mBufferPool = bufferPool; + mBuffer = mBufferPool.obtainBuffer(); + } + + @Override + public Void doInBackground(Void... args) { + try (final AutoCloseInputStream is = new AutoCloseInputStream(mPfd); + final SmbFile file = mClient.openFile(mUri, "w")){ + int size; + byte[] buf = new byte[mBuffer.capacity()]; + while ((mSignal == null || !mSignal.isCanceled()) + && (size = is.read(buf)) > 0) { + mBuffer.put(buf, 0, size); + file.write(mBuffer, size); + mBuffer.clear(); + } + } catch (IOException e) { + Log.e(TAG, "Failed to write file.", e); + + try { + mPfd.closeWithError(e.getMessage()); + } catch (IOException exc) { + Log.e(TAG, "Can't even close PFD with error.", exc); + } + } + + return null; + } + + @Override + public void onPostExecute(Void arg) { + mBufferPool.recycleBuffer(mBuffer); + + mCallback.onTaskFinished(OnTaskFinishedCallback.SUCCEEDED, mUri, null); + } +} diff --git a/app/src/main/res/drawable/ic_folder_shared.xml b/app/src/main/res/drawable/ic_folder_shared.xml new file mode 100644 index 0000000..385424b --- /dev/null +++ b/app/src/main/res/drawable/ic_folder_shared.xml @@ -0,0 +1,31 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..bce1d96 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +