diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..de25127
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,16 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
+.idea
\ No newline at end of file
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..c6f01c6
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,660 @@
+# GNU AFFERO GENERAL PUBLIC LICENSE
+
+Version 3, 19 November 2007
+
+Copyright (C) 2007 Free Software Foundation, Inc.
+
+
+Everyone is permitted to copy and distribute verbatim copies of this
+license document, but changing it is not allowed.
+
+## Preamble
+
+The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains
+free software for all its users.
+
+When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing
+under this license.
+
+The precise terms and conditions for copying, distribution and
+modification follow.
+
+## TERMS AND CONDITIONS
+
+### 0. Definitions.
+
+"This License" refers to version 3 of the GNU Affero General Public
+License.
+
+"Copyright" also means copyright-like laws that apply to other kinds
+of works, such as semiconductor masks.
+
+"The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of
+an exact copy. The resulting work is called a "modified version" of
+the earlier work or a work "based on" the earlier work.
+
+A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user
+through a computer network, with no transfer of a copy, is not
+conveying.
+
+An interactive user interface displays "Appropriate Legal Notices" to
+the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+### 1. Source Code.
+
+The "source code" for a work means the preferred form of the work for
+making modifications to it. "Object code" means any non-source form of
+a work.
+
+A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+The Corresponding Source need not include anything that users can
+regenerate automatically from other parts of the Corresponding Source.
+
+The Corresponding Source for a work in source code form is that same
+work.
+
+### 2. Basic Permissions.
+
+All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+You may make, run and propagate covered works that you do not convey,
+without conditions so long as your license otherwise remains in force.
+You may convey covered works to others for the sole purpose of having
+them make modifications exclusively for you, or provide you with
+facilities for running those works, provided that you comply with the
+terms of this License in conveying all material for which you do not
+control copyright. Those thus making or running the covered works for
+you must do so exclusively on your behalf, under your direction and
+control, on terms that prohibit them from making any copies of your
+copyrighted material outside their relationship with you.
+
+Conveying under any other circumstances is permitted solely under the
+conditions stated below. Sublicensing is not allowed; section 10 makes
+it unnecessary.
+
+### 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such
+circumvention is effected by exercising rights under this License with
+respect to the covered work, and you disclaim any intention to limit
+operation or modification of the work as a means of enforcing, against
+the work's users, your or third parties' legal rights to forbid
+circumvention of technological measures.
+
+### 4. Conveying Verbatim Copies.
+
+You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+### 5. Conveying Modified Source Versions.
+
+You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these
+conditions:
+
+- a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+- b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under
+ section 7. This requirement modifies the requirement in section 4
+ to "keep intact all notices".
+- c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+- d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+### 6. Conveying Non-Source Forms.
+
+You may convey a covered work in object code form under the terms of
+sections 4 and 5, provided that you also convey the machine-readable
+Corresponding Source under the terms of this License, in one of these
+ways:
+
+- a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+- b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the Corresponding
+ Source from a network server at no charge.
+- c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+- d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+- e) Convey the object code using peer-to-peer transmission,
+ provided you inform other peers where the object code and
+ Corresponding Source of the work are being offered to the general
+ public at no charge under subsection 6d.
+
+A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal,
+family, or household purposes, or (2) anything designed or sold for
+incorporation into a dwelling. In determining whether a product is a
+consumer product, doubtful cases shall be resolved in favor of
+coverage. For a particular product received by a particular user,
+"normally used" refers to a typical or common use of that class of
+product, regardless of the status of the particular user or of the way
+in which the particular user actually uses, or expects or is expected
+to use, the product. A product is a consumer product regardless of
+whether the product has substantial commercial, industrial or
+non-consumer uses, unless such uses represent the only significant
+mode of use of the product.
+
+"Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to
+install and execute modified versions of a covered work in that User
+Product from a modified version of its Corresponding Source. The
+information must suffice to ensure that the continued functioning of
+the modified object code is in no case prevented or interfered with
+solely because modification has been made.
+
+If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or
+updates for a work that has been modified or installed by the
+recipient, or for the User Product in which it has been modified or
+installed. Access to a network may be denied when the modification
+itself materially and adversely affects the operation of the network
+or violates the rules and protocols for communication across the
+network.
+
+Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+### 7. Additional Terms.
+
+"Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders
+of that material) supplement the terms of this License with terms:
+
+- a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+- b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+- c) Prohibiting misrepresentation of the origin of that material,
+ or requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+- d) Limiting the use for publicity purposes of names of licensors
+ or authors of the material; or
+- e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+- f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions
+ of it) with contractual assumptions of liability to the recipient,
+ for any liability that these contractual assumptions directly
+ impose on those licensors and authors.
+
+All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions; the
+above requirements apply either way.
+
+### 8. Termination.
+
+You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+However, if you cease all violation of this License, then your license
+from a particular copyright holder is reinstated (a) provisionally,
+unless and until the copyright holder explicitly and finally
+terminates your license, and (b) permanently, if the copyright holder
+fails to notify you of the violation by some reasonable means prior to
+60 days after the cessation.
+
+Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+### 9. Acceptance Not Required for Having Copies.
+
+You are not required to accept this License in order to receive or run
+a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+### 10. Automatic Licensing of Downstream Recipients.
+
+Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+### 11. Patents.
+
+A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+A contributor's "essential patent claims" are all patent claims owned
+or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+A patent license is "discriminatory" if it does not include within the
+scope of its coverage, prohibits the exercise of, or is conditioned on
+the non-exercise of one or more of the rights that are specifically
+granted under this License. You may not convey a covered work if you
+are a party to an arrangement with a third party that is in the
+business of distributing software, under which you make payment to the
+third party based on the extent of your activity of conveying the
+work, and under which the third party grants, to any of the parties
+who would receive the covered work from you, a discriminatory patent
+license (a) in connection with copies of the covered work conveyed by
+you (or copies made from those copies), or (b) primarily for and in
+connection with specific products or compilations that contain the
+covered work, unless you entered into that arrangement, or that patent
+license was granted, prior to 28 March 2007.
+
+Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+### 12. No Surrender of Others' Freedom.
+
+If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under
+this License and any other pertinent obligations, then as a
+consequence you may not convey it at all. For example, if you agree to
+terms that obligate you to collect a royalty for further conveying
+from those to whom you convey the Program, the only way you could
+satisfy both those terms and this License would be to refrain entirely
+from conveying the Program.
+
+### 13. Remote Network Interaction; Use with the GNU General Public License.
+
+Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your
+version supports such interaction) an opportunity to receive the
+Corresponding Source of your version by providing access to the
+Corresponding Source from a network server at no charge, through some
+standard or customary means of facilitating copying of software. This
+Corresponding Source shall include the Corresponding Source for any
+work covered by version 3 of the GNU General Public License that is
+incorporated pursuant to the following paragraph.
+
+Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+### 14. Revised Versions of this License.
+
+The Free Software Foundation may publish revised and/or new versions
+of the GNU Affero General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever
+published by the Free Software Foundation.
+
+If the Program specifies that a proxy can decide which future versions
+of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+### 15. Disclaimer of Warranty.
+
+THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT
+WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
+PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE
+DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
+CORRECTION.
+
+### 16. Limitation of Liability.
+
+IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR
+CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES
+ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT
+NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR
+LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
+TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
+PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+### 17. Interpretation of Sections 15 and 16.
+
+If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+END OF TERMS AND CONDITIONS
+
+## How to Apply These Terms to Your New Programs
+
+If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these
+terms.
+
+To do so, attach the following notices to the program. It is safest to
+attach them to the start of each source file to most effectively state
+the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as
+ published by the Free Software Foundation, either version 3 of the
+ License, or (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper
+mail.
+
+If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for
+the specific requirements.
+
+You should also get your employer (if you work as a programmer) or
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. For more information on this, and how to apply and follow
+the GNU AGPL, see .
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..06dd775
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+# NoteTand
+
+Simple plain-text notes app with Bluetooth sync!
\ No newline at end of file
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..48f9341
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,29 @@
+plugins {
+ alias(libs.plugins.android.application)
+}
+
+android {
+ namespace 'org.eu.octt.notetand'
+ compileSdk 35
+
+ defaultConfig {
+ applicationId "org.eu.octt.notetand"
+ minSdk 10
+ targetSdk 35
+ versionCode 1
+ versionName "1.0"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_11
+ targetCompatibility JavaVersion.VERSION_11
+ }
+}
+
+dependencies {}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# 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 *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/app/release/app-release.apk b/app/release/app-release.apk
new file mode 100644
index 0000000..62989fb
Binary files /dev/null and b/app/release/app-release.apk differ
diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json
new file mode 100644
index 0000000..7db3c84
--- /dev/null
+++ b/app/release/output-metadata.json
@@ -0,0 +1,21 @@
+{
+ "version": 3,
+ "artifactType": {
+ "type": "APK",
+ "kind": "Directory"
+ },
+ "applicationId": "org.eu.octt.notetand",
+ "variantName": "release",
+ "elements": [
+ {
+ "type": "SINGLE",
+ "filters": [],
+ "attributes": [],
+ "versionCode": 1,
+ "versionName": "1.0",
+ "outputFile": "app-release.apk"
+ }
+ ],
+ "elementType": "File",
+ "minSdkVersionForDexing": 10
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..0bd85ac
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png
new file mode 100644
index 0000000..c441e21
Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ
diff --git a/app/src/main/java/org/eu/octt/notetand/BluetoothManager.java b/app/src/main/java/org/eu/octt/notetand/BluetoothManager.java
new file mode 100644
index 0000000..d9e0ffb
--- /dev/null
+++ b/app/src/main/java/org/eu/octt/notetand/BluetoothManager.java
@@ -0,0 +1,38 @@
+package org.eu.octt.notetand;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.bluetooth.BluetoothAdapter;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Build;
+
+public class BluetoothManager {
+ static final int REQUEST_ENABLE_BT = 1001;
+ static final int REQUEST_PERMISSION_BT = 1002;
+
+ static boolean getPermission(Activity activity) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && activity.checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
+ activity.requestPermissions(new String[]{Manifest.permission.BLUETOOTH_CONNECT}, REQUEST_PERMISSION_BT);
+ return false;
+ }
+ return true;
+ }
+
+ @SuppressLint("MissingPermission") // we're checking it but the IDE won't budge
+ static boolean requireBluetooth(Activity activity) {
+ if (!getPermission(activity))
+ return false;
+ var adapter = BluetoothAdapter.getDefaultAdapter();
+ if (adapter != null) {
+ if (adapter.isEnabled()) {
+ return true;
+ } else {
+ Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
+ activity.startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
+ }
+ }
+ return false;
+ }
+}
diff --git a/app/src/main/java/org/eu/octt/notetand/BluetoothSyncActivity.java b/app/src/main/java/org/eu/octt/notetand/BluetoothSyncActivity.java
new file mode 100644
index 0000000..58037ea
--- /dev/null
+++ b/app/src/main/java/org/eu/octt/notetand/BluetoothSyncActivity.java
@@ -0,0 +1,797 @@
+package org.eu.octt.notetand;
+
+import android.Manifest;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothServerSocket;
+import android.bluetooth.BluetoothSocket;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+
+public class BluetoothSyncActivity extends Activity {
+
+ private static final String TAG = "BluetoothSync";
+ private static final UUID SERVICE_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
+ private static final String SERVICE_NAME = "SyncNotesApp";
+ private static final int REQUEST_DISCOVERABLE = 2001;
+ private static final int DISCOVERABLE_DURATION = 300; // 5 minutes
+
+ // Protocol constants
+ private static final String PROTOCOL_HANDSHAKE = "SYNCNOTES_HANDSHAKE";
+ private static final String PROTOCOL_FILE_LIST = "FILE_LIST";
+ private static final String PROTOCOL_FILE_REQUEST = "FILE_REQUEST";
+ private static final String PROTOCOL_FILE_DATA = "FILE_DATA";
+ private static final String PROTOCOL_SYNC_COMPLETE = "SYNC_COMPLETE";
+ private static final String PROTOCOL_ERROR = "ERROR";
+
+ private BluetoothAdapter bluetoothAdapter;
+ private TextView txtBluetoothStatus;
+ private TextView txtSyncProgress;
+ private Button btnMakeDiscoverable;
+ private Button btnScanDevices;
+ private Button btnCancelSync;
+ private ListView listPairedDevices;
+ private ListView listDiscoveredDevices;
+
+ private ArrayAdapter pairedDevicesAdapter;
+ private ArrayAdapter discoveredDevicesAdapter;
+ private ArrayList pairedDevicesList;
+ private ArrayList discoveredDevicesList;
+
+ private AcceptThread acceptThread;
+ private ConnectThread connectThread;
+ private ConnectedThread connectedThread;
+ private Handler mainHandler;
+ private File notesDirectory;
+ private boolean isScanning = false;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.bluetooth_sync_activity);
+
+ // Enable up navigation
+ if (getActionBar() != null) {
+ getActionBar().setDisplayHomeAsUpEnabled(true);
+ getActionBar().setTitle("Bluetooth Sync");
+ }
+
+ mainHandler = new Handler(Looper.getMainLooper());
+ initializeViews();
+ setupNotesDirectory();
+ initializeBluetooth();
+ setupListeners();
+ }
+
+ private void initializeViews() {
+ txtBluetoothStatus = findViewById(R.id.txt_bluetooth_status);
+ txtSyncProgress = findViewById(R.id.txt_sync_progress);
+ btnMakeDiscoverable = findViewById(R.id.btn_make_discoverable);
+ btnScanDevices = findViewById(R.id.btn_scan_devices);
+ btnCancelSync = findViewById(R.id.btn_cancel_sync);
+ listPairedDevices = findViewById(R.id.list_paired_devices);
+ listDiscoveredDevices = findViewById(R.id.list_discovered_devices);
+
+ // Initialize device lists
+ pairedDevicesList = new ArrayList<>();
+ discoveredDevicesList = new ArrayList<>();
+
+ pairedDevicesAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1);
+ discoveredDevicesAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1);
+
+ listPairedDevices.setAdapter(pairedDevicesAdapter);
+ listDiscoveredDevices.setAdapter(discoveredDevicesAdapter);
+ }
+
+ private void setupNotesDirectory() {
+ String notesPath = getIntent().getStringExtra("notes_directory");
+ if (notesPath != null) {
+ notesDirectory = new File(notesPath);
+ } else {
+ File externalFilesDir = getExternalFilesDir(null);
+ if (externalFilesDir != null) {
+ notesDirectory = new File(externalFilesDir, "notes");
+ } else {
+ notesDirectory = new File(getFilesDir(), "notes");
+ }
+ }
+ }
+
+ private void initializeBluetooth() {
+ bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+
+ if (bluetoothAdapter == null) {
+ updateStatus("Bluetooth not supported on this device");
+ disableAllButtons();
+ return;
+ }
+
+ if (!bluetoothAdapter.isEnabled()) {
+ updateStatus("Bluetooth is disabled");
+ disableAllButtons();
+ return;
+ }
+
+ updateStatus("Bluetooth ready");
+ loadPairedDevices();
+ startAcceptThread();
+ }
+
+ private void setupListeners() {
+ btnMakeDiscoverable.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ makeDiscoverable();
+ }
+ });
+
+ btnScanDevices.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (isScanning) {
+ stopDeviceDiscovery();
+ } else {
+ startDeviceDiscovery();
+ }
+ }
+ });
+
+ btnCancelSync.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ finish();
+ }
+ });
+
+ listPairedDevices.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ BluetoothDevice device = pairedDevicesList.get(position);
+ connectToDevice(device);
+ }
+ });
+
+ listDiscoveredDevices.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ BluetoothDevice device = discoveredDevicesList.get(position);
+ connectToDevice(device);
+ }
+ });
+
+ // Register for broadcasts when a device is discovered
+ IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
+ filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
+ registerReceiver(discoveryReceiver, filter);
+ }
+
+ private void loadPairedDevices() {
+ pairedDevicesList.clear();
+ pairedDevicesAdapter.clear();
+
+ if (checkBluetoothPermission()) {
+ Set pairedDevices = bluetoothAdapter.getBondedDevices();
+ if (pairedDevices.size() > 0) {
+ for (BluetoothDevice device : pairedDevices) {
+ pairedDevicesList.add(device);
+ String deviceInfo = device.getName() + "\n" + device.getAddress();
+ pairedDevicesAdapter.add(deviceInfo);
+ }
+ } else {
+ pairedDevicesAdapter.add("No paired devices found");
+ }
+ } else {
+ pairedDevicesAdapter.add("Bluetooth permission required");
+ }
+
+ pairedDevicesAdapter.notifyDataSetChanged();
+ }
+
+ private void makeDiscoverable() {
+ if (!checkBluetoothPermission()) {
+ Toast.makeText(this, "Bluetooth permission required", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
+ discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, DISCOVERABLE_DURATION);
+ startActivityForResult(discoverableIntent, REQUEST_DISCOVERABLE);
+ }
+
+ private void startDeviceDiscovery() {
+ if (!checkBluetoothPermission()) {
+ Toast.makeText(this, "Bluetooth permission required", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ discoveredDevicesList.clear();
+ discoveredDevicesAdapter.clear();
+ discoveredDevicesAdapter.notifyDataSetChanged();
+
+ if (bluetoothAdapter.isDiscovering()) {
+ bluetoothAdapter.cancelDiscovery();
+ }
+
+ boolean started = bluetoothAdapter.startDiscovery();
+ if (started) {
+ isScanning = true;
+ btnScanDevices.setText("Stop Scan");
+ updateProgress("Scanning for devices...");
+ } else {
+ Toast.makeText(this, "Failed to start device discovery", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ private void stopDeviceDiscovery() {
+ if (checkBluetoothPermission() && bluetoothAdapter.isDiscovering()) {
+ bluetoothAdapter.cancelDiscovery();
+ }
+ isScanning = false;
+ btnScanDevices.setText("Scan for Devices");
+ updateProgress("");
+ }
+
+ private void connectToDevice(final BluetoothDevice device) {
+ if (!checkBluetoothPermission()) {
+ Toast.makeText(this, "Bluetooth permission required", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ // Stop discovery to save resources
+ if (bluetoothAdapter.isDiscovering()) {
+ bluetoothAdapter.cancelDiscovery();
+ isScanning = false;
+ btnScanDevices.setText("Scan for Devices");
+ }
+
+ new AlertDialog.Builder(this)
+ .setTitle("Connect to Device")
+ .setMessage("Connect to " + device.getName() + " (" + device.getAddress() + ") for sync?")
+ .setPositiveButton("Connect", new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ startConnectThread(device);
+ }
+ })
+ .setNegativeButton("Cancel", null)
+ .show();
+ }
+
+ private void startConnectThread(BluetoothDevice device) {
+ // Cancel any existing connection attempts
+ if (connectThread != null) {
+ connectThread.cancel();
+ connectThread = null;
+ }
+
+ connectThread = new ConnectThread(device);
+ connectThread.start();
+ updateProgress("Connecting to " + device.getName() + "...");
+ }
+
+ private void startAcceptThread() {
+ if (acceptThread != null) {
+ acceptThread.cancel();
+ }
+ acceptThread = new AcceptThread();
+ acceptThread.start();
+ }
+
+ private boolean checkBluetoothPermission() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ return checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED;
+ }
+ return true;
+ }
+
+ private void updateStatus(String status) {
+ mainHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ txtBluetoothStatus.setText(status);
+ }
+ });
+ Log.d(TAG, "Status: " + status);
+ }
+
+ private void updateProgress(String progress) {
+ mainHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ txtSyncProgress.setText(progress);
+ }
+ });
+ Log.d(TAG, "Progress: " + progress);
+ }
+
+ private void disableAllButtons() {
+ btnMakeDiscoverable.setEnabled(false);
+ btnScanDevices.setEnabled(false);
+ }
+
+ // BroadcastReceiver for Bluetooth device discovery
+ private final BroadcastReceiver discoveryReceiver = new BroadcastReceiver() {
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+
+ if (BluetoothDevice.ACTION_FOUND.equals(action)) {
+ BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+ if (device != null && checkBluetoothPermission()) {
+ String deviceName = device.getName();
+ if (deviceName == null) {
+ deviceName = "Unknown Device";
+ }
+
+ // Avoid duplicates
+ boolean alreadyAdded = false;
+ for (BluetoothDevice existingDevice : discoveredDevicesList) {
+ if (existingDevice.getAddress().equals(device.getAddress())) {
+ alreadyAdded = true;
+ break;
+ }
+ }
+
+ if (!alreadyAdded) {
+ discoveredDevicesList.add(device);
+ String deviceInfo = deviceName + "\n" + device.getAddress();
+ discoveredDevicesAdapter.add(deviceInfo);
+ discoveredDevicesAdapter.notifyDataSetChanged();
+ }
+ }
+ } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {
+ isScanning = false;
+ btnScanDevices.setText("Scan for Devices");
+ updateProgress("Discovery finished. Found " + discoveredDevicesList.size() + " devices.");
+ }
+ }
+ };
+
+ // Thread for accepting incoming connections
+ private class AcceptThread extends Thread {
+ private BluetoothServerSocket serverSocket;
+
+ public AcceptThread() {
+ try {
+ if (checkBluetoothPermission()) {
+ serverSocket = bluetoothAdapter.listenUsingRfcommWithServiceRecord(
+ SERVICE_NAME, SERVICE_UUID);
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Socket listen() failed", e);
+ }
+ }
+
+ @Override
+ public void run() {
+ BluetoothSocket socket = null;
+
+ while (true) {
+ try {
+ socket = serverSocket.accept();
+ } catch (IOException e) {
+ Log.e(TAG, "Socket accept() failed", e);
+ break;
+ }
+
+ if (socket != null) {
+ // Connection accepted
+ manageConnectedSocket(socket);
+ try {
+ serverSocket.close();
+ } catch (IOException e) {
+ Log.e(TAG, "Could not close server socket", e);
+ }
+ break;
+ }
+ }
+ }
+
+ public void cancel() {
+ try {
+ if (serverSocket != null) {
+ serverSocket.close();
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Could not close server socket", e);
+ }
+ }
+ }
+
+ // Thread for connecting to a device
+ private class ConnectThread extends Thread {
+ private BluetoothSocket socket;
+ private BluetoothDevice device;
+
+ public ConnectThread(BluetoothDevice device) {
+ this.device = device;
+
+ try {
+ if (checkBluetoothPermission()) {
+ socket = device.createRfcommSocketToServiceRecord(SERVICE_UUID);
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Socket create() failed", e);
+ }
+ }
+
+ @Override
+ public void run() {
+ // Cancel discovery to save resources
+ if (checkBluetoothPermission() && bluetoothAdapter.isDiscovering()) {
+ bluetoothAdapter.cancelDiscovery();
+ }
+
+ try {
+ if (checkBluetoothPermission()) {
+ socket.connect();
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Could not connect to device", e);
+ mainHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ updateProgress("Connection failed: " + e.getMessage());
+ Toast.makeText(BluetoothSyncActivity.this, "Connection failed", Toast.LENGTH_SHORT).show();
+ }
+ });
+
+ try {
+ socket.close();
+ } catch (IOException closeException) {
+ Log.e(TAG, "Could not close client socket", closeException);
+ }
+ return;
+ }
+
+ // Connection successful
+ manageConnectedSocket(socket);
+ }
+
+ public void cancel() {
+ try {
+ if (socket != null) {
+ socket.close();
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Could not close client socket", e);
+ }
+ }
+ }
+
+ // Thread for managing a connected socket
+ private class ConnectedThread extends Thread {
+ private BluetoothSocket socket;
+ private InputStream inputStream;
+ private OutputStream outputStream;
+
+ public ConnectedThread(BluetoothSocket socket) {
+ this.socket = socket;
+
+ try {
+ inputStream = socket.getInputStream();
+ outputStream = socket.getOutputStream();
+ } catch (IOException e) {
+ Log.e(TAG, "Error occurred when creating input/output streams", e);
+ }
+ }
+
+ @Override
+ public void run() {
+ byte[] buffer = new byte[1024];
+ int numBytes;
+
+ // Keep listening to the InputStream while connected
+ while (true) {
+ try {
+ numBytes = inputStream.read(buffer);
+ String receivedMessage = new String(buffer, 0, numBytes, "UTF-8");
+ handleReceivedMessage(receivedMessage);
+ } catch (IOException e) {
+ Log.d(TAG, "Input stream was disconnected", e);
+ mainHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ updateProgress("Connection lost");
+ }
+ });
+ break;
+ }
+ }
+ }
+
+ public void write(String message) {
+ try {
+ byte[] bytes = message.getBytes("UTF-8");
+ outputStream.write(bytes);
+ Log.d(TAG, "Sent: " + message);
+ } catch (IOException e) {
+ Log.e(TAG, "Error occurred when sending data", e);
+ }
+ }
+
+ public void cancel() {
+ try {
+ socket.close();
+ } catch (IOException e) {
+ Log.e(TAG, "Could not close connected socket", e);
+ }
+ }
+ }
+
+ private void manageConnectedSocket(BluetoothSocket socket) {
+ mainHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ updateProgress("Connected! Starting sync...");
+ }
+ });
+
+ // Cancel existing threads
+ if (connectThread != null) {
+ connectThread.cancel();
+ connectThread = null;
+ }
+ if (acceptThread != null) {
+ acceptThread.cancel();
+ acceptThread = null;
+ }
+
+ // Start the connected thread
+ connectedThread = new ConnectedThread(socket);
+ connectedThread.start();
+
+ // Start the sync protocol
+ startSyncProtocol();
+ }
+
+ private void handleReceivedMessage(String message) {
+ Log.d(TAG, "Received: " + message);
+
+ String[] parts = message.split("\\|", 2);
+ String command = parts[0];
+ String data = parts.length > 1 ? parts[1] : "";
+
+ switch (command) {
+ case PROTOCOL_HANDSHAKE:
+ handleHandshake(data);
+ break;
+ case PROTOCOL_FILE_LIST:
+ handleFileList(data);
+ break;
+ case PROTOCOL_FILE_REQUEST:
+ handleFileRequest(data);
+ break;
+ case PROTOCOL_FILE_DATA:
+ handleFileData(data);
+ break;
+ case PROTOCOL_SYNC_COMPLETE:
+ handleSyncComplete();
+ break;
+ case PROTOCOL_ERROR:
+ handleError(data);
+ break;
+ }
+ }
+
+ private void startSyncProtocol() {
+ if (connectedThread != null) {
+ connectedThread.write(PROTOCOL_HANDSHAKE + "|" + android.os.Build.MODEL);
+ }
+ }
+
+ private void handleHandshake(String deviceInfo) {
+ mainHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ updateProgress("Handshake received from " + deviceInfo);
+ }
+ });
+
+ // Send our file list
+ sendFileList();
+ }
+
+ private void sendFileList() {
+ List notes = NoteManager.getAllNoteInfo(notesDirectory);
+ StringBuilder fileList = new StringBuilder();
+
+ for (NoteManager.NoteInfo note : notes) {
+ if (fileList.length() > 0) {
+ fileList.append(";");
+ }
+ fileList.append(note.name).append(",")
+ .append(note.lastModified).append(",")
+ .append(note.hash);
+ }
+
+ if (connectedThread != null) {
+ connectedThread.write(PROTOCOL_FILE_LIST + "|" + fileList.toString());
+ }
+ }
+
+ private void handleFileList(String fileListData) {
+ // Parse remote file list and determine what files to sync
+ mainHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ updateProgress("Comparing files...");
+ }
+ });
+
+ // Simple implementation: request all files that are different
+ // In a full implementation, you'd compare timestamps and hashes
+ String[] files = fileListData.split(";");
+ for (String fileInfo : files) {
+ if (!fileInfo.isEmpty()) {
+ String[] parts = fileInfo.split(",");
+ if (parts.length >= 3) {
+ String fileName = parts[0];
+ long timestamp = Long.parseLong(parts[1]);
+ String hash = parts[2];
+
+ // Check if we need this file
+ NoteManager.NoteInfo localNote = NoteManager.getNoteInfo(notesDirectory, fileName);
+ if (localNote == null || localNote.lastModified < timestamp || !localNote.hash.equals(hash)) {
+ // Request this file
+ if (connectedThread != null) {
+ connectedThread.write(PROTOCOL_FILE_REQUEST + "|" + fileName);
+ }
+ }
+ }
+ }
+ }
+
+ // Send sync complete when done
+ mainHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (connectedThread != null) {
+ connectedThread.write(PROTOCOL_SYNC_COMPLETE + "|");
+ }
+ }
+ }, 1000);
+ }
+
+ private void handleFileRequest(String fileName) {
+ String content = NoteManager.loadNote(notesDirectory, fileName);
+ if (content != null) {
+ if (connectedThread != null) {
+ connectedThread.write(PROTOCOL_FILE_DATA + "|" + fileName + ":" + content);
+ }
+ } else {
+ if (connectedThread != null) {
+ connectedThread.write(PROTOCOL_ERROR + "|File not found: " + fileName);
+ }
+ }
+ }
+
+ private void handleFileData(String fileData) {
+ int colonIndex = fileData.indexOf(':');
+ if (colonIndex > 0) {
+ String fileName = fileData.substring(0, colonIndex);
+ String content = fileData.substring(colonIndex + 1);
+
+ boolean saved = NoteManager.saveNote(notesDirectory, fileName, content);
+
+ mainHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (saved) {
+ updateProgress("Received file: " + fileName);
+ } else {
+ updateProgress("Failed to save: " + fileName);
+ }
+ }
+ });
+ }
+ }
+
+ private void handleSyncComplete() {
+ mainHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ updateProgress("Sync completed successfully!");
+ Toast.makeText(BluetoothSyncActivity.this, "Sync completed!", Toast.LENGTH_LONG).show();
+
+ // Close connection after a delay
+ mainHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ finish();
+ }
+ }, 2000);
+ }
+ });
+ }
+
+ private void handleError(String error) {
+ mainHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ updateProgress("Error: " + error);
+ Toast.makeText(BluetoothSyncActivity.this, "Sync error: " + error, Toast.LENGTH_SHORT).show();
+ }
+ });
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ if (requestCode == REQUEST_DISCOVERABLE) {
+ if (resultCode > 0) {
+ updateStatus("Device is discoverable for " + resultCode + " seconds");
+ updateProgress("Waiting for incoming connections...");
+ } else {
+ updateStatus("Discoverable request denied");
+ }
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ finish();
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+
+ // Clean up threads
+ if (connectThread != null) {
+ connectThread.cancel();
+ }
+ if (connectedThread != null) {
+ connectedThread.cancel();
+ }
+ if (acceptThread != null) {
+ acceptThread.cancel();
+ }
+
+ // Unregister broadcast receiver
+ try {
+ unregisterReceiver(discoveryReceiver);
+ } catch (IllegalArgumentException e) {
+ // Receiver was not registered
+ }
+
+ // Stop discovery
+ if (bluetoothAdapter != null && checkBluetoothPermission() && bluetoothAdapter.isDiscovering()) {
+ bluetoothAdapter.cancelDiscovery();
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/eu/octt/notetand/CustomActivity.java b/app/src/main/java/org/eu/octt/notetand/CustomActivity.java
new file mode 100644
index 0000000..cd17b74
--- /dev/null
+++ b/app/src/main/java/org/eu/octt/notetand/CustomActivity.java
@@ -0,0 +1,60 @@
+package org.eu.octt.notetand;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.Window;
+
+import java.lang.reflect.Method;
+
+abstract public class CustomActivity extends Activity {
+ @SuppressLint("InlinedApi")
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ SettingsManager.setup(this);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ switch (SettingsManager.getTheme()) {
+ case "holo_dark":
+ setTheme(android.R.style.Theme_Holo);
+ break;
+ case "holo_light":
+ setTheme(android.R.style.Theme_Holo_Light);
+ break;
+ case "material_dark":
+ setTheme(android.R.style.Theme_Material);
+ break;
+ case "material_light":
+ setTheme(android.R.style.Theme_Material_Light);
+ break;
+ default:
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
+ setTheme(android.R.style.Theme_DeviceDefault_DayNight);
+ break;
+ }
+ }
+ }
+
+ @Override
+ public boolean onMenuOpened(int featureId, Menu menu) {
+ if (featureId == Window.FEATURE_ACTION_BAR && menu != null) {
+ try {
+ Method method = menu.getClass().getDeclaredMethod("setOptionalIconsVisible", Boolean.TYPE);
+ method.setAccessible(true);
+ method.invoke(menu, true);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ return super.onMenuOpened(featureId, menu);
+ }
+
+ void setActionBarBack() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB && getActionBar() != null) {
+ getActionBar().setDisplayHomeAsUpEnabled(true);
+ }
+ }
+}
diff --git a/app/src/main/java/org/eu/octt/notetand/MainActivity.java b/app/src/main/java/org/eu/octt/notetand/MainActivity.java
new file mode 100644
index 0000000..c60c8bb
--- /dev/null
+++ b/app/src/main/java/org/eu/octt/notetand/MainActivity.java
@@ -0,0 +1,87 @@
+package org.eu.octt.notetand;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+
+import java.util.List;
+
+public class MainActivity extends CustomActivity {
+ ListView listNotes;
+ List notesList;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ NoteManager.setup(this);
+
+ listNotes = findViewById(R.id.list_notes);
+// var notesList = NoteManager.getAllNoteNames(); // new ArrayList();
+// var notesAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, notesList);
+// listNotes.setAdapter(notesAdapter);
+
+// // Use Android/data/packagename/files/notes structure
+// var notesDirectory = new File(getExternalFilesDir(null), "notes");
+// if (!notesDirectory.exists()) {
+// notesDirectory.mkdirs();
+// }
+
+ // for (var file : notesDirectory.listFiles()) {
+// for (var file : NoteManager.notesDirectory.listFiles()) {
+// if (file.isFile() && file.getName().toLowerCase().endsWith(".txt")) {
+// notesList.add(file.getName());
+// }
+// }
+ // notesAdapter.notifyDataSetChanged();
+
+ listNotes.setOnItemClickListener((parent, view, position, id) ->
+ launchNote(notesList.get(position), false));
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+
+ notesList = NoteManager.getAllNoteNames(); // new ArrayList();
+ var notesAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, notesList);
+ listNotes.setAdapter(notesAdapter);
+ notesAdapter.notifyDataSetChanged();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.main_menu, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ var id = item.getItemId();
+ if (id == R.id.action_new) {
+ launchNote(null, true);
+ } else if (id == R.id.action_receive) {
+ startActivity(new Intent(this, ReceiveActivity.class));
+ } else if (id == R.id.action_settings) {
+ startActivity(new Intent(this, SettingsActivity.class));
+ } else if (id == R.id.action_about) {
+ startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://gitlab.com/octospacc/NoteTand")));
+ } else {
+ return super.onOptionsItemSelected(item);
+ }
+ return true;
+ }
+
+ void launchNote(String name, boolean isNew) {
+ startActivity(
+ new Intent(this, NoteActivity.class)
+ .putExtra("is_new", isNew)
+ .putExtra("note_name", name)
+ );
+ }
+}
diff --git a/app/src/main/java/org/eu/octt/notetand/MainActivity1.java b/app/src/main/java/org/eu/octt/notetand/MainActivity1.java
new file mode 100644
index 0000000..47e8037
--- /dev/null
+++ b/app/src/main/java/org/eu/octt/notetand/MainActivity1.java
@@ -0,0 +1,380 @@
+package org.eu.octt.notetand;
+
+import android.Manifest;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.bluetooth.BluetoothAdapter;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.io.File;
+import java.util.ArrayList;
+
+public class MainActivity1 extends Activity {
+
+ private static final int REQUEST_ENABLE_BT = 1001;
+ private static final int REQUEST_PERMISSIONS = 1002;
+ private static final int REQUEST_BLUETOOTH_PERMISSIONS = 1003;
+
+ private ListView listNotes;
+ private TextView txtStatus;
+ private TextView txtNoteCount;
+ private Button btnSync;
+ private Button btnAddNote;
+
+ private ArrayList notesList;
+ private ArrayAdapter notesAdapter;
+ private File notesDirectory;
+ private BluetoothAdapter bluetoothAdapter;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.main_activity_1);
+
+ initializeViews();
+ setupNotesDirectory();
+ checkPermissions();
+ initializeBluetooth();
+ loadNotes();
+ setupListeners();
+ }
+
+ private void initializeViews() {
+ listNotes = findViewById(R.id.list_notes);
+ txtStatus = findViewById(R.id.txt_status);
+ txtNoteCount = findViewById(R.id.txt_note_count);
+ btnSync = findViewById(R.id.btn_sync);
+ btnAddNote = findViewById(R.id.btn_add_note);
+
+ notesList = new ArrayList<>();
+ notesAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, notesList);
+ listNotes.setAdapter(notesAdapter);
+
+ // Register context menu for long press
+ registerForContextMenu(listNotes);
+ }
+
+ private void setupNotesDirectory() {
+ // Use Android/data/packagename/files/notes structure
+ File externalFilesDir = getExternalFilesDir(null);
+ if (externalFilesDir != null) {
+ notesDirectory = new File(externalFilesDir, "notes");
+ if (!notesDirectory.exists()) {
+ boolean created = notesDirectory.mkdirs();
+ if (created) {
+ updateStatus("Notes directory created");
+ } else {
+ updateStatus("Failed to create notes directory");
+ }
+ }
+ } else {
+ // Fallback to internal storage
+ notesDirectory = new File(getFilesDir(), "notes");
+ if (!notesDirectory.exists()) {
+ notesDirectory.mkdirs();
+ }
+ updateStatus("Using internal storage");
+ }
+ }
+
+ private void checkPermissions() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ ArrayList permissionsNeeded = new ArrayList<>();
+
+ // Storage permissions (for API < 30)
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+ if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE)
+ != PackageManager.PERMISSION_GRANTED) {
+ permissionsNeeded.add(Manifest.permission.READ_EXTERNAL_STORAGE);
+ }
+ if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ != PackageManager.PERMISSION_GRANTED) {
+ permissionsNeeded.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);
+ }
+ }
+
+ // Location permission for Bluetooth scanning
+ if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)
+ != PackageManager.PERMISSION_GRANTED) {
+ permissionsNeeded.add(Manifest.permission.ACCESS_FINE_LOCATION);
+ }
+
+ if (!permissionsNeeded.isEmpty()) {
+ requestPermissions(permissionsNeeded.toArray(new String[0]), REQUEST_PERMISSIONS);
+ }
+ }
+ }
+
+ private void initializeBluetooth() {
+ bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+
+ if (bluetoothAdapter == null) {
+ updateStatus("Bluetooth not supported");
+ btnSync.setEnabled(false);
+ return;
+ }
+
+ if (!bluetoothAdapter.isEnabled()) {
+ updateStatus("Bluetooth disabled");
+ btnSync.setText("Enable BT");
+ } else {
+ updateStatus("Bluetooth ready");
+ btnSync.setText("Sync");
+ }
+ }
+
+ private void loadNotes() {
+ notesList.clear();
+
+ if (notesDirectory != null && notesDirectory.exists()) {
+ File[] files = notesDirectory.listFiles();
+ if (files != null) {
+ for (File file : files) {
+ if (file.isFile() && file.getName().endsWith(".txt")) {
+ String noteName = file.getName().replace(".txt", "");
+ notesList.add(noteName);
+ }
+ }
+ }
+ }
+
+ notesAdapter.notifyDataSetChanged();
+ updateNoteCount();
+
+ if (notesList.isEmpty()) {
+ updateStatus("No notes found. Tap + to create one.");
+ } else {
+ updateStatus("Loaded " + notesList.size() + " notes");
+ }
+ }
+
+ private void setupListeners() {
+ btnAddNote.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ createNewNote();
+ }
+ });
+
+ btnSync.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ handleSyncButtonClick();
+ }
+ });
+
+ listNotes.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ String noteName = notesList.get(position);
+ openNote(noteName);
+ }
+ });
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ if (v.getId() == R.id.list_notes) {
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+ String noteName = notesList.get(info.position);
+ menu.setHeaderTitle(noteName);
+ menu.add(0, 1, 0, "Open");
+ menu.add(0, 2, 0, "Delete");
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
+ final String noteName = notesList.get(info.position);
+
+ switch (item.getItemId()) {
+ case 1: // Open
+ openNote(noteName);
+ return true;
+ case 2: // Delete
+ new AlertDialog.Builder(this)
+ .setTitle("Delete Note")
+ .setMessage("Are you sure you want to delete '" + noteName + "'?")
+ .setPositiveButton("Delete", new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ deleteNote(noteName);
+ }
+ })
+ .setNegativeButton("Cancel", null)
+ .show();
+ return true;
+ default:
+ return super.onContextItemSelected(item);
+ }
+ }
+
+ private void createNewNote() {
+ // For now, create a simple numbered note
+ String newNoteName = "Note_" + System.currentTimeMillis();
+ Intent intent = new Intent(this, NoteActivity.class);
+ intent.putExtra("note_name", newNoteName);
+ intent.putExtra("is_new", true);
+ startActivity(intent);
+ }
+
+ private void openNote(String noteName) {
+ Intent intent = new Intent(this, NoteActivity.class);
+ intent.putExtra("note_name", noteName);
+ intent.putExtra("is_new", false);
+ startActivity(intent);
+ }
+
+ private void deleteNote(String noteName) {
+ File noteFile = new File(notesDirectory, noteName + ".txt");
+ if (noteFile.exists()) {
+ boolean deleted = noteFile.delete();
+ if (deleted) {
+ updateStatus("Note deleted: " + noteName);
+ loadNotes();
+ } else {
+ updateStatus("Failed to delete note");
+ }
+ }
+ }
+
+ private void handleSyncButtonClick() {
+ if (bluetoothAdapter == null) {
+ Toast.makeText(this, "Bluetooth not supported", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ if (!bluetoothAdapter.isEnabled()) {
+ // Request to enable Bluetooth
+ Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
+ startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
+ } else {
+ // Check Bluetooth permissions for newer Android versions
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ checkBluetoothPermissions();
+ } else {
+ startBluetoothSync();
+ }
+ }
+ }
+
+ private void checkBluetoothPermissions() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ ArrayList permissionsNeeded = new ArrayList<>();
+
+ if (checkSelfPermission(Manifest.permission.BLUETOOTH_SCAN)
+ != PackageManager.PERMISSION_GRANTED) {
+ permissionsNeeded.add(Manifest.permission.BLUETOOTH_SCAN);
+ }
+ if (checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT)
+ != PackageManager.PERMISSION_GRANTED) {
+ permissionsNeeded.add(Manifest.permission.BLUETOOTH_CONNECT);
+ }
+ if (checkSelfPermission(Manifest.permission.BLUETOOTH_ADVERTISE)
+ != PackageManager.PERMISSION_GRANTED) {
+ permissionsNeeded.add(Manifest.permission.BLUETOOTH_ADVERTISE);
+ }
+
+ if (!permissionsNeeded.isEmpty()) {
+ requestPermissions(permissionsNeeded.toArray(new String[0]),
+ REQUEST_BLUETOOTH_PERMISSIONS);
+ } else {
+ startBluetoothSync();
+ }
+ } else {
+ startBluetoothSync();
+ }
+ }
+
+ private void startBluetoothSync() {
+ Intent intent = new Intent(this, BluetoothSyncActivity.class);
+ intent.putExtra("notes_directory", notesDirectory.getAbsolutePath());
+ startActivity(intent);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ if (requestCode == REQUEST_ENABLE_BT) {
+ if (resultCode == RESULT_OK) {
+ updateStatus("Bluetooth enabled");
+ btnSync.setText("Sync");
+ } else {
+ updateStatus("Bluetooth enable cancelled");
+ }
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+
+ switch (requestCode) {
+ case REQUEST_PERMISSIONS:
+ boolean allGranted = true;
+ for (int result : grantResults) {
+ if (result != PackageManager.PERMISSION_GRANTED) {
+ allGranted = false;
+ break;
+ }
+ }
+ if (allGranted) {
+ updateStatus("Permissions granted");
+ } else {
+ updateStatus("Some permissions denied");
+ }
+ break;
+
+ case REQUEST_BLUETOOTH_PERMISSIONS:
+ boolean bluetoothGranted = true;
+ for (int result : grantResults) {
+ if (result != PackageManager.PERMISSION_GRANTED) {
+ bluetoothGranted = false;
+ break;
+ }
+ }
+ if (bluetoothGranted) {
+ startBluetoothSync();
+ } else {
+ Toast.makeText(this, "Bluetooth permissions required for sync",
+ Toast.LENGTH_SHORT).show();
+ }
+ break;
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ loadNotes(); // Refresh notes list when returning from editor
+ }
+
+ private void updateStatus(String status) {
+ txtStatus.setText(status);
+ }
+
+ private void updateNoteCount() {
+ int count = notesList.size();
+ txtNoteCount.setText(count + (count == 1 ? " note" : " notes"));
+ }
+
+ public File getNotesDirectory() {
+ return notesDirectory;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/eu/octt/notetand/NoteActivity.java b/app/src/main/java/org/eu/octt/notetand/NoteActivity.java
new file mode 100644
index 0000000..e9ce0d2
--- /dev/null
+++ b/app/src/main/java/org/eu/octt/notetand/NoteActivity.java
@@ -0,0 +1,334 @@
+package org.eu.octt.notetand;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.io.File;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.Random;
+
+public class NoteActivity extends CustomActivity {
+ String[] EMOJIS = {"🐕", "🐶", "🐩", "🐈", "🐱", "🐀", "🐁", "🐭", "🐹", "🐢", "🐇", "🐰", "🐓", "🐔", "🐣", "🐤", "🐥", "🐦", "🐏", "🐑", "🐐", "🐺", "🐃", "🐂", "🐄", "🐮", "🐴", "🐗", "🐖", "🐷", "🐽", "🐸", "🐍", "🐼", "🐧", "🐘", "🐨", "🐒", "🐵", "🐆", "🐯", "🐻", "🐫", "🐪", "🐊", "🐳", "🐋", "🐟", "🐠", "🐡", "🐙", "🐚", "🐬", "🐌", "🐛", "🐜", "🐝", "🐞", "🐲", "🐉", "🐾", "👻", "👹", "👺", "👽", "👾", "👿", "💀", "💖", "💗", "💘", "💝", "💞", "💟", "🍙", "🍘", "🍠", "🍌", "🍎", "🍏", "🍊", "🍋", "🍄", "🍅", "🍆", "🍇", "🍈", "🍉", "🍐", "🍑", "🍒", "🍓", "🍍", "🌰", "🌱", "🌲", "🌳", "🌴", "🌵", "🌷", "🌸", "🌹", "🍀", "🍁", "🍂", "🍃", "🌺", "🌻", "🌼", "🌽", "🌾", "🌿"};
+
+ private EditText editNoteName;
+ private EditText editNoteContent;
+ private TextView txtNoteInfo;
+// private Button btnSave;
+// private Button btnDelete;
+
+ private String originalNoteName;
+ private String originalContent;
+ private boolean isNewNote;
+ private boolean hasUnsavedChanges;
+ private File notesDirectory;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_note);
+ setActionBarBack();
+
+ NoteManager.setup(this);
+
+ initializeViews();
+ setupNotesDirectory();
+ loadNoteFromIntent();
+ setupListeners();
+ updateNoteInfo();
+ }
+
+ private void initializeViews() {
+ editNoteName = findViewById(R.id.edit_note_name);
+ editNoteContent = findViewById(R.id.edit_note_content);
+ txtNoteInfo = findViewById(R.id.txt_note_info);
+// btnSave = findViewById(R.id.btn_save);
+// btnDelete = findViewById(R.id.btn_delete);
+ }
+
+ private void setupNotesDirectory() {
+ // Get notes directory from MainActivity
+ File externalFilesDir = getExternalFilesDir(null);
+ if (externalFilesDir != null) {
+ notesDirectory = new File(externalFilesDir, "notes");
+ } else {
+ notesDirectory = new File(getFilesDir(), "notes");
+ }
+ }
+
+ private void loadNoteFromIntent() {
+ Intent intent = getIntent();
+
+ if (Intent.ACTION_SEND.equals(intent.getAction()) && "text/plain".equals(intent.getType())) {
+ originalContent = intent.getStringExtra(Intent.EXTRA_TEXT);
+ } else {
+ originalContent = intent.getStringExtra("content");
+ originalNoteName = intent.getStringExtra("note_name");
+ isNewNote = intent.getBooleanExtra("is_new", false);
+ }
+
+ if (originalNoteName == null) {
+ // originalNoteName = "Note_" + System.currentTimeMillis() + ".txt";
+ originalNoteName = (new SimpleDateFormat("yyyy-MM-dd HH-mm-ss.SSS")).format(new Date()) + ' ' + EMOJIS[(new Random()).nextInt(EMOJIS.length)] + ".txt";
+ isNewNote = true;
+ }
+
+ editNoteName.setText(originalNoteName);
+
+ if (!isNewNote || originalContent != null) {
+ // Load existing note content
+ if (originalNoteName != null && originalContent == null)
+ originalContent = NoteManager.loadNote(notesDirectory, originalNoteName);
+ if (originalContent != null) {
+ editNoteContent.setText(originalContent);
+ editNoteContent.setSelection(originalContent.length()); // Move cursor to end
+ } else {
+ originalContent = "";
+ Toast.makeText(this, "Error loading note", Toast.LENGTH_SHORT).show();
+ }
+ // btnDelete.setVisibility(View.VISIBLE);
+ //} else if (originalNoteName == null && originalContent == null) {
+ // originalContent = intent.getStringExtra("content");
+ } // else {
+ // originalContent = "";
+ // // btnDelete.setVisibility(View.GONE);
+ // }
+
+ if (originalContent == null) {
+ originalContent = "";
+ }
+
+ hasUnsavedChanges = (isNewNote && !originalContent.isEmpty()); // false;
+ if (hasUnsavedChanges)
+ setTitle("* " + getString(R.string.note));
+ }
+
+ private void setupListeners() {
+// btnSave.setOnClickListener(v -> saveNote());
+//
+// btnDelete.setOnClickListener(v -> confirmDeleteNote());
+
+ // Track changes to note name
+ editNoteName.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ checkForChanges();
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {}
+ });
+
+ // Track changes to note content
+ editNoteContent.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ checkForChanges();
+ updateNoteInfo();
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {}
+ });
+ }
+
+ private void checkForChanges() {
+ String currentName = editNoteName.getText().toString().trim();
+ String currentContent = editNoteContent.getText().toString();
+
+ hasUnsavedChanges = !currentName.equals(originalNoteName) || !currentContent.equals(originalContent);
+
+ // Update title to indicate unsaved changes
+ //if (getActionBar() != null) {
+ if (hasUnsavedChanges) {
+ /*getActionBar().*/setTitle("* " + getString(R.string.note) /* currentName */);
+ } else {
+ /*getActionBar().*/setTitle(getString(R.string.note) /* currentName */);
+ }
+ //}
+ }
+
+ private void updateNoteInfo() {
+ String content = editNoteContent.getText().toString();
+ int characterCount = content.length();
+ int wordCount = content.trim().isEmpty() ? 0 : content.trim().split("\\s+").length;
+ int lineCount = content.split("\n").length;
+
+ if (isNewNote && !hasUnsavedChanges) {
+ txtNoteInfo.setText(R.string.new_note);
+ } else {
+ txtNoteInfo.setText(String.format(Locale.getDefault(), "%d chars, %d words, %d lines", characterCount, wordCount, lineCount));
+ }
+ }
+
+ private void saveNote() {
+ String noteName = editNoteName.getText().toString().trim();
+ // String content = editNoteContent.getText().toString();
+
+ // Validate note name
+ if (noteName.isEmpty()) {
+ editNoteName.setError("Note name cannot be empty");
+ editNoteName.requestFocus();
+ return;
+ }
+
+ // Sanitize note name
+ String sanitizedName = NoteManager.sanitizeNoteName(noteName);
+ if (!sanitizedName.equals(noteName)) {
+ editNoteName.setText(sanitizedName);
+ noteName = sanitizedName;
+ Toast.makeText(this, "Note name was sanitized for compatibility", Toast.LENGTH_SHORT).show();
+ }
+
+ // Check if we're renaming and the new name already exists
+ if (!noteName.equals(originalNoteName) && NoteManager.noteExists(notesDirectory, noteName)) {
+ new AlertDialog.Builder(this)
+ .setTitle("Note Already Exists")
+ .setMessage("A note with the name '" + noteName + "' already exists. Do you want to overwrite it?")
+ .setPositiveButton("Overwrite", (dialog, which) -> performSave())
+ .setNeutralButton("Cancel", null)
+ .show();
+ } else {
+ performSave();
+ }
+ }
+
+ private void performSave() {
+ String noteName = editNoteName.getText().toString().trim();
+ String content = editNoteContent.getText().toString();
+
+ // Delete old note if we're renaming
+ if (!isNewNote && !noteName.equals(originalNoteName)) {
+ NoteManager.deleteNote(notesDirectory, originalNoteName);
+ }
+
+ // Save the note
+ boolean success = NoteManager.saveNote(notesDirectory, noteName, content);
+
+ if (success) {
+ originalNoteName = noteName;
+ originalContent = content;
+ isNewNote = false;
+ hasUnsavedChanges = false;
+ // btnDelete.setVisibility(View.VISIBLE);
+
+ //if (getActionBar() != null) {
+ /*getActionBar().*/setTitle(getString(R.string.note) /* noteName */);
+ //}
+
+ Toast.makeText(this, R.string.note_saved, Toast.LENGTH_SHORT).show();
+ updateNoteInfo();
+ } else {
+ Toast.makeText(this, "Error saving note", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ private void confirmDeleteNote() {
+ if (isNewNote) {
+ // Just finish if it's a new unsaved note
+ finish();
+ return;
+ }
+
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.delete_note)
+ .setMessage(getString(R.string.delete_note_message, originalNoteName))
+ .setPositiveButton(R.string.delete, (dialog, which) -> deleteNote())
+ .setNeutralButton(R.string.cancel, null)
+ .show();
+ }
+
+ private void deleteNote() {
+ boolean success = NoteManager.deleteNote(notesDirectory, originalNoteName);
+ if (success) {
+ Toast.makeText(this, R.string.note_deleted, Toast.LENGTH_SHORT).show();
+ finish();
+ } else {
+ Toast.makeText(this, "Error deleting note", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.note_menu, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ var id = item.getItemId();
+ if (id == R.id.action_save) {
+ saveNote();
+ } else if (id == R.id.action_delete) {
+ confirmDeleteNote();
+ } else if (id == R.id.action_send) {
+ startActivity(new Intent(this, SendActivity.class).putExtra("note", originalNoteName));
+ } else if (id == R.id.action_share) {
+ startActivity(new Intent(Intent.ACTION_SEND).setType("text/plain").putExtra(Intent.EXTRA_TEXT, originalContent));
+ } else if (id == android.R.id.home) {
+ onBackPressed();
+ } else {
+ return super.onOptionsItemSelected(item);
+ }
+ return true;
+ }
+
+// @Override
+// public boolean onOptionsItemSelected(MenuItem item) {
+// switch (item.getItemId()) {
+// case android.R.id.home:
+// onBackPressed();
+// return true;
+// default:
+// return super.onOptionsItemSelected(item);
+// }
+// }
+
+ @Override
+ public void onBackPressed() {
+ if (hasUnsavedChanges) {
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.unsaved_changes)
+ .setMessage(R.string.unsaved_changes_message)
+ .setPositiveButton(R.string.save, (dialog, which) -> {
+ saveNote();
+ if (!hasUnsavedChanges) { // Only finish if save was successful
+ finish();
+ }
+ })
+ .setNegativeButton(R.string.discard, (dialog, which) -> finish())
+ .setNeutralButton(R.string.cancel, null)
+ .show();
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ // Handle Ctrl+S for save (for external keyboards)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB && keyCode == KeyEvent.KEYCODE_S && event.isCtrlPressed()) {
+ saveNote();
+ return true;
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/eu/octt/notetand/NoteManager.java b/app/src/main/java/org/eu/octt/notetand/NoteManager.java
new file mode 100644
index 0000000..88630f4
--- /dev/null
+++ b/app/src/main/java/org/eu/octt/notetand/NoteManager.java
@@ -0,0 +1,328 @@
+package org.eu.octt.notetand;
+
+import android.content.Context;
+import android.util.Log;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class NoteManager {
+ private static final String TAG = "NoteManager";
+
+ public static File notesDirectory;
+
+ public static class NoteInfo {
+ public String name;
+ public String content;
+ public long lastModified;
+ public String hash;
+ public long size;
+
+ public NoteInfo(String name, String content, long lastModified) {
+ this.name = name;
+ this.content = content;
+ this.lastModified = lastModified;
+ this.size = content.getBytes().length;
+ this.hash = calculateHash(content);
+ }
+ }
+
+ /**
+ * Save a note to the specified directory
+ */
+ public static boolean saveNote(File notesDir, String noteName, String content) {
+ try {
+ File noteFile = new File(notesDir, noteName /* + ".txt" */);
+ FileOutputStream fos = new FileOutputStream(noteFile);
+ fos.write(content.getBytes("UTF-8"));
+ fos.close();
+ Log.d(TAG, "Note saved: " + noteName);
+ return true;
+ } catch (IOException e) {
+ Log.e(TAG, "Error saving note: " + noteName, e);
+ return false;
+ }
+ }
+
+ public static boolean saveNote(String noteName, String content) {
+ try {
+ File noteFile = new File(notesDirectory, noteName /* + ".txt" */);
+ FileOutputStream fos = new FileOutputStream(noteFile);
+ fos.write(content.getBytes("UTF-8"));
+ fos.close();
+ Log.d(TAG, "Note saved: " + noteName);
+ return true;
+ } catch (IOException e) {
+ Log.e(TAG, "Error saving note: " + noteName, e);
+ return false;
+ }
+ }
+
+ /**
+ * Load a note from the specified directory
+ */
+ public static String loadNote(File notesDir, String noteName) {
+ try {
+ File noteFile = new File(notesDir, noteName /* + ".txt" */);
+ if (!noteFile.exists()) {
+ return null;
+ }
+
+ FileInputStream fis = new FileInputStream(noteFile);
+ BufferedReader reader = new BufferedReader(new InputStreamReader(fis, "UTF-8"));
+ StringBuilder content = new StringBuilder();
+ String line;
+
+ while ((line = reader.readLine()) != null) {
+ content.append(line).append("\n");
+ }
+
+ reader.close();
+ fis.close();
+
+ // Remove last newline if present
+ if (content.length() > 0 && content.charAt(content.length() - 1) == '\n') {
+ content.deleteCharAt(content.length() - 1);
+ }
+
+ return content.toString();
+ } catch (IOException e) {
+ Log.e(TAG, "Error loading note: " + noteName, e);
+ return null;
+ }
+ }
+
+ public static String loadNote(String noteName) {
+ try {
+ File noteFile = new File(notesDirectory, noteName);
+ if (!noteFile.exists()) {
+ return null;
+ }
+ FileInputStream fis = new FileInputStream(noteFile);
+ BufferedReader reader = new BufferedReader(new InputStreamReader(fis, "UTF-8"));
+ StringBuilder content = new StringBuilder();
+ String line;
+ while ((line = reader.readLine()) != null) {
+ content.append(line).append("\n");
+ }
+ reader.close();
+ fis.close();
+ // Remove last newline if present
+ if (content.length() > 0 && content.charAt(content.length() - 1) == '\n') {
+ content.deleteCharAt(content.length() - 1);
+ }
+ return content.toString();
+ } catch (IOException e) {
+ Log.e(TAG, "Error loading note: " + noteName, e);
+ return null;
+ }
+ }
+
+ /**
+ * Delete a note from the specified directory
+ */
+ public static boolean deleteNote(File notesDir, String noteName) {
+ File noteFile = new File(notesDir, noteName /* + ".txt" */);
+ if (noteFile.exists()) {
+ boolean deleted = noteFile.delete();
+ if (deleted) {
+ Log.d(TAG, "Note deleted: " + noteName);
+ } else {
+ Log.e(TAG, "Failed to delete note: " + noteName);
+ }
+ return deleted;
+ }
+ return false;
+ }
+
+ /**
+ * Get all notes from the specified directory
+ */
+// public static List getAllNoteNames(File notesDir) {
+// List noteNames = new ArrayList<>();
+//
+// if (notesDir != null && notesDir.exists()) {
+// File[] files = notesDir.listFiles();
+// if (files != null) {
+// for (File file : files) {
+// if (file.isFile() && file.getName().toLowerCase().endsWith(".txt")) {
+// String noteName = file.getName();//.replace(".txt", "");
+// noteNames.add(noteName);
+// }
+// }
+// }
+// }
+//
+// return noteNames;
+// }
+
+ public static List getAllNoteNames() {
+ var noteNames = new ArrayList();
+ var files = notesDirectory.listFiles();
+ if (files != null) {
+ for (File file : files) {
+ if (file.isFile() && file.getName().toLowerCase().endsWith(".txt")) {
+ noteNames.add(file.getName());
+ }
+ }
+ }
+ Collections.reverse(noteNames);
+ return noteNames;
+ }
+
+ /**
+ * Get detailed information about all notes
+ */
+ public static List getAllNoteInfo(File notesDir) {
+ List noteInfoList = new ArrayList<>();
+
+ if (notesDir != null && notesDir.exists()) {
+ File[] files = notesDir.listFiles();
+ if (files != null) {
+ for (File file : files) {
+ if (file.isFile() && file.getName().toLowerCase().endsWith(".txt")) {
+ String noteName = file.getName();//.replace(".txt", "");
+ String content = loadNote(notesDir, noteName);
+ if (content != null) {
+ NoteInfo info = new NoteInfo(noteName, content, file.lastModified());
+ noteInfoList.add(info);
+ }
+ }
+ }
+ }
+ }
+
+ return noteInfoList;
+ }
+
+ /**
+ * Get information about a specific note
+ */
+ public static NoteInfo getNoteInfo(File notesDir, String noteName) {
+ File noteFile = new File(notesDir, noteName /* + ".txt" */);
+ if (!noteFile.exists()) {
+ return null;
+ }
+
+ String content = loadNote(notesDir, noteName);
+ if (content != null) {
+ return new NoteInfo(noteName, content, noteFile.lastModified());
+ }
+
+ return null;
+ }
+
+ /**
+ * Calculate SHA-256 hash of content for sync comparison
+ */
+ public static String calculateHash(String content) {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ byte[] hash = digest.digest(content.getBytes("UTF-8"));
+ StringBuilder hexString = new StringBuilder();
+
+ for (byte b : hash) {
+ String hex = Integer.toHexString(0xff & b);
+ if (hex.length() == 1) {
+ hexString.append('0');
+ }
+ hexString.append(hex);
+ }
+
+ return hexString.toString();
+ } catch (NoSuchAlgorithmException | IOException e) {
+ Log.e(TAG, "Error calculating hash", e);
+ return String.valueOf(content.hashCode()); // Fallback to simple hash
+ }
+ }
+
+ /**
+ * Check if a note exists
+ */
+ public static boolean noteExists(File notesDir, String noteName) {
+ File noteFile = new File(notesDir, noteName /* + ".txt" */);
+ return noteFile.exists();
+ }
+
+ /**
+ * Get the size of notes directory
+ */
+ public static long getDirectorySize(File notesDir) {
+ long size = 0;
+ if (notesDir != null && notesDir.exists()) {
+ File[] files = notesDir.listFiles();
+ if (files != null) {
+ for (File file : files) {
+ if (file.isFile()) {
+ size += file.length();
+ }
+ }
+ }
+ }
+ return size;
+ }
+
+ /**
+ * Validate note name (no special characters that could cause file system issues)
+ */
+ public static boolean isValidNoteName(String noteName) {
+ if (noteName == null || noteName.trim().isEmpty()) {
+ return false;
+ }
+
+ // Check for invalid characters
+ String invalidChars = "\\/:*?\"<>|";
+ for (char c : invalidChars.toCharArray()) {
+ if (noteName.indexOf(c) != -1) {
+ return false;
+ }
+ }
+
+ // Check length (filesystem limitations)
+ return noteName.length() <= 100;
+ }
+
+ /**
+ * Sanitize note name for file system compatibility
+ */
+ public static String sanitizeNoteName(String noteName) {
+ if (noteName == null) {
+ return "untitled";
+ }
+
+ // Replace invalid characters with underscores
+ String sanitized = noteName.replaceAll("[\\\\/:*?\"<>|]", "_");
+
+ // Trim and limit length
+ sanitized = sanitized.trim();
+ if (sanitized.length() > 100) {
+ sanitized = sanitized.substring(0, 100);
+ }
+
+ // Ensure it's not empty
+ if (sanitized.isEmpty()) {
+ sanitized = "untitled";
+ }
+
+ return sanitized;
+ }
+
+ public static void setup(Context context) {
+ if (NoteManager.notesDirectory == null) {
+ var notesDirectory = new File(context.getExternalFilesDir(null), "notes");
+ if (!notesDirectory.exists()) {
+ notesDirectory.mkdirs();
+ }
+ NoteManager.notesDirectory = notesDirectory;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/eu/octt/notetand/NoteTand.java b/app/src/main/java/org/eu/octt/notetand/NoteTand.java
new file mode 100644
index 0000000..fdb6f79
--- /dev/null
+++ b/app/src/main/java/org/eu/octt/notetand/NoteTand.java
@@ -0,0 +1,16 @@
+package org.eu.octt.notetand;
+
+import java.util.UUID;
+
+public class NoteTand {
+ static final UUID SERVICE_UUID = UUID.fromString("fb7befa5-311b-436e-9c2f-9150fe635a40");
+
+ static String censorMac(String mac) {
+ if (SettingsManager.getCensorMac()) {
+ var parts = mac.split(":");
+ return parts[0] + ":••:••:••:••:" + parts[5];
+ } else {
+ return mac;
+ }
+ }
+}
diff --git a/app/src/main/java/org/eu/octt/notetand/NotesProvider.java b/app/src/main/java/org/eu/octt/notetand/NotesProvider.java
new file mode 100644
index 0000000..26d3d0a
--- /dev/null
+++ b/app/src/main/java/org/eu/octt/notetand/NotesProvider.java
@@ -0,0 +1,40 @@
+package org.eu.octt.notetand;
+
+import android.annotation.TargetApi;
+import android.database.Cursor;
+import android.os.Build;
+import android.os.CancellationSignal;
+import android.os.ParcelFileDescriptor;
+import android.provider.DocumentsProvider;
+
+import java.io.FileNotFoundException;
+
+@TargetApi(Build.VERSION_CODES.KITKAT)
+public class NotesProvider extends DocumentsProvider {
+
+ @Override
+ public Cursor queryRoots(String[] projection) throws FileNotFoundException {
+ return null;
+ }
+
+ @Override
+ public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException {
+ return null;
+ }
+
+ @Override
+ public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException {
+ return null;
+ }
+
+ @Override
+ public ParcelFileDescriptor openDocument(String documentId, String mode, CancellationSignal signal) throws FileNotFoundException {
+ return null;
+ }
+
+ @Override
+ public boolean onCreate() {
+ NoteManager.setup(getContext());
+ return false;
+ }
+}
diff --git a/app/src/main/java/org/eu/octt/notetand/ReceiveActivity.java b/app/src/main/java/org/eu/octt/notetand/ReceiveActivity.java
new file mode 100644
index 0000000..6732443
--- /dev/null
+++ b/app/src/main/java/org/eu/octt/notetand/ReceiveActivity.java
@@ -0,0 +1,163 @@
+package org.eu.octt.notetand;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothServerSocket;
+import android.bluetooth.BluetoothSocket;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+
+public class ReceiveActivity extends CustomActivity {
+ BluetoothServerSocket server;
+ BluetoothSocket socket;
+ TextView textStatus;
+ boolean stopped = false;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_receive);
+
+ textStatus = findViewById(R.id.text_status);
+ runServer();
+ }
+
+ @Override
+ public boolean onNavigateUp() {
+ onBackPressed();
+ return true;
+ }
+
+ @Override
+ public void onBackPressed() {
+ super.onBackPressed();
+ stopServer();
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+
+ if (requestCode == BluetoothManager.REQUEST_PERMISSION_BT) {
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED)
+ runServer();
+ else {
+ Toast.makeText(this, "Bluetooth permission not granted! Please retry.", Toast.LENGTH_SHORT).show();
+ finish();
+ }
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ if (requestCode == BluetoothManager.REQUEST_ENABLE_BT && resultCode == RESULT_OK)
+ runServer();
+ else {
+ Toast.makeText(this, "Bluetooth permission not granted! Please retry.", Toast.LENGTH_SHORT).show();
+ finish();
+ }
+ }
+
+ @SuppressLint("MissingPermission")
+ void runServer() {
+ if (!BluetoothManager.requireBluetooth(this)) return;
+ new Thread(() -> {
+ try {
+ writeStatus("Initializing Bluetooth server...");
+ server = BluetoothAdapter.getDefaultAdapter().listenUsingRfcommWithServiceRecord(getString(R.string.app_name), NoteTand.SERVICE_UUID);
+
+ while (true) {
+ writeStatus("Waiting for new client connection...");
+ socket = server.accept();
+
+ // if (!socket.isConnected()) return;
+
+ var client = socket.getRemoteDevice();
+ // textStatus.append("Connected to client!\n");
+ // textStatus.append("Connected: " + client.getName() + " : " + client.getAddress() + '\n');
+ writeStatus("Connected: " + client.getName() + " : " + NoteTand.censorMac(client.getAddress()));
+
+ // byte[] buffer = new byte[1024];
+// int bytes;
+//
+// while ((bytes = socket.getInputStream().read(buffer)) != -1) {
+// var received = new String(buffer, 0, bytes);
+//
+// // runOnUiThread(() -> Toast.makeText(this, "Received: " + received, Toast.LENGTH_SHORT).show());
+// // textStatus.append(received + '\n');
+// writeStatus("Receiving data...");
+// writeStatus("> " + received);
+// }
+
+ writeStatus("Reading data...");
+
+ var titleLength = ByteBuffer.wrap(readFully(4)).getInt(); // ByteBuffer.wrap(intBuffer).order(ByteOrder.BIG_ENDIAN).getInt(); // or LITTLE_ENDIAN
+ var titleBytes = readFully(titleLength);
+ var noteTitle = new String(titleBytes, "UTF-8");
+
+ var bodyLength = ByteBuffer.wrap(readFully(4)).getInt(); // ByteBuffer.wrap(intBuffer).order(ByteOrder.BIG_ENDIAN).getInt();
+ var bodyBytes = readFully(bodyLength);
+ var noteBody = new String(bodyBytes, "UTF-8");
+
+ if (titleLength == titleBytes.length && bodyLength == bodyBytes.length) {
+ NoteManager.saveNote(noteTitle, noteBody);
+ writeStatus("Received and saved note (" + titleLength + " + " + bodyLength + " bytes): " + noteTitle);
+ } else {
+ writeStatus("Content length mismatch! Expected " + titleLength + " + " + bodyLength + ", got " + titleBytes.length + " + " + bodyBytes.length);
+ }
+
+ // writeStatus("Finished receiving!");
+ }
+ } catch (IOException e) {
+ if (!stopped) {
+ writeStatus("Fatal error! Restarting...");
+ e.printStackTrace();
+ runServer();
+ }
+ }
+ }).start();
+ }
+
+ void stopServer() {
+ stopped = true;
+ if (server != null) {
+ try {
+ if (socket != null) {
+ socket.getInputStream().close();
+ socket.getOutputStream().close();
+ socket.close();
+ }
+ server.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ void writeStatus(String text) {
+ runOnUiThread(() -> textStatus.append(text + '\n'));
+ }
+
+ byte[] readFully(/* InputStream in, byte[] buffer, */ int length) throws IOException {
+ byte[] buffer = new byte[length];
+ int offset = 0;
+ while (offset < length) {
+ int count = socket.getInputStream().read(buffer, offset, length - offset);
+ if (count == -1) throw new EOFException("Stream ended early");
+ offset += count;
+ }
+ return buffer;
+ }
+}
diff --git a/app/src/main/java/org/eu/octt/notetand/SendActivity.java b/app/src/main/java/org/eu/octt/notetand/SendActivity.java
new file mode 100644
index 0000000..7381767
--- /dev/null
+++ b/app/src/main/java/org/eu/octt/notetand/SendActivity.java
@@ -0,0 +1,170 @@
+package org.eu.octt.notetand;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothSocket;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.Toast;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Objects;
+import java.util.Set;
+
+public class SendActivity extends CustomActivity {
+ BluetoothSocket socket;
+ AlertDialog dialog;
+ String statusLog;
+
+ @SuppressLint("MissingPermission")
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_send);
+
+ setActionBarBack();
+ showTargets();
+ }
+
+ @Override
+ public boolean onNavigateUp() {
+ onBackPressed();
+ return true;
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+
+ if (requestCode == BluetoothManager.REQUEST_PERMISSION_BT) {
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED)
+ showTargets();
+ else {
+ Toast.makeText(this, "Bluetooth permission not granted! Please retry.", Toast.LENGTH_SHORT).show();
+ finish();
+ }
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ if (requestCode == BluetoothManager.REQUEST_ENABLE_BT && resultCode == RESULT_OK)
+ showTargets();
+ else {
+ Toast.makeText(this, "Bluetooth permission not granted! Please retry.", Toast.LENGTH_SHORT).show();
+ finish();
+ }
+ }
+
+ @SuppressLint("MissingPermission")
+ void showTargets() {
+ if (!BluetoothManager.requireBluetooth(this)) return;
+
+ ListView listPairedDevices = findViewById(R.id.list_paired_devices);
+ var pairedDevicesList = new ArrayList();
+ var pairedDevicesAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1);
+ listPairedDevices.setAdapter(pairedDevicesAdapter);
+
+ var pairedDevices = BluetoothAdapter.getDefaultAdapter().getBondedDevices();
+ for (BluetoothDevice device : pairedDevices) {
+ pairedDevicesList.add(device);
+ pairedDevicesAdapter.add(device.getName() + '\n' + NoteTand.censorMac(device.getAddress()));
+ }
+ pairedDevicesAdapter.notifyDataSetChanged();
+
+ listPairedDevices.setOnItemClickListener((parent, view, position, id) -> {
+ statusLog = "";
+ dialog = new AlertDialog.Builder(this)
+ .setTitle("Sending Note")
+ .setMessage("Preparing...")
+ // .setNegativeButton("Abort", null)
+ .setNeutralButton("Close", null)
+ .show();
+ //.create();
+ setDialogCancelable(false);
+ //dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setEnabled(false);
+ //dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setVisibility(ListView.GONE);
+ //dialog.show();
+ var device = pairedDevicesList.get(position);
+ new Thread(() -> {
+ try {
+ writeStatus("Creating socket to target device...");
+ socket = device.createRfcommSocketToServiceRecord(NoteTand.SERVICE_UUID);
+
+ writeStatus("Trying to connect...");
+ socket.connect();
+ writeStatus("Connected successfully!");
+ Log.d("Bluetooth", "Connection successful");
+ var output = socket.getOutputStream();
+
+ var noteName = getIntent().getStringExtra("note");
+ var noteContent = NoteManager.loadNote(noteName);
+
+ var nameBytes = noteName.getBytes("UTF-8");
+ var contentBytes = noteContent.getBytes("UTF-8");
+
+ writeStatus("Sending data...");
+
+ output.write(ByteBuffer.allocate(4).putInt(nameBytes.length /*noteName.length()*/).array());
+ output.write(nameBytes);
+
+ output.write(ByteBuffer.allocate(4).putInt(contentBytes.length /*noteContent.length()*/).array());
+ output.write(contentBytes);
+
+ writeStatus("Note sent!");
+ //dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setVisibility(ListView.VISIBLE);
+ //dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setEnabled(true);
+ //dialog.setCancelable(true);
+ setDialogCancelable(true);
+
+ socket.getInputStream().close();
+ socket.getOutputStream().close();
+ socket.close();
+ } catch (IOException e) {
+ // e.printStackTrace();
+ Log.e("Bluetooth", "Connection failed", e);
+ writeStatus(e.getMessage());
+ setDialogCancelable(true);
+ }
+ closeSocket();
+ }).start();
+ });
+ }
+
+ void closeSocket() {
+ if (socket != null) {
+ try {
+ socket.getInputStream().close();
+ socket.getOutputStream().close();
+ socket.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ void writeStatus(String text) {
+ statusLog = statusLog + text + '\n';
+ runOnUiThread(() -> dialog.setMessage(statusLog.trim()));
+ }
+
+ void setDialogCancelable(boolean status) {
+ runOnUiThread(() -> {
+ //dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setVisibility(ListView.VISIBLE);
+ dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setEnabled(status);
+ dialog.setCancelable(status);
+ });
+ }
+}
diff --git a/app/src/main/java/org/eu/octt/notetand/SettingsActivity.java b/app/src/main/java/org/eu/octt/notetand/SettingsActivity.java
new file mode 100644
index 0000000..4704121
--- /dev/null
+++ b/app/src/main/java/org/eu/octt/notetand/SettingsActivity.java
@@ -0,0 +1,128 @@
+package org.eu.octt.notetand;
+
+import android.app.Fragment;
+import android.content.SharedPreferences;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.BaseAdapter;
+import android.widget.CheckBox;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+public class SettingsActivity extends CustomActivity {
+// @Override
+// protected void onCreate(Bundle savedInstanceState) {
+// super.onCreate(savedInstanceState);
+// // getFragmentManager().beginTransaction().add(new SettingsFragment(), "").commit();
+// // addPreferencesFromResource(R.xml.preferences);
+//
+// }
+
+// class SettingsFragment extends Fragment {
+//
+// }
+
+ private ListView listView;
+ private ArrayList settingViews = new ArrayList<>();
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ listView = new ListView(this);
+
+ settingViews.add(createCheckboxSetting(getString(R.string.censor_mac), "censor_mac", true));
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
+ settingViews.add(createSpinnerSetting(getString(R.string.app_theme), "theme", new String[]{"system", "material_dark", "material_light", "holo_dark", "holo_light"}, "system"));
+ else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
+ settingViews.add(createSpinnerSetting(getString(R.string.app_theme), "theme", new String[]{"system", "holo_dark", "holo_light"}, "system"));
+
+ // settingViews.add(createNumberSetting(getString(R.string.font_size), "font_size"));
+
+ // Adapter to wrap views into ListView
+ listView.setAdapter(new BaseAdapter() {
+ @Override
+ public int getCount() {
+ return settingViews.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return settingViews.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ return settingViews.get(position);
+ }
+ });
+
+ setContentView(listView);
+ }
+
+ private View createCheckboxSetting(String label, String key, boolean defaultValue) {
+ LinearLayout layout = new LinearLayout(this);
+ layout.setOrientation(LinearLayout.HORIZONTAL);
+ layout.setPadding(32, 32, 32, 32);
+
+ TextView textView = new TextView(this);
+ textView.setText(label);
+ textView.setTextSize(16);
+ textView.setLayoutParams(new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1));
+
+ CheckBox checkBox = new CheckBox(this);
+ checkBox.setChecked(SettingsManager.prefs.getBoolean(key, defaultValue));
+ checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ SettingsManager.prefs.edit().putBoolean(key, isChecked).apply();
+ });
+
+ layout.addView(textView);
+ layout.addView(checkBox);
+ return layout;
+ }
+
+ private View createSpinnerSetting(String label, String key, String[] options, String defaultValue) {
+ LinearLayout layout = new LinearLayout(this);
+ layout.setOrientation(LinearLayout.VERTICAL);
+ layout.setPadding(32, 32, 32, 32);
+
+ TextView textView = new TextView(this);
+ textView.setText(label);
+ textView.setTextSize(16);
+
+ Spinner spinner = new Spinner(this);
+ ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, options);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ spinner.setAdapter(adapter);
+
+ String currentValue = SettingsManager.prefs.getString(key, defaultValue);
+ int selectedIndex = Arrays.asList(options).indexOf(currentValue);
+ spinner.setSelection(Math.max(selectedIndex, 0));
+
+ spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ public void onItemSelected(AdapterView> parent, View view, int position, long id) {
+ SettingsManager.prefs.edit().putString(key, options[position]).apply();
+ }
+ public void onNothingSelected(AdapterView> parent) {}
+ });
+
+ layout.addView(textView);
+ layout.addView(spinner);
+ return layout;
+ }
+}
diff --git a/app/src/main/java/org/eu/octt/notetand/SettingsManager.java b/app/src/main/java/org/eu/octt/notetand/SettingsManager.java
new file mode 100644
index 0000000..9a05b3d
--- /dev/null
+++ b/app/src/main/java/org/eu/octt/notetand/SettingsManager.java
@@ -0,0 +1,23 @@
+package org.eu.octt.notetand;
+
+import static android.content.Context.MODE_PRIVATE;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+public class SettingsManager {
+ static SharedPreferences prefs;
+
+ static void setup(Context context) {
+ if (prefs == null)
+ prefs = context.getSharedPreferences("settings", MODE_PRIVATE);
+ }
+
+ static boolean getCensorMac() {
+ return prefs.getBoolean("censor_mac", true);
+ }
+
+ static String getTheme() {
+ return prefs.getString("theme", "system");
+ }
+}
diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..f269a20
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_note.xml b/app/src/main/res/layout/activity_note.xml
new file mode 100644
index 0000000..7f92392
--- /dev/null
+++ b/app/src/main/res/layout/activity_note.xml
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_receive.xml b/app/src/main/res/layout/activity_receive.xml
new file mode 100644
index 0000000..57c495f
--- /dev/null
+++ b/app/src/main/res/layout/activity_receive.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_send.xml b/app/src/main/res/layout/activity_send.xml
new file mode 100644
index 0000000..cb2eb3a
--- /dev/null
+++ b/app/src/main/res/layout/activity_send.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/bluetooth_sync_activity.xml b/app/src/main/res/layout/bluetooth_sync_activity.xml
new file mode 100644
index 0000000..666c683
--- /dev/null
+++ b/app/src/main/res/layout/bluetooth_sync_activity.xml
@@ -0,0 +1,152 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/main_activity_1.xml b/app/src/main/res/layout/main_activity_1.xml
new file mode 100644
index 0000000..cd33489
--- /dev/null
+++ b/app/src/main/res/layout/main_activity_1.xml
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/main_menu.xml b/app/src/main/res/menu/main_menu.xml
new file mode 100644
index 0000000..6380765
--- /dev/null
+++ b/app/src/main/res/menu/main_menu.xml
@@ -0,0 +1,26 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/note_menu.xml b/app/src/main/res/menu/note_menu.xml
new file mode 100644
index 0000000..4dfc05a
--- /dev/null
+++ b/app/src/main/res/menu/note_menu.xml
@@ -0,0 +1,26 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..036d09b
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..036d09b
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..6013ee1
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..b934f7e
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..6013ee1
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..cb2f12f
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..8096365
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..cb2f12f
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..0613cfa
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..b434655
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..0613cfa
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..da75b0b
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..efabfa0
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..da75b0b
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..62e4f37
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..dad3af5
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62e4f37
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000..27beafe
--- /dev/null
+++ b/app/src/main/res/values-it/strings.xml
@@ -0,0 +1,29 @@
+
+
+ Nuova Nota in NoteTand
+
+ Info
+ Impostazioni
+ Censura Indirizzi MAC
+ Tema della App
+
+ Nota
+ Nome della Nota
+ Nuova Nota
+ Invia Nota
+ Ricevi Note
+ Elimina Nota
+ Vuoi davvero eliminare \"%s\"?
+ Nota Salvata
+ Nota Eliminata
+ Scrivi la tua nota qui...
+
+ Condividi Esternamente
+ Invia
+ Salva
+ Scarta
+ Elimina
+ Annulla
+ Modifiche Non Salvate
+ Hai modifiche non salvate. Cosa vuoi fare?
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..f8c6127
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 0000000..cdbc994
--- /dev/null
+++ b/app/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #4289D4
+
\ No newline at end of file
diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml
new file mode 100644
index 0000000..f967fc6
--- /dev/null
+++ b/app/src/main/res/values/ids.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..aeb7c0b
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,112 @@
+
+ NoteTand
+ Nuova Nota in NoteTand
+
+ About
+ Settings
+ Censor MAC Addresses
+ App Theme
+
+ Note
+ Note Name
+ New Note
+ Send Note
+ Receive Notes
+ Delete Note
+ Are you sure you want to delete \'%s\'?
+ Note Saved
+ Note Deleted
+ Type your note here...
+
+ Share Externally
+ Send
+ Save
+ Discard
+ Delete
+ Cancel
+ Unsaved Changes
+ You have unsaved changes. What would you like to do?
+
+
+ My Notes
+ Sync
+ +
+ Ready
+ 0 notes
+ 1 note
+ %d notes
+
+
+ Note name
+ Start typing your note here...
+ Save
+ Delete
+ %d chars, %d words, %d lines
+
+
+ Bluetooth Sync
+ Status
+ Make Discoverable
+ Scan for Devices
+ Stop Scan
+ Cancel
+ Paired Devices:
+ Discovered Devices:
+ Available Devices
+
+
+ Bluetooth not supported
+ Bluetooth disabled
+ Bluetooth ready
+ Bluetooth permission required
+ No paired devices found
+ Scanning for devices...
+ Discovery finished. Found %d devices.
+ Connecting to %s...
+ Connection failed
+ Connected! Starting sync...
+ Sync completed!
+ Sync error: %s
+ Connection lost
+ Comparing files...
+ Received file: %s
+ Failed to save: %s
+ Waiting for incoming connections...
+ Device is discoverable for %d seconds
+ Discoverable request denied
+
+
+ Delete Note
+ Delete
+
+ Unsaved Changes
+
+ Note Already Exists
+ A note with the name \'%s\' already exists. Do you want to overwrite it?
+ Overwrite
+
+ Connect to Device
+ Connect to %s (%s) for sync?
+ Connect
+
+
+ Note name cannot be empty
+ Error loading note
+ Error saving note
+ Error deleting note
+ Note name was sanitized for compatibility
+ Notes directory created
+ Failed to create notes directory
+ Using internal storage
+ Loaded %d notes
+ No notes found. Tap + to create one.
+ Permissions granted
+ Some permissions denied
+ Failed to start device discovery
+
+
+ Add new note
+ Sync notes via Bluetooth
+ Save note
+ Delete note
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..fa0f996
--- /dev/null
+++ b/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..9ee9997
--- /dev/null
+++ b/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..565f8c2
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,4 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+alias(libs.plugins.android.application) apply false
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..4387edc
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,21 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. For more details, visit
+# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 0000000..b2b4542
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,24 @@
+[versions]
+agp = "8.8.1"
+junit = "4.13.2"
+junitVersion = "1.2.1"
+espressoCore = "3.6.1"
+appcompat = "1.7.0"
+material = "1.12.0"
+activity = "1.8.0"
+constraintlayout = "2.1.4"
+preference = "1.2.1"
+
+[libraries]
+junit = { group = "junit", name = "junit", version.ref = "junit" }
+ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
+espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
+appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
+material = { group = "com.google.android.material", name = "material", version.ref = "material" }
+activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
+constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
+preference = { group = "androidx.preference", name = "preference", version.ref = "preference" }
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e708b1c
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..f16e2cd
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Tue Sep 16 00:48:03 CEST 2025
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100644
index 0000000..4f906e0
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..107acd3
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..e9b7b22
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,23 @@
+pluginManagement {
+ repositories {
+ google {
+ content {
+ includeGroupByRegex("com\\.android.*")
+ includeGroupByRegex("com\\.google.*")
+ includeGroupByRegex("androidx.*")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "NoteTand"
+include ':app'