RebekkaMa před 4 roky
revize
860551cdce
48 změnil soubory, kde provedl 3020 přidání a 0 odebrání
  1. 21 0
      .github/workflows/android.yml
  2. 29 0
      .github/workflows/buildApk.yml
  3. 14 0
      .gitignore
  4. 661 0
      LICENSE
  5. 2 0
      README.md
  6. 1 0
      app/.gitignore
  7. 90 0
      app/build.gradle
  8. 21 0
      app/proguard-rules.pro
  9. 80 0
      app/src/main/AndroidManifest.xml
  10. 5 0
      app/src/main/java/net/folivo/android/smsGatewayServer/ComposeSmsActivity.kt
  11. 12 0
      app/src/main/java/net/folivo/android/smsGatewayServer/HeadlessSmsSendService.kt
  12. 400 0
      app/src/main/java/net/folivo/android/smsGatewayServer/MainActivity.kt
  13. 10 0
      app/src/main/java/net/folivo/android/smsGatewayServer/Message.kt
  14. 240 0
      app/src/main/java/net/folivo/android/smsGatewayServer/MessageManager.kt
  15. 10 0
      app/src/main/java/net/folivo/android/smsGatewayServer/MmsReceiver.kt
  16. 326 0
      app/src/main/java/net/folivo/android/smsGatewayServer/RestApiWorker.kt
  17. 35 0
      app/src/main/java/net/folivo/android/smsGatewayServer/SmsReceiver.kt
  18. 30 0
      app/src/main/res/drawable-v24/ic_launcher_foreground.xml
  19. 5 0
      app/src/main/res/drawable/border.xml
  20. 170 0
      app/src/main/res/drawable/ic_launcher_background.xml
  21. 190 0
      app/src/main/res/layout/activity_main.xml
  22. 5 0
      app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  23. 5 0
      app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
  24. binární
      app/src/main/res/mipmap-hdpi/ic_launcher.png
  25. binární
      app/src/main/res/mipmap-hdpi/ic_launcher_round.png
  26. binární
      app/src/main/res/mipmap-mdpi/ic_launcher.png
  27. binární
      app/src/main/res/mipmap-mdpi/ic_launcher_round.png
  28. binární
      app/src/main/res/mipmap-xhdpi/ic_launcher.png
  29. binární
      app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
  30. binární
      app/src/main/res/mipmap-xxhdpi/ic_launcher.png
  31. binární
      app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
  32. binární
      app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  33. binární
      app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
  34. 17 0
      app/src/main/res/values-de/strings.xml
  35. 6 0
      app/src/main/res/values/colors.xml
  36. 24 0
      app/src/main/res/values/strings.xml
  37. 10 0
      app/src/main/res/values/styles.xml
  38. 9 0
      app/src/test/java/net/folivo/android/smsGatewayServer/KotestConfig.kt
  39. 185 0
      app/src/test/java/net/folivo/android/smsGatewayServer/MessageManagerTest.kt
  40. 90 0
      app/src/test/java/net/folivo/android/smsGatewayServer/RestApiWorkerTest.kt
  41. 5 0
      app/src/test/java/net/folivo/android/smsGatewayServer/TestApplication.kt
  42. 27 0
      build.gradle
  43. 21 0
      gradle.properties
  44. binární
      gradle/wrapper/gradle-wrapper.jar
  45. 6 0
      gradle/wrapper/gradle-wrapper.properties
  46. 172 0
      gradlew
  47. 84 0
      gradlew.bat
  48. 2 0
      settings.gradle

+ 21 - 0
.github/workflows/android.yml

@@ -0,0 +1,21 @@
+name: Android CI
+
+on:
+  push:
+    branches: [ master ]
+  pull_request:
+    branches: [ master ]
+
+jobs:
+  build:
+
+    runs-on: ubuntu-latest
+
+    steps:
+    - uses: actions/checkout@v2
+    - name: set up JDK 1.8
+      uses: actions/setup-java@v1
+      with:
+        java-version: 1.8
+    - name: Build with Gradle
+      run: ./gradlew build

+ 29 - 0
.github/workflows/buildApk.yml

@@ -0,0 +1,29 @@
+name: Build Apk
+
+on:
+    push:
+      branches: [ master ]
+    pull_request:
+      branches: [ master ]
+
+jobs:
+
+
+  build:
+    name: Generate APK
+    runs-on: ubuntu-latest
+
+    steps:
+    - uses: actions/checkout@v2
+    - name: set up JDK 1.8
+      uses: actions/setup-java@v1
+      with:
+        java-version: 1.8
+    - name: Build Debug APK
+      run: bash ./gradlew :app:assembleDebug
+              
+    - name: Upload APK
+      uses: actions/upload-artifact@v2
+      with:
+        name: App
+        path: ${{ github.workspace }}/app/build/outputs/apk/debug/app-debug.apk

+ 14 - 0
.gitignore

@@ -0,0 +1,14 @@
+*.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

+ 661 - 0
LICENSE

@@ -0,0 +1,661 @@
+                    GNU AFFERO GENERAL PUBLIC LICENSE
+                       Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ 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.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    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 <https://www.gnu.org/licenses/>.
+
+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
+<https://www.gnu.org/licenses/>.

+ 2 - 0
README.md

@@ -0,0 +1,2 @@
+# android-sms-gateway-server
+Android App, which allows you to send and receive sms via REST-API

+ 1 - 0
app/.gitignore

@@ -0,0 +1 @@
+/build

+ 90 - 0
app/build.gradle

@@ -0,0 +1,90 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-android-extensions'
+
+android {
+    compileSdkVersion 29
+
+    testOptions {
+        unitTests.returnDefaultValues = true
+        unitTests.all {
+            useJUnitPlatform()
+        }
+        unitTests {
+            includeAndroidResources = true
+        }
+    }
+
+
+    defaultConfig {
+        applicationId "net.folivo.android.smsGatewayServer"
+        minSdkVersion 27
+        targetSdkVersion 29
+        versionCode 1
+        versionName "1.0"
+
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+
+    packagingOptions {
+        exclude 'META-INF/*'
+    }
+
+    kotlinOptions {
+        jvmTarget = "1.8"
+    }
+
+
+}
+
+
+dependencies {
+    implementation 'androidx.legacy:legacy-support-v4:1.0.0'
+    def fragment_version = "1.3.0-alpha08"
+    def kotest_version = "4.2.5"
+    def activity_version = "1.2.0-alpha08"
+    def mockk_version = "1.10.0"
+    def ktor_version = "1.4.1"
+
+
+    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
+
+    testImplementation "io.kotest:kotest-runner-junit5:$kotest_version" // for kotest framework
+    testImplementation "io.kotest:kotest-assertions-core:$kotest_version"
+    // for kotest core jvm assertions
+    testImplementation "io.kotest:kotest-property:$kotest_version" // for kotest property test
+    testImplementation "io.kotest:kotest-extensions-robolectric:4.0.1"
+    testImplementation "io.mockk:mockk:$mockk_version"
+    testImplementation 'androidx.test:core:1.3.0'
+    testImplementation 'org.robolectric:robolectric:4.4'
+    testImplementation 'junit:junit:4.13'
+
+
+
+    implementation "io.ktor:ktor:$ktor_version"
+    implementation "io.ktor:ktor-server-netty:$ktor_version"
+    implementation "io.ktor:ktor-gson:$ktor_version"
+    implementation "io.ktor:ktor-auth:$ktor_version"
+    implementation "io.ktor:ktor-jackson:$ktor_version"
+    implementation "io.ktor:ktor-network-tls-certificates:$ktor_version"
+    implementation "io.ktor:ktor-client-cio:$ktor_version"
+    implementation "androidx.work:work-runtime-ktx:2.4.0"
+    implementation "androidx.activity:activity-ktx:$activity_version"
+    implementation "androidx.fragment:fragment:$fragment_version"
+    implementation 'androidx.core:core-ktx:1.3.2'
+    implementation 'androidx.appcompat:appcompat:1.2.0'
+    implementation 'androidx.constraintlayout:constraintlayout:2.0.3'
+    implementation 'ch.qos.logback:logback-classic:1.2.3'
+    implementation "org.jetbrains.kotlin:kotlin-stdlib:1.4.10"
+    implementation 'com.google.android.material:material:1.2.1'
+
+
+}

+ 21 - 0
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

+ 80 - 0
app/src/main/AndroidManifest.xml

@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="net.folivo.android.smsGatewayServer">
+
+    <uses-permission android:name="android.permission.SEND_SMS" />
+    <uses-permission android:name="android.permission.READ_SMS" />
+    <uses-permission android:name="android.permission.WRITE_SMS" />
+    <uses-permission android:name="android.permission.RECEIVE_SMS" />
+    <uses-permission android:name="android.permission.RECEIVE_MMS" />
+    <uses-permission android:name="android.permission.RECEIVE_WAP_PUSH" />
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+
+    <application
+        android:allowBackup="true"
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/app_name"
+        android:roundIcon="@mipmap/ic_launcher_round"
+        android:supportsRtl="true"
+        android:theme="@style/AppTheme">
+
+        <service
+            android:name="net.folivo.android.smsGatewayServer.HeadlessSmsSendService"
+            android:enabled="true"
+            android:exported="true"
+            android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE">
+            <intent-filter>
+                <action android:name="android.intent.action.RESPOND_VIA_MESSAGE" />
+
+                <category android:name="android.intent.category.DEFAULT" />
+
+                <data android:scheme="sms" />
+                <data android:scheme="smsto" />
+                <data android:scheme="mms" />
+                <data android:scheme="mmsto" />
+            </intent-filter>
+        </service>
+
+        <activity android:name="net.folivo.android.smsGatewayServer.ComposeSmsActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.SEND" />
+                <action android:name="android.intent.action.SENDTO" />
+
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+
+                <data android:scheme="sms" />
+                <data android:scheme="smsto" />
+                <data android:scheme="mms" />
+                <data android:scheme="mmsto" />
+            </intent-filter>
+        </activity>
+        <activity android:name="net.folivo.android.smsGatewayServer.MainActivity" android:configChanges="orientation|screenSize|screenLayout|keyboardHidden">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+
+                <category android:name="android.intent.category.LAUNCHER" />
+
+                <category android:name="android.app.role.SMS" />
+            </intent-filter>
+        </activity> <!-- BroadcastReceiver that listens for incoming SMS messages -->
+        <receiver
+            android:name="net.folivo.android.smsGatewayServer.SmsReceiver"
+            android:permission="android.permission.BROADCAST_SMS">
+            <intent-filter>
+                <action android:name="android.provider.Telephony.SMS_DELIVER" />
+            </intent-filter>
+        </receiver> <!-- BroadcastReceiver that listens for incoming MMS messages -->
+        <receiver
+            android:name="net.folivo.android.smsGatewayServer.MmsReceiver"
+            android:permission="android.permission.BROADCAST_WAP_PUSH">
+            <intent-filter>
+                <action android:name="android.provider.Telephony.WAP_PUSH_DELIVER" />
+
+                <data android:mimeType="application/vnd.wap.mms-message" />
+            </intent-filter>
+        </receiver>
+    </application>
+
+</manifest>

+ 5 - 0
app/src/main/java/net/folivo/android/smsGatewayServer/ComposeSmsActivity.kt

@@ -0,0 +1,5 @@
+package net.folivo.android.smsGatewayServer
+
+import androidx.appcompat.app.AppCompatActivity
+
+class ComposeSmsActivity : AppCompatActivity()

+ 12 - 0
app/src/main/java/net/folivo/android/smsGatewayServer/HeadlessSmsSendService.kt

@@ -0,0 +1,12 @@
+package net.folivo.android.smsGatewayServer
+
+import android.app.Service
+import android.content.Intent
+import android.os.IBinder
+
+class HeadlessSmsSendService : Service() {
+
+    override fun onBind(intent: Intent): IBinder? {
+        return null
+    }
+}

+ 400 - 0
app/src/main/java/net/folivo/android/smsGatewayServer/MainActivity.kt

@@ -0,0 +1,400 @@
+package net.folivo.android.smsGatewayServer
+
+import android.Manifest
+import android.app.Activity
+import android.app.role.RoleManager
+import android.content.Intent
+import android.content.SharedPreferences
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.provider.Telephony
+import android.util.Log
+import android.widget.Button
+import android.widget.EditText
+import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.widget.AppCompatImageButton
+import androidx.appcompat.widget.SwitchCompat
+import androidx.work.*
+import java.lang.Exception
+import java.security.KeyStore
+import java.time.Duration
+import java.util.*
+import java.util.concurrent.ExecutionException
+import kotlin.time.ExperimentalTime
+
+
+class MainActivity : AppCompatActivity() {
+
+    private val logTag = MainActivity::class.java.simpleName
+
+    private val workManager = WorkManager.getInstance(this)
+    private val uniqueWorkName = "restApiWorker"
+
+    private lateinit var sharedPref: SharedPreferences
+
+
+    private val smsDefaultAppResultLauncher = registerForActivityResult(
+        StartActivityForResult()
+    ) {
+        if (it.resultCode == Activity.RESULT_OK) {
+            Log.i(logTag, "Default SMS app permission is granted.")
+        } else {
+            Log.i(logTag, "Default SMS app permission is not granted.")
+        }
+    }
+
+    private val certificateResultLauncher = registerForActivityResult(
+        StartActivityForResult()
+    ) {
+        if (it.resultCode == Activity.RESULT_OK) {
+            val intent = it.data
+            intent?.data?.also { uri ->
+                val sharedPref: SharedPreferences = this.getPreferences(MODE_PRIVATE)
+                val editor = sharedPref.edit()
+                editor.putString(getString(R.string.saved_certificate_uri_str_key), uri.toString())
+                editor.commit()
+                findViewById<EditText>(R.id.editTextPathKeyStore).setText(uri.toString())
+                Log.i(logTag, "certificate uri: $uri")
+
+                val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION
+                contentResolver.takePersistableUriPermission(uri, takeFlags)
+            }
+        }
+    }
+
+    private val storagePermissionResultLauncher = registerForActivityResult(
+        ActivityResultContracts.RequestPermission()
+    ) { isGranted: Boolean ->
+        if (isGranted) {
+            Log.i(logTag, "Storage permission is granted.")
+        } else {
+            Log.i(logTag, "Storage permission is not granted.")
+        }
+    }
+
+    private val onSharedPreferenceChangeListener =
+        OnSharedPreferenceChangeListener { sharedPreferences, key ->
+            when (key) {
+                getString(R.string.saved_username_key) -> findViewById<EditText>(R.id.editTextUserName).hint =
+                    sharedPreferences.getString(key, "admin")
+                getString(R.string.saved_password_key) -> findViewById<EditText>(R.id.editTextPassword).hint =
+                    sharedPreferences.getString(key, "123")
+                getString(R.string.saved_keyStore_password_key) -> findViewById<EditText>(R.id.editTextKeyStorePassword).hint =
+                    sharedPreferences.getString(key, "")
+                getString(R.string.saved_certificate_uri_str_key) -> findViewById<EditText>(R.id.editTextPathKeyStore).hint =
+                    sharedPreferences.getString(key, "")
+            }
+        }
+
+
+    @ExperimentalTime
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.activity_main)
+
+        val editTextUserName = findViewById<EditText>(R.id.editTextUserName)
+        val editTextPassword = findViewById<EditText>(R.id.editTextPassword)
+        val editTextKeyStorePassword = findViewById<EditText>(R.id.editTextKeyStorePassword)
+        val editTextPathKeyStore = findViewById<EditText>(R.id.editTextPathKeyStore)
+
+
+        sharedPref = this.getPreferences(MODE_PRIVATE)
+        sharedPref.registerOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener)
+
+        editTextUserName.hint =
+            sharedPref.getString(getString(R.string.saved_username_key), "admin")
+        editTextPassword.hint =
+            sharedPref.getString(getString(R.string.saved_password_key), "123")
+        editTextKeyStorePassword.hint =
+            sharedPref.getString(getString(R.string.saved_keyStore_password_key), "")
+        editTextPathKeyStore.hint =
+            sharedPref.getString(getString(R.string.saved_certificate_uri_str_key), "")
+
+        findViewById<SwitchCompat>(R.id.switchStartServer).isEnabled = isCertificateAvailable()
+
+        findViewById<Button>(R.id.buttonCheckCertificateAvailability).setOnClickListener {
+
+            val keyStorePassword =
+                editTextKeyStorePassword.text.toString()
+            val keyStorePath =
+                editTextPathKeyStore.text.toString()
+            val editor = sharedPref.edit()
+
+
+            if (!keyStorePassword.isBlank()) {
+                editor.putString(
+                    getString(R.string.saved_keyStore_password_key),
+                    keyStorePassword
+                )
+                editor.commit()
+            }
+
+            if (!keyStorePath.isBlank()) {
+                editor.putString(getString(R.string.saved_certificate_uri_str_key), keyStorePath)
+                editor.commit()
+            }
+
+            if (!isCertificateAvailable()) {
+                Toast.makeText(this, "Error loading certificate: ", Toast.LENGTH_SHORT).show()
+                findViewById<SwitchCompat>(R.id.switchStartServer).isEnabled = false
+            } else {
+                Toast.makeText(this, "Certificate loaded successfully", Toast.LENGTH_SHORT).show()
+                findViewById<SwitchCompat>(R.id.switchStartServer).isEnabled = true
+
+            }
+
+        }
+
+
+        findViewById<AppCompatImageButton>(R.id.buttonCertificate).setOnClickListener {
+
+
+            if (storagePermissionGranted()) {
+                // Choose a directory using the system's file picker.
+                val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
+                    addCategory(Intent.CATEGORY_OPENABLE)
+                    type = "application/x-pkcs12"
+                }
+                certificateResultLauncher.launch(intent)
+
+            } else {
+                storagePermissionResultLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
+
+            }
+
+
+        }
+
+        findViewById<SwitchCompat>(R.id.switchStartServer).setOnCheckedChangeListener { viewButton, isChecked ->
+
+            if (isDefaultSmsApp()) {
+                if (isChecked && !isUniqueWorkScheduled(uniqueWorkName)) {
+
+                    val txtUserName = editTextUserName.text.toString()
+                    val txtPassword = editTextPassword.text.toString()
+
+                    val editor = sharedPref.edit()
+
+                    if (!txtPassword.isBlank()) {
+                        editor.putString(getString(R.string.saved_password_key), txtPassword)
+                        editor.commit()
+                    }
+
+                    if (!txtUserName.isBlank()) {
+                        editor.putString(getString(R.string.saved_username_key), txtUserName)
+                        editor.commit()
+                    }
+
+
+                    if (!isCertificateAvailable()) {
+                        Toast.makeText(this, "Error loading the certificate", Toast.LENGTH_SHORT)
+                            .show()
+                        findViewById<SwitchCompat>(R.id.switchStartServer).isEnabled = false
+                    }
+
+
+                    val data =
+                        Data.Builder()
+                            .putString(
+                                "username",
+                                sharedPref.getString(
+                                    getString(R.string.saved_username_key),
+                                    "admin"
+                                )
+                            )
+                            .putString(
+                                "password",
+                                sharedPref.getString(getString(R.string.saved_password_key), "123")
+                            )
+                            .putString(
+                                "certificateUriStr",
+                                sharedPref.getString(
+                                    getString(R.string.saved_certificate_uri_str_key),
+                                    null
+                                )
+                            )
+                            .putString(
+                                "keyStorePassword",
+                                sharedPref.getString(
+                                    getString(R.string.saved_keyStore_password_key),
+                                    ""
+                                )
+                            )
+                            .build()
+
+                    val restApiWorkRequest =
+                        OneTimeWorkRequestBuilder<RestApiWorker>().setInputData(data)
+                            .addTag(uniqueWorkName)
+                            .build()
+
+                    workManager.enqueueUniqueWork(
+                        uniqueWorkName,
+                        ExistingWorkPolicy.KEEP,
+                        restApiWorkRequest
+                    )
+                }
+                if (!isChecked && isUniqueWorkScheduled(uniqueWorkName)) {
+                    workManager.cancelUniqueWork(uniqueWorkName)
+
+                }
+            } else {
+                requestSmsDefaultAppState()
+                viewButton.toggle()
+            }
+        }
+
+        findViewById<Button>(R.id.buttonDeleteMessages).setOnClickListener{
+
+            var toastText = "Messages deleted"
+            val messageManager = MessageManager(applicationContext)
+            val durationInMillis = Duration.ofDays(14).toMillis()
+            try {
+                messageManager.deleteInboxMessagesOlderThan(Calendar.getInstance().timeInMillis.minus(durationInMillis))
+            }catch (exception : Exception){
+                Log.w(logTag, exception.toString())
+                toastText = "Messages could not be deleted"
+            }
+            Toast.makeText(this, toastText, Toast.LENGTH_SHORT).show()
+
+        }
+
+        workManager.getWorkInfosForUniqueWorkLiveData(uniqueWorkName).observe(this) {
+
+            when (it.singleOrNull()?.state) {
+                WorkInfo.State.SUCCEEDED, WorkInfo.State.CANCELLED -> {
+                    Toast.makeText(
+                        this,
+                        getString(R.string.toast_server_terminated_successfully),
+                        Toast.LENGTH_SHORT
+                    ).show()
+                    Log.i(
+                        logTag,
+                        getString(R.string.toast_server_terminated_successfully) + ": " + it.single().state
+                    )
+                }
+                WorkInfo.State.FAILED -> {
+                    Toast.makeText(
+                        this,
+                        getString(R.string.toast_server_terminated_failed),
+                        Toast.LENGTH_SHORT
+                    ).show()
+                    Log.i(
+                        logTag,
+                        getString(R.string.toast_server_terminated_failed) + ": " + it.single().state
+                    )
+                    findViewById<SwitchCompat>(R.id.switchStartServer).toggle()
+                }
+
+                else -> {}
+            }
+        }
+    }
+
+    override fun onResume() {
+        super.onResume()
+        val switch = findViewById<SwitchCompat>(R.id.switchStartServer)
+        if ((isUniqueWorkScheduled(uniqueWorkName) && !switch.isChecked) || (!isUniqueWorkScheduled(
+                uniqueWorkName
+            ) && switch.isChecked)
+        ) {
+            switch.toggle()
+        }
+
+
+    }
+
+
+    override fun onDestroy() {
+        super.onDestroy()
+        if (isUniqueWorkScheduled(uniqueWorkName)) {
+            workManager.cancelUniqueWork(uniqueWorkName)
+        }
+    }
+
+    private fun isUniqueWorkScheduled(tag: String): Boolean {
+        return try {
+            val workerState = workManager.getWorkInfosForUniqueWork(tag).get().singleOrNull()?.state
+            workerState == WorkInfo.State.RUNNING || workerState == WorkInfo.State.ENQUEUED
+        } catch (e: ExecutionException) {
+            Log.w(logTag, e.stackTraceToString())
+            false
+        } catch (e: InterruptedException) {
+            Log.w(logTag, e.stackTraceToString())
+            false
+        }
+    }
+
+    private fun storagePermissionGranted(): Boolean {
+        return checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED
+    }
+
+    private fun isDefaultSmsApp(): Boolean {
+        return this.packageName == Telephony.Sms.getDefaultSmsPackage(this)
+    }
+
+
+    private fun requestSmsDefaultAppState() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+            val roleManager: RoleManager = this.getSystemService(RoleManager::class.java)
+            // check if the app is having permission to be as default SMS app
+            val isRoleAvailable = roleManager.isRoleAvailable(RoleManager.ROLE_SMS)
+            if (isRoleAvailable) {
+                // check whether your app is already holding the default SMS app role.
+                val isRoleHeld = roleManager.isRoleHeld(RoleManager.ROLE_SMS)
+                if (!isRoleHeld) {
+                    val roleRequestIntent =
+                        roleManager.createRequestRoleIntent(RoleManager.ROLE_SMS)
+
+                    smsDefaultAppResultLauncher.launch(roleRequestIntent)
+
+                }
+            }
+        } else {
+            val intent = Intent(Telephony.Sms.Intents.ACTION_CHANGE_DEFAULT)
+            intent.putExtra(Telephony.Sms.Intents.EXTRA_PACKAGE_NAME, packageName)
+            smsDefaultAppResultLauncher.launch(intent)
+        }
+    }
+
+    private fun isCertificateAvailable(): Boolean {
+
+        val certificateUriStr =
+            sharedPref.getString(getString(R.string.saved_certificate_uri_str_key), "").orEmpty()
+        val keyStorePassword =
+            sharedPref.getString(getString(R.string.saved_keyStore_password_key), "").orEmpty()
+        val keyStore: KeyStore
+        val certificateFileNameWithoutExtension: String
+
+
+        try {
+            certificateFileNameWithoutExtension =
+                RestApiWorker.getKeyStoreFileName(
+                    applicationContext.contentResolver,
+                    Uri.parse(certificateUriStr)
+                )
+            keyStore = RestApiWorker.loadKeyStore(
+                applicationContext.contentResolver,
+                Uri.parse(certificateUriStr),
+                keyStorePassword
+            )
+        } catch (exception: Exception) {
+            Log.i(logTag, "isCertificateAvailable: false -> $exception")
+            return false
+        }
+        if (keyStore.getCertificate(certificateFileNameWithoutExtension) == null) {
+            Log.i(
+                logTag,
+                "isCertificateAvailable: false -> given alias does not exist or does not contain a certificate"
+            )
+            return false
+        }
+        return true
+    }
+
+}

+ 10 - 0
app/src/main/java/net/folivo/android/smsGatewayServer/Message.kt

@@ -0,0 +1,10 @@
+package net.folivo.android.smsGatewayServer
+
+class Message(
+    val id: Long,
+    val number: String,
+    val body: String,
+    val dateReceived: String,
+    val dateSent: String,
+    val type: String
+)

+ 240 - 0
app/src/main/java/net/folivo/android/smsGatewayServer/MessageManager.kt

@@ -0,0 +1,240 @@
+package net.folivo.android.smsGatewayServer
+
+import android.app.Activity
+import android.app.PendingIntent
+import android.content.*
+import android.database.Cursor
+import android.database.sqlite.SQLiteCantOpenDatabaseException
+import android.database.sqlite.SQLiteException
+import android.net.Uri
+import android.provider.Telephony
+import android.telephony.SmsManager
+import android.util.Log
+import io.ktor.http.*
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.channels.sendBlocking
+import java.lang.IllegalArgumentException
+import java.time.Instant
+import java.util.*
+import kotlin.collections.ArrayList
+
+
+class MessageManager(private val applicationContext: Context) {
+
+    private val logTag = MessageManager::class.java.simpleName
+
+
+    private val inboxURI: Uri = Telephony.Sms.Inbox.CONTENT_URI
+//    private val outboxURI: Uri = Telephony.Sms.Outbox.CONTENT_URI
+//    private val sentURI: Uri = Telephony.Sms.Sent.CONTENT_URI
+    private val allURI: Uri = Telephony.Sms.CONTENT_URI
+    private val messageProjection = arrayOf(Telephony.Sms._ID, Telephony.Sms.ADDRESS, Telephony.Sms.BODY, Telephony.Sms.DATE, Telephony.Sms.DATE_SENT, Telephony.Sms.TYPE)
+    private val contentResolver = applicationContext.contentResolver
+
+    private val sentAction = "sent"
+
+    @Throws(IllegalArgumentException::class)
+    suspend fun sendSms(
+        recipientPhoneNumber: String,
+        message: String,
+    ): Pair<HttpStatusCode, String> {
+
+        val sendStatusChannel = Channel<Int>(Channel.UNLIMITED)
+
+        applicationContext.registerReceiver(object : BroadcastReceiver() {
+            override fun onReceive(context: Context?, intent: Intent?) {
+                sendStatusChannel.sendBlocking(resultCode)
+            }
+        }, IntentFilter(sentAction))
+
+        val smsManager: SmsManager = SmsManager.getDefault()
+        val multipartMessageArray = smsManager.divideMessage(message)
+        val resultPair: Pair<HttpStatusCode, String>
+
+
+        // Send simple text message
+        if (multipartMessageArray.count() <= 1) {
+            val sentIntent = PendingIntent.getBroadcast(applicationContext,
+                Random().nextInt(),
+                Intent(sentAction),
+                PendingIntent.FLAG_UPDATE_CURRENT)
+
+            smsManager.sendTextMessage(
+                recipientPhoneNumber,
+                null,
+                message,
+                sentIntent,
+                null
+            )
+            Log.i(
+                logTag,
+                "Request to send simple text message: [message: $message , phone number: $recipientPhoneNumber]"
+            )
+
+            resultPair = convertSmsResultCode(sendStatusChannel.receive())
+
+
+            // Send multipart text message
+        } else {
+
+            val sentIntents = ArrayList<PendingIntent>()
+
+            for (i in 0 until multipartMessageArray.count()) {
+                sentIntents.add(PendingIntent.getBroadcast(applicationContext, i, Intent(sentAction), 0))
+            }
+            smsManager.sendMultipartTextMessage(
+                recipientPhoneNumber,
+                null,
+                multipartMessageArray,
+                sentIntents,
+                null
+            )
+            Log.i(
+                logTag,
+                "Request to send multipart text message: [message: $multipartMessageArray , phone number: $recipientPhoneNumber]"
+            )
+
+            val resultCodes = mutableListOf<Int>()
+            for (i in 0 until multipartMessageArray.count()) {
+                resultCodes.add(sendStatusChannel.receive())
+            }
+
+            val mergedResultCode = resultCodes.find{it != Activity.RESULT_OK} ?: Activity.RESULT_OK
+            resultPair = Pair(convertSmsResultCode(mergedResultCode).first, resultCodes.map{convertSmsResultCode(it)}.joinToString(prefix = "[", postfix = "]"))
+
+        }
+        if (resultPair.first.isSuccess()) {
+            Log.i(logTag, "Message has been successfully sent")
+        } else {
+            Log.i(logTag, "Message has not been successfully sent")
+        }
+        return resultPair
+    }
+
+    @Throws(SQLiteCantOpenDatabaseException::class, IllegalArgumentException::class, SQLiteException::class)
+    fun getInboxMessages(): ArrayList<Message> {
+        val cursor = contentResolver.query(
+            inboxURI,
+            messageProjection,
+            null,
+            null,
+            Telephony.Sms._ID + " ASC"
+        )
+            ?: throw (SQLiteCantOpenDatabaseException("Provider's table not found"))
+        return getArrayListFromTable(cursor)
+    }
+
+    @Throws(SQLiteCantOpenDatabaseException::class, IllegalArgumentException::class, SQLiteException::class)
+    fun getInboxMessagesFromIdUp(requestId: String): ArrayList<Message> {
+        val cursor = contentResolver.query(
+            inboxURI,
+            messageProjection,
+            Telephony.Sms._ID + " > " + requestId,
+            null,
+            Telephony.Sms._ID + " ASC"
+        )
+            ?: throw (SQLiteCantOpenDatabaseException("Provider's table not found"))
+
+        return getArrayListFromTable(cursor)
+    }
+
+    @Throws(IllegalArgumentException::class)
+    private fun getArrayListFromTable(cursor: Cursor?): ArrayList<Message> {
+        val smsList = ArrayList<Message>()
+        cursor?.use {
+            while (it.moveToNext()) {
+                val id = it.getLong(it.getColumnIndexOrThrow(Telephony.Sms._ID))
+                val number =
+                    it.getString(it.getColumnIndexOrThrow(Telephony.Sms.ADDRESS))
+                val body = it.getString(it.getColumnIndexOrThrow(Telephony.Sms.BODY))
+                val dateReceived =
+                    Instant.ofEpochMilli(it.getLong(it.getColumnIndexOrThrow(Telephony.Sms.DATE)))
+                        .toString()
+                val dateSent =
+                    Instant.ofEpochMilli(it.getLong(it.getColumnIndexOrThrow(Telephony.Sms.DATE_SENT)))
+                        .toString()
+                val type = it.getString(it.getColumnIndexOrThrow(Telephony.Sms.TYPE))
+
+                smsList.add(Message(id, number, body, dateReceived, dateSent, type))
+            }
+        }
+        return smsList
+    }
+
+    private fun convertSmsResultCode(resultCode: Int): Pair<HttpStatusCode, String> {
+        return when (resultCode) {
+            Activity.RESULT_OK -> Pair(HttpStatusCode.OK, "Transmission successful")
+            SmsManager.RESULT_ERROR_GENERIC_FAILURE -> Pair(
+                HttpStatusCode.InternalServerError,
+                "Transmission failed"
+            )
+            SmsManager.RESULT_ERROR_RADIO_OFF -> Pair(
+                HttpStatusCode.InternalServerError,
+                "Radio off"
+            )
+            SmsManager.RESULT_ERROR_NULL_PDU -> Pair(
+                HttpStatusCode.InternalServerError,
+                "No PDU defined"
+            )
+            SmsManager.RESULT_ERROR_NO_SERVICE -> Pair(
+                HttpStatusCode.InternalServerError,
+                "No service"
+            )
+            SmsManager.RESULT_ERROR_LIMIT_EXCEEDED -> Pair(
+                HttpStatusCode.InternalServerError,
+                "Failed because we reached the sending queue limit"
+            )
+            SmsManager.RESULT_ERROR_SHORT_CODE_NOT_ALLOWED -> Pair(
+                HttpStatusCode.InternalServerError,
+                "Failed because user denied the sending of this short code"
+            )
+            SmsManager.RESULT_ERROR_SHORT_CODE_NEVER_ALLOWED -> Pair(
+                HttpStatusCode.InternalServerError,
+                "Failed because the user has denied this app ever send premium short codes"
+            )
+            else -> Pair(
+                HttpStatusCode.InternalServerError,
+                "Error"
+            )
+        }
+    }
+
+    @Throws(SQLiteCantOpenDatabaseException::class, IllegalArgumentException::class, SQLiteException::class)
+    fun getNextBatch(): Long {
+        val cursor = contentResolver.query(
+            inboxURI,
+            messageProjection,
+            Telephony.Sms._ID,
+            null,
+            Telephony.Sms._ID + " DESC"
+        )
+            ?: throw (SQLiteCantOpenDatabaseException("Provider's table not found"))
+        if (cursor.moveToFirst()){
+            cursor.use {
+                return it.getLong(cursor.getColumnIndexOrThrow(Telephony.Sms._ID))
+            }
+        }
+        return 0
+    }
+
+    @Throws(SQLiteException::class)
+    private fun putSentMessageToDatabase(
+        recipientPhoneNumber: String,
+        message: String,
+    ) {
+        val contentValues = ContentValues()
+        contentValues.put(Telephony.Sms.BODY, message)
+        contentValues.put(Telephony.Sms.ADDRESS, recipientPhoneNumber)
+        contentValues.put(Telephony.Sms.DATE_SENT, Calendar.getInstance().timeInMillis.toString())
+        contentValues.put(Telephony.Sms.TYPE, Telephony.Sms.MESSAGE_TYPE_SENT)
+        if (contentResolver.insert(allURI, contentValues) == null) {
+                throw SQLiteException("The message could not be saved")
+            }
+        Log.i(logTag, "Message has been stored successfully")
+    }
+
+
+    fun deleteInboxMessagesOlderThan(time: Long){
+        applicationContext.contentResolver.delete(allURI, Telephony.Sms.DATE +" < "+ time, null)
+    }
+}

+ 10 - 0
app/src/main/java/net/folivo/android/smsGatewayServer/MmsReceiver.kt

@@ -0,0 +1,10 @@
+package net.folivo.android.smsGatewayServer
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+
+class MmsReceiver : BroadcastReceiver() {
+    override fun onReceive(p0: Context?, p1: Intent?) {
+    }
+}

+ 326 - 0
app/src/main/java/net/folivo/android/smsGatewayServer/RestApiWorker.kt

@@ -0,0 +1,326 @@
+package net.folivo.android.smsGatewayServer
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.content.ContentResolver
+import android.content.Context
+import android.database.Cursor
+import android.net.Uri
+import android.provider.OpenableColumns
+import android.util.Log
+import androidx.core.app.NotificationCompat
+import androidx.core.text.isDigitsOnly
+import androidx.work.CoroutineWorker
+import androidx.work.ForegroundInfo
+import androidx.work.WorkerParameters
+import io.ktor.application.*
+import io.ktor.auth.*
+import io.ktor.features.*
+import io.ktor.gson.*
+import io.ktor.http.*
+import io.ktor.request.*
+import io.ktor.response.*
+import io.ktor.routing.*
+import io.ktor.server.engine.*
+import io.ktor.server.netty.*
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.delay
+import java.io.FileInputStream
+import java.io.FileNotFoundException
+import java.io.IOException
+import java.lang.IllegalStateException
+import java.lang.IndexOutOfBoundsException
+import java.security.KeyStore
+import java.security.KeyStoreException
+import java.security.NoSuchAlgorithmException
+import java.security.cert.CertificateException
+import java.util.concurrent.CancellationException
+import java.util.concurrent.TimeUnit
+import kotlin.IllegalArgumentException
+import kotlin.time.Duration
+import kotlin.time.ExperimentalTime
+
+class RestApiWorker(appContext: Context, workerParams: WorkerParameters) :
+    CoroutineWorker(appContext, workerParams) {
+
+
+    private val notificationManager =
+        appContext.getSystemService(Context.NOTIFICATION_SERVICE) as
+                NotificationManager
+
+    private val messageManager = MessageManager(applicationContext)
+    private val logTag = RestApiWorker::class.java.simpleName
+
+
+    data class SmsMessageRequestContent(val recipientPhoneNumber: String, val message: String)
+
+
+    @ExperimentalTime
+    override suspend fun doWork(): Result = coroutineScope {
+
+
+        setForegroundAsync(createForegroundInfo("Server started"))
+
+
+        val password = inputData.getString("password")
+            .orEmpty()
+        val username = inputData.getString("username")
+            .orEmpty()
+        val keyStorePassword = inputData.getString("keyStorePassword")
+            .orEmpty()
+        val certificateUri = Uri.parse(
+            inputData.getString("certificateUriStr")
+                .orEmpty()
+        )
+
+        val certificateFileNameWithoutExtension: String
+        val keyStore: KeyStore
+        val appVersion: String = BuildConfig.VERSION_NAME
+
+        try {
+            certificateFileNameWithoutExtension =
+                getKeyStoreFileName(applicationContext.contentResolver, certificateUri)
+            keyStore =
+                loadKeyStore(applicationContext.contentResolver, certificateUri, keyStorePassword)
+        } catch (exception: java.lang.Exception) {
+            Log.w(logTag, exception.toString())
+            return@coroutineScope Result.failure()
+        }
+
+
+        try {
+            val server = embeddedServer(
+                Netty,
+                applicationEngineEnvironment {
+
+                    sslConnector(
+                        keyStore = keyStore,
+                        keyAlias = certificateFileNameWithoutExtension,
+                        keyStorePassword = {
+                            keyStorePassword.toCharArray()
+                        },
+                        privateKeyPassword = { "".toCharArray() }) {
+                        host = "0.0.0.0"
+                        port = 9090
+                    }
+
+//                    connector {
+//                        host = "0.0.0.0"
+//                        port = 8080
+//                    }
+
+                    module {
+
+                        install(CallLogging)
+                        install(Authentication) {
+                            basic {
+                                realm = "Ktor Server"
+                                validate { credentials ->
+                                    if (credentials.password == password && credentials.name == username
+                                    )
+                                        UserIdPrincipal(credentials.name) else null
+                                }
+                            }
+                        }
+                        install(ContentNegotiation) {
+                            gson {
+//                                setPrettyPrinting()
+                            }
+                        }
+                        routing {
+                            authenticate {
+                                post("/messages/out") {
+                                    val result: Pair<HttpStatusCode, String>
+
+                                    try {
+                                        val smsMessage = call.receive<SmsMessageRequestContent>()
+
+                                        result = messageManager.sendSms(
+                                            smsMessage.recipientPhoneNumber,
+                                            smsMessage.message
+                                        )
+                                    } catch (exception: java.lang.Exception) {
+                                        Log.i(logTag, exception.toString())
+                                        call.respondText(
+                                            exception.toString(),
+                                            status = HttpStatusCode.BadRequest
+                                        )
+                                        return@post
+                                    }
+                                    call.respond(result)
+                                }
+                                get("/") {
+                                    call.respondText { "android sms gateway server ($appVersion) is running" }
+                                }
+                                route("/messages/in") {
+                                    optionalParam("after") {
+                                        get {
+
+                                            val requestId = call.request.queryParameters["after"]
+                                            val smsList: ArrayList<Message>
+                                            val nextBatch: Long
+
+
+                                            if (requestId.isNullOrEmpty()) {
+
+                                                try {
+                                                    smsList =
+                                                        messageManager.getInboxMessages()
+                                                    nextBatch = messageManager.getNextBatch()
+                                                } catch (e: Exception) {
+                                                    Log.w(logTag, e.stackTraceToString())
+                                                    call.respondText(
+                                                        e.toString(),
+                                                        status = HttpStatusCode.InternalServerError
+                                                    )
+                                                    return@get
+                                                }
+                                                call.respond(
+                                                    hashMapOf(
+                                                        "messages" to smsList,
+                                                        "nextBatch" to nextBatch
+                                                    )
+                                                )
+                                                return@get
+                                            }
+                                            if (requestId.isDigitsOnly()) {
+
+                                                try {
+                                                    smsList =
+                                                        messageManager.getInboxMessagesFromIdUp(
+
+                                                            requestId
+                                                        )
+                                                    nextBatch = messageManager.getNextBatch()
+
+                                                } catch (e: Exception) {
+                                                    Log.w(logTag, e.stackTraceToString())
+                                                    call.respondText(
+                                                        e.toString(),
+                                                        status = HttpStatusCode.InternalServerError
+                                                    )
+                                                    return@get
+                                                }
+
+                                                call.respond(
+                                                    hashMapOf(
+                                                        "messages" to smsList,
+                                                        "nextBatch" to nextBatch
+                                                    )
+                                                )
+
+                                            } else {
+                                                call.respondText(
+                                                    "ID must be of type long and positive!",
+                                                    status = HttpStatusCode.BadRequest
+                                                )
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+
+                })
+            server.start()
+            try {
+                delay(Duration.INFINITE)
+            } finally {
+                server.stop(0, 10, TimeUnit.MILLISECONDS)
+            }
+        } catch (exception: java.lang.Exception) {
+            if (exception !is CancellationException) {
+                Log.w(logTag, exception.toString())
+                return@coroutineScope Result.failure()
+            }
+        }
+        Result.success()
+    }
+
+
+    private fun createForegroundInfo(progress: String): ForegroundInfo {
+
+        notificationManager.createNotificationChannel(
+            NotificationChannel(
+                "default",
+                "Default",
+                NotificationManager.IMPORTANCE_HIGH
+            )
+        )
+
+        val notification = NotificationCompat.Builder(applicationContext, "default")
+            .setContentTitle("Server started")
+            .setTicker("Running in background")
+            .setContentText(progress)
+            .setSmallIcon(R.drawable.ic_launcher_foreground)
+            .setOngoing(true)
+            .build()
+
+        return ForegroundInfo(1, notification)
+    }
+
+
+    companion object {
+
+        @Throws(
+            IndexOutOfBoundsException::class,
+            IllegalArgumentException::class,
+            KeyStoreException::class,
+            IllegalStateException::class
+        )
+        fun getKeyStoreFileName(contentResolver: ContentResolver, uri: Uri): String {
+
+            var fileName: String? = null
+            if (uri.scheme == "content") {
+                val cursor: Cursor? = contentResolver.query(uri, null, null, null, null)
+                cursor?.use {
+                    if (it.moveToFirst()) {
+                        fileName =
+                            it.getString(it.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
+                    }
+                }
+            } else {
+                val uriPath = uri.path
+                fileName = uriPath?.substring(uriPath.lastIndexOf("/") + 1)
+            }
+
+            if (fileName.isNullOrBlank()) {
+                throw KeyStoreException("The KeyStore file name could not be extracted.")
+            }
+
+            return fileName?.lastIndexOf('.')?.let { fileName?.substring(0, it) }
+                ?: throw KeyStoreException("The KeyStore file name could not be extracted")
+        }
+
+
+        @Throws(
+            KeyStoreException::class,
+            FileNotFoundException::class,
+            IOException::class,
+            NoSuchAlgorithmException::class,
+            CertificateException::class
+        )
+        fun loadKeyStore(
+            contentResolver: ContentResolver,
+            uri: Uri,
+            keyStorePassword: String
+        ): KeyStore {
+
+            val keyStore = KeyStore.getInstance("pkcs12")
+            if (uri.scheme == "content") {
+                contentResolver.openInputStream(uri)?.use { inputStream ->
+                    keyStore.load(inputStream, keyStorePassword.toCharArray())
+                }
+            } else {
+                keyStore.load(
+                    FileInputStream(uri.path),
+                    keyStorePassword.toCharArray()
+                )
+            }
+            return keyStore
+        }
+    }
+
+
+}

+ 35 - 0
app/src/main/java/net/folivo/android/smsGatewayServer/SmsReceiver.kt

@@ -0,0 +1,35 @@
+package net.folivo.android.smsGatewayServer
+
+import android.content.BroadcastReceiver
+import android.content.ContentValues
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.provider.Telephony
+import java.util.*
+
+class SmsReceiver : BroadcastReceiver() {
+
+    override fun onReceive(context: Context?, intent: Intent?) {
+        // Get SMS map from Intent
+        if (context == null || intent == null || intent.action == null) {
+            return
+        }
+
+        val smsMessages = Telephony.Sms.Intents.getMessagesFromIntent(intent)
+        val body = smsMessages.joinToString(separator = "") { it.displayMessageBody }
+
+        putSmsToDatabase(context, body, smsMessages.first().displayOriginatingAddress)
+    }
+
+    private fun putSmsToDatabase(context: Context, body: String, address: String): Uri? {
+        val contentResolver = context.contentResolver
+        val contentValues = ContentValues()
+        contentValues.put(Telephony.Sms.BODY, body)
+        contentValues.put(Telephony.Sms.DATE, Calendar.getInstance().timeInMillis.toString())
+        contentValues.put(Telephony.Sms.ADDRESS, address)
+        contentValues.put(Telephony.Sms.TYPE, Telephony.Sms.MESSAGE_TYPE_INBOX)
+        return contentResolver.insert(Telephony.Sms.CONTENT_URI, contentValues)
+    }
+
+}

+ 30 - 0
app/src/main/res/drawable-v24/ic_launcher_foreground.xml

@@ -0,0 +1,30 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportWidth="108"
+    android:viewportHeight="108">
+    <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
+        <aapt:attr name="android:fillColor">
+            <gradient
+                android:endX="85.84757"
+                android:endY="92.4963"
+                android:startX="42.9492"
+                android:startY="49.59793"
+                android:type="linear">
+                <item
+                    android:color="#44000000"
+                    android:offset="0.0" />
+                <item
+                    android:color="#00000000"
+                    android:offset="1.0" />
+            </gradient>
+        </aapt:attr>
+    </path>
+    <path
+        android:fillColor="#FFFFFF"
+        android:fillType="nonZero"
+        android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
+        android:strokeWidth="1"
+        android:strokeColor="#00000000" />
+</vector>

+ 5 - 0
app/src/main/res/drawable/border.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
+    <solid android:color="@color/design_default_color_background" />
+    <stroke android:width="1dp" android:color="@android:color/darker_gray" />
+</shape>

+ 170 - 0
app/src/main/res/drawable/ic_launcher_background.xml

@@ -0,0 +1,170 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportWidth="108"
+    android:viewportHeight="108">
+    <path
+        android:fillColor="#3DDC84"
+        android:pathData="M0,0h108v108h-108z" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M9,0L9,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,0L19,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M29,0L29,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M39,0L39,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M49,0L49,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M59,0L59,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M69,0L69,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M79,0L79,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M89,0L89,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M99,0L99,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,9L108,9"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,19L108,19"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,29L108,29"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,39L108,39"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,49L108,49"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,59L108,59"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,69L108,69"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,79L108,79"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,89L108,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,99L108,99"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,29L89,29"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,39L89,39"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,49L89,49"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,59L89,59"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,69L89,69"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,79L89,79"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M29,19L29,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M39,19L39,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M49,19L49,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M59,19L59,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M69,19L69,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M79,19L79,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+</vector>

+ 190 - 0
app/src/main/res/layout/activity_main.xml

@@ -0,0 +1,190 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/constraintLayout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:layout_gravity="center"
+    android:baselineAligned="false"
+    android:gravity="center|fill_vertical"
+    tools:context="net.folivo.android.smsGatewayServer.MainActivity">
+
+    <LinearLayout
+        android:id="@+id/linLayoutServerOptions"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="16dp"
+        android:layout_marginTop="4dp"
+        android:layout_marginEnd="16dp"
+        android:layout_marginBottom="4dp"
+        android:background="@drawable/border"
+        android:backgroundTintMode="src_atop"
+        android:baselineAligned="false"
+        android:gravity="center"
+        android:orientation="vertical"
+        android:padding="10dp"
+        app:layout_constraintBottom_toTopOf="@+id/linLayoutCertificateOptions"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent">
+
+        <androidx.appcompat.widget.SwitchCompat
+            android:id="@+id/switchStartServer"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/btn_start_server" />
+
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:baselineAligned="true"
+            android:gravity="center">
+
+            <TextView
+                android:id="@+id/txtViewUsername"
+                android:layout_width="150dp"
+                android:layout_height="wrap_content"
+                android:layout_weight="2"
+                android:labelFor="@id/editTextUserName"
+                android:text="@string/txtView_username" />
+
+            <EditText
+                android:id="@+id/editTextUserName"
+                android:layout_width="200dp"
+                android:layout_height="wrap_content"
+                android:layout_weight="3"
+                android:autofillHints="username"
+                android:ems="10"
+                android:hint="@string/editTextUserName_hint"
+                android:inputType="textPersonName" />
+        </LinearLayout>
+
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:gravity="center">
+
+            <TextView
+                android:id="@+id/textViewPassword"
+                android:layout_width="150dp"
+                android:layout_height="wrap_content"
+                android:layout_weight="2"
+                android:labelFor="@id/editTextPassword"
+                android:text="@string/txtView_access_password" />
+
+            <EditText
+                android:id="@+id/editTextPassword"
+                android:layout_width="200dp"
+                android:layout_height="wrap_content"
+                android:layout_weight="3"
+                android:autofillHints="password"
+                android:ems="10"
+                android:hint="@string/editTextPasswort_hint"
+                android:inputType="textPassword" />
+        </LinearLayout>
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:id="@+id/linLayoutCertificateOptions"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="16dp"
+        android:layout_marginEnd="16dp"
+        android:layout_marginBottom="4dp"
+        android:background="@drawable/border"
+        android:gravity="center"
+        android:orientation="vertical"
+        android:padding="10dp"
+        app:layout_constraintBottom_toTopOf="@id/linLayoutDeleteMessagesOptions"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/linLayoutServerOptions">
+
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:gravity="center">
+
+            <TextView
+                android:id="@+id/textViewPathKeyStore"
+                android:layout_width="150dp"
+                android:layout_height="wrap_content"
+                android:layout_weight="2"
+                android:labelFor="@id/editTextPathKeyStore"
+                android:text="@string/txtView_keystore_path" />
+
+            <EditText
+                android:id="@+id/editTextPathKeyStore"
+                android:layout_width="144dp"
+                android:layout_height="wrap_content"
+                android:layout_weight="3"
+                android:autofillHints=""
+                android:ems="10"
+                android:inputType="textUri" />
+
+            <ImageButton
+                android:id="@+id/buttonCertificate"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_weight="2"
+                android:contentDescription="@string/btn_certificate_content_Description"
+                app:srcCompat="?android:attr/actionModeFindDrawable" />
+
+        </LinearLayout>
+
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:gravity="center">
+
+            <TextView
+                android:id="@+id/textViewKeyStorePassword"
+                android:layout_width="150dp"
+                android:layout_height="wrap_content"
+                android:layout_weight="2"
+                android:labelFor="@id/editTextKeyStorePassword"
+                android:text="@string/txtView_keystore_password" />
+
+            <EditText
+                android:id="@+id/editTextKeyStorePassword"
+                android:layout_width="200dp"
+                android:layout_height="wrap_content"
+                android:layout_weight="3"
+                android:autofillHints=""
+                android:ems="10"
+                android:inputType="textPassword" />
+
+        </LinearLayout>
+
+        <Button
+            android:id="@+id/buttonCheckCertificateAvailability"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/btn_check_certificate" />
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:id="@+id/linLayoutDeleteMessagesOptions"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="16dp"
+        android:layout_marginEnd="16dp"
+        android:layout_marginBottom="4dp"
+        android:gravity="center"
+        android:orientation="vertical"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/linLayoutCertificateOptions">
+
+        <Button
+            android:id="@+id/buttonDeleteMessages"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/btn_delete_messages" />
+    </LinearLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 5 - 0
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@drawable/ic_launcher_background" />
+    <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>

+ 5 - 0
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@drawable/ic_launcher_background" />
+    <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>

binární
app/src/main/res/mipmap-hdpi/ic_launcher.png


binární
app/src/main/res/mipmap-hdpi/ic_launcher_round.png


binární
app/src/main/res/mipmap-mdpi/ic_launcher.png


binární
app/src/main/res/mipmap-mdpi/ic_launcher_round.png


binární
app/src/main/res/mipmap-xhdpi/ic_launcher.png


binární
app/src/main/res/mipmap-xhdpi/ic_launcher_round.png


binární
app/src/main/res/mipmap-xxhdpi/ic_launcher.png


binární
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png


binární
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png


binární
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png


+ 17 - 0
app/src/main/res/values-de/strings.xml

@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="app_name">SMSService</string>
+
+    <string name="btn_check_certificate">Überprüfe Zertifikatverfügbarkeit</string>
+    <string name="btn_start_server">Server starten</string>
+    <string name="btn_delete_messages">Lösche Nachrichten\n(älter als 2 Wochen)</string>
+    <string name="txtView_keystore_password">Keystore Passwort</string>
+    <string name="txtView_access_password">Zugangspasswort</string>
+    <string name="txtView_username">Benutzername</string>
+    <string name="txtView_keystore_path">Keystore Pfad</string>
+
+    <string name="toast_server_terminated_successfully">Der Server wurde erfolgreich gestoppt</string>
+    <string name="toast_server_terminated_failed">Der Server wurde aufgrund eines Fehlers gestoppt</string>
+    <string name="btn_certificate_content_Description">Wähle Keystore mit der Systemauswahl</string>
+
+</resources>

+ 6 - 0
app/src/main/res/values/colors.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="colorPrimary">#6200EE</color>
+    <color name="colorPrimaryDark">#3700B3</color>
+    <color name="colorAccent">#03DAC5</color>
+</resources>

+ 24 - 0
app/src/main/res/values/strings.xml

@@ -0,0 +1,24 @@
+<resources>
+    <string name="app_name">SMSService</string>
+
+    <string name="btn_check_certificate">Check certificate availability</string>
+    <string name="btn_start_server">Start Server</string>
+    <string name="btn_delete_messages">Delete Messages\n(older than 2 Weeks)</string>
+    <string name="txtView_keystore_password">Keystore Password</string>
+    <string name="txtView_access_password">Access Password</string>
+    <string name="txtView_username">Username</string>
+    <string name="txtView_keystore_path">Keystore Path</string>
+    <string name="toast_server_terminated_successfully">Server terminated successfully</string>
+    <string name="toast_server_terminated_failed">Server was stopped due a major error</string>
+
+
+    <string name="saved_username_key" translatable="false">username</string>
+    <string name="saved_password_key" translatable="false">password</string>
+    <string name="saved_keyStore_password_key" translatable="false">keyStorePassword</string>
+    <string name="saved_certificate_uri_str_key" translatable="false">certificateUriStr</string>
+    <string name="btn_certificate_content_Description">Select Keystore with system picker</string>
+    <string name="editTextPasswort_hint" translatable="false">123</string>
+    <string name="editTextUserName_hint" translatable="false">admin</string>
+
+
+</resources>

+ 10 - 0
app/src/main/res/values/styles.xml

@@ -0,0 +1,10 @@
+<resources>
+    <!-- Base application theme. -->
+    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
+        <!-- Customize your theme here. -->
+        <item name="colorPrimary">@color/colorPrimary</item>
+        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+        <item name="colorAccent">@color/colorAccent</item>
+    </style>
+
+</resources>

+ 9 - 0
app/src/test/java/net/folivo/android/smsGatewayServer/KotestConfig.kt

@@ -0,0 +1,9 @@
+package net.folivo.android.smsGatewayServer
+
+import io.kotest.core.config.AbstractProjectConfig
+import io.kotest.core.extensions.Extension
+import io.kotest.experimental.robolectric.RobolectricExtension
+
+class KotestConfig : AbstractProjectConfig() {
+    override fun extensions(): List<Extension> = super.extensions() + RobolectricExtension()
+}

+ 185 - 0
app/src/test/java/net/folivo/android/smsGatewayServer/MessageManagerTest.kt

@@ -0,0 +1,185 @@
+package net.folivo.android.smsGatewayServer
+
+import android.app.Activity
+import android.content.ContentResolver
+import android.content.Context
+import android.database.MatrixCursor
+import android.provider.Telephony
+import android.telephony.SmsManager
+import androidx.test.core.app.ApplicationProvider
+import io.kotest.matchers.shouldBe
+import io.ktor.http.*
+import io.mockk.every
+import io.mockk.mockk
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.Shadows.shadowOf
+
+
+@RunWith(RobolectricTestRunner::class)
+class MessageManagerTest {
+
+//    @Test
+//    fun `when here are multiple sms messages it should get last id from inbox messages`() {
+//        val contentResolverMock = mockk<ContentResolver>()
+//        val contextMock = mockk<Context> {
+//            every { contentResolver } returns contentResolverMock
+//        }
+//        val cut = MessageManager(contextMock)
+//
+//        val matrixCursor = MatrixCursor(arrayOf(Telephony.Sms._ID))
+//        matrixCursor.addRow(arrayOf(4))
+//        matrixCursor.addRow(arrayOf(2))
+//        every {
+//            contentResolverMock.query(
+//                Telephony.Sms.Inbox.CONTENT_URI,
+//                null,
+//                Telephony.Sms._ID,
+//                null,
+//                Telephony.Sms._ID + " DESC"
+//            )
+//        } returns matrixCursor
+//        cut.getNextBatch().shouldBe(4)
+//    }
+//
+//    @Test
+//    fun `when here are no sms messages it should return 0`() {
+//        val contentResolverMock = mockk<ContentResolver>()
+//        val contextMock = mockk<Context> {
+//            every { contentResolver } returns contentResolverMock
+//        }
+//        val cut = MessageManager(contextMock)
+//
+//        val matrixCursor = MatrixCursor(arrayOf(Telephony.Sms._ID))
+//        every {
+//            contentResolverMock.query(
+//                Telephony.Sms.Inbox.CONTENT_URI,
+//                null,
+//                Telephony.Sms._ID,
+//                null,
+//                Telephony.Sms._ID + " DESC"
+//            )
+//        } returns matrixCursor
+//        cut.getNextBatch().shouldBe(0)
+//    }
+//
+//    @Test
+//    fun `when there is one simple text message to sent it should send simple text message"`() {
+//        val context = ApplicationProvider.getApplicationContext<Context>()
+//
+//        val cut = MessageManager(context)
+//        runBlocking {
+//            launch {
+//                println("sendSms")
+//                cut.sendSms("012345678", "Dies ist eine einfache Textnachricht.")
+//                    .shouldBe(Pair(HttpStatusCode.OK, "Transmission successful"))
+//                println("send fertig")
+//            }
+//            launch {
+//                var smsManager = SmsManager.getDefault()
+//                var shadowSmsManager = shadowOf(smsManager)
+//                println("loop")
+//                while (true) {
+//                    val lastSentTextMessageParams = shadowSmsManager.lastSentTextMessageParams
+//                    if (lastSentTextMessageParams != null) {
+//
+//                        lastSentTextMessageParams.sentIntent.send(Activity.RESULT_OK)
+//                        lastSentTextMessageParams.destinationAddress.shouldBe("012345678")
+//                        lastSentTextMessageParams.text.shouldBe("Dies ist eine einfache Textnachricht.")
+//
+//                        break
+//                    }
+//                    delay(100)
+//                }
+//                println("fertig")
+//            }
+//        }
+//        println("end")
+//    }
+//
+//    @Test
+//    fun `where there is one multiple text message to sent it should send multiple text message`() {
+//        val context = ApplicationProvider.getApplicationContext<Context>()
+//        val cut = MessageManager(context)
+//        var smsManager = SmsManager.getDefault()
+//        var shadowSmsManager = shadowOf(smsManager)
+//        GlobalScope.launch {
+//            cut.sendSms(
+//                "012345678",
+//                "Dies ist eine Multipart Textnachricht. Dies ist eine Multipart Textnachricht. " +
+//                        "Dies ist eine Multipart Textnachricht. Dies ist eine Multipart Textnachricht. " +
+//                        "Dies ist eine Multipart Textnachricht. Dies ist eine Multipart Textnachricht. " +
+//                        "Dies ist eine Multipart Textnachricht. Dies ist eine Multipart Textnachricht. " +
+//                        "Dies ist eine Multipart Textnachricht. Dies ist eine Multipart Textnachricht."
+//            )
+//        }
+//        val lastSentTextMessageParams =
+//            shadowSmsManager.lastSentMultipartTextMessageParams
+//        lastSentTextMessageParams.destinationAddress.shouldBe("012345678")
+//        lastSentTextMessageParams.parts.shouldBe(listOf(
+//            "Dies ist eine Multipart Textnachricht. Dies ist eine Multipart Textnachricht. Dies ist eine Multipart Textnachricht. Dies ist eine Multipart Textnachricht. Dies",
+//            " ist eine Multipart Textnachricht. Dies ist eine Multipart Textnachricht. Dies ist eine Multipart Textnachricht. Dies ist eine Multipart Textnachricht. Dies ist",
+//            " eine Multipart Textnachricht."
+//        ))
+//    }
+
+}
+
+//private fun testBody(): DescribeSpec.() -> Unit {
+//    return {
+//
+//        describe(MessageManager::sendSms.name) {
+//
+//            val context = ApplicationProvider.getApplicationContext<Context>()
+//            val cut = MessageManager(context)
+//            describe("there is one simple text message to sent") {
+//                var smsManager = SmsManager.getDefault()
+//                smsManager.sendTextMessage("1234", null, "jklösldkjfösd", null, null)
+//                var shadowSmsManager = shadowOf(smsManager)
+//
+//                it("should send simple text message") {
+//                    GlobalScope.launch {
+//                        cut.sendSms("012345678", "Dies ist eine einfache Textnachricht.")
+//                    }
+//                    delay(500)
+//                    val lastSentTextMessageParams = shadowSmsManager.lastSentTextMessageParams
+//                    lastSentTextMessageParams.sentIntent.send(context,
+//                        Activity.RESULT_OK,
+//                        Intent("sent"))
+//                    lastSentTextMessageParams.destinationAddress.shouldBe("012345678")
+//                    lastSentTextMessageParams.text.shouldBe("Dies ist eine einfache Textnachricht.")
+//                }
+//
+//            }
+//            describe("there are is multiple text message to sent") {
+//                it("should send multiple text message") {
+//                    cut.sendSms(
+//                        "012345678",
+//                        "Dies ist eine Multipart Textnachricht. Dies ist eine Multipart Textnachricht. " +
+//                                "Dies ist eine Multipart Textnachricht. Dies ist eine Multipart Textnachricht. " +
+//                                "Dies ist eine Multipart Textnachricht. Dies ist eine Multipart Textnachricht. " +
+//                                "Dies ist eine Multipart Textnachricht. Dies ist eine Multipart Textnachricht. " +
+//                                "Dies ist eine Multipart Textnachricht. Dies ist eine Multipart Textnachricht."
+//                    )
+//                    val shadowSmsManager = ShadowSmsManager()
+//                    val lastSentTextMessageParams =
+//                        shadowSmsManager.lastSentMultipartTextMessageParams
+//                    lastSentTextMessageParams.destinationAddress.shouldBe("012345678")
+//                    lastSentTextMessageParams.parts.shouldBe(listOf(
+//                        "Dies ist eine Multipart Textnachricht. Dies ist eine Multipart Textnachricht. Dies ist eine Multipart Textnachricht. Dies ist eine Multipart Textnachricht. Dies",
+//                        " ist eine Multipart Textnachricht. Dies ist eine Multipart Textnachricht. Dies ist eine Multipart Textnachricht. Dies ist eine Multipart Textnachricht. Dies ist",
+//                        " eine Multipart Textnachricht."
+//                    ))
+//                }
+//
+//            }
+//
+//        }
+//
+//    }
+//}

+ 90 - 0
app/src/test/java/net/folivo/android/smsGatewayServer/RestApiWorkerTest.kt

@@ -0,0 +1,90 @@
+package net.folivo.android.smsGatewayServer
+
+import android.content.ContentResolver
+import android.database.MatrixCursor
+import android.net.Uri
+import android.provider.OpenableColumns
+import io.kotest.matchers.shouldBe
+import io.mockk.every
+import io.mockk.mockk
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import java.security.KeyStoreException
+
+@RunWith(RobolectricTestRunner::class)
+class RestApiWorkerTest {
+
+    //getKeyStoreFileName
+    @Test
+    fun `when there is a file path starts with "content" and includes not the file name, it should get the name`() {
+        val testUri = Uri.parse("content://com.android.providers.downloads.documents/document/msf%3A1035")
+        val contentResolverMock = mockk<ContentResolver>()
+        val matrixCursor = MatrixCursor(arrayOf(OpenableColumns.DISPLAY_NAME))
+        matrixCursor.addRow(arrayOf("certificate.p12"))
+        every {
+            contentResolverMock.query(
+                testUri,
+                null,
+                null,
+                null,
+                null
+            )
+        } returns matrixCursor
+        RestApiWorker.getKeyStoreFileName(contentResolverMock, testUri).shouldBe("certificate")
+    }
+
+    @Test
+    fun `when there is a file path starts with "content" and includes the file name, it should get the name`() {
+        val testUri = Uri.parse("content://com.android.providers.downloads.documents/document/certificate.p12")
+        val contentResolverMock = mockk<ContentResolver>()
+        val matrixCursor = MatrixCursor(arrayOf(OpenableColumns.DISPLAY_NAME))
+        matrixCursor.addRow(arrayOf("certificate.p12"))
+        every {
+            contentResolverMock.query(
+                testUri,
+                null,
+                null,
+                null,
+                null
+            )
+        } returns matrixCursor
+        RestApiWorker.getKeyStoreFileName(contentResolverMock, testUri).shouldBe("certificate")
+    }
+
+    @Test
+    fun `when there is a file path starts not with "content" and includes the file name, it should get the name`() {
+        val testUri = Uri.parse("/storage/emulated/0/Download/certificate.p12")
+        val contentResolverMock = mockk<ContentResolver>()
+        val matrixCursor = MatrixCursor(arrayOf(OpenableColumns.DISPLAY_NAME))
+        matrixCursor.addRow(arrayOf("certificate.p12"))
+        every {
+            contentResolverMock.query(
+                testUri,
+                null,
+                null,
+                null,
+                null
+            )
+        } returns matrixCursor
+        RestApiWorker.getKeyStoreFileName(contentResolverMock, testUri).shouldBe("certificate")
+    }
+
+
+    @Test(expected = KeyStoreException::class)
+    fun `when there is an empty file path, it should throw a KeyStoreException`(){
+        val testUri = Uri.parse("")
+        val contentResolverMock = mockk<ContentResolver>()
+        every {
+            contentResolverMock.query(
+                testUri,
+                null,
+                null,
+                null,
+                null
+            )
+        } returns null
+        RestApiWorker.getKeyStoreFileName(contentResolverMock, testUri)
+    }
+
+}

+ 5 - 0
app/src/test/java/net/folivo/android/smsGatewayServer/TestApplication.kt

@@ -0,0 +1,5 @@
+package net.folivo.android.smsGatewayServer
+
+import android.app.Application
+
+class TestApplication : Application()

+ 27 - 0
build.gradle

@@ -0,0 +1,27 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+buildscript {
+    ext.kotlin_version = "1.4.10"
+    repositories {
+        google()
+        jcenter()
+    }
+    dependencies {
+        classpath 'com.android.tools.build:gradle:4.1.0'
+        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.10"
+
+        // NOTE: Do not place your application dependencies here; they belong
+        // in the individual module build.gradle files
+    }
+}
+
+allprojects {
+    repositories {
+        google()
+        jcenter()
+    }
+}
+
+
+task clean(type: Delete) {
+    delete rootProject.buildDir
+}

+ 21 - 0
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
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# 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
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official

binární
gradle/wrapper/gradle-wrapper.jar


+ 6 - 0
gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,6 @@
+#Tue Oct 13 15:18:31 CEST 2020
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip

+ 172 - 0
gradlew

@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+##  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=""
+
+# 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, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Escape application args
+save () {
+    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+    echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+  cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"

+ 84 - 0
gradlew.bat

@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega

+ 2 - 0
settings.gradle

@@ -0,0 +1,2 @@
+include ':app'
+rootProject.name = "androidSmsGatewayServer"