[
  {
    "path": ".gitignore",
    "content": "*.iml\n.gradle\n/local.properties\n/.idea\n.DS_Store\n/build\n/captures\n.externalNativeBuild\n"
  },
  {
    "path": "LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "NOTICE",
    "content": "SSTV Encoder 2\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nMode specifications were taken from \"Dayton Paper\" of JL Barber:\nhttp://www.barberdsp.com/downloads/Dayton%20Paper.pdf\n\nsym_keyboard_done_lxx_dark.png icons were taken from\nandroid.googlesource.com\n\nic_launcher.png were created using GIMP:\nhttp://www.gimp.org/\n\nSMPTE Color Bars image (CC BY-SA 3.0) was taken from Wikipedia:\nhttp://en.wikipedia.org/wiki/SMPTE_color_bars#mediaviewer/File:SMPTE_Color_Bars.svg\n"
  },
  {
    "path": "README.md",
    "content": "![Icon](app/src/main/res/mipmap-xhdpi/ic_launcher.png)\n# SSTV Encoder 2\n\nImage encoder for Slow-Scan Television (SSTV) audio signals\n\n### Modes\n\nSupported SSTV modes:\n* **Martin Modes**:  Martin 1, Martin 2  \n* **PD Modes**:      PD 50, PD 90, PD 120, PD 160, PD 180, PD 240, PD 290   \n* **Robot Modes**:   Robot 36 Color, Robot 72 Color  \n* **Scottie Modes**: Scottie 1, Scottie 2, Scottie DX \n* **Wraase Modes**:  Wraase SC2 180\n\nThe mode specifications are taken from the Dayton Paper, JL Barber, \"Proposal for SSTV Mode Specifications\", 2000:  \nhttp://www.barberdsp.com/downloads/Dayton%20Paper.pdf\n\n### Image\n\nTo load an image:\n* tap **\"Take Picture\"** or **\"Pick Picture\"** menu button, or  \n* use the **Share** option of an app like e.g. Gallery.\n\nTo keep the aspect ratio, black borders will be added if necessary.  \nOriginal image can be resend using another mode without reloading.  \nAfter image rotation or mode changing the image will be scaled to that mode's native size.  \nAfter closing the app the loaded image will not be stored.\n\n### Text Overlay\n\nActions for working with text overlays:\n* Single tap **to add** a text overlay.  \n* Single tap on text overlay **to edit** it.  \n* Long press **to move** text overlay.  \n* Remove the text **to remove** a text overlay.\n\nAfter closing the app all text overlays will be stored and reloaded when restarting.\n\n### Menu\n\nAvailable menu options:\n* **\"Play\"**: Sends the image\n* **\"Stop\"**: Stops the current sending and empties the queue\n* **\"Pick Picture\"**: Opens an image viewer app to select a picture\n* **\"Take Picture\"**: Starts a camera app to take a picture\n* **\"Save as WAVE File\"**: Creates a wave file in the Music folder in SSTV Encoder album\n* **\"Transform Image\"**:\n  * **\"Rotate\"**: Rotates the image by 90 degrees\n  * **\"Reset\"**: Resets image rotation and scaling\n* **\"Modes\"**: Lists all supported modes\n\n### Installation\n\nThe working app \"SSTV Encoder\" can be installed \n\non Google Play:  \nhttps://play.google.com/store/apps/details?id=om.sstvencoder\n\nor on F-Droid:  \nhttps://f-droid.org/packages/om.sstvencoder/\n\n# SSTV Image Decoder\n\nOpen Source Code:  \nhttps://github.com/xdsopl/robot36/tree/android\n\n### Installation\n\nThe working app \"Robot36 - SSTV Image Decoder\" can be installed\n\non Google Play:  \nhttps://play.google.com/store/apps/details?id=xdsopl.robot36\n\nor on F-Droid:  \nhttps://f-droid.org/packages/xdsopl.robot36/\n"
  },
  {
    "path": "app/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "app/build.gradle",
    "content": "apply plugin: 'com.android.application'\n\nandroid {\n    compileSdk 35\n    defaultConfig {\n        applicationId \"om.sstvencoder\"\n        minSdk 21\n        targetSdk 35\n        versionCode 34\n        versionName \"2.13\"\n    }\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n        }\n    }\n    namespace 'om.sstvencoder'\n    buildFeatures {\n        buildConfig true\n    }\n}\n\ndependencies {\n    implementation fileTree(dir: 'libs', include: ['*.jar'])\n    implementation 'androidx.appcompat:appcompat:1.7.0'\n    implementation \"androidx.exifinterface:exifinterface:1.3.7\"\n}\n"
  },
  {
    "path": "app/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in /home/olga/Android/Sdk/tools/proguard/proguard-android.txt\n# You can edit the include path and order by changing the proguardFiles\n# directive in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# Add any project specific keep options here:\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <uses-permission\n        android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"\n        android:maxSdkVersion=\"29\"/>\n    <uses-feature\n        android:name=\"android.hardware.camera\"\n        android:required=\"false\"/>\n    <queries>\n        <intent>\n            <action android:name=\"android.intent.action.SEND\"/>\n            <data android:mimeType=\"image/*\"/>\n        </intent>\n        <intent>\n            <action android:name=\"android.media.action.IMAGE_CAPTURE\"/>\n        </intent>\n        <intent>\n            <action android:name=\"android.intent.action.PICK\" />\n            <data android:mimeType=\"image/*\"/>\n        </intent>\n        <intent>\n            <action android:name=\"android.intent.action.VIEW\" />\n            <category android:name=\"android.intent.category.BROWSABLE\" />\n            <data android:scheme=\"https\" />\n        </intent>\n    </queries>\n    <application\n        android:requestLegacyExternalStorage=\"true\"\n        android:allowBackup=\"false\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:theme=\"@style/AppTheme\">\n        <provider\n            android:name=\"androidx.core.content.FileProvider\"\n            android:authorities=\"om.sstvencoder\"\n            android:exported=\"false\"\n            android:grantUriPermissions=\"true\">\n            <meta-data\n                android:name=\"android.support.FILE_PROVIDER_PATHS\"\n                android:resource=\"@xml/paths\"/>\n        </provider>\n\n        <activity\n            android:name=\".MainActivity\"\n            android:configChanges=\"keyboardHidden|orientation|screenSize\"\n            android:label=\"@string/app_name\"\n            android:launchMode=\"singleTask\"\n            android:exported=\"true\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\"/>\n                <category android:name=\"android.intent.category.LAUNCHER\"/>\n            </intent-filter>\n            <intent-filter>\n                <action android:name=\"android.intent.action.SEND\"/>\n                <category android:name=\"android.intent.category.DEFAULT\"/>\n                <data android:mimeType=\"image/*\"/>\n            </intent-filter>\n        </activity>\n\n        <activity\n            android:name=\".EditTextActivity\"\n            android:configChanges=\"keyboardHidden|orientation|screenSize\"\n            android:windowSoftInputMode=\"stateVisible|adjustPan\"/>\n\n    </application>\n</manifest>"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/ColorFragment.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder;\n\nimport android.app.AlertDialog;\nimport android.app.Dialog;\nimport android.graphics.Color;\nimport android.os.Bundle;\nimport androidx.annotation.NonNull;\nimport androidx.fragment.app.DialogFragment;\nimport android.view.View;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport om.sstvencoder.ColorPalette.ColorPaletteView;\n\npublic class ColorFragment extends DialogFragment\n        implements ColorPaletteView.OnColorSelectedListener {\n\n    public interface OnColorSelectedListener {\n        void onColorSelected(DialogFragment fragment, int color);\n\n        void onCancel(DialogFragment fragment);\n    }\n\n    private List<OnColorSelectedListener> mListeners;\n    private int mTitle;\n    private int mColor;\n\n    public ColorFragment() {\n        mListeners = new ArrayList<>();\n        mTitle = R.string.color;\n        mColor = Color.WHITE;\n    }\n\n    public void setTitle(int title) {\n        mTitle = title;\n    }\n\n    public void setColor(int color) {\n        mColor = color;\n    }\n\n    public void addOnColorSelectedListener(OnColorSelectedListener listener) {\n        mListeners.add(listener);\n    }\n\n    @NonNull\n    @Override\n    public Dialog onCreateDialog(Bundle savedInstanceState) {\n        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());\n        View view = getActivity().getLayoutInflater().inflate(R.layout.fragment_color, null);\n        ColorPaletteView colorView = view.findViewById(R.id.select_color);\n        colorView.setColor(mColor);\n        colorView.addOnColorSelectedListener(this);\n        builder.setTitle(mTitle);\n        builder.setView(view);\n        return builder.create();\n    }\n\n    @Override\n    public void onColorChanged(View v, int color) {\n    }\n\n    @Override\n    public void onColorSelected(View v, int color) {\n        for (OnColorSelectedListener listener : mListeners)\n            listener.onColorSelected(this, color);\n        dismiss();\n    }\n\n    @Override\n    public void onCancel(View v) {\n        for (OnColorSelectedListener listener : mListeners)\n            listener.onCancel(this);\n        dismiss();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/ColorPalette/ColorPaletteView.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.ColorPalette;\n\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.PorterDuff;\nimport androidx.annotation.NonNull;\nimport android.util.AttributeSet;\nimport android.view.MotionEvent;\nimport android.view.View;\n\nimport java.util.ArrayList;\n\npublic class ColorPaletteView extends View {\n\n    public interface OnColorSelectedListener {\n        void onColorChanged(View v, int color);\n\n        void onColorSelected(View v, int color);\n\n        void onCancel(View v);\n    }\n\n    private final ArrayList<OnColorSelectedListener> mListeners;\n    private final IColorPalette mPalette;\n\n    public ColorPaletteView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        mListeners = new ArrayList<>();\n        mPalette = new GridColorPalette(GridColorPalette.getStandardColors(),\n                getResources().getDisplayMetrics().density);\n    }\n\n    public int getColor() {\n        return mPalette.getSelectedColor();\n    }\n\n    public void setColor(int color) {\n        mPalette.selectColor(color);\n    }\n\n    @Override\n    protected void onSizeChanged(int w, int h, int old_w, int old_h) {\n        super.onSizeChanged(w, h, old_w, old_h);\n        mPalette.updateSize(w, h);\n    }\n\n    @Override\n    protected void onDraw(@NonNull Canvas canvas) {\n        canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);\n        mPalette.draw(canvas);\n    }\n\n    @Override\n    public boolean onTouchEvent(@NonNull MotionEvent e) {\n        boolean consumed = false;\n        switch (e.getAction()) {\n            case MotionEvent.ACTION_DOWN:\n            case MotionEvent.ACTION_MOVE: {\n                update(e.getX(), e.getY());\n                consumed = true;\n                break;\n            }\n            case MotionEvent.ACTION_UP: {\n                float x = e.getX();\n                float y = e.getY();\n                if (getLeft() <= x && x <= getRight() && getTop() <= y && y <= getBottom())\n                    colorSelectedCallback();\n                else\n                    cancelCallback();\n                consumed = true;\n                break;\n            }\n        }\n        return consumed || super.onTouchEvent(e);\n    }\n\n    private void update(float x, float y) {\n        if (mPalette.selectColor(x, y)) {\n            invalidate();\n            colorChangedCallback();\n        }\n    }\n\n    public void addOnColorSelectedListener(OnColorSelectedListener listener) {\n        mListeners.add(listener);\n    }\n\n    private void colorChangedCallback() {\n        for (OnColorSelectedListener listener : mListeners) {\n            listener.onColorChanged(this, mPalette.getSelectedColor());\n        }\n    }\n\n    private void colorSelectedCallback() {\n        for (OnColorSelectedListener listener : mListeners) {\n            listener.onColorSelected(this, mPalette.getSelectedColor());\n        }\n    }\n\n    private void cancelCallback() {\n        for (OnColorSelectedListener listener : mListeners) {\n            listener.onCancel(this);\n        }\n    }\n}\n\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/ColorPalette/GridColorPalette.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.ColorPalette;\n\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.Paint;\nimport android.graphics.RectF;\n\nclass GridColorPalette implements IColorPalette {\n\n    static int[] getStandardColors() {\n        return new int[]{\n                Color.BLACK,\n                Color.GRAY,\n                Color.LTGRAY,\n                Color.WHITE,\n                Color.YELLOW,\n                Color.CYAN,\n                Color.GREEN,\n                Color.MAGENTA,\n                Color.RED,\n                Color.BLUE\n        };\n    }\n\n    private final static float STROKE_WIDTH_FACTOR = 6f;\n    private final static float BOX_SIZE_DP = 96f;\n    private final static float SPACE_FACTOR = 6f;\n    private final int[] mColorList;\n    private final Paint mPaint;\n    private final RectF mSelectedBounds;\n    private final float mDisplayMetricsDensity;\n    private int mColumns, mRows;\n    private float mWidth, mHeight;\n    private float mBoxSize, mSpace, mStrokeWidth, mCornerRadius;\n    private int mSelectedColorIndex;\n    private boolean mValid;\n\n    GridColorPalette(int[] colorList, float displayMetricsDensity) {\n        final float CORNER_RADIUS = 3f;\n        mColorList = colorList;\n        mDisplayMetricsDensity = displayMetricsDensity;\n        mCornerRadius = CORNER_RADIUS * mDisplayMetricsDensity;\n        mPaint = new Paint();\n        setPaintStyleForBox();\n        mSelectedBounds = new RectF();\n        mSelectedColorIndex = 0;\n        mValid = false;\n    }\n\n    @Override\n    public void updateSize(float width, float height) {\n        mValid = width > 0 && height > 0;\n\n        if (mValid && (mWidth != width || mHeight != height)) {\n            mWidth = width;\n            mHeight = height;\n            updateGrid();\n            mStrokeWidth = mSpace / STROKE_WIDTH_FACTOR;\n            setSelectedColor(mSelectedColorIndex);\n        }\n    }\n\n    // The approximately same box size independently on resolution has the higher priority.\n    // Thus the possible filling of the last row is not supported here.\n    private void updateGrid() {\n        int boxes = mColorList.length;\n        mBoxSize = BOX_SIZE_DP * mDisplayMetricsDensity;\n        mSpace = mBoxSize / SPACE_FACTOR;\n\n        mColumns = min((int) ((mWidth - mSpace) / (mBoxSize + mSpace) + 0.5f), boxes);\n        mRows = (boxes + mColumns - 1) / mColumns; // ceil\n        updateBoxSizeAndSpace();\n\n        while (mRows * (mBoxSize + mSpace) + mSpace > mHeight) {\n            ++mColumns;\n            mRows = (boxes + mColumns - 1) / mColumns;\n            updateBoxSizeAndSpace();\n        }\n    }\n\n    private int min(int a, int b) {\n        return a <= b ? a : b;\n    }\n\n    // Fill out the whole width of the View.\n    private void updateBoxSizeAndSpace() {\n        // Set 'space = boxSize / spaceFactor' into\n        // 'boxSize = (width - (columns + 1) * space ) / columns'\n        // and solve for boxSize:\n        mBoxSize = SPACE_FACTOR * mWidth / (1f + mColumns * (SPACE_FACTOR + 1f));\n        mSpace = mBoxSize / SPACE_FACTOR;\n    }\n\n    @Override\n    public void draw(Canvas canvas) {\n        if (!mValid)\n            return;\n\n        float x = mSpace, y = mSpace;\n        float maxX = mColumns * (mBoxSize + mSpace);\n        for (int color : mColorList) {\n            RectF rect = new RectF(x, y, x + mBoxSize, y + mBoxSize);\n            mPaint.setColor(color);\n            canvas.drawRoundRect(rect, mCornerRadius, mCornerRadius, mPaint);\n            x += mBoxSize + mSpace;\n            if (x > maxX) {\n                x = mSpace;\n                y += mBoxSize + mSpace;\n            }\n        }\n        drawSelectedRect(canvas);\n    }\n\n    private void drawSelectedRect(Canvas canvas) {\n        float padding = mSpace / 2f;\n        float l = mSelectedBounds.left;\n        float t = mSelectedBounds.top;\n        float r = mSelectedBounds.right;\n        float b = mSelectedBounds.bottom;\n        RectF rect = new RectF(l - padding, t - padding, r + padding, b + padding);\n        Paint.Style paintStyle = mPaint.getStyle();\n        setPaintStyleForSelectedBox();\n        canvas.drawRoundRect(rect, mCornerRadius, mCornerRadius, mPaint);\n        mPaint.setStyle(paintStyle);\n    }\n\n    private void setPaintStyleForSelectedBox() {\n        mPaint.setStyle(Paint.Style.STROKE);\n        mPaint.setStrokeWidth(mStrokeWidth);\n        mPaint.setColor(Color.WHITE);\n    }\n\n    private void setPaintStyleForBox() {\n        mPaint.setStyle(Paint.Style.FILL);\n        mPaint.setAntiAlias(true);\n    }\n\n    @Override\n    public int getSelectedColor() {\n        return mColorList[mSelectedColorIndex];\n    }\n\n    @Override\n    public boolean selectColor(float x, float y) {\n        if (!mValid || mSelectedBounds.contains(x, y))\n            return false;\n\n        int column = (int) (x / (mBoxSize + mSpace));\n        int row = (int) (y / (mBoxSize + mSpace));\n        if (0 > row || row >= mRows || 0 > column || column >= mColumns)\n            return false;\n\n        int i = row * mColumns + column;\n        if (i >= mColorList.length || i == mSelectedColorIndex)\n            return false;\n\n        float left = mSpace + column * (mBoxSize + mSpace);\n        float top = mSpace + row * (mBoxSize + mSpace);\n        if (left > x || x > left + mBoxSize || top > y || y > top + mBoxSize)\n            return false;\n\n        mSelectedBounds.set(left, top, left + mBoxSize, top + mBoxSize);\n        mSelectedColorIndex = i;\n        return true;\n    }\n\n    @Override\n    public boolean selectColor(int color) {\n        for (int i = 0; i < mColorList.length; ++i) {\n            if (color == mColorList[i]) {\n                if (mValid)\n                    setSelectedColor(i);\n                else\n                    mSelectedColorIndex = i;\n                return true;\n            }\n        }\n        return false;\n    }\n\n    private void setSelectedColor(int i) {\n        int row = i / mColumns;\n        int column = i - row * mColumns;\n        float x = mSpace + column * (mBoxSize + mSpace);\n        float y = mSpace + row * (mBoxSize + mSpace);\n        mSelectedBounds.set(x, y, x + mBoxSize, y + mBoxSize);\n        mSelectedColorIndex = i;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/ColorPalette/IColorPalette.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.ColorPalette;\n\nimport android.graphics.Canvas;\n\ninterface IColorPalette {\n    void updateSize(float width, float height);\n\n    void draw(Canvas canvas);\n\n    int getSelectedColor();\n\n    boolean selectColor(float x, float y);\n\n    boolean selectColor(int color);\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/CropView.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder;\n\nimport android.content.Context;\nimport android.content.ContextWrapper;\nimport android.graphics.Bitmap;\nimport android.graphics.BitmapFactory;\nimport android.graphics.BitmapRegionDecoder;\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.Paint;\nimport android.graphics.Rect;\nimport android.graphics.RectF;\nimport androidx.annotation.NonNull;\nimport androidx.core.view.GestureDetectorCompat;\nimport androidx.appcompat.widget.AppCompatImageView;\nimport android.util.AttributeSet;\nimport android.view.GestureDetector;\nimport android.view.MotionEvent;\nimport android.view.ScaleGestureDetector;\n\nimport java.io.BufferedInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\n\nimport om.sstvencoder.ModeInterfaces.ModeSize;\nimport om.sstvencoder.TextOverlay.Label;\nimport om.sstvencoder.TextOverlay.LabelCollection;\n\npublic class CropView extends AppCompatImageView {\n    private class GestureListener extends GestureDetector.SimpleOnGestureListener {\n        @Override\n        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {\n            if (!mLongPress) {\n                moveImage(distanceX, distanceY);\n                return true;\n            }\n            return false;\n        }\n\n        @Override\n        public void onLongPress(MotionEvent e) {\n            mLongPress = false;\n            if (!mInScale && mLabelCollection.moveLabelBegin(e.getX(), e.getY())) {\n                invalidate();\n                mLongPress = true;\n            }\n        }\n\n        @Override\n        public boolean onSingleTapConfirmed(MotionEvent e) {\n            if (!mLongPress) {\n                editLabelBegin(e.getX(), e.getY());\n                return true;\n            }\n            return false;\n        }\n    }\n\n    private class ScaleGestureListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {\n        @Override\n        public boolean onScaleBegin(ScaleGestureDetector detector) {\n            if (!mLongPress) {\n                mInScale = true;\n                return true;\n            }\n            return false;\n        }\n\n        @Override\n        public boolean onScale(ScaleGestureDetector detector) {\n            scaleImage(detector.getScaleFactor());\n            return true;\n        }\n\n        @Override\n        public void onScaleEnd(ScaleGestureDetector detector) {\n            mInScale = false;\n        }\n    }\n\n    private GestureDetectorCompat mDetectorCompat;\n    private ScaleGestureDetector mScaleDetector;\n    private boolean mLongPress, mInScale;\n    private ModeSize mModeSize;\n    private final Paint mPaint, mRectPaint, mBorderPaint;\n    private RectF mInputRect;\n    private Rect mOutputRect;\n    private BitmapRegionDecoder mRegionDecoder;\n    private int mImageWidth, mImageHeight;\n    private Bitmap mCacheBitmap;\n    private boolean mSmallImage;\n    private boolean mImageOK;\n    private final Rect mCanvasDrawRect, mImageDrawRect;\n    private int mOrientation;\n    private Rect mCacheRect;\n    private int mCacheSampleSize;\n    private final BitmapFactory.Options mBitmapOptions;\n    private LabelCollection mLabelCollection;\n\n    public CropView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        mDetectorCompat = new GestureDetectorCompat(getContext(), new GestureListener());\n        mScaleDetector = new ScaleGestureDetector(getContext(), new ScaleGestureListener());\n\n        mBitmapOptions = new BitmapFactory.Options();\n\n        mPaint = new Paint(Paint.FILTER_BITMAP_FLAG);\n        mRectPaint = new Paint();\n        mRectPaint.setStyle(Paint.Style.STROKE);\n        mRectPaint.setStrokeWidth(1f);\n        mBorderPaint = new Paint();\n        mBorderPaint.setColor(Color.BLACK);\n\n        mCanvasDrawRect = new Rect();\n        mImageDrawRect = new Rect();\n        mCacheRect = new Rect();\n        mOutputRect = new Rect();\n\n        mSmallImage = false;\n        mImageOK = false;\n\n        mLabelCollection = new LabelCollection();\n    }\n\n    public void setModeSize(ModeSize size) {\n        mModeSize = size;\n        mOutputRect = Utility.getEmbeddedRect(getWidth(), getHeight(), mModeSize.width(), mModeSize.height());\n        if (mImageOK)\n            resetInputRect();\n        invalidate();\n    }\n\n    private void resetInputRect() {\n        float iw = mModeSize.width();\n        float ih = mModeSize.height();\n        float ow = mImageWidth;\n        float oh = mImageHeight;\n        if (iw * oh > ow * ih) {\n            float right = (iw * oh) / ih;\n            mInputRect = new RectF(0f, 0f, right, oh);\n            mInputRect.offset((ow - right) / 2f, 0f);\n        } else {\n            float bottom = (ih * ow) / iw;\n            mInputRect = new RectF(0f, 0f, ow, bottom);\n            mInputRect.offset(0f, (oh - bottom) / 2f);\n        }\n    }\n\n    public void rotateImage(int orientation) {\n        if (!mImageOK)\n            return;\n        mOrientation += orientation;\n        mOrientation %= 360;\n        if (orientation == 90 || orientation == 270) {\n            int tmp = mImageWidth;\n            mImageWidth = mImageHeight;\n            mImageHeight = tmp;\n        }\n        resetInputRect();\n        invalidate();\n    }\n\n    public void resetImage() {\n        if (!mImageOK)\n            return;\n        if (mOrientation == 90 || mOrientation == 270) {\n            int tmp = mImageWidth;\n            mImageWidth = mImageHeight;\n            mImageHeight = tmp;\n        }\n        mOrientation = 0;\n        resetInputRect();\n        invalidate();\n    }\n\n    public void setNoBitmap() {\n        mImageOK = false;\n        mOrientation = 0;\n        recycle();\n        invalidate();\n    }\n\n    public void setBitmap(@NonNull InputStream stream) throws Exception {\n        mImageOK = false;\n        mOrientation = 0;\n        recycle();\n        loadImage(stream);\n        invalidate();\n    }\n\n    private void loadImage(InputStream stream) throws Exception {\n        BitmapFactory.Options options = new BitmapFactory.Options();\n        options.inJustDecodeBounds = true;\n        byte[] streamBytes = null;\n        String errorMessage = null;\n        try {\n            int length = stream.available();\n            if (length > 0) {\n                streamBytes = new byte[length];\n                if (length == stream.read(streamBytes, 0, streamBytes.length)) {\n                    BitmapFactory.decodeByteArray(streamBytes, 0, streamBytes.length, options);\n                }\n                else\n                    streamBytes = null;\n            }\n        } catch (Exception ex) {\n            errorMessage = ex.getMessage();\n            streamBytes = null;\n        }\n\n        mImageWidth = options.outWidth;\n        mImageHeight = options.outHeight;\n\n        if (streamBytes != null && mImageWidth > 0 && mImageHeight > 0) {\n            mSmallImage = mImageWidth * mImageHeight < 1024 * 1024;\n            if (mSmallImage) {\n                mCacheBitmap = BitmapFactory.decodeByteArray(streamBytes, 0, streamBytes.length, null);\n            } else {\n                mRegionDecoder = BitmapRegionDecoder.newInstance(streamBytes, 0, streamBytes.length, true);\n                mCacheRect.setEmpty();\n            }\n        }\n\n        if (mCacheBitmap == null && mRegionDecoder == null) {\n            String message = errorMessage;\n            if (message == null) {\n                message = \"Stream could not be decoded.\";\n                if (mImageWidth > 0 && mImageHeight > 0) {\n                    message += \" Image size: \" + mImageWidth + \"x\" + mImageHeight;\n                }\n            }\n            throw new Exception(message);\n        }\n\n        mImageOK = true;\n        resetInputRect();\n    }\n\n    private void recycle() {\n        if (mRegionDecoder != null) {\n            mRegionDecoder.recycle();\n            mRegionDecoder = null;\n        }\n        if (mCacheBitmap != null) {\n            mCacheBitmap.recycle();\n            mCacheBitmap = null;\n        }\n    }\n\n    public void scaleImage(float scaleFactor) {\n        if (!mImageOK)\n            return;\n        float newW = mInputRect.width() / scaleFactor;\n        float newH = mInputRect.height() / scaleFactor;\n        float dx = 0.5f * (mInputRect.width() - newW);\n        float dy = 0.5f * (mInputRect.height() - newH);\n        float max = 2f * Math.max(mImageWidth, mImageHeight);\n        if (Math.min(newW, newH) >= 4f && Math.max(newW, newH) <= max) {\n            mInputRect.inset(dx, dy);\n            invalidate();\n        }\n    }\n\n    public void moveImage(float distanceX, float distanceY) {\n        if (!mImageOK)\n            return;\n        float dx = (mInputRect.width() * distanceX) / mOutputRect.width();\n        float dy = (mInputRect.height() * distanceY) / mOutputRect.height();\n        float min_w = mInputRect.width() * 0.1f;\n        float min_h = mInputRect.height() * 0.1f;\n        dx = Math.max(min_w, mInputRect.right + dx) - mInputRect.right;\n        dy = Math.max(min_h, mInputRect.bottom + dy) - mInputRect.bottom;\n        dx = Math.min(mImageWidth - min_w, mInputRect.left + dx) - mInputRect.left;\n        dy = Math.min(mImageHeight - min_h, mInputRect.top + dy) - mInputRect.top;\n        mInputRect.offset(dx, dy);\n        invalidate();\n    }\n\n    @Override\n    public boolean onTouchEvent(@NonNull MotionEvent e) {\n        boolean consumed = false;\n        if (mLongPress) {\n            switch (e.getAction()) {\n                case MotionEvent.ACTION_MOVE:\n                    mLabelCollection.moveLabel(e.getX(), e.getY());\n                    invalidate();\n                    consumed = true;\n                    break;\n                case MotionEvent.ACTION_UP:\n                case MotionEvent.ACTION_CANCEL:\n                    mLabelCollection.moveLabelEnd();\n                    invalidate();\n                    mLongPress = false;\n                    consumed = true;\n                    break;\n            }\n        }\n        consumed = mScaleDetector.onTouchEvent(e) || consumed;\n        return mDetectorCompat.onTouchEvent(e) || consumed || super.onTouchEvent(e);\n    }\n\n    @Override\n    protected void onSizeChanged(int w, int h, int old_w, int old_h) {\n        super.onSizeChanged(w, h, old_w, old_h);\n        if (mModeSize != null)\n            mOutputRect = Utility.getEmbeddedRect(w, h, mModeSize.width(), mModeSize.height());\n        mLabelCollection.update(w, h, Utility.getTextSizeFactor(w, h));\n    }\n\n    @Override\n    protected void onDraw(@NonNull Canvas canvas) {\n        if (mImageOK) {\n            maximizeImageToCanvasRect();\n            adjustCanvasAndImageRect(getWidth(), getHeight());\n            canvas.drawRect(mOutputRect, mBorderPaint);\n            drawBitmap(canvas);\n        }\n        mLabelCollection.draw(canvas);\n        drawModeRect(canvas);\n    }\n\n    private void maximizeImageToCanvasRect() {\n        float l = mOutputRect.left * mInputRect.width() / mOutputRect.width();\n        float t = mOutputRect.top * mInputRect.height() / mOutputRect.height();\n        float r = (mOutputRect.right - getWidth()) * mInputRect.width() / mOutputRect.width();\n        float b = (mOutputRect.bottom - getHeight()) * mInputRect.height() / mOutputRect.height();\n        mImageDrawRect.left = Math.round(mInputRect.left - l);\n        mImageDrawRect.top = Math.round(mInputRect.top - t);\n        mImageDrawRect.right = Math.round(mInputRect.right - r);\n        mImageDrawRect.bottom = Math.round(mInputRect.bottom - b);\n    }\n\n    private void adjustCanvasAndImageRect(int width, int height) {\n        mCanvasDrawRect.set(0, 0, width, height);\n        if (mImageDrawRect.left < 0) {\n            mCanvasDrawRect.left -= (mImageDrawRect.left * mCanvasDrawRect.width()) / mImageDrawRect.width();\n            mImageDrawRect.left = 0;\n        }\n        if (mImageDrawRect.top < 0) {\n            mCanvasDrawRect.top -= (mImageDrawRect.top * mCanvasDrawRect.height()) / mImageDrawRect.height();\n            mImageDrawRect.top = 0;\n        }\n        if (mImageDrawRect.right > mImageWidth) {\n            mCanvasDrawRect.right -= ((mImageDrawRect.right - mImageWidth) * mCanvasDrawRect.width()) / mImageDrawRect.width();\n            mImageDrawRect.right = mImageWidth;\n        }\n        if (mImageDrawRect.bottom > mImageHeight) {\n            mCanvasDrawRect.bottom -= ((mImageDrawRect.bottom - mImageHeight) * mCanvasDrawRect.height()) / mImageDrawRect.height();\n            mImageDrawRect.bottom = mImageHeight;\n        }\n    }\n\n    private void drawModeRect(Canvas canvas) {\n        mRectPaint.setColor(Color.BLUE);\n        canvas.drawRect(mOutputRect, mRectPaint);\n        mRectPaint.setColor(Color.GREEN);\n        drawRectInset(canvas, mOutputRect, -1);\n        mRectPaint.setColor(Color.RED);\n        drawRectInset(canvas, mOutputRect, -2);\n    }\n\n    private void drawRectInset(Canvas canvas, Rect rect, int inset) {\n        canvas.drawRect(\n                rect.left + inset,\n                rect.top + inset,\n                rect.right - inset,\n                rect.bottom - inset, mRectPaint);\n    }\n\n    private Rect getIntRect(RectF rect) {\n        return new Rect(\n                Math.round(rect.left),\n                Math.round(rect.top),\n                Math.round(rect.right),\n                Math.round(rect.bottom));\n    }\n\n    private int getSampleSize() {\n        int sx = Math.round(mInputRect.width() / mModeSize.width());\n        int sy = Math.round(mInputRect.height() / mModeSize.height());\n        int scale = Math.max(1, Math.max(sx, sy));\n        return Integer.highestOneBit(scale);\n    }\n\n    public Bitmap getBitmap() {\n        Bitmap result = Bitmap.createBitmap(mModeSize.width(), mModeSize.height(), Bitmap.Config.ARGB_8888);\n        Canvas canvas = new Canvas(result);\n        canvas.drawColor(Color.BLACK);\n        if (mImageOK) {\n            mImageDrawRect.set(getIntRect(mInputRect));\n            adjustCanvasAndImageRect(mModeSize.width(), mModeSize.height());\n            drawBitmap(canvas);\n        }\n        mLabelCollection.draw(canvas, mOutputRect, new Rect(0, 0, mModeSize.width(), mModeSize.height()));\n        return result;\n    }\n\n    private void drawBitmap(Canvas canvas) {\n        canvas.save();\n        canvas.rotate(mOrientation);\n        rotateDrawRectangles();\n        if (!mSmallImage) {\n            updateCache();\n            mImageDrawRect.offset(-mCacheRect.left, -mCacheRect.top);\n            mImageDrawRect.left /= mCacheSampleSize;\n            mImageDrawRect.top /= mCacheSampleSize;\n            mImageDrawRect.right /= mCacheSampleSize;\n            mImageDrawRect.bottom /= mCacheSampleSize;\n        }\n        canvas.drawBitmap(mCacheBitmap, mImageDrawRect, mCanvasDrawRect, mPaint);\n        canvas.restore();\n    }\n\n    private void rotateDrawRectangles() {\n        int w = mImageWidth;\n        int h = mImageHeight;\n        for (int i = 0; i < mOrientation / 90; ++i) {\n            int tmp = w;\n            w = h;\n            h = tmp;\n            mImageDrawRect.set(\n                    mImageDrawRect.top,\n                    h - mImageDrawRect.left,\n                    mImageDrawRect.bottom,\n                    h - mImageDrawRect.right);\n            mCanvasDrawRect.set(\n                    mCanvasDrawRect.top,\n                    -mCanvasDrawRect.right,\n                    mCanvasDrawRect.bottom,\n                    -mCanvasDrawRect.left);\n        }\n        mImageDrawRect.sort();\n    }\n\n    private void updateCache() {\n        int sampleSize = getSampleSize();\n        if (sampleSize >= mCacheSampleSize && mCacheRect.contains(mImageDrawRect))\n            return;\n\n        if (mCacheBitmap != null)\n            mCacheBitmap.recycle();\n\n        int cacheWidth = mImageDrawRect.width();\n        int cacheHeight = mImageDrawRect.height();\n        while (cacheWidth * cacheHeight < (sampleSize * 1024 * sampleSize * 1024)) {\n            cacheWidth += mImageDrawRect.width();\n            cacheHeight += mImageDrawRect.height();\n        }\n        int left = ~(sampleSize - 1) & (mImageDrawRect.centerX() - cacheWidth / 2);\n        int top = ~(sampleSize - 1) & (mImageDrawRect.centerY() - cacheHeight / 2);\n        int right = ~(sampleSize - 1) & (mImageDrawRect.centerX() + cacheWidth / 2 + sampleSize - 1);\n        int bottom = ~(sampleSize - 1) & (mImageDrawRect.centerY() + cacheHeight / 2 + sampleSize - 1);\n        mCacheRect.set(\n                Math.max(0, left),\n                Math.max(0, top),\n                Math.min(mRegionDecoder.getWidth(), right),\n                Math.min(mRegionDecoder.getHeight(), bottom));\n        mBitmapOptions.inSampleSize = mCacheSampleSize = sampleSize;\n        mCacheBitmap = mRegionDecoder.decodeRegion(mCacheRect, mBitmapOptions);\n    }\n\n    private void editLabelBegin(float x, float y) {\n        Label label = mLabelCollection.editLabelBegin(x, y);\n        GetActivity().startEditTextActivity(label);\n    }\n\n    public void editLabelEnd(Label label) {\n        mLabelCollection.editLabelEnd(label);\n        invalidate();\n    }\n\n    public LabelCollection getLabels() {\n        return mLabelCollection;\n    }\n\n    private MainActivity GetActivity() {\n        MainActivity activity;\n        Context context = getContext();\n        if (!(context instanceof MainActivity))\n            context = ((ContextWrapper) context).getBaseContext();\n        activity = (MainActivity) context;\n        return activity;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/EditTextActivity.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder;\n\nimport android.content.Intent;\nimport android.graphics.Color;\nimport android.graphics.drawable.Drawable;\n\nimport androidx.annotation.ColorInt;\nimport androidx.fragment.app.DialogFragment;\nimport androidx.core.content.ContextCompat;\nimport androidx.appcompat.app.AppCompatActivity;\n\nimport android.os.Bundle;\nimport android.text.Editable;\nimport android.text.TextWatcher;\nimport android.view.Menu;\nimport android.view.MenuItem;\nimport android.view.MotionEvent;\nimport android.view.View;\nimport android.widget.AdapterView;\nimport android.widget.ArrayAdapter;\nimport android.widget.CheckBox;\nimport android.widget.EditText;\nimport android.widget.Spinner;\n\nimport java.util.List;\n\nimport om.sstvencoder.TextOverlay.Label;\n\npublic class EditTextActivity extends AppCompatActivity\n        implements AdapterView.OnItemSelectedListener, ColorFragment.OnColorSelectedListener {\n\n    private enum EditColorMode {\n        None,\n        Text,\n        Outline\n    }\n\n    public static final int REQUEST_CODE = 101;\n    public static final String EXTRA = \"EDIT_TEXT_EXTRA\";\n    private Label mLabel;\n    private EditColorMode mEditColor;\n    private List<String> mFontFamilyNameList;\n    private CheckBox mEditItalic, mEditBold, mEditOutline;\n    private int mClearTextButtonWidth;\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.activity_edit_text);\n        mEditColor = EditColorMode.None;\n        mEditBold = findViewById(R.id.edit_bold);\n        mEditItalic = findViewById(R.id.edit_italic);\n        mEditOutline = findViewById(R.id.edit_outline);\n    }\n\n    @Override\n    protected void onStart() {\n        super.onStart();\n        mLabel = ((Label) getIntent().getSerializableExtra(EXTRA)).getClone();\n        initText();\n        initTextSizeSpinner(mLabel.getTextSize());\n        mEditBold.setChecked(mLabel.getBold());\n        mEditItalic.setChecked(mLabel.getItalic());\n        initFontFamilySpinner(mLabel.getFamilyName());\n        mEditOutline.setChecked(mLabel.getOutline());\n        initOutlineSizeSpinner(mLabel.getOutlineSize());\n        findViewById(R.id.edit_color).setBackgroundColor(mLabel.getForeColor());\n        findViewById(R.id.edit_outline_color).setBackgroundColor(mLabel.getOutlineColor());\n        enableOutline(mEditOutline.isChecked());\n    }\n\n    private void initText() {\n        EditText editText = findViewById(R.id.edit_text);\n        int clearTextIcon = android.R.drawable.ic_menu_close_clear_cancel;\n        Drawable drawable = ContextCompat.getDrawable(this, clearTextIcon);\n        editText.setText(mLabel.getText());\n        editText.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null);\n        mClearTextButtonWidth = 2 * drawable.getIntrinsicWidth();\n\n        editText.addTextChangedListener(new TextWatcher() {\n            @Override\n            public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {\n            }\n\n            @Override\n            public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {\n                mLabel.setText(charSequence.toString());\n            }\n\n            @Override\n            public void afterTextChanged(Editable editable) {\n            }\n        });\n\n        editText.setOnTouchListener(new View.OnTouchListener() {\n            private boolean mClear;\n\n            @Override\n            public boolean onTouch(View view, MotionEvent e) {\n                switch (e.getAction()) {\n                    case MotionEvent.ACTION_DOWN:\n                        if (HitClearTextButton(view, e)) {\n                            mClear = true;\n                            return true;\n                        }\n                        break;\n                    case MotionEvent.ACTION_MOVE:\n                        if (!HitClearTextButton(view, e))\n                            mClear = false;\n                        break;\n                    case MotionEvent.ACTION_UP:\n                        if (HitClearTextButton(view, e) && mClear) {\n                            ((EditText) view).setText(\"\");\n                            return true;\n                        }\n                        mClear = false;\n                        break;\n                    case MotionEvent.ACTION_CANCEL:\n                        mClear = false;\n                        break;\n                }\n                return false;\n            }\n\n            private boolean HitClearTextButton(View view, MotionEvent e) {\n                int left = view.getRight() - mClearTextButtonWidth;\n                return left < e.getX();\n            }\n        });\n    }\n\n    private void initFontFamilySpinner(String familyName) {\n        Spinner spinner = findViewById(R.id.edit_font_family);\n        spinner.setOnItemSelectedListener(this);\n        mFontFamilyNameList = Utility.getSystemFontFamilyList();\n        spinner.setAdapter(new ArrayAdapter<>(this,\n                android.R.layout.simple_spinner_dropdown_item, mFontFamilyNameList));\n        spinner.setSelection(mFontFamilyNameList.indexOf(familyName));\n    }\n\n    private void initTextSizeSpinner(float textSize) {\n        Spinner spinner = findViewById(R.id.edit_text_size);\n        spinner.setOnItemSelectedListener(this);\n        String[] sizeList = new String[]\n                {\n                        getString(R.string.font_size_small),\n                        getString(R.string.font_size_normal),\n                        getString(R.string.font_size_large),\n                        getString(R.string.font_size_huge)\n                };\n        spinner.setAdapter(new ArrayAdapter<>(this,\n                android.R.layout.simple_spinner_dropdown_item, sizeList));\n        spinner.setSelection(textSizeToPosition(textSize));\n    }\n\n    private void initOutlineSizeSpinner(float outlineSize) {\n        Spinner spinner = findViewById(R.id.edit_outline_size);\n        spinner.setOnItemSelectedListener(this);\n        String[] sizeList = new String[]\n                {\n                        getString(R.string.outline_size_thin),\n                        getString(R.string.outline_size_normal),\n                        getString(R.string.outline_size_thick)\n                };\n        spinner.setAdapter(new ArrayAdapter<>(this,\n                android.R.layout.simple_spinner_dropdown_item, sizeList));\n        spinner.setSelection(outlineSizeToPosition(outlineSize));\n    }\n\n    private int textSizeToPosition(float textSize) {\n        int position = (int) (textSize - 1f);\n        if (0 <= position && position <= 3)\n            return position;\n        mLabel.setTextSize(Label.TEXT_SIZE_NORMAL);\n        return 1;\n    }\n\n    private float positionToTextSize(int position) {\n        return position + 1f;\n    }\n\n    private int outlineSizeToPosition(float outlineSize) {\n        int position = (int) (outlineSize * 2f / Label.OUTLINE_SIZE_NORMAL - 1f);\n        if (0 <= position && position <= 2)\n            return position;\n        mLabel.setOutlineSize(Label.OUTLINE_SIZE_NORMAL);\n        return 1;\n    }\n\n    private float positionToOutlineSize(int position) {\n        return Label.OUTLINE_SIZE_NORMAL * 0.5f * (position + 1f);\n    }\n\n    @Override\n    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {\n        int parentId = parent.getId();\n        if (parentId == R.id.edit_text_size) {\n            mLabel.setTextSize(positionToTextSize(position));\n        }\n        else if (parentId == R.id.edit_outline_size) {\n            mLabel.setOutlineSize(positionToOutlineSize(position));\n        }\n        else if (parentId == R.id.edit_font_family) {\n            mLabel.setFamilyName(mFontFamilyNameList.get(position));\n        }\n    }\n\n    private void enableOutline(boolean enabled) {\n        findViewById(R.id.text_outline_size).setEnabled(enabled);\n        findViewById(R.id.edit_outline_size).setEnabled(enabled);\n        findViewById(R.id.text_outline_color).setEnabled(enabled);\n        findViewById(R.id.edit_outline_color).setEnabled(enabled);\n        @ColorInt\n        int color = enabled ? mLabel.getOutlineColor() : Color.DKGRAY;\n        findViewById(R.id.edit_outline_color).setBackgroundColor(color);\n    }\n\n    @Override\n    public void onNothingSelected(AdapterView<?> parent) {\n    }\n\n    @Override\n    public boolean onCreateOptionsMenu(Menu menu) {\n        getMenuInflater().inflate(R.menu.menu_edit_text, menu);\n        return true;\n    }\n\n    @Override\n    public boolean onOptionsItemSelected(MenuItem item) {\n        int id = item.getItemId();\n        if (id == R.id.action_done) {\n            done();\n            return true;\n        }\n        return super.onOptionsItemSelected(item);\n    }\n\n    public void onBoldClick(View view) {\n        mLabel.setBold(mEditBold.isChecked());\n    }\n\n    public void onItalicClick(View view) {\n        mLabel.setItalic(mEditItalic.isChecked());\n    }\n\n    public void onOutlineClick(View view) {\n        if (view.getId() == R.id.text_outline)\n            mEditOutline.setChecked(!mEditOutline.isChecked());\n        boolean outline = mEditOutline.isChecked();\n        mLabel.setOutline(outline);\n        enableOutline(outline);\n    }\n\n    public void onColorClick(View view) {\n        showColorDialog(R.string.color, mLabel.getForeColor());\n        mEditColor = EditColorMode.Text;\n    }\n\n    public void onOutlineColorClick(View view) {\n        if (mEditOutline.isChecked()) {\n            showColorDialog(R.string.outline_color, mLabel.getOutlineColor());\n            mEditColor = EditColorMode.Outline;\n        }\n    }\n\n    private void showColorDialog(int title, int color) {\n        ColorFragment fragment = new ColorFragment();\n        fragment.setTitle(title);\n        fragment.setColor(color);\n        fragment.addOnColorSelectedListener(this);\n        fragment.show(getSupportFragmentManager(), ColorFragment.class.getName());\n    }\n\n    @Override\n    public void onColorSelected(DialogFragment fragment, int color) {\n        switch (mEditColor) {\n            case Text:\n                mLabel.setForeColor(color);\n                findViewById(R.id.edit_color).setBackgroundColor(color);\n                break;\n            case Outline:\n                mLabel.setOutlineColor(color);\n                findViewById(R.id.edit_outline_color).setBackgroundColor(color);\n                break;\n        }\n        mEditColor = EditColorMode.None;\n    }\n\n    @Override\n    public void onCancel(DialogFragment fragment) {\n        mEditColor = EditColorMode.None;\n    }\n\n    private void done() {\n        Intent intent = new Intent();\n        intent.putExtra(EXTRA, mLabel);\n        setResult(RESULT_OK, intent);\n        finish();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Encoder.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder;\n\nimport android.graphics.Bitmap;\n\nimport java.util.LinkedList;\nimport java.util.List;\n\nimport om.sstvencoder.ModeInterfaces.IMode;\nimport om.sstvencoder.ModeInterfaces.IModeInfo;\nimport om.sstvencoder.Modes.ModeFactory;\nimport om.sstvencoder.Output.IOutput;\nimport om.sstvencoder.Output.OutputFactory;\nimport om.sstvencoder.Output.WaveFileOutputContext;\n\n// Creates IMode instance\nclass Encoder {\n    private final MainActivityMessenger mMessenger;\n    private final Thread mThread;\n    private Thread mSaveWaveThread;\n    private final List<IMode> mQueue;\n    private final ProgressBarWrapper mProgressBar, mProgressBar2;\n    private boolean mQuit, mStop;\n    private Class<?> mModeClass;\n\n    Encoder(MainActivityMessenger messenger,\n            ProgressBarWrapper progressBar, ProgressBarWrapper progressBar2) {\n        mMessenger = messenger;\n        mProgressBar = progressBar;\n        mProgressBar2 = progressBar2;\n        mQueue = new LinkedList<>();\n        mQuit = false;\n        mStop = false;\n        mModeClass = ModeFactory.getDefaultMode();\n\n        mThread = new Thread() {\n            @Override\n            public void run() {\n                while (true) {\n                    IMode mode;\n                    synchronized (this) {\n                        while (mQueue.isEmpty() && !mQuit) {\n                            try {\n                                wait();\n                            } catch (Exception ignore) {\n                            }\n                        }\n                        if (mQuit)\n                            return;\n\n                        mStop = false;\n                        mode = mQueue.remove(0);\n                    }\n                    mode.init();\n                    mProgressBar.begin(mode.getProcessCount(),\n                            mMessenger.getString(R.string.progressbar_message_sending));\n\n                    while (mode.process()) {\n                        mProgressBar.step();\n\n                        synchronized (this) {\n                            if (mQuit || mStop)\n                                break;\n                        }\n                    }\n                    mode.finish(mStop);\n                    mProgressBar.end();\n                }\n            }\n        };\n        mThread.start();\n    }\n\n    boolean setMode(String className) {\n        try {\n            mModeClass = Class.forName(className);\n        } catch (Exception ignore) {\n            return false;\n        }\n        return true;\n    }\n\n    IModeInfo getModeInfo() {\n        return ModeFactory.getModeInfo(mModeClass);\n    }\n\n    IModeInfo[] getModeInfoList() {\n        return ModeFactory.getModeInfoList();\n    }\n\n    void play(Bitmap bitmap) {\n        IOutput output = OutputFactory.createOutputForSending();\n        IMode mode = ModeFactory.CreateMode(mModeClass, bitmap, output);\n        if (mode != null)\n            enqueue(mode);\n    }\n\n    void save(Bitmap bitmap, WaveFileOutputContext context) {\n        if (mSaveWaveThread != null && mSaveWaveThread.isAlive())\n            return;\n        IOutput output = OutputFactory.createOutputForSavingAsWave(context);\n        IMode mode = ModeFactory.CreateMode(mModeClass, bitmap, output);\n        if (mode != null)\n            save(mode, context);\n    }\n\n    private void save(final IMode mode, final WaveFileOutputContext context) {\n        mSaveWaveThread = new Thread() {\n            @Override\n            public void run() {\n                mode.init();\n                mProgressBar2.begin(mode.getProcessCount(),\n                        mMessenger.getString(R.string.progressbar_message_saving_to_file, context.getFileName()));\n\n                while (mode.process()) {\n                    mProgressBar2.step();\n\n                    synchronized (this) {\n                        if (mQuit)\n                            break;\n                    }\n                }\n                mode.finish(mQuit);\n                mProgressBar2.end();\n                if (!mQuit)\n                    mMessenger.carrySaveAsWaveIsDoneMessage(context);\n            }\n        };\n        mSaveWaveThread.start();\n    }\n\n    void stop() {\n        synchronized (mThread) {\n            mStop = true;\n            int size = mQueue.size();\n            for (int i = 0; i < size; ++i)\n                mQueue.remove(0).finish(true);\n        }\n    }\n\n    private void enqueue(IMode mode) {\n        synchronized (mThread) {\n            mQueue.add(mode);\n            mThread.notify();\n        }\n    }\n\n    void destroy() {\n        synchronized (mThread) {\n            mQuit = true;\n            mThread.notify();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/FontFamilySet.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder;\n\nimport androidx.annotation.NonNull;\n\nimport android.content.Context;\nimport android.util.Xml;\n\nimport org.xmlpull.v1.XmlPullParser;\nimport org.xmlpull.v1.XmlPullParserException;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.ArrayList;\nimport java.util.List;\n\nclass FontFamilySet {\n    class FontFamily {\n        String name;\n        String displayName;\n        boolean bold;\n        boolean italic;\n    }\n\n    private final List<FontFamily> mFamilySet;\n    private final Context mContext;\n\n    FontFamilySet(Context context) {\n        mContext = context;\n        mFamilySet = new ArrayList<>();\n        fillWithSystemFonts(mFamilySet);\n        if (mFamilySet.size() == 0)\n            mFamilySet.add(getDefaultFontFamily());\n    }\n\n    @NonNull\n    private FontFamily getDefaultFontFamily() {\n        FontFamily defaultFontFamily = new FontFamily();\n        defaultFontFamily.name = null;\n        defaultFontFamily.displayName = mContext.getString(R.string.font_default);\n        defaultFontFamily.bold = true;\n        defaultFontFamily.italic = true;\n        return defaultFontFamily;\n    }\n\n    @NonNull\n    FontFamily getFontFamily(String name) {\n        if (name != null) {\n            for (FontFamily fontFamily : mFamilySet) {\n                if (name.equals(fontFamily.name))\n                    return fontFamily;\n            }\n        }\n        return mFamilySet.get(0);\n    }\n\n    @NonNull\n    FontFamily getFontFamilyFromDisplayName(@NonNull String displayName) {\n        for (FontFamily fontFamily : mFamilySet) {\n            if (displayName.equals(fontFamily.displayName))\n                return fontFamily;\n        }\n        return mFamilySet.get(0);\n    }\n\n    @NonNull\n    List<String> getFontFamilyDisplayNameList() {\n        List<String> names = new ArrayList<>();\n        for (FontFamily fontFamily : mFamilySet)\n            names.add(fontFamily.displayName);\n        return names;\n    }\n\n    private void fillWithSystemFonts(@NonNull List<FontFamily> familySet) {\n        File fontsFile = new File(\"/system/etc/system_fonts.xml\");\n        if (!fontsFile.exists()) {\n            fontsFile = new File(\"/system/etc/fonts.xml\");\n            if (!fontsFile.exists())\n                return;\n        }\n        InputStream in = null;\n        try {\n            in = new FileInputStream(fontsFile);\n            XmlPullParser parser = Xml.newPullParser();\n            parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);\n            parser.setInput(in, null);\n            parser.next();\n            if (parser.getEventType() == XmlPullParser.START_TAG && parser.getName().equals(\"familyset\"))\n                readFamilySet(parser, familySet);\n        } catch (Exception ignore) {\n        } finally {\n            if (in != null) {\n                try {\n                    in.close();\n                } catch (Exception ignore) {\n                }\n            }\n        }\n    }\n\n    private void readFamilySet(@NonNull XmlPullParser parser, @NonNull List<FontFamily> familySet)\n            throws XmlPullParserException, IOException {\n        while (parser.next() != XmlPullParser.END_TAG) {\n            if (parser.getEventType() == XmlPullParser.START_TAG && parser.getName().equals(\"family\")) {\n                FontFamily fontFamily = readFamily(parser);\n                if (fontFamily.displayName != null)\n                    familySet.add(fontFamily);\n            }\n        }\n    }\n\n    @NonNull\n    private FontFamily readFamily(@NonNull XmlPullParser parser)\n            throws XmlPullParserException, IOException {\n        FontFamily fontFamily = new FontFamily();\n\n        while (parser.next() != XmlPullParser.END_TAG) {\n            if (parser.getEventType() == XmlPullParser.START_TAG) {\n                switch (parser.getName()) {\n                    case \"nameset\":\n                        readNameSet(parser, fontFamily);\n                        break;\n                    case \"fileset\":\n                        readFileSet(parser, fontFamily);\n                        break;\n                }\n            }\n        }\n        return fontFamily;\n    }\n\n    private void readNameSet(@NonNull XmlPullParser parser, @NonNull FontFamily fontFamily)\n            throws XmlPullParserException, IOException {\n        while (parser.next() != XmlPullParser.END_TAG) {\n            if (parser.getEventType() == XmlPullParser.START_TAG && parser.getName().equals(\"name\")) {\n                if (fontFamily.name == null)\n                    fontFamily.name = readText(parser);\n                else {\n                    // skip all other names\n                    parser.next();\n                    parser.next();\n                }\n            }\n        }\n    }\n\n    private void readFileSet(@NonNull XmlPullParser parser, @NonNull FontFamily fontFamily)\n            throws XmlPullParserException, IOException {\n        while (parser.next() != XmlPullParser.END_TAG) {\n            if (parser.getEventType() == XmlPullParser.START_TAG && parser.getName().equals(\"file\"))\n                parseDisplayNameAndStyle(readText(parser), fontFamily);\n        }\n    }\n\n    private void parseDisplayNameAndStyle(String fontFileName, @NonNull FontFamily fontFamily) {\n        // Example: RobotoCondensed-LightItalic.ttf\n        // { \"RobotoCondensed\", \"LightItalic\" }\n        String[] familyInfo = fontFileName.split(\"\\\\.\")[0].split(\"-\");\n        String s = \"\";\n        if (familyInfo.length > 1) {\n            s = familyInfo[1];\n            if (s.contains(\"Bold\"))\n                fontFamily.bold = true;\n            if (s.contains(\"Italic\"))\n                fontFamily.italic = true;\n        }\n        if (fontFamily.displayName == null) {\n            // \"Light\"\n            s = s.replace(\"Regular\", \"\").replace(\"Bold\", \"\").replace(\"Italic\", \"\");\n            // \"Roboto Condensed Light\"\n            fontFamily.displayName = (familyInfo[0] + s).replaceAll(\"(\\\\p{Ll})(\\\\p{Lu})\", \"$1 $2\");\n        }\n    }\n\n    private String readText(@NonNull XmlPullParser parser)\n            throws IOException, XmlPullParserException {\n        String text = \"\";\n        if (parser.next() == XmlPullParser.TEXT) {\n            text = parser.getText();\n            parser.next();\n        }\n        return text;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/MainActivity.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder;\n\nimport android.Manifest;\nimport android.app.AlertDialog;\nimport android.content.ContentResolver;\nimport android.content.DialogInterface;\nimport android.content.Intent;\nimport android.content.pm.PackageManager;\nimport android.database.Cursor;\nimport androidx.exifinterface.media.ExifInterface;\nimport android.net.Uri;\nimport android.os.Build;\nimport android.os.Bundle;\nimport android.provider.MediaStore;\nimport androidx.annotation.NonNull;\nimport androidx.core.app.ActivityCompat;\nimport androidx.core.content.ContextCompat;\nimport androidx.appcompat.app.AppCompatActivity;\nimport android.system.ErrnoException;\nimport android.system.OsConstants;\nimport android.view.Menu;\nimport android.view.MenuItem;\nimport android.view.SubMenu;\nimport android.widget.ProgressBar;\nimport android.widget.TextView;\nimport android.widget.Toast;\n\nimport java.io.InputStream;\n\nimport om.sstvencoder.ModeInterfaces.IModeInfo;\nimport om.sstvencoder.Output.WaveFileOutputContext;\nimport om.sstvencoder.TextOverlay.Label;\n\npublic class MainActivity extends AppCompatActivity {\n    private static final String CLASS_NAME = \"ClassName\";\n    private static final int REQUEST_LOAD_IMAGE_PERMISSION = 1;\n    private static final int REQUEST_SAVE_WAVE_PERMISSION = 2;\n    private static final int REQUEST_IMAGE_CAPTURE_PERMISSION = 3;\n    private static final int REQUEST_PICK_IMAGE = 11;\n    private static final int REQUEST_IMAGE_CAPTURE = 12;\n    private Settings mSettings;\n    private TextOverlayTemplate mTextOverlayTemplate;\n    private CropView mCropView;\n    private Encoder mEncoder;\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.activity_main);\n        mCropView = findViewById(R.id.cropView);\n        mEncoder = new Encoder(new MainActivityMessenger(this), getProgressBar(), getProgressBar2());\n\n        mSettings = new Settings(this);\n        mSettings.load();\n\n        mTextOverlayTemplate = new TextOverlayTemplate();\n        mTextOverlayTemplate.load(mCropView.getLabels(), mSettings.getTextOverlayFile());\n\n        setMode(mSettings.getModeClassName());\n        loadImage(getIntent());\n    }\n\n    private ProgressBarWrapper getProgressBar() {\n        ProgressBar progressBar = findViewById(R.id.progressBar);\n        TextView progressBarText = findViewById(R.id.progressBarText);\n        return new ProgressBarWrapper(progressBar, progressBarText);\n    }\n\n    private ProgressBarWrapper getProgressBar2() {\n        ProgressBar progressBar = findViewById(R.id.progressBar2);\n        TextView progressBarText = findViewById(R.id.progressBarText2);\n        return new ProgressBarWrapper(progressBar, progressBarText);\n    }\n\n    @Override\n    protected void onNewIntent(Intent intent) {\n        loadImage(intent);\n        super.onNewIntent(intent);\n    }\n\n    private void loadImage(Intent intent) {\n        Uri uri = getImageUriFromIntent(intent);\n        boolean verbose = true;\n        if (uri == null) {\n            // SecurityException in loadImage for Build.VERSION.SDK_INT >= Build.VERSION_CODES.M\n            // uri = mSettings.getImageUri();\n            verbose = false;\n        }\n        loadImage(uri, verbose);\n    }\n\n    private Uri getImageUriFromIntent(Intent intent) {\n        Uri uri = null;\n        if (isIntentTypeValid(intent.getType()) && isIntentActionValid(intent.getAction())) {\n            uri = intent.hasExtra(Intent.EXTRA_STREAM) ?\n                    (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM) : intent.getData();\n            if (uri == null) {\n                String s = getString(R.string.load_img_err_txt_unsupported);\n                showErrorMessage(getString(R.string.load_img_err_title), s, s + \"\\n\\n\" + intent);\n            }\n        }\n        return uri;\n    }\n\n    // Set verbose to false for any Uri that might have expired (e.g. shared from browser).\n    private boolean loadImage(Uri uri, boolean verbose) {\n        boolean succeeded = false;\n        ContentResolver resolver = getContentResolver();\n        if (uri != null) {\n            try {\n                InputStream stream = resolver.openInputStream(uri);\n                if (stream != null)\n                    mCropView.setBitmap(stream);\n                succeeded = true;\n            } catch (Exception ex) { // e.g. FileNotFoundException, SecurityException\n                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && isPermissionException(ex)\n                        && needsRequestReadPermission()) {\n                    requestReadPermission(REQUEST_LOAD_IMAGE_PERMISSION);\n                }\n                else\n                    showFileNotLoadedMessage(ex, verbose);\n            }\n        }\n        if (succeeded) {\n            mCropView.rotateImage(getOrientation(resolver, uri));\n            mSettings.setImageUri(uri);\n        }\n        else\n            setDefaultBitmap();\n        return succeeded;\n    }\n\n    private void setDefaultBitmap() {\n        try {\n            mCropView.setBitmap(getResources().openRawResource(R.raw.smpte_color_bars));\n        } catch (Exception ignore) {\n            mCropView.setNoBitmap();\n        }\n        mSettings.setImageUri(null);\n    }\n\n    private boolean isIntentActionValid(String action) {\n        return Intent.ACTION_SEND.equals(action);\n    }\n\n    private boolean isIntentTypeValid(String type) {\n        return type != null && type.startsWith(\"image/\");\n    }\n\n    private boolean isPermissionException(Exception ex) {\n        return ex.getCause() instanceof ErrnoException\n                && ((ErrnoException) ex.getCause()).errno == OsConstants.EACCES;\n    }\n\n    private boolean needsRequestReadPermission() {\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)\n            return false;\n        String permission = Manifest.permission.READ_EXTERNAL_STORAGE;\n        int state = ContextCompat.checkSelfPermission(this, permission);\n        return state != PackageManager.PERMISSION_GRANTED;\n    }\n\n    private boolean needsRequestWritePermission() {\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M ||\n                Build.VERSION.SDK_INT > Build.VERSION_CODES.Q)\n            return false;\n        String permission = Manifest.permission.WRITE_EXTERNAL_STORAGE;\n        int state = ContextCompat.checkSelfPermission(this, permission);\n        return state != PackageManager.PERMISSION_GRANTED;\n    }\n\n    private void requestReadPermission(int requestCode) {\n        String[] permissions = new String[]{Manifest.permission.READ_EXTERNAL_STORAGE};\n        ActivityCompat.requestPermissions(this, permissions, requestCode);\n    }\n\n    private void requestWritePermission(int requestCode) {\n        String[] permissions = new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE};\n        ActivityCompat.requestPermissions(this, permissions, requestCode);\n    }\n\n    @Override\n    public void onRequestPermissionsResult(int requestCode,\n                                           @NonNull String[] permissions,\n                                           @NonNull int[] grantResults) {\n        switch (requestCode) {\n            case REQUEST_LOAD_IMAGE_PERMISSION:\n                if (permissionGranted(grantResults))\n                    loadImage(mSettings.getImageUri(), false);\n                else\n                    setDefaultBitmap();\n                break;\n            case REQUEST_IMAGE_CAPTURE_PERMISSION:\n                if (permissionGranted(grantResults))\n                    dispatchTakePictureIntent();\n                break;\n            case REQUEST_SAVE_WAVE_PERMISSION:\n                if (permissionGranted(grantResults))\n                    save();\n                break;\n            default:\n                break;\n        }\n        super.onRequestPermissionsResult(requestCode, permissions, grantResults);\n    }\n\n    private boolean permissionGranted(@NonNull int[] grantResults) {\n        return grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;\n    }\n\n    private void showFileNotLoadedMessage(Exception ex, boolean verbose) {\n        String s;\n        if (verbose)\n            s = getString(R.string.load_img_err_title) + \": \\n\" + ex.getMessage();\n        else\n            s = getString(R.string.message_prev_img_not_loaded);\n        Toast.makeText(this, s, Toast.LENGTH_LONG).show();\n    }\n\n    private void showErrorMessage(final String title, final String shortText, final String longText) {\n        AlertDialog.Builder builder = new AlertDialog.Builder(this);\n        builder.setTitle(title);\n        builder.setMessage(shortText);\n        builder.setNeutralButton(getString(R.string.btn_ok), null);\n        builder.setPositiveButton(getString(R.string.btn_send_email), new DialogInterface.OnClickListener() {\n            @Override\n            public void onClick(DialogInterface dialog, int which) {\n                String device = Build.MANUFACTURER + \", \" + Build.BRAND + \", \" + Build.MODEL + \", \" + Build.VERSION.RELEASE;\n                String text = longText + \"\\n\\n\" + BuildConfig.VERSION_NAME + \", \" + device;\n                Intent intent = Utility.createEmailIntent(getString(R.string.email_subject), text);\n                startActivity(Intent.createChooser(intent, getString(R.string.chooser_title)));\n            }\n        });\n        builder.show();\n    }\n\n    private void showOrientationErrorMessage(Uri uri, Exception ex) {\n        String title = getString(R.string.load_img_orientation_err_title);\n        String longText = title + \"\\n\\n\" + Utility.createMessage(ex) + \"\\n\\n\" + uri;\n        showErrorMessage(title, ex.getMessage(), longText);\n    }\n\n    public int getOrientation(ContentResolver resolver, Uri uri) {\n        int orientation = 0;\n        try {\n            Cursor cursor = resolver.query(uri,\n                    new String[]{MediaStore.Images.ImageColumns.ORIENTATION},\n                    null, null, null);\n            if (cursor.moveToFirst())\n                orientation = cursor.getInt(0);\n            cursor.close();\n        } catch (Exception ignore) {\n            orientation = getExifOrientation(resolver, uri);\n        }\n        return orientation;\n    }\n\n    private int getExifOrientation(ContentResolver resolver, Uri uri) {\n        int orientation = 0;\n        InputStream in = null;\n        try {\n            in = resolver.openInputStream(uri);\n            int orientationAttribute = (new ExifInterface(in)).getAttributeInt(ExifInterface.TAG_ORIENTATION, 0);\n            orientation = Utility.convertToDegrees(orientationAttribute);\n        } catch (Exception ex) {\n            showOrientationErrorMessage(uri, ex);\n        } finally {\n            if (in != null) {\n                try {\n                    in.close();\n                } catch (Exception ignore) {\n                }\n            }\n        }\n        return orientation;\n    }\n\n    @Override\n    public boolean onCreateOptionsMenu(Menu menu) {\n        getMenuInflater().inflate(R.menu.menu_main, menu);\n        createModesMenu(menu);\n        return true;\n    }\n\n    private void createModesMenu(Menu menu) {\n        SubMenu modesSubMenu = menu.findItem(R.id.action_modes).getSubMenu();\n        IModeInfo[] modeInfoList = mEncoder.getModeInfoList();\n        for (IModeInfo modeInfo : modeInfoList) {\n            MenuItem item = modesSubMenu.add(modeInfo.getModeName());\n            Intent intent = new Intent();\n            intent.putExtra(CLASS_NAME, modeInfo.getModeClassName());\n            item.setIntent(intent);\n        }\n    }\n\n    @Override\n    public boolean onOptionsItemSelected(MenuItem item) {\n        int id = item.getItemId();\n        if (id == R.id.action_pick_picture) {\n            dispatchPickPictureIntent();\n        }\n        else if (id == R.id.action_take_picture) {\n            takePicture();\n        }\n        else if (id == R.id.action_save_wave) {\n            if (needsRequestWritePermission())\n                requestWritePermission(REQUEST_SAVE_WAVE_PERMISSION);\n            else\n                save();\n        }\n        else if (id == R.id.action_play) {\n            play();\n        }\n        else if (id == R.id.action_stop) {\n            mEncoder.stop();\n        }\n        else if (id == R.id.action_rotate) {\n            mCropView.rotateImage(90);\n        }\n        else if (id == R.id.action_reset) {\n            mCropView.resetImage();\n        }\n        else if (id == R.id.action_privacy_policy) {\n            openLinkInBrowser(\"https://sites.google.com/view/olgamiller/sstvencoder/privacypolicy/\");\n        }\n        else if (id == R.id.action_about) {\n            showTextPage(getString(R.string.action_about), getString(R.string.action_about_text, BuildConfig.VERSION_NAME));\n        }\n        else if (id != R.id.action_modes && id != R.id.action_transform) {\n            String className = item.getIntent().getStringExtra(CLASS_NAME);\n            setMode(className);\n        }\n        return true;\n    }\n\n    private void openLinkInBrowser(String link) {\n        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(link));\n        if (intent.resolveActivity(getPackageManager()) != null) {\n            startActivity(intent);\n        }\n        else {\n            showErrorMessage(getString(R.string.another_activity_start_err), link, \"\");\n        }\n    }\n\n    private void showTextPage(String title, String message) {\n        AlertDialog.Builder builder = new AlertDialog.Builder(this);\n        builder.setTitle(title);\n        builder.setMessage(message);\n        builder.setNeutralButton(R.string.btn_ok, null);\n        builder.show();\n    }\n\n    private void setMode(String modeClassName) {\n        if (mEncoder.setMode(modeClassName)) {\n            IModeInfo modeInfo = mEncoder.getModeInfo();\n            mCropView.setModeSize(modeInfo.getModeSize());\n            setTitle(modeInfo.getModeName());\n            mSettings.setModeClassName(modeClassName);\n        }\n    }\n\n    private void takePicture() {\n        if (!hasCamera()) {\n            Toast.makeText(this, getString(R.string.message_no_camera), Toast.LENGTH_LONG).show();\n            return;\n        }\n        if (needsRequestWritePermission())\n            requestWritePermission(REQUEST_IMAGE_CAPTURE_PERMISSION);\n        else\n            dispatchTakePictureIntent();\n    }\n\n    private boolean hasCamera() {\n        return getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);\n    }\n\n    public void startEditTextActivity(@NonNull Label label) {\n        Intent intent = new Intent(this, EditTextActivity.class);\n        intent.putExtra(EditTextActivity.EXTRA, label);\n        tryToStartActivityForResult(intent, EditTextActivity.REQUEST_CODE);\n    }\n\n    private void dispatchTakePictureIntent() {\n        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);\n        Uri uri = Utility.createImageUri(this);\n        if (uri != null) {\n            mSettings.setImageUri(uri);\n            intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);\n            tryToStartActivityForResult(intent, REQUEST_IMAGE_CAPTURE);\n        }\n    }\n\n    private void dispatchPickPictureIntent() {\n        Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);\n        tryToStartActivityForResult(intent, REQUEST_PICK_IMAGE);\n    }\n\n    private void tryToStartActivityForResult(Intent intent, int requestCode) {\n        if (intent.resolveActivity(getPackageManager()) == null) {\n            Toast.makeText(this, R.string.another_activity_resolve_err, Toast.LENGTH_LONG).show();\n            return;\n        }\n        try {\n            startActivityForResult(intent, requestCode);\n        } catch (Exception ignore) {\n            Toast.makeText(this, R.string.another_activity_start_err, Toast.LENGTH_LONG).show();\n        }\n    }\n\n    @Override\n    public void onActivityResult(int requestCode, int resultCode, Intent data) {\n        switch (requestCode) {\n            case EditTextActivity.REQUEST_CODE:\n                Label label = null;\n                if (resultCode == RESULT_OK && data != null)\n                    label = (Label) data.getSerializableExtra(EditTextActivity.EXTRA);\n                mCropView.editLabelEnd(label);\n                break;\n            case REQUEST_IMAGE_CAPTURE:\n                if (resultCode == RESULT_OK) {\n                    Uri uri = mSettings.getImageUri();\n                    if (loadImage(uri, true))\n                        addImageToGallery(uri);\n                }\n                break;\n            case REQUEST_PICK_IMAGE:\n                if (resultCode == RESULT_OK && data != null)\n                    loadImage(data.getData(), true);\n                break;\n            default:\n                super.onActivityResult(requestCode, resultCode, data);\n                break;\n        }\n    }\n\n    private void addImageToGallery(Uri uri) {\n        Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);\n        intent.setData(uri);\n        sendBroadcast(intent);\n    }\n\n    private void play() {\n        mEncoder.play(mCropView.getBitmap());\n    }\n\n    private void save() {\n        if (Utility.isExternalStorageWritable()) {\n            WaveFileOutputContext context\n                    = new WaveFileOutputContext(getContentResolver(), Utility.createWaveFileName());\n            mEncoder.save(mCropView.getBitmap(), context);\n        }\n    }\n\n    public void completeSaving(WaveFileOutputContext context) {\n        context.update();\n    }\n\n    @Override\n    protected void onPause() {\n        super.onPause();\n        mSettings.save();\n        mTextOverlayTemplate.save(mCropView.getLabels(), mSettings.getTextOverlayFile());\n    }\n\n    @Override\n    protected void onDestroy() {\n        super.onDestroy();\n        mEncoder.destroy();\n    }\n}"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/MainActivityMessenger.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder;\n\nimport android.os.Handler;\n\nimport om.sstvencoder.Output.WaveFileOutputContext;\n\nclass MainActivityMessenger {\n    private final MainActivity mMainActivity;\n    private final Handler mHandler;\n\n    MainActivityMessenger(MainActivity activity) {\n        mMainActivity = activity;\n        mHandler = new Handler();\n    }\n\n    void carrySaveAsWaveIsDoneMessage(final WaveFileOutputContext context) {\n        mHandler.post(new Runnable() {\n            @Override\n            public void run() {\n                mMainActivity.completeSaving(context);\n            }\n        });\n    }\n\n    public String getString(int resId, Object... formatArgs) {\n        return mMainActivity.getString(resId, formatArgs);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/ModeInterfaces/IMode.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.ModeInterfaces;\n\npublic interface IMode {\n    void init();\n\n    int getProcessCount();\n\n    boolean process();\n\n    void finish(boolean cancel);\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/ModeInterfaces/IModeInfo.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.ModeInterfaces;\n\npublic interface IModeInfo {\n    String getModeName();\n\n    String getModeClassName();\n\n    ModeSize getModeSize();\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/ModeInterfaces/ModeSize.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.ModeInterfaces;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n@Target({ElementType.TYPE})\n@Retention(RetentionPolicy.RUNTIME)\npublic @interface ModeSize {\n    int width();\n\n    int height();\n}"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/ImageFormats/NV21.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.Modes.ImageFormats;\n\nimport android.graphics.Bitmap;\n\nclass NV21 extends Yuv {\n    NV21(Bitmap bitmap) {\n        super(bitmap);\n    }\n\n    protected void convertBitmapToYuv(Bitmap bitmap) {\n        mYuv = new byte[(3 * mWidth * mHeight) / 2];\n        int pos = 0;\n\n        for (int h = 0; h < mHeight; ++h)\n            for (int w = 0; w < mWidth; ++w)\n                mYuv[pos++] = (byte) YuvConverter.convertToY(bitmap.getPixel(w, h));\n\n        for (int h = 0; h < mHeight; h += 2) {\n            for (int w = 0; w < mWidth; w += 2) {\n                int v0 = YuvConverter.convertToV(bitmap.getPixel(w, h));\n                int v1 = YuvConverter.convertToV(bitmap.getPixel(w + 1, h));\n                int v2 = YuvConverter.convertToV(bitmap.getPixel(w, h + 1));\n                int v3 = YuvConverter.convertToV(bitmap.getPixel(w + 1, h + 1));\n                mYuv[pos++] = (byte) ((v0 + v1 + v2 + v3) / 4);\n                int u0 = YuvConverter.convertToU(bitmap.getPixel(w, h));\n                int u1 = YuvConverter.convertToU(bitmap.getPixel(w + 1, h));\n                int u2 = YuvConverter.convertToU(bitmap.getPixel(w, h + 1));\n                int u3 = YuvConverter.convertToU(bitmap.getPixel(w + 1, h + 1));\n                mYuv[pos++] = (byte) ((u0 + u1 + u2 + u3) / 4);\n            }\n        }\n    }\n\n    public int getY(int x, int y) {\n        return 255 & mYuv[mWidth * y + x];\n    }\n\n    public int getU(int x, int y) {\n        return 255 & mYuv[mWidth * mHeight + mWidth * (y >> 1) + (x | 1)];\n    }\n\n    public int getV(int x, int y) {\n        return 255 & mYuv[mWidth * mHeight + mWidth * (y >> 1) + (x & ~1)];\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/ImageFormats/YUV440P.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.Modes.ImageFormats;\n\nimport android.graphics.Bitmap;\n\nclass YUV440P extends Yuv {\n    YUV440P(Bitmap bitmap) {\n        super(bitmap);\n    }\n\n    protected void convertBitmapToYuv(Bitmap bitmap) {\n        mYuv = new byte[2 * mWidth * mHeight];\n        int pos = 0;\n\n        for (int h = 0; h < mHeight; ++h)\n            for (int w = 0; w < mWidth; ++w)\n                mYuv[pos++] = (byte) YuvConverter.convertToY(bitmap.getPixel(w, h));\n\n        for (int h = 0; h < mHeight; h += 2) {\n            for (int w = 0; w < mWidth; ++w) {\n                int u0 = YuvConverter.convertToU(bitmap.getPixel(w, h));\n                int u1 = YuvConverter.convertToU(bitmap.getPixel(w, h + 1));\n                mYuv[pos++] = (byte) ((u0 + u1) / 2);\n            }\n        }\n\n        for (int h = 0; h < mHeight; h += 2) {\n            for (int w = 0; w < mWidth; ++w) {\n                int v0 = YuvConverter.convertToV(bitmap.getPixel(w, h));\n                int v1 = YuvConverter.convertToV(bitmap.getPixel(w, h + 1));\n                mYuv[pos++] = (byte) ((v0 + v1) / 2);\n            }\n        }\n    }\n\n    public int getY(int x, int y) {\n        return 255 & mYuv[mWidth * y + x];\n    }\n\n    public int getU(int x, int y) {\n        return 255 & mYuv[mWidth * mHeight + mWidth * (y >> 1) + x];\n    }\n\n    public int getV(int x, int y) {\n        return 255 & mYuv[((3 * mWidth * mHeight) >> 1) + mWidth * (y >> 1) + x];\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/ImageFormats/YUY2.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.Modes.ImageFormats;\n\nimport android.graphics.Bitmap;\n\nclass YUY2 extends Yuv {\n    YUY2(Bitmap bitmap) {\n        super(bitmap);\n    }\n\n    protected void convertBitmapToYuv(Bitmap bitmap) {\n        mYuv = new byte[2 * mWidth * mHeight];\n\n        for (int pos = 0, h = 0; h < mHeight; ++h) {\n            for (int w = 0; w < mWidth; w += 2) {\n                mYuv[pos++] = (byte) YuvConverter.convertToY(bitmap.getPixel(w, h));\n                int u0 = YuvConverter.convertToU(bitmap.getPixel(w, h));\n                int u1 = YuvConverter.convertToU(bitmap.getPixel(w + 1, h));\n                mYuv[pos++] = (byte) ((u0 + u1) / 2);\n                mYuv[pos++] = (byte) YuvConverter.convertToY(bitmap.getPixel(w + 1, h));\n                int v0 = YuvConverter.convertToV(bitmap.getPixel(w, h));\n                int v1 = YuvConverter.convertToV(bitmap.getPixel(w + 1, h));\n                mYuv[pos++] = (byte) ((v0 + v1) / 2);\n            }\n        }\n    }\n\n    public int getY(int x, int y) {\n        return 255 & mYuv[2 * mWidth * y + 2 * x];\n    }\n\n    public int getU(int x, int y) {\n        return 255 & mYuv[2 * mWidth * y + (((x & ~1) << 1) | 1)];\n    }\n\n    public int getV(int x, int y) {\n        return 255 & mYuv[2 * mWidth * y + ((x << 1) | 3)];\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/ImageFormats/YV12.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.Modes.ImageFormats;\n\nimport android.graphics.Bitmap;\n\nclass YV12 extends Yuv {\n    YV12(Bitmap bitmap) {\n        super(bitmap);\n    }\n\n    protected void convertBitmapToYuv(Bitmap bitmap) {\n        mYuv = new byte[(3 * mWidth * mHeight) / 2];\n        int pos = 0;\n\n        for (int h = 0; h < mHeight; ++h)\n            for (int w = 0; w < mWidth; ++w)\n                mYuv[pos++] = (byte) YuvConverter.convertToY(bitmap.getPixel(w, h));\n\n        for (int h = 0; h < mHeight; h += 2) {\n            for (int w = 0; w < mWidth; w += 2) {\n                int u0 = YuvConverter.convertToU(bitmap.getPixel(w, h));\n                int u1 = YuvConverter.convertToU(bitmap.getPixel(w + 1, h));\n                int u2 = YuvConverter.convertToU(bitmap.getPixel(w, h + 1));\n                int u3 = YuvConverter.convertToU(bitmap.getPixel(w + 1, h + 1));\n                mYuv[pos++] = (byte) ((u0 + u1 + u2 + u3) / 4);\n            }\n        }\n\n        for (int h = 0; h < mHeight; h += 2) {\n            for (int w = 0; w < mWidth; w += 2) {\n                int v0 = YuvConverter.convertToV(bitmap.getPixel(w, h));\n                int v1 = YuvConverter.convertToV(bitmap.getPixel(w + 1, h));\n                int v2 = YuvConverter.convertToV(bitmap.getPixel(w, h + 1));\n                int v3 = YuvConverter.convertToV(bitmap.getPixel(w + 1, h + 1));\n                mYuv[pos++] = (byte) ((v0 + v1 + v2 + v3) / 4);\n            }\n        }\n    }\n\n    public int getY(int x, int y) {\n        return 255 & mYuv[mWidth * y + x];\n    }\n\n    public int getU(int x, int y) {\n        return 255 & mYuv[mWidth * mHeight + (mWidth >> 1) * (y >> 1) + (x >> 1)];\n    }\n\n    public int getV(int x, int y) {\n        return 255 & mYuv[((5 * mWidth * mHeight) >> 2) + (mWidth >> 1) * (y >> 1) + (x >> 1)];\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/ImageFormats/Yuv.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.Modes.ImageFormats;\n\nimport android.graphics.Bitmap;\n\npublic abstract class Yuv {\n    protected byte[] mYuv;\n    final int mWidth;\n    final int mHeight;\n\n    Yuv(Bitmap bitmap) {\n        mWidth = bitmap.getWidth();\n        mHeight = bitmap.getHeight();\n        convertBitmapToYuv(bitmap);\n    }\n\n    protected abstract void convertBitmapToYuv(Bitmap bitmap);\n\n    public int getWidth() {\n        return mWidth;\n    }\n\n    public int getHeight() {\n        return mHeight;\n    }\n\n    public abstract int getY(int x, int y);\n\n    public abstract int getU(int x, int y);\n\n    public abstract int getV(int x, int y);\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/ImageFormats/YuvConverter.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.Modes.ImageFormats;\n\nimport android.graphics.Color;\n\nfinal class YuvConverter {\n    static int convertToY(int color) {\n        double R = Color.red(color);\n        double G = Color.green(color);\n        double B = Color.blue(color);\n        return clamp(16.0 + (.003906 * ((65.738 * R) + (129.057 * G) + (25.064 * B))));\n    }\n\n    static int convertToU(int color) {\n        double R = Color.red(color);\n        double G = Color.green(color);\n        double B = Color.blue(color);\n        return clamp(128.0 + (.003906 * ((-37.945 * R) + (-74.494 * G) + (112.439 * B))));\n    }\n\n    static int convertToV(int color) {\n        double R = Color.red(color);\n        double G = Color.green(color);\n        double B = Color.blue(color);\n        return clamp(128.0 + (.003906 * ((112.439 * R) + (-94.154 * G) + (-18.285 * B))));\n    }\n\n    private static int clamp(double value) {\n        return value < 0.0 ? 0 : (value > 255.0 ? 255 : (int) value);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/ImageFormats/YuvFactory.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.Modes.ImageFormats;\n\nimport android.graphics.Bitmap;\n\npublic final class YuvFactory {\n    public static Yuv createYuv(Bitmap bitmap, int format) {\n        switch (format) {\n            case YuvImageFormat.YV12:\n                return new YV12(bitmap);\n            case YuvImageFormat.NV21:\n                return new NV21(bitmap);\n            case YuvImageFormat.YUY2:\n                return new YUY2(bitmap);\n            case YuvImageFormat.YUV440P:\n                return new YUV440P(bitmap);\n            default:\n                throw new IllegalArgumentException(\"Only support YV12, NV21, YUY2 and YUV440P\");\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/ImageFormats/YuvImageFormat.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.Modes.ImageFormats;\n\nimport android.graphics.ImageFormat;\n\npublic class YuvImageFormat extends ImageFormat {\n    public static final int YUV440P = 0x50303434;\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/Martin.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.Modes;\n\nimport android.graphics.Bitmap;\nimport android.graphics.Color;\n\nimport om.sstvencoder.Output.IOutput;\n\nabstract class Martin extends Mode {\n    private final int mSyncPulseSamples;\n    private final double mSyncPulseFrequency;\n\n    private final int mSyncPorchSamples;\n    private final double mSyncPorchFrequency;\n\n    private final int mSeparatorSamples;\n    private final double mSeparatorFrequency;\n\n    protected double mColorScanDurationMs;\n    protected int mColorScanSamples;\n\n    Martin(Bitmap bitmap, IOutput output) {\n        super(bitmap, output);\n\n        mSyncPulseSamples = convertMsToSamples(4.862);\n        mSyncPulseFrequency = 1200.0;\n\n        mSyncPorchSamples = convertMsToSamples(0.572);\n        mSyncPorchFrequency = 1500.0;\n\n        mSeparatorSamples = convertMsToSamples(0.572);\n        mSeparatorFrequency = 1500.0;\n    }\n\n    protected int getTransmissionSamples() {\n        int lineSamples = mSyncPulseSamples + mSyncPorchSamples\n                + 3 * (mSeparatorSamples + mColorScanSamples);\n        return mBitmap.getHeight() * lineSamples;\n    }\n\n    protected void writeEncodedLine() {\n        addSyncPulse();\n        addSyncPorch();\n        addGreenScan(mLine);\n        addSeparator();\n        addBlueScan(mLine);\n        addSeparator();\n        addRedScan(mLine);\n        addSeparator();\n    }\n\n    private void addSyncPulse() {\n        for (int i = 0; i < mSyncPulseSamples; ++i)\n            setTone(mSyncPulseFrequency);\n    }\n\n    private void addSyncPorch() {\n        for (int i = 0; i < mSyncPorchSamples; ++i)\n            setTone(mSyncPorchFrequency);\n    }\n\n    private void addSeparator() {\n        for (int i = 0; i < mSeparatorSamples; ++i)\n            setTone(mSeparatorFrequency);\n    }\n\n    private void addGreenScan(int y) {\n        for (int i = 0; i < mColorScanSamples; ++i)\n            setColorTone(Color.green(getColor(i, y)));\n    }\n\n    private void addBlueScan(int y) {\n        for (int i = 0; i < mColorScanSamples; ++i)\n            setColorTone(Color.blue(getColor(i, y)));\n    }\n\n    private void addRedScan(int y) {\n        for (int i = 0; i < mColorScanSamples; ++i)\n            setColorTone(Color.red(getColor(i, y)));\n    }\n\n    private int getColor(int colorScanSample, int y) {\n        int x = colorScanSample * mBitmap.getWidth() / mColorScanSamples;\n        return mBitmap.getPixel(x, y);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/Martin1.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.Modes;\n\nimport android.graphics.Bitmap;\n\nimport om.sstvencoder.ModeInterfaces.ModeSize;\nimport om.sstvencoder.Output.IOutput;\n\n@ModeSize(width = 320, height = 256)\n@ModeDescription(name = Martin1.Name)\nclass Martin1 extends Martin {\n    public static final String Name = \"Martin 1\";\n\n    Martin1(Bitmap bitmap, IOutput output) {\n        super(bitmap, output);\n        mVISCode = 44;\n        mColorScanDurationMs = 146.432;\n        mColorScanSamples = convertMsToSamples(mColorScanDurationMs);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/Martin2.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.Modes;\n\nimport android.graphics.Bitmap;\n\nimport om.sstvencoder.ModeInterfaces.ModeSize;\nimport om.sstvencoder.Output.IOutput;\n\n@ModeSize(width = 320, height = 256)\n@ModeDescription(name = Martin2.Name)\nclass Martin2 extends Martin {\n    public static final String Name = \"Martin 2\";\n\n    Martin2(Bitmap bitmap, IOutput output) {\n        super(bitmap, output);\n        mVISCode = 40;\n        mColorScanDurationMs = 73.216;\n        mColorScanSamples = convertMsToSamples(mColorScanDurationMs);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/Mode.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.Modes;\n\nimport android.graphics.Bitmap;\n\nimport om.sstvencoder.ModeInterfaces.IMode;\nimport om.sstvencoder.Output.IOutput;\n\nabstract class Mode implements IMode {\n    protected Bitmap mBitmap;\n    protected int mVISCode;\n    protected int mLine;\n    private IOutput mOutput;\n    private double mSampleRate;\n    private double mRunningIntegral;\n\n    protected Mode(Bitmap bitmap, IOutput output) {\n        mOutput = output;\n        mSampleRate = mOutput.getSampleRate();\n        mBitmap = bitmap;\n    }\n\n    @Override\n    public void init() {\n        mRunningIntegral = 0.0;\n        mLine = 0;\n        mOutput.init(getTotalSamples());\n        writeCalibrationHeader();\n    }\n\n    @Override\n    public int getProcessCount() {\n        return mBitmap.getHeight();\n    }\n\n    @Override\n    public boolean process() {\n        if (mLine >= mBitmap.getHeight())\n            return false;\n\n        writeEncodedLine();\n        ++mLine;\n        return true;\n    }\n\n    // Note that also Bitmap will be recycled here\n    @Override\n    public void finish(boolean cancel) {\n        mOutput.finish(cancel);\n        destroyBitmap();\n    }\n\n    private int getTotalSamples() {\n        return getHeaderSamples() + getTransmissionSamples();\n    }\n\n    private int getHeaderSamples() {\n        return 2 * convertMsToSamples(300.0)\n                + convertMsToSamples(10.0)\n                + 10 * convertMsToSamples(30.0);\n    }\n\n    protected abstract int getTransmissionSamples();\n\n    private void writeCalibrationHeader() {\n        int leaderToneSamples = convertMsToSamples(300.0);\n        double leaderToneFrequency = 1900.0;\n\n        int breakSamples = convertMsToSamples(10.0);\n        double breakFrequency = 1200.0;\n\n        int visBitSamples = convertMsToSamples(30.0);\n        double visBitSSFrequency = 1200.0;\n        double[] visBitFrequency = new double[]{1300.0, 1100.0};\n\n        for (int i = 0; i < leaderToneSamples; ++i)\n            setTone(leaderToneFrequency);\n\n        for (int i = 0; i < breakSamples; ++i)\n            setTone(breakFrequency);\n\n        for (int i = 0; i < leaderToneSamples; ++i)\n            setTone(leaderToneFrequency);\n\n        for (int i = 0; i < visBitSamples; ++i)\n            setTone(visBitSSFrequency);\n\n        int parity = 0;\n        for (int pos = 0; pos < 7; ++pos) {\n            int bit = (mVISCode >> pos) & 1;\n            parity ^= bit;\n            for (int i = 0; i < visBitSamples; ++i)\n                setTone(visBitFrequency[bit]);\n        }\n\n        for (int i = 0; i < visBitSamples; ++i)\n            setTone(visBitFrequency[parity]);\n\n        for (int i = 0; i < visBitSamples; ++i)\n            setTone(visBitSSFrequency);\n    }\n\n    protected abstract void writeEncodedLine();\n\n    protected int convertMsToSamples(double durationMs) {\n        return (int) Math.round(durationMs * mSampleRate / 1000.0);\n    }\n\n    protected void setTone(double frequency) {\n        mRunningIntegral += 2.0 * frequency * Math.PI / mSampleRate;\n        mRunningIntegral %= 2.0 * Math.PI;\n        mOutput.write(Math.sin(mRunningIntegral));\n    }\n\n    protected void setColorTone(int color) {\n        double blackFrequency = 1500.0;\n        double whiteFrequency = 2300.0;\n        setTone(color * (whiteFrequency - blackFrequency) / 255.0 + blackFrequency);\n    }\n\n    private void destroyBitmap() {\n        if (mBitmap != null && !mBitmap.isRecycled()) {\n            mBitmap.recycle();\n            mBitmap = null;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/ModeDescription.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.Modes;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n@Target({ElementType.TYPE})\n@Retention(RetentionPolicy.RUNTIME)\n@interface ModeDescription {\n    String name();\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/ModeFactory.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.Modes;\n\nimport android.graphics.Bitmap;\n\nimport java.lang.reflect.Constructor;\n\nimport om.sstvencoder.ModeInterfaces.IMode;\nimport om.sstvencoder.ModeInterfaces.IModeInfo;\nimport om.sstvencoder.ModeInterfaces.ModeSize;\nimport om.sstvencoder.Output.IOutput;\n\npublic final class ModeFactory {\n    public static Class<?> getDefaultMode() {\n        return Robot36.class;\n    }\n\n    public static String getDefaultModeClassName() {\n        return (new ModeInfo(getDefaultMode())).getModeClassName();\n    }\n\n    public static IModeInfo[] getModeInfoList() {\n        return new IModeInfo[]{\n                new ModeInfo(Martin1.class), new ModeInfo(Martin2.class),\n                new ModeInfo(PD50.class), new ModeInfo(PD90.class), new ModeInfo(PD120.class),\n                new ModeInfo(PD160.class), new ModeInfo(PD180.class),\n                new ModeInfo(PD240.class), new ModeInfo(PD290.class),\n                new ModeInfo(Scottie1.class), new ModeInfo(Scottie2.class), new ModeInfo(ScottieDX.class),\n                new ModeInfo(Robot36.class), new ModeInfo(Robot72.class),\n                new ModeInfo(Wraase.class)\n        };\n    }\n\n    public static IModeInfo getModeInfo(Class<?> modeClass) {\n        if (!isModeClassValid(modeClass))\n            return null;\n\n        return new ModeInfo(modeClass);\n    }\n\n    public static IMode CreateMode(Class<?> modeClass, Bitmap bitmap, IOutput output) {\n        Mode mode = null;\n\n        if (bitmap != null && output != null && isModeClassValid(modeClass)) {\n            ModeSize size = modeClass.getAnnotation(ModeSize.class);\n\n            if (bitmap.getWidth() == size.width() && bitmap.getHeight() == size.height()) {\n                try {\n                    Constructor constructor = modeClass.getDeclaredConstructor(Bitmap.class, IOutput.class);\n                    mode = (Mode) constructor.newInstance(bitmap, output);\n                } catch (Exception ignore) {\n                }\n            }\n        }\n\n        return mode;\n    }\n\n    private static boolean isModeClassValid(Class<?> modeClass) {\n        return Mode.class.isAssignableFrom(modeClass) &&\n                modeClass.isAnnotationPresent(ModeSize.class) &&\n                modeClass.isAnnotationPresent(ModeDescription.class);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/ModeInfo.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.Modes;\n\nimport om.sstvencoder.ModeInterfaces.IModeInfo;\nimport om.sstvencoder.ModeInterfaces.ModeSize;\n\nclass ModeInfo implements IModeInfo {\n    private final Class<?> mModeClass;\n\n    ModeInfo(Class<?> modeClass) {\n        mModeClass = modeClass;\n    }\n\n    public String getModeName() {\n        return mModeClass.getAnnotation(ModeDescription.class).name();\n    }\n\n    public String getModeClassName() {\n        return mModeClass.getName();\n    }\n\n    public ModeSize getModeSize() {\n        return mModeClass.getAnnotation(ModeSize.class);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/PD.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.Modes;\n\nimport android.graphics.Bitmap;\n\nimport om.sstvencoder.Modes.ImageFormats.Yuv;\nimport om.sstvencoder.Modes.ImageFormats.YuvFactory;\nimport om.sstvencoder.Modes.ImageFormats.YuvImageFormat;\nimport om.sstvencoder.Output.IOutput;\n\nabstract class PD extends Mode {\n    private final Yuv mYuv;\n\n    private final int mSyncPulseSamples;\n    private final double mSyncPulseFrequency;\n\n    private final int mPorchSamples;\n    private final double mPorchFrequency;\n\n    protected double mColorScanDurationMs;\n    protected int mColorScanSamples;\n\n    PD(Bitmap bitmap, IOutput output) {\n        super(bitmap, output);\n\n        mYuv = YuvFactory.createYuv(mBitmap, YuvImageFormat.YUV440P);\n\n        mSyncPulseSamples = convertMsToSamples(20.0);\n        mSyncPulseFrequency = 1200.0;\n\n        mPorchSamples = convertMsToSamples(2.08);\n        mPorchFrequency = 1500.0;\n    }\n\n    protected int getTransmissionSamples() {\n        int lineSamples = mSyncPulseSamples + mPorchSamples + 4 * mColorScanSamples;\n        return mBitmap.getHeight() / 2 * lineSamples;\n    }\n\n    @Override\n    public int getProcessCount() {\n        return mBitmap.getHeight() / 2;\n    }\n\n    protected void writeEncodedLine() {\n        addSyncPulse();\n        addPorch();\n        addYScan(mLine);\n        addVScan(mLine);\n        addUScan(mLine);\n        addYScan(++mLine);\n    }\n\n    private void addSyncPulse() {\n        for (int i = 0; i < mSyncPulseSamples; ++i)\n            setTone(mSyncPulseFrequency);\n    }\n\n    private void addPorch() {\n        for (int i = 0; i < mPorchSamples; ++i)\n            setTone(mPorchFrequency);\n    }\n\n    private void addYScan(int y) {\n        for (int i = 0; i < mColorScanSamples; ++i)\n            setColorTone(mYuv.getY((i * mYuv.getWidth()) / mColorScanSamples, y));\n    }\n\n    private void addUScan(int y) {\n        for (int i = 0; i < mColorScanSamples; ++i)\n            setColorTone(mYuv.getU((i * mYuv.getWidth()) / mColorScanSamples, y));\n    }\n\n    private void addVScan(int y) {\n        for (int i = 0; i < mColorScanSamples; ++i)\n            setColorTone(mYuv.getV((i * mYuv.getWidth()) / mColorScanSamples, y));\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/PD120.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.Modes;\n\nimport android.graphics.Bitmap;\n\nimport om.sstvencoder.ModeInterfaces.ModeSize;\nimport om.sstvencoder.Output.IOutput;\n\n@ModeSize(width = 640, height = 496)\n@ModeDescription(name = PD120.Name)\nclass PD120 extends PD {\n    public static final String Name = \"PD 120\";\n\n    PD120(Bitmap bitmap, IOutput output) {\n        super(bitmap, output);\n        mVISCode = 95;\n        mColorScanDurationMs = 121.6;\n        mColorScanSamples = convertMsToSamples(mColorScanDurationMs);\n    }\n}\n\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/PD160.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.Modes;\n\nimport android.graphics.Bitmap;\n\nimport om.sstvencoder.ModeInterfaces.ModeSize;\nimport om.sstvencoder.Output.IOutput;\n\n@ModeSize(width = 512, height = 400)\n@ModeDescription(name = PD160.Name)\nclass PD160 extends PD {\n    public static final String Name = \"PD 160\";\n\n    PD160(Bitmap bitmap, IOutput output) {\n        super(bitmap, output);\n        mVISCode = 98;\n        mColorScanDurationMs = 195.584;\n        mColorScanSamples = convertMsToSamples(mColorScanDurationMs);\n    }\n}\n\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/PD180.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.Modes;\n\nimport android.graphics.Bitmap;\n\nimport om.sstvencoder.ModeInterfaces.ModeSize;\nimport om.sstvencoder.Output.IOutput;\n\n@ModeSize(width = 640, height = 496)\n@ModeDescription(name = PD180.Name)\nclass PD180 extends PD {\n    public static final String Name = \"PD 180\";\n\n    PD180(Bitmap bitmap, IOutput output) {\n        super(bitmap, output);\n        mVISCode = 96;\n        mColorScanDurationMs = 183.04;\n        mColorScanSamples = convertMsToSamples(mColorScanDurationMs);\n    }\n}\n\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/PD240.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.Modes;\n\nimport android.graphics.Bitmap;\n\nimport om.sstvencoder.ModeInterfaces.ModeSize;\nimport om.sstvencoder.Output.IOutput;\n\n@ModeSize(width = 640, height = 496)\n@ModeDescription(name = PD240.Name)\nclass PD240 extends PD {\n    public static final String Name = \"PD 240\";\n\n    PD240(Bitmap bitmap, IOutput output) {\n        super(bitmap, output);\n        mVISCode = 97;\n        mColorScanDurationMs = 244.48;\n        mColorScanSamples = convertMsToSamples(mColorScanDurationMs);\n    }\n}\n\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/PD290.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.Modes;\n\nimport android.graphics.Bitmap;\n\nimport om.sstvencoder.ModeInterfaces.ModeSize;\nimport om.sstvencoder.Output.IOutput;\n\n@ModeSize(width = 800, height = 616)\n@ModeDescription(name = PD290.Name)\nclass PD290 extends PD {\n    public static final String Name = \"PD 290\";\n\n    PD290(Bitmap bitmap, IOutput output) {\n        super(bitmap, output);\n        mVISCode = 94;\n        mColorScanDurationMs = 228.8;\n        mColorScanSamples = convertMsToSamples(mColorScanDurationMs);\n    }\n}\n\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/PD50.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.Modes;\n\nimport android.graphics.Bitmap;\n\nimport om.sstvencoder.ModeInterfaces.ModeSize;\nimport om.sstvencoder.Output.IOutput;\n\n@ModeSize(width = 320, height = 256)\n@ModeDescription(name = PD50.Name)\nclass PD50 extends PD {\n    public static final String Name = \"PD 50\";\n\n    PD50(Bitmap bitmap, IOutput output) {\n        super(bitmap, output);\n        mVISCode = 93;\n        mColorScanDurationMs = 91.52;\n        mColorScanSamples = convertMsToSamples(mColorScanDurationMs);\n    }\n}\n\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/PD90.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.Modes;\n\nimport android.graphics.Bitmap;\n\nimport om.sstvencoder.ModeInterfaces.ModeSize;\nimport om.sstvencoder.Output.IOutput;\n\n@ModeSize(width = 320, height = 256)\n@ModeDescription(name = PD90.Name)\nclass PD90 extends PD {\n    public static final String Name = \"PD 90\";\n\n    PD90(Bitmap bitmap, IOutput output) {\n        super(bitmap, output);\n        mVISCode = 99;\n        mColorScanDurationMs = 170.24;\n        mColorScanSamples = convertMsToSamples(mColorScanDurationMs);\n    }\n}\n\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/Robot36.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.Modes;\n\nimport android.graphics.Bitmap;\n\nimport om.sstvencoder.Modes.ImageFormats.Yuv;\nimport om.sstvencoder.Modes.ImageFormats.YuvFactory;\nimport om.sstvencoder.Modes.ImageFormats.YuvImageFormat;\nimport om.sstvencoder.ModeInterfaces.ModeSize;\nimport om.sstvencoder.Output.IOutput;\n\n@ModeSize(width = 320, height = 240)\n@ModeDescription(name = Robot36.Name)\nclass Robot36 extends Mode {\n    public static final String Name = \"Robot 36\";\n\n    private final Yuv mYuv;\n\n    private final int mLumaScanSamples;\n    private final int mChrominanceScanSamples;\n\n    private final int mSyncPulseSamples;\n    private final double mSyncPulseFrequency;\n\n    private final int mSyncPorchSamples;\n    private final double mSyncPorchFrequency;\n\n    private final int mPorchSamples;\n    private final double mPorchFrequency;\n\n    private final int mSeparatorSamples;\n    private final double mEvenSeparatorFrequency;\n    private final double mOddSeparatorFrequency;\n\n    Robot36(Bitmap bitmap, IOutput output) {\n        super(bitmap, output);\n\n        mYuv = YuvFactory.createYuv(mBitmap, YuvImageFormat.NV21);\n        mVISCode = 8;\n\n        mLumaScanSamples = convertMsToSamples(88.0);\n        mChrominanceScanSamples = convertMsToSamples(44.0);\n\n        mSyncPulseSamples = convertMsToSamples(9.0);\n        mSyncPulseFrequency = 1200.0;\n\n        mSyncPorchSamples = convertMsToSamples(3.0);\n        mSyncPorchFrequency = 1500.0;\n\n        mPorchSamples = convertMsToSamples(1.5);\n        mPorchFrequency = 1900.0;\n\n        mSeparatorSamples = convertMsToSamples(4.5);\n        mEvenSeparatorFrequency = 1500.0;\n        mOddSeparatorFrequency = 2300.0;\n    }\n\n    protected int getTransmissionSamples() {\n        int lineSamples = mSyncPulseSamples + mSyncPorchSamples\n                + mLumaScanSamples + mSeparatorSamples\n                + mPorchSamples + mChrominanceScanSamples;\n        return mBitmap.getHeight() * lineSamples;\n    }\n\n    protected void writeEncodedLine() {\n        addSyncPulse();\n        addSyncPorch();\n        addYScan(mLine);\n\n        if (mLine % 2 == 0) {\n            addSeparator(mEvenSeparatorFrequency);\n            addPorch();\n            addVScan(mLine);\n        } else {\n            addSeparator(mOddSeparatorFrequency);\n            addPorch();\n            addUScan(mLine);\n        }\n    }\n\n    private void addSyncPulse() {\n        for (int i = 0; i < mSyncPulseSamples; ++i)\n            setTone(mSyncPulseFrequency);\n    }\n\n    private void addSyncPorch() {\n        for (int i = 0; i < mSyncPorchSamples; ++i)\n            setTone(mSyncPorchFrequency);\n    }\n\n    private void addSeparator(double separatorFrequency) {\n        for (int i = 0; i < mSeparatorSamples; ++i)\n            setTone(separatorFrequency);\n    }\n\n    private void addPorch() {\n        for (int i = 0; i < mPorchSamples; ++i)\n            setTone(mPorchFrequency);\n    }\n\n    private void addYScan(int y) {\n        for (int i = 0; i < mLumaScanSamples; ++i)\n            setColorTone(mYuv.getY((i * mYuv.getWidth()) / mLumaScanSamples, y));\n    }\n\n    private void addUScan(int y) {\n        for (int i = 0; i < mChrominanceScanSamples; ++i)\n            setColorTone(mYuv.getU((i * mYuv.getWidth()) / mChrominanceScanSamples, y));\n    }\n\n    private void addVScan(int y) {\n        for (int i = 0; i < mChrominanceScanSamples; ++i)\n            setColorTone(mYuv.getV((i * mYuv.getWidth()) / mChrominanceScanSamples, y));\n    }\n}\n\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/Robot72.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.Modes;\n\nimport android.graphics.Bitmap;\n\nimport om.sstvencoder.Modes.ImageFormats.Yuv;\nimport om.sstvencoder.Modes.ImageFormats.YuvFactory;\nimport om.sstvencoder.Modes.ImageFormats.YuvImageFormat;\nimport om.sstvencoder.ModeInterfaces.ModeSize;\nimport om.sstvencoder.Output.IOutput;\n\n@ModeSize(width = 320, height = 240)\n@ModeDescription(name = Robot72.Name)\nclass Robot72 extends Mode {\n    public static final String Name = \"Robot 72\";\n\n    private final Yuv mYuv;\n\n    private final int mLumaScanSamples;\n    private final int mChrominanceScanSamples;\n\n    private final int mSyncPulseSamples;\n    private final double mSyncPulseFrequency;\n\n    private final int mSyncPorchSamples;\n    private final double mSyncPorchFrequency;\n\n    private final int mPorchSamples;\n    private final double mPorchFrequency;\n\n    private final int mSeparatorSamples;\n    private final double mFirstSeparatorFrequency;\n    private final double mSecondSeparatorFrequency;\n\n    Robot72(Bitmap bitmap, IOutput output) {\n        super(bitmap, output);\n\n        mYuv = YuvFactory.createYuv(mBitmap, YuvImageFormat.YUY2);\n        mVISCode = 12;\n\n        mLumaScanSamples = convertMsToSamples(138.0);\n        mChrominanceScanSamples = convertMsToSamples(69.0);\n\n        mSyncPulseSamples = convertMsToSamples(9.0);\n        mSyncPulseFrequency = 1200.0;\n\n        mSyncPorchSamples = convertMsToSamples(3.0);\n        mSyncPorchFrequency = 1500.0;\n\n        mPorchSamples = convertMsToSamples(1.5);\n        mPorchFrequency = 1900.0;\n\n        mSeparatorSamples = convertMsToSamples(4.5);\n        mFirstSeparatorFrequency = 1500.0;\n        mSecondSeparatorFrequency = 2300.0;\n    }\n\n    protected int getTransmissionSamples() {\n        int lineSamples = mSyncPulseSamples + mSyncPorchSamples + mLumaScanSamples\n                + 2 * (mSeparatorSamples + mPorchSamples + mChrominanceScanSamples);\n        return mBitmap.getHeight() * lineSamples;\n    }\n\n    protected void writeEncodedLine() {\n        addSyncPulse();\n        addSyncPorch();\n        addYScan(mLine);\n        addSeparator(mFirstSeparatorFrequency);\n        addPorch();\n        addVScan(mLine);\n        addSeparator(mSecondSeparatorFrequency);\n        addPorch();\n        addUScan(mLine);\n    }\n\n    private void addSyncPulse() {\n        for (int i = 0; i < mSyncPulseSamples; ++i)\n            setTone(mSyncPulseFrequency);\n    }\n\n    private void addSyncPorch() {\n        for (int i = 0; i < mSyncPorchSamples; ++i)\n            setTone(mSyncPorchFrequency);\n    }\n\n    private void addSeparator(double separatorFrequency) {\n        for (int i = 0; i < mSeparatorSamples; ++i)\n            setTone(separatorFrequency);\n    }\n\n    private void addPorch() {\n        for (int i = 0; i < mPorchSamples; ++i)\n            setTone(mPorchFrequency);\n    }\n\n    private void addYScan(int y) {\n        for (int i = 0; i < mLumaScanSamples; ++i)\n            setColorTone(mYuv.getY((i * mYuv.getWidth()) / mLumaScanSamples, y));\n    }\n\n    private void addUScan(int y) {\n        for (int i = 0; i < mChrominanceScanSamples; ++i)\n            setColorTone(mYuv.getU((i * mYuv.getWidth()) / mChrominanceScanSamples, y));\n    }\n\n    private void addVScan(int y) {\n        for (int i = 0; i < mChrominanceScanSamples; ++i)\n            setColorTone(mYuv.getV((i * mYuv.getWidth()) / mChrominanceScanSamples, y));\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/Scottie.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.Modes;\n\nimport android.graphics.Bitmap;\nimport android.graphics.Color;\n\nimport om.sstvencoder.Output.IOutput;\n\nabstract class Scottie extends Mode {\n    private final int mSyncPulseSamples;\n    private final double mSyncPulseFrequency;\n\n    private final int mSyncPorchSamples;\n    private final double mSyncPorchFrequency;\n\n    private final int mSeparatorSamples;\n    private final double mSeparatorFrequency;\n\n    protected double mColorScanDurationMs;\n    protected int mColorScanSamples;\n\n    Scottie(Bitmap bitmap, IOutput output) {\n        super(bitmap, output);\n\n        mSyncPulseSamples = convertMsToSamples(9.0);\n        mSyncPulseFrequency = 1200.0;\n\n        mSyncPorchSamples = convertMsToSamples(1.5);\n        mSyncPorchFrequency = 1500.0;\n\n        mSeparatorSamples = convertMsToSamples(1.5);\n        mSeparatorFrequency = 1500.0;\n    }\n\n    protected int getTransmissionSamples() {\n        int lineSamples = 2 * mSeparatorSamples + 3 * mColorScanSamples +\n                mSyncPulseSamples + mSyncPorchSamples;\n        return mSyncPulseSamples + mBitmap.getHeight() * lineSamples;\n    }\n\n    protected void writeEncodedLine() {\n        if (mLine == 0)\n            addSyncPulse();\n\n        addSeparator();\n        addGreenScan(mLine);\n        addSeparator();\n        addBlueScan(mLine);\n        addSyncPulse();\n        addSyncPorch();\n        addRedScan(mLine);\n    }\n\n    private void addSyncPulse() {\n        for (int i = 0; i < mSyncPulseSamples; ++i)\n            setTone(mSyncPulseFrequency);\n    }\n\n    private void addSyncPorch() {\n        for (int i = 0; i < mSyncPorchSamples; ++i)\n            setTone(mSyncPorchFrequency);\n    }\n\n    private void addSeparator() {\n        for (int i = 0; i < mSeparatorSamples; ++i)\n            setTone(mSeparatorFrequency);\n    }\n\n    private void addGreenScan(int y) {\n        for (int i = 0; i < mColorScanSamples; ++i)\n            setColorTone(Color.green(getColor(i, y)));\n    }\n\n    private void addBlueScan(int y) {\n        for (int i = 0; i < mColorScanSamples; ++i)\n            setColorTone(Color.blue(getColor(i, y)));\n    }\n\n    private void addRedScan(int y) {\n        for (int i = 0; i < mColorScanSamples; ++i)\n            setColorTone(Color.red(getColor(i, y)));\n    }\n\n    private int getColor(int colorScanSample, int y) {\n        int x = colorScanSample * mBitmap.getWidth() / mColorScanSamples;\n        return mBitmap.getPixel(x, y);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/Scottie1.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.Modes;\n\nimport android.graphics.Bitmap;\n\nimport om.sstvencoder.ModeInterfaces.ModeSize;\nimport om.sstvencoder.Output.IOutput;\n\n@ModeSize(width = 320, height = 256)\n@ModeDescription(name = Scottie1.Name)\nclass Scottie1 extends Scottie {\n    public static final String Name = \"Scottie 1\";\n\n    Scottie1(Bitmap bitmap, IOutput output) {\n        super(bitmap, output);\n        mVISCode = 60;\n        mColorScanDurationMs = 138.24;\n        mColorScanSamples = convertMsToSamples(mColorScanDurationMs);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/Scottie2.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.Modes;\n\nimport android.graphics.Bitmap;\n\nimport om.sstvencoder.ModeInterfaces.ModeSize;\nimport om.sstvencoder.Output.IOutput;\n\n@ModeSize(width = 320, height = 256)\n@ModeDescription(name = Scottie2.Name)\nclass Scottie2 extends Scottie {\n    public static final String Name = \"Scottie 2\";\n\n    Scottie2(Bitmap bitmap, IOutput output){\n        super(bitmap, output);\n        mVISCode = 56;\n        mColorScanDurationMs = 88.064;\n        mColorScanSamples = convertMsToSamples(mColorScanDurationMs);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/ScottieDX.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.Modes;\n\nimport android.graphics.Bitmap;\n\nimport om.sstvencoder.ModeInterfaces.ModeSize;\nimport om.sstvencoder.Output.IOutput;\n\n@ModeSize(width = 320, height = 256)\n@ModeDescription(name = ScottieDX.Name)\nclass ScottieDX extends Scottie {\n    public static final String Name = \"Scottie DX\";\n\n    ScottieDX(Bitmap bitmap, IOutput output) {\n        super(bitmap, output);\n        mVISCode = 76;\n        mColorScanDurationMs = 345.6;\n        mColorScanSamples = convertMsToSamples(mColorScanDurationMs);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/Wraase.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.Modes;\n\nimport android.graphics.Bitmap;\nimport android.graphics.Color;\n\nimport om.sstvencoder.ModeInterfaces.ModeSize;\nimport om.sstvencoder.Output.IOutput;\n\n@ModeSize(width = 320, height = 256)\n@ModeDescription(name = Wraase.Name)\nclass Wraase extends Mode {\n    public static final String Name = \"Wraase SC2 180\";\n\n    private final int mSyncPulseSamples;\n    private final double mSyncPulseFrequency;\n\n    private final int mPorchSamples;\n    private final double mPorchFrequency;\n\n    private final int mColorScanSamples;\n\n    Wraase(Bitmap bitmap, IOutput output) {\n        super(bitmap, output);\n\n        mVISCode = 55;\n        mColorScanSamples = convertMsToSamples(235.0);\n\n        mSyncPulseSamples = convertMsToSamples(5.5225);\n        mSyncPulseFrequency = 1200.0;\n\n        mPorchSamples = convertMsToSamples(0.5);\n        mPorchFrequency = 1500.0;\n    }\n\n    protected int getTransmissionSamples() {\n        int lineSamples = mSyncPulseSamples + mPorchSamples + 3 * mColorScanSamples;\n        return mBitmap.getHeight() * lineSamples;\n    }\n\n    protected void writeEncodedLine() {\n        addSyncPulse();\n        addPorch();\n        addRedScan(mLine);\n        addGreenScan(mLine);\n        addBlueScan(mLine);\n    }\n\n    private void addSyncPulse() {\n        for (int i = 0; i < mSyncPulseSamples; ++i)\n            setTone(mSyncPulseFrequency);\n    }\n\n    private void addPorch() {\n        for (int i = 0; i < mPorchSamples; ++i)\n            setTone(mPorchFrequency);\n    }\n\n    private void addRedScan(int y) {\n        for (int i = 0; i < mColorScanSamples; ++i)\n            setColorTone(Color.red(getColor(i, y)));\n    }\n\n    private void addGreenScan(int y) {\n        for (int i = 0; i < mColorScanSamples; ++i)\n            setColorTone(Color.green(getColor(i, y)));\n    }\n\n    private void addBlueScan(int y) {\n        for (int i = 0; i < mColorScanSamples; ++i)\n            setColorTone(Color.blue(getColor(i, y)));\n    }\n\n    private int getColor(int colorScanSample, int y) {\n        int x = colorScanSample * mBitmap.getWidth() / mColorScanSamples;\n        return mBitmap.getPixel(x, y);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Output/AudioOutput.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.Output;\n\nimport android.media.AudioFormat;\nimport android.media.AudioManager;\nimport android.media.AudioTrack;\n\nclass AudioOutput implements IOutput {\n    private final double mSampleRate;\n    private short[] mAudioBuffer;\n    private AudioTrack mAudioTrack;\n    private int mBufferPos;\n\n    AudioOutput(double sampleRate) {\n        mSampleRate = sampleRate;\n        mBufferPos = 0;\n    }\n\n    @Override\n    public void init(int samples) {\n        mAudioBuffer = new short[(5 * (int) mSampleRate) / 2]; // 2.5 seconds of buffer\n        mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,\n                (int) mSampleRate, AudioFormat.CHANNEL_OUT_MONO,\n                AudioFormat.ENCODING_PCM_16BIT, mAudioBuffer.length * 2,\n                AudioTrack.MODE_STREAM);\n        mAudioTrack.play();\n    }\n\n    @Override\n    public double getSampleRate() {\n        return mSampleRate;\n    }\n\n    @Override\n    public void write(double value) {\n        if (mBufferPos == mAudioBuffer.length) {\n            mAudioTrack.write(mAudioBuffer, 0, mAudioBuffer.length);\n            mBufferPos = 0;\n        }\n\n        mAudioBuffer[mBufferPos++] = (short) (value * Short.MAX_VALUE);\n    }\n\n    @Override\n    public void finish(boolean cancel) {\n        if (mAudioTrack != null) {\n            if (!cancel)\n                drainBuffer();\n            mAudioTrack.stop();\n            mAudioTrack.release();\n            mAudioTrack = null;\n            mAudioBuffer = null;\n        }\n    }\n\n    private void drainBuffer() {\n        // The second run makes sure that the previous buffer indeed got played\n        for (int i = 0; i < 2; ++i) {\n            while (mBufferPos < mAudioBuffer.length)\n                mAudioBuffer[mBufferPos++] = 0;\n            mAudioTrack.write(mAudioBuffer, 0, mAudioBuffer.length);\n            mBufferPos = 0;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Output/IOutput.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.Output;\n\npublic interface IOutput {\n    double getSampleRate();\n\n    void init(int samples);\n\n    void write(double value);\n\n    void finish(boolean cancel);\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Output/OutputFactory.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.Output;\n\npublic final class OutputFactory {\n\n    public static IOutput createOutputForSending() {\n        double sampleRate = 44100.0;\n        return new AudioOutput(sampleRate);\n    }\n\n    public static IOutput createOutputForSavingAsWave(WaveFileOutputContext context) {\n        double sampleRate = 44100.0;\n        return new WaveFileOutput(context, sampleRate);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Output/WaveFileOutput.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.Output;\n\nimport java.io.BufferedOutputStream;\n\nclass WaveFileOutput implements IOutput {\n    private final double mSampleRate;\n    private WaveFileOutputContext mContext;\n    private BufferedOutputStream mOutputStream;\n    private int mSamples, mWrittenSamples;\n\n    WaveFileOutput(WaveFileOutputContext context, double sampleRate) {\n        mContext = context;\n        mSampleRate = sampleRate;\n    }\n\n    public void init(int samples) {\n        int offset = (int) ((0.01 * mSampleRate) / 2.0);\n        mSamples = samples + 2 * offset;\n        mWrittenSamples = 0;\n        InitOutputStream();\n        writeHeader();\n        padWithZeros(offset);\n    }\n\n    private void writeHeader() {\n        try {\n            int numChannels = 1; // mono\n            int bitsPerSample = Short.SIZE;\n            int blockAlign = numChannels * bitsPerSample / Byte.SIZE;\n            int subchunk2Size = mSamples * blockAlign;\n\n            mOutputStream.write(\"RIFF\".getBytes()); // ChunkID\n            mOutputStream.write(toLittleEndian(36 + subchunk2Size)); // ChunkSize\n            mOutputStream.write(\"WAVE\".getBytes()); // Format\n\n            mOutputStream.write(\"fmt \".getBytes()); // Subchunk1ID\n            mOutputStream.write(toLittleEndian(16)); // Subchunk1Size\n            mOutputStream.write(toLittleEndian((short) 1)); // AudioFormat\n            mOutputStream.write(toLittleEndian((short) numChannels)); // NumChannels\n            mOutputStream.write(toLittleEndian((int) mSampleRate)); // SampleRate\n            mOutputStream.write(toLittleEndian((int) mSampleRate * blockAlign)); // ByteRate\n            mOutputStream.write(toLittleEndian((short) blockAlign)); // BlockAlign\n            mOutputStream.write(toLittleEndian((short) bitsPerSample)); // BitsPerSample\n\n            mOutputStream.write(\"data\".getBytes()); // Subchunk2ID\n            mOutputStream.write(toLittleEndian(subchunk2Size)); // Subchunk2Size\n        } catch (Exception ignore) {\n        }\n    }\n\n    private void InitOutputStream() {\n        try {\n            mOutputStream = new BufferedOutputStream(mContext.getOutputStream());\n        } catch (Exception ignore) {\n        }\n    }\n\n    @Override\n    public double getSampleRate() {\n        return mSampleRate;\n    }\n\n    @Override\n    public void write(double value) {\n        short tmp = (short) (value * Short.MAX_VALUE);\n        ++mWrittenSamples;\n        try {\n            mOutputStream.write(toLittleEndian(tmp));\n        } catch (Exception ignore) {\n        }\n    }\n\n    @Override\n    public void finish(boolean cancel) {\n        if (!cancel)\n            padWithZeros(mSamples);\n\n        try {\n            mOutputStream.close();\n            mOutputStream = null;\n        } catch (Exception ignore) {\n        }\n\n        if (cancel)\n            mContext.deleteFile();\n    }\n\n    private void padWithZeros(int count) {\n        try {\n            while (mWrittenSamples++ < count)\n                mOutputStream.write(toLittleEndian((short) 0));\n        } catch (Exception ignore) {\n        }\n    }\n\n    private byte[] toLittleEndian(int value) {\n        byte[] buffer = new byte[4];\n        buffer[0] = (byte) (value & 255);\n        buffer[1] = (byte) ((value >> 8) & 255);\n        buffer[2] = (byte) ((value >> 16) & 255);\n        buffer[3] = (byte) ((value >> 24) & 255);\n        return buffer;\n    }\n\n    private byte[] toLittleEndian(short value) {\n        byte[] buffer = new byte[2];\n        buffer[0] = (byte) (value & 255);\n        buffer[1] = (byte) ((value >> 8) & 255);\n        return buffer;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Output/WaveFileOutputContext.java",
    "content": "/*\nCopyright 2020 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.Output;\n\nimport android.content.ContentResolver;\nimport android.content.ContentValues;\nimport android.net.Uri;\nimport android.os.Build;\nimport android.os.Environment;\nimport android.provider.MediaStore;\n\nimport java.io.File;\nimport java.io.FileOutputStream;\nimport java.io.OutputStream;\n\npublic class WaveFileOutputContext {\n    private ContentResolver mContentResolver;\n    private String mFileName;\n    private File mFile;\n    private Uri mUri;\n    private ContentValues mValues;\n\n    public WaveFileOutputContext(ContentResolver contentResolver, String fileName) {\n        mContentResolver = contentResolver;\n        mFileName = fileName;\n        mValues = getContentValues(fileName);\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)\n            mUri = mContentResolver.insert(MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), mValues);\n        else\n            mFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC), mFileName);\n    }\n    \n    private ContentValues getContentValues(String fileName) {\n        ContentValues values = new ContentValues();\n        values.put(MediaStore.Audio.Media.MIME_TYPE, \"audio/wav\");\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {\n            values.put(MediaStore.Audio.Media.DISPLAY_NAME, fileName);\n            values.put(MediaStore.Audio.Media.RELATIVE_PATH, (new File(Environment.DIRECTORY_MUSIC, \"SSTV Encoder\")).getPath());\n            values.put(MediaStore.Audio.Media.IS_PENDING, 1);\n        } else {\n            values.put(MediaStore.Audio.Media.ALBUM, \"SSTV Encoder\");\n            values.put(MediaStore.Audio.Media.TITLE, fileName);\n            values.put(MediaStore.Audio.Media.IS_MUSIC, true);\n        }\n        return values;\n    }\n\n    public String getFileName() {\n        return mFileName;\n    }\n\n    public OutputStream getOutputStream() {\n        try {\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {\n                return mContentResolver.openOutputStream(mUri);\n            } else\n                return new FileOutputStream(mFile);\n        } catch (Exception ignore) {\n        }\n        return null;\n    }\n\n    public void update() {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {\n            if (mUri != null && mValues != null) {\n                mValues.clear();\n                mValues.put(MediaStore.Audio.Media.IS_PENDING, 0);\n                mContentResolver.update(mUri, mValues, null, null);\n            }\n        } else {\n            if (mFile != null && mValues != null) {\n                mValues.put(MediaStore.Audio.Media.DATA, mFile.toString());\n                mUri = mContentResolver.insert(MediaStore.Audio.Media.getContentUriForPath(mFile.getAbsolutePath()), mValues);\n            }\n        }\n    }\n\n    public void deleteFile() {\n        try {\n            if (mFile == null)\n                mFile = new File(mUri.getPath());\n            mFile.delete();\n        } catch (Exception ignore) {\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/ProgressBarWrapper.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder;\n\nimport android.os.Handler;\nimport android.view.View;\nimport android.widget.ProgressBar;\nimport android.widget.TextView;\n\nclass ProgressBarWrapper {\n    private final ProgressBar mProgressBar;\n    private final TextView mText;\n    private final Handler mHandler;\n    private final int mSteps;\n    private int mLastStep;\n    private int mPosition, mMaxPosition;\n\n    ProgressBarWrapper(ProgressBar progressBar, TextView text) {\n        mProgressBar = progressBar;\n        mProgressBar.setVisibility(View.GONE);\n        mText = text;\n        mText.setVisibility(View.GONE);\n        mHandler = new Handler();\n        mSteps = 10;\n    }\n\n    private void startProgressBar(final int max, final String text) {\n        mHandler.post(new Runnable() {\n            @Override\n            public void run() {\n                mProgressBar.setMax(max);\n                mProgressBar.setProgress(0);\n                mProgressBar.setVisibility(View.VISIBLE);\n                if (text != null) {\n                    mText.setText(text);\n                    mText.setVisibility(View.VISIBLE);\n                }\n            }\n        });\n    }\n\n    private void stepProgressBar(final int progress) {\n        mHandler.post(new Runnable() {\n            @Override\n            public void run() {\n                mProgressBar.setProgress(progress);\n            }\n        });\n    }\n\n    private void endProgressBar() {\n        mHandler.post(new Runnable() {\n            @Override\n            public void run() {\n                mProgressBar.setVisibility(View.GONE);\n                mText.setVisibility(View.GONE);\n            }\n        });\n    }\n\n    void begin(int max, String text) {\n        mLastStep = 0;\n        mPosition = 0;\n        mMaxPosition = max;\n        startProgressBar(mSteps, text);\n    }\n\n    void step() {\n        ++mPosition;\n        int newStep = (mSteps * mPosition + mMaxPosition / 2) / mMaxPosition;\n        if (newStep != mLastStep) {\n            stepProgressBar(newStep);\n            mLastStep = newStep;\n        }\n    }\n\n    void end() {\n        endProgressBar();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Settings.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder;\n\nimport android.content.Context;\nimport android.net.Uri;\nimport android.util.JsonReader;\nimport android.util.JsonToken;\nimport android.util.JsonWriter;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.io.OutputStream;\nimport java.io.OutputStreamWriter;\n\nimport om.sstvencoder.Modes.ModeFactory;\n\nclass Settings {\n    private final static String VERSION = \"version\";\n    private final static String IMAGE_URI = \"image_uri\";\n    private final static String TEXT_OVERLAY_PATH = \"text_overlay_path\";\n    private final static String MODE_CLASS_NAME = \"mode_class_name\";\n    private final int mVersion;\n    private final String mFileName;\n    private Context mContext;\n    private String mModeClassName;\n    private String mImageUri;\n    private String mTextOverlayPath;\n\n    private Settings() {\n        mVersion = 1;\n        mFileName = \"settings.json\";\n        mModeClassName = ModeFactory.getDefaultModeClassName();\n    }\n\n    Settings(Context context) {\n        this();\n        mContext = context;\n    }\n\n    boolean load() {\n        boolean loaded = false;\n        JsonReader reader = null;\n        try {\n            InputStream in = new FileInputStream(getFile());\n            reader = new JsonReader(new InputStreamReader(in, \"UTF-8\"));\n            read(reader);\n            loaded = true;\n        } catch (Exception ignore) {\n        } finally {\n            if (reader != null) {\n                try {\n                    reader.close();\n                } catch (Exception ignore) {\n                }\n            }\n        }\n        return loaded;\n    }\n\n    boolean save() {\n        boolean saved = false;\n        JsonWriter writer = null;\n        try {\n            OutputStream out = new FileOutputStream(getFile());\n            writer = new JsonWriter(new OutputStreamWriter(out, \"UTF-8\"));\n            writer.setIndent(\" \");\n            write(writer);\n            saved = true;\n        } catch (Exception ignore) {\n        } finally {\n            if (writer != null) {\n                try {\n                    writer.close();\n                } catch (Exception ignore) {\n                }\n            }\n        }\n        return saved;\n    }\n\n    void setModeClassName(String modeClassName) {\n        mModeClassName = modeClassName;\n    }\n\n    String getModeClassName() {\n        return mModeClassName;\n    }\n\n    void setImageUri(Uri uri) {\n        mImageUri = uri == null ? null : uri.toString();\n    }\n\n    Uri getImageUri() {\n        if (mImageUri == null)\n            return null;\n        return Uri.parse(mImageUri);\n    }\n\n    File getTextOverlayFile() {\n        if (mTextOverlayPath == null)\n            mTextOverlayPath = new File(mContext.getFilesDir(), \"text_overlay.json\").getPath();\n        return new File(mTextOverlayPath);\n    }\n\n    private File getFile() {\n        return new File(mContext.getFilesDir(), mFileName);\n    }\n\n    private void write(JsonWriter writer) throws IOException {\n        writer.beginObject();\n        {\n            writeVersion(writer);\n            writeModeClassName(writer);\n            writeImageUri(writer);\n            writeTextOverlayPath(writer);\n        }\n        writer.endObject();\n    }\n\n    private void writeVersion(JsonWriter writer) throws IOException {\n        writer.name(VERSION).value(mVersion);\n    }\n\n    private void writeModeClassName(JsonWriter writer) throws IOException {\n        writer.name(MODE_CLASS_NAME).value(mModeClassName);\n    }\n\n    private void writeImageUri(JsonWriter writer) throws IOException {\n        writer.name(IMAGE_URI).value(mImageUri);\n    }\n\n    private void writeTextOverlayPath(JsonWriter writer) throws IOException {\n        writer.name(TEXT_OVERLAY_PATH).value(mTextOverlayPath);\n    }\n\n    private void read(JsonReader reader) throws IOException {\n        reader.beginObject();\n        {\n            if (readVersion(reader) == mVersion) {\n                readModeClassName(reader);\n                readImageUri(reader);\n                readTextOverlayPath(reader);\n            }\n        }\n        reader.endObject();\n    }\n\n    private int readVersion(JsonReader reader) throws IOException {\n        reader.nextName();\n        return reader.nextInt();\n    }\n\n    private void readModeClassName(JsonReader reader) throws IOException {\n        reader.nextName();\n        mModeClassName = reader.nextString();\n    }\n\n    private void readImageUri(JsonReader reader) throws IOException {\n        reader.nextName();\n        if (reader.peek() == JsonToken.NULL) {\n            reader.nextNull();\n            mImageUri = null;\n        } else\n            mImageUri = reader.nextString();\n    }\n\n    private void readTextOverlayPath(JsonReader reader) throws IOException {\n        reader.nextName();\n        mTextOverlayPath = reader.nextString();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/TextOverlay/IReader.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.TextOverlay;\n\nimport java.io.IOException;\n\npublic interface IReader {\n    void beginRootObject() throws IOException;\n\n    void beginObject() throws IOException;\n\n    void endObject() throws IOException;\n\n    void beginArray() throws IOException;\n\n    void endArray() throws IOException;\n\n    boolean hasNext() throws IOException;\n\n    String readString() throws IOException;\n\n    boolean readBoolean() throws IOException;\n\n    float readFloat() throws IOException;\n\n    int readInt() throws IOException;\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/TextOverlay/IWriter.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.TextOverlay;\n\nimport androidx.annotation.NonNull;\n\nimport java.io.IOException;\n\npublic interface IWriter {\n    void beginRootObject() throws IOException;\n\n    void beginObject(@NonNull String name) throws IOException;\n\n    void endObject() throws IOException;\n\n    void beginArray(@NonNull String name) throws IOException;\n\n    void endArray() throws IOException;\n\n    void write(@NonNull String name, String value) throws IOException;\n\n    void write(@NonNull String name, boolean value) throws IOException;\n\n    void write(@NonNull String name, float value) throws IOException;\n\n    void write(@NonNull String name, int value) throws IOException;\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/TextOverlay/Label.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.TextOverlay;\n\nimport android.graphics.Color;\n\nimport java.io.Serializable;\nimport java.lang.reflect.Field;\nimport java.lang.reflect.Modifier;\n\npublic class Label implements Serializable {\n    public static final float TEXT_SIZE_NORMAL = 2f;\n    public static final float OUTLINE_SIZE_NORMAL = 0.05f;\n    private String mText;\n    private float mTextSize, mOutlineSize;\n    private String mFamilyName;\n    private boolean mBold, mItalic, mOutline;\n    private int mForeColor, mBackColor, mOutlineColor;\n\n    public Label() {\n        mText = \"\";\n        mTextSize = TEXT_SIZE_NORMAL;\n        mFamilyName = null;\n        mBold = true;\n        mItalic = false;\n        mForeColor = Color.BLACK;\n        mBackColor = Color.TRANSPARENT;\n        mOutline = true;\n        mOutlineSize = OUTLINE_SIZE_NORMAL;\n        mOutlineColor = Color.WHITE;\n    }\n\n    public String getText() {\n        return mText;\n    }\n\n    public void setText(String text) {\n        if (text != null)\n            mText = text;\n    }\n\n    public float getTextSize() {\n        return mTextSize;\n    }\n\n    public void setTextSize(float size) {\n        if (size > 0f)\n            mTextSize = size;\n    }\n\n    public String getFamilyName() {\n        return mFamilyName;\n    }\n\n    public void setFamilyName(String familyName) {\n        mFamilyName = familyName;\n    }\n\n    public boolean getBold() {\n        return mBold;\n    }\n\n    public void setBold(boolean bold) {\n        mBold = bold;\n    }\n\n    public boolean getItalic() {\n        return mItalic;\n    }\n\n    public void setItalic(boolean italic) {\n        mItalic = italic;\n    }\n\n    public int getForeColor() {\n        return mForeColor;\n    }\n\n    public void setForeColor(int color) {\n        mForeColor = color;\n    }\n\n    public int getBackColor() {\n        return mBackColor;\n    }\n\n    public void setBackColor(int color) {\n        mBackColor = color;\n    }\n\n    public boolean getOutline() {\n        return mOutline;\n    }\n\n    public void setOutline(boolean outline) {\n        mOutline = outline;\n    }\n\n    public float getOutlineSize() {\n        return mOutlineSize;\n    }\n\n    public void setOutlineSize(float size) {\n        mOutlineSize = size;\n    }\n\n    public int getOutlineColor() {\n        return mOutlineColor;\n    }\n\n    public void setOutlineColor(int color) {\n        mOutlineColor = color;\n    }\n\n    public Label getClone() {\n        Label clone = new Label();\n        try {\n            for (Field field : getClass().getDeclaredFields()) {\n                if (!Modifier.isFinal(field.getModifiers()))\n                    field.set(clone, field.get(this));\n            }\n        } catch (Exception ignore) {\n        }\n        return clone;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/TextOverlay/LabelCollection.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.TextOverlay;\n\nimport android.graphics.Canvas;\nimport android.graphics.Rect;\nimport androidx.annotation.NonNull;\n\nimport java.io.IOException;\nimport java.util.LinkedList;\nimport java.util.List;\n\npublic class LabelCollection {\n    private class Size {\n        private float mW, mH;\n\n        Size(float w, float h) {\n            mW = w;\n            mH = h;\n        }\n\n        float width() {\n            return mW;\n        }\n\n        float height() {\n            return mH;\n        }\n    }\n\n    private final int mVersion;\n    private final List<LabelContainer> mLabels;\n    private Size mScreenSize;\n    private float mTextSizeFactor;\n    private LabelContainer mActiveLabel, mEditLabel;\n    private float mPreviousX, mPreviousY;\n\n    public LabelCollection() {\n        mVersion = 1;\n        mLabels = new LinkedList<>();\n        mPreviousX = 0f;\n        mPreviousY = 0f;\n    }\n\n    public void update(float w, float h, float textSizeFactor) {\n        if (mScreenSize != null) {\n            float x = (w - mScreenSize.width()) / 2f;\n            float y = (h - mScreenSize.height()) / 2f;\n            for (LabelContainer label : mLabels)\n                label.offset(x, y);\n        }\n        mScreenSize = new Size(w, h);\n        mTextSizeFactor = textSizeFactor;\n        for (LabelContainer label : mLabels)\n            label.update(mTextSizeFactor, w, h);\n    }\n\n    public void draw(Canvas canvas) {\n        for (LabelContainer label : mLabels)\n            label.draw(canvas);\n        if (mActiveLabel != null)\n            mActiveLabel.drawActive(canvas);\n    }\n\n    public void draw(Canvas canvas, Rect src, Rect dst) {\n        for (LabelContainer label : mLabels)\n            label.draw(canvas, src, dst);\n    }\n\n    public boolean moveLabelBegin(float x, float y) {\n        mActiveLabel = find(x, y);\n        if (mActiveLabel == null)\n            return false;\n        mLabels.remove(mActiveLabel);\n        mPreviousX = x;\n        mPreviousY = y;\n        mActiveLabel.jumpInside(mTextSizeFactor, mScreenSize.width(), mScreenSize.height());\n        return true;\n    }\n\n    public void moveLabel(float x, float y) {\n        mActiveLabel.offset(x - mPreviousX, y - mPreviousY);\n        mActiveLabel.update(mTextSizeFactor, mScreenSize.width(), mScreenSize.height());\n        mPreviousX = x;\n        mPreviousY = y;\n    }\n\n    public void moveLabelEnd() {\n        mLabels.add(mActiveLabel);\n        mActiveLabel = null;\n        mPreviousX = 0f;\n        mPreviousY = 0f;\n    }\n\n    public Label editLabelBegin(float x, float y) {\n        mEditLabel = find(x, y);\n        if (mEditLabel == null) {\n            mEditLabel = new LabelContainer(new Label());\n            mEditLabel.offset(x, y);\n        }\n        return mEditLabel.getContent();\n    }\n\n    public void editLabelEnd(Label label) {\n        if (mEditLabel != null && label != null) {\n            if (\"\".equals(label.getText().trim())) {\n                if (mLabels.contains(mEditLabel))\n                    mLabels.remove(mEditLabel);\n            } else {\n                if (!mLabels.contains(mEditLabel))\n                    mLabels.add(mEditLabel);\n                mEditLabel.setContent(label);\n                mEditLabel.update(mTextSizeFactor, mScreenSize.width(), mScreenSize.height());\n            }\n        }\n        mEditLabel = null;\n    }\n\n    private LabelContainer find(float x, float y) {\n        for (LabelContainer label : mLabels) {\n            if (label.contains(x, y))\n                return label;\n        }\n        return null;\n    }\n\n    private void add(LabelContainer label) {\n        if (mLabels.size() == 0)\n            mLabels.add(label);\n        else\n            mLabels.add(0, label);\n    }\n\n    public void write(@NonNull IWriter writer) throws IOException {\n        writer.beginRootObject();\n        {\n            writer.write(\"version\", mVersion);\n            writer.write(\"width\", mScreenSize.width());\n            writer.write(\"height\", mScreenSize.height());\n            writer.write(\"factor\", mTextSizeFactor);\n            writer.beginArray(\"labels\");\n            {\n                for (LabelContainer label : mLabels)\n                    label.write(writer);\n            }\n            writer.endArray();\n        }\n        writer.endObject();\n    }\n\n    public boolean read(@NonNull IReader reader) throws IOException {\n        reader.beginRootObject();\n        {\n            if (reader.readInt() != mVersion)\n                return false;\n\n            float w = reader.readFloat();\n            float h = reader.readFloat();\n            float textSizeFactor = reader.readFloat();\n            reader.beginArray();\n            {\n                while (reader.hasNext()) {\n                    LabelContainer label = new LabelContainer(new Label());\n                    label.read(reader);\n                    add(label);\n                }\n            }\n            reader.endArray();\n            update(w, h, textSizeFactor);\n        }\n        reader.endObject();\n        return true;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/TextOverlay/LabelContainer.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.TextOverlay;\n\nimport android.graphics.Canvas;\nimport android.graphics.Rect;\nimport androidx.annotation.NonNull;\n\nimport java.io.IOException;\n\nclass LabelContainer {\n    private Label mLabel;\n    private LabelPainter mPainter;\n    private Position mPosition; // left-bottom corner\n\n    LabelContainer(@NonNull Label label) {\n        mLabel = label;\n        mPainter = new LabelPainter(label);\n        mPosition = new Position();\n    }\n\n    boolean contains(float x, float y) {\n        return mPainter.getBounds().contains(x, y);\n    }\n\n    void draw(Canvas canvas) {\n        mPainter.draw(canvas);\n    }\n\n    void drawActive(Canvas canvas) {\n        mPainter.drawActive(canvas);\n    }\n\n    void draw(Canvas canvas, Rect src, Rect dst) {\n        mPainter.draw(canvas, src, dst);\n    }\n\n    void jumpInside(float textSizeFactor, float screenW, float screenH) {\n        mPainter.moveLabelInside(textSizeFactor, screenW, screenH, mPosition);\n    }\n\n    void offset(float x, float y) {\n        mPosition.offset(x, y);\n    }\n\n    void update(float textSizeFactor, float screenW, float screenH) {\n        mPainter.update(textSizeFactor, screenW, screenH, mPosition);\n    }\n\n    Label getContent() {\n        return mLabel;\n    }\n\n    void setContent(@NonNull Label label) {\n        mLabel = label;\n        mPainter.setLabel(label);\n    }\n\n    void write(IWriter writer) throws IOException {\n        writer.beginRootObject();\n        {\n            writer.write(\"position_x\", mPosition.getX());\n            writer.write(\"position_y\", mPosition.getY());\n            writer.beginObject(\"label\");\n            {\n                writeLabel(writer, mLabel);\n            }\n            writer.endObject();\n        }\n        writer.endObject();\n    }\n\n    void read(IReader reader) throws IOException {\n        reader.beginRootObject();\n        {\n            mPosition.set(reader.readFloat(), reader.readFloat());\n            reader.beginObject();\n            {\n                readLabel(reader, mLabel);\n            }\n            reader.endObject();\n        }\n        reader.endObject();\n    }\n\n    private void writeLabel(IWriter writer, Label label) throws IOException {\n        writer.write(\"text\", label.getText());\n        writer.write(\"text_size\", label.getTextSize());\n        writer.write(\"family_name\", label.getFamilyName());\n        writer.write(\"bold\", label.getBold());\n        writer.write(\"italic\", label.getItalic());\n        writer.write(\"fore_color\", label.getForeColor());\n        writer.write(\"back_color\", label.getBackColor());\n        writer.write(\"outline\", label.getOutline());\n        writer.write(\"outline_size\", label.getOutlineSize());\n        writer.write(\"outline_color\", label.getOutlineColor());\n    }\n\n    private void readLabel(IReader reader, Label label) throws IOException {\n        label.setText(reader.readString());\n        label.setTextSize(reader.readFloat());\n        label.setFamilyName(reader.readString());\n        label.setBold(reader.readBoolean());\n        label.setItalic(reader.readBoolean());\n        label.setForeColor(reader.readInt());\n        label.setBackColor(reader.readInt());\n        label.setOutline(reader.readBoolean());\n        label.setOutlineSize(reader.readFloat());\n        label.setOutlineColor(reader.readInt());\n    }\n}\n\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/TextOverlay/LabelPainter.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.TextOverlay;\n\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.Paint;\nimport android.graphics.Path;\nimport android.graphics.Rect;\nimport android.graphics.RectF;\nimport android.graphics.Typeface;\n\nimport androidx.annotation.NonNull;\nimport om.sstvencoder.Utility;\n\nclass LabelPainter {\n    private interface IDrawer {\n        void draw(Canvas canvas);\n\n        void drawShadow(Canvas canvas);\n\n        void draw(Canvas canvas, Rect src, Rect dst);\n\n        RectF getBounds();\n    }\n\n    private class InDrawer implements IDrawer {\n        private float mSizeFactor;\n        private float mX, mY;\n\n        private InDrawer(float sizeFactor, float x, float y) {\n            mSizeFactor = sizeFactor;\n            setPosition(x, y);\n            setPaintSettings(mSizeFactor);\n        }\n\n        @Override\n        public void draw(Canvas canvas) {\n            drawOutline(canvas, mX, mY);\n            canvas.drawText(mLabel.getText(), mX, mY, mPaint);\n        }\n\n        @Override\n        public void drawShadow(Canvas canvas) {\n            RectF bounds = new RectF(getBounds());\n            float rx = 10f;\n            float ry = 10f;\n            mPaint.setStrokeWidth(0f);\n\n            mPaint.setColor(Color.LTGRAY);\n            mPaint.setAlpha(100);\n            mPaint.setStyle(Paint.Style.FILL);\n            canvas.drawRoundRect(bounds, rx, ry, mPaint);\n\n            mPaint.setAlpha(255);\n            mPaint.setStyle(Paint.Style.STROKE);\n\n            mPaint.setColor(Color.BLUE);\n            canvas.drawRoundRect(bounds, rx, ry, mPaint);\n\n            mPaint.setColor(Color.GREEN);\n            bounds.inset(-1f, -1f);\n            canvas.drawRoundRect(bounds, rx, ry, mPaint);\n\n            mPaint.setColor(Color.RED);\n            bounds.inset(-1f, -1f);\n            canvas.drawRoundRect(bounds, rx, ry, mPaint);\n\n            setPaintSettings(mSizeFactor);\n        }\n\n        @Override\n        public void draw(Canvas canvas, Rect src, Rect dst) {\n            float factor = (dst.height() / (float) src.height());\n            float x = (mX - src.left) * factor;\n            float y = (mY - src.top) * factor;\n            setSizePaintSettings(factor * mSizeFactor);\n            drawOutline(canvas, x, y);\n            canvas.drawText(mLabel.getText(), x, y, mPaint);\n            setSizePaintSettings(mSizeFactor);\n        }\n\n        @Override\n        public RectF getBounds() {\n            RectF bounds = new RectF(getTextBounds());\n            bounds.offset(mX, mY);\n            if (mLabel.getOutline()) {\n                float inset = mLabel.getOutlineSize() * mPaint.getTextSize();\n                bounds.inset(-inset, -inset);\n            }\n            return bounds;\n        }\n\n        private void setPosition(float x, float y) {\n            mX = x;\n            mY = y;\n        }\n\n        private float getOneLetterSize() {\n            Rect bounds = new Rect();\n            mPaint.getTextBounds(\"M\", 0, 1, bounds);\n            return bounds.width();\n        }\n\n        private Rect getTextBounds() {\n            Rect bounds = new Rect();\n            String text = mLabel.getText();\n            mPaint.getTextBounds(text, 0, text.length(), bounds);\n            return bounds;\n        }\n\n        private void drawOutline(Canvas canvas, float x, float y) {\n            if (mLabel.getOutline()) {\n                setOutlinePaintSettings();\n                canvas.drawText(mLabel.getText(), x, y, mPaint);\n                setTextPaintSettings();\n            }\n        }\n\n        private void setPaintSettings(float sizeFactor) {\n            mPaint.setAlpha(255);\n            try {\n                mPaint.setTypeface(createTypeface());\n            } catch (Exception ignore) {\n            }\n            setTextPaintSettings();\n            setSizePaintSettings(sizeFactor);\n        }\n\n        private void setOutlinePaintSettings() {\n            mPaint.setStyle(Paint.Style.STROKE);\n            mPaint.setColor(mLabel.getOutlineColor());\n        }\n\n        private void setTextPaintSettings() {\n            mPaint.setStyle(Paint.Style.FILL);\n            mPaint.setColor(mLabel.getForeColor());\n        }\n\n        private void setSizePaintSettings(float sizeFactor) {\n            float textSize = mLabel.getTextSize() * sizeFactor;\n            mPaint.setTextSize(textSize);\n            mPaint.setStrokeWidth(mLabel.getOutlineSize() * textSize);\n        }\n\n        private Typeface createTypeface() {\n            int style = Typeface.NORMAL;\n\n            if (mLabel.getBold() && mLabel.getItalic())\n                style = Typeface.BOLD_ITALIC;\n            else {\n                if (mLabel.getBold())\n                    style = Typeface.BOLD;\n                else if (mLabel.getItalic())\n                    style = Typeface.ITALIC;\n            }\n\n            String fontFilePath = Utility.getFontFilePath(mLabel.getFamilyName(), style);\n            Typeface family = Typeface.createFromFile(fontFilePath);\n\n            return Typeface.create(family, style);\n        }\n    }\n\n    private class OutDrawer implements IDrawer {\n        private Path mPath;\n        private RectF mBoundsOutside;\n        private float mMinSize, mX, mY;\n\n        private OutDrawer(float min) {\n            mMinSize = min * 0.5f;\n            mPaint.setAlpha(255);\n            mPaint.setStrokeWidth(0f);\n        }\n\n        private void leftOut(RectF rect, float screenH) {\n            mX = 0f;\n            mY = Math.min(Math.max(mMinSize, rect.top + rect.height() * 0.5f), screenH - mMinSize);\n            mPath = getLeftAlignedTriangle(mX, mY, mMinSize);\n            mBoundsOutside = new RectF(mX, mY - mMinSize, mX + mMinSize, mY + mMinSize);\n        }\n\n        private void topOut(RectF rect, float screenW) {\n            mX = Math.min(Math.max(mMinSize, rect.left + rect.width() * 0.5f), screenW - mMinSize);\n            mY = 0f;\n            mPath = getTopAlignedTriangle(mX, mY, mMinSize);\n            mBoundsOutside = new RectF(mX - mMinSize, mY, mX + mMinSize * 0.5f, mY + mMinSize);\n        }\n\n        private void rightOut(RectF rect, float screenW, float screenH) {\n            mX = screenW;\n            mY = Math.min(Math.max(mMinSize, rect.top + rect.height() * 0.5f), screenH - mMinSize);\n            mPath = getRightAlignedTriangle(mX, mY, mMinSize);\n            mBoundsOutside = new RectF(mX - mMinSize, mY - mMinSize, mX, mY + mMinSize);\n        }\n\n        private void bottomOut(RectF rect, float screenW, float screenH) {\n            mX = Math.min(Math.max(mMinSize, rect.left + rect.width() * 0.5f), screenW - mMinSize);\n            mY = screenH;\n            mPath = getBottomAlignedTriangle(mX, mY, mMinSize);\n            mBoundsOutside = new RectF(mX - mMinSize, mY - mMinSize, mX + mMinSize, mY);\n        }\n\n        private Path getLeftAlignedTriangle(float x, float y, float r) {\n            Path path = new Path();\n            path.moveTo(x, y - r);\n            path.lineTo(x, y + r);\n            path.lineTo(x + r * 0.6f, y);\n            path.lineTo(x, y - r);\n            return path;\n        }\n\n        private Path getTopAlignedTriangle(float x, float y, float r) {\n            Path path = new Path();\n            path.moveTo(x - r, y);\n            path.lineTo(x, y + r * 0.6f);\n            path.lineTo(x + r, y);\n            path.lineTo(x - r, y);\n            return path;\n        }\n\n        private Path getRightAlignedTriangle(float x, float y, float r) {\n            Path path = new Path();\n            path.moveTo(x, y - r);\n            path.lineTo(x - r * 0.6f, y);\n            path.lineTo(x, y + r);\n            path.lineTo(x, y - r);\n            return path;\n        }\n\n        private Path getBottomAlignedTriangle(float x, float y, float r) {\n            Path path = new Path();\n            path.moveTo(x - r, y);\n            path.lineTo(x, y - r * 0.6f);\n            path.lineTo(x + r, y);\n            path.lineTo(x - r, y);\n            return path;\n        }\n\n        @Override\n        public void draw(Canvas canvas) {\n            mPaint.setColor(mLabel.getForeColor());\n            mPaint.setStyle(Paint.Style.FILL);\n            canvas.drawPath(mPath, mPaint);\n\n            mPaint.setColor(Color.WHITE);\n            mPaint.setStyle(Paint.Style.STROKE);\n            canvas.drawPath(mPath, mPaint);\n        }\n\n        @Override\n        public void draw(Canvas canvas, Rect src, Rect dst) {\n        }\n\n        @Override\n        public void drawShadow(Canvas canvas) {\n            float r = 2f * mMinSize;\n\n            mPaint.setColor(Color.LTGRAY);\n            mPaint.setAlpha(100);\n            mPaint.setStyle(Paint.Style.FILL);\n            canvas.drawCircle(mX, mY, r, mPaint);\n\n            mPaint.setAlpha(255);\n            mPaint.setStyle(Paint.Style.STROKE);\n\n            mPaint.setColor(Color.RED);\n            canvas.drawCircle(mX, mY, r + 1f, mPaint);\n            mPaint.setColor(Color.GREEN);\n            canvas.drawCircle(mX, mY, r, mPaint);\n            mPaint.setColor(Color.BLUE);\n            canvas.drawCircle(mX, mY, r - 1f, mPaint);\n        }\n\n        @Override\n        public RectF getBounds() {\n            return mBoundsOutside;\n        }\n    }\n\n    private final Paint mPaint;\n    private Label mLabel;\n    private IDrawer mDrawer;\n\n    LabelPainter(@NonNull Label label) {\n        mLabel = label;\n        mPaint = new Paint();\n        mPaint.setAntiAlias(true);\n    }\n\n    void draw(Canvas canvas) {\n        mDrawer.draw(canvas);\n    }\n\n    void drawActive(Canvas canvas) {\n        mDrawer.drawShadow(canvas);\n        mDrawer.draw(canvas);\n    }\n\n    void draw(Canvas canvas, Rect src, Rect dst) {\n        mDrawer.draw(canvas, src, dst);\n    }\n\n    RectF getBounds() {\n        return mDrawer.getBounds();\n    }\n\n    void setLabel(@NonNull Label label) {\n        mLabel = label;\n    }\n\n    void moveLabelInside(float sizeFactor, float screenW, float screenH, Position position) {\n        if (isLabelInside())\n            return;\n\n        float x = position.getX();\n        float y = position.getY();\n        InDrawer inDrawer = new InDrawer(sizeFactor, x, y);\n        RectF rect = inDrawer.getBounds();\n        float min = Math.min(getMinSize(sizeFactor), inDrawer.getOneLetterSize());\n\n        if (rect.right < min)  // left out\n            x = min - rect.width();\n        else if (rect.bottom < min) // top out\n            y = min;\n        else if (rect.left > (screenW - min))  // right out\n            x = screenW - min;\n        else if (rect.top > (screenH - min))  // bottom out\n            y = screenH + rect.height() - min;\n\n        inDrawer.setPosition(x, y);\n        mDrawer = inDrawer;\n        position.set(x, y);\n    }\n\n    void update(float sizeFactor, float screenW, float screenH, Position position) {\n        InDrawer inDrawer = new InDrawer(sizeFactor, position.getX(), position.getY());\n        RectF rect = inDrawer.getBounds();\n        float minSize = getMinSize(sizeFactor);\n        float min = Math.min(minSize, inDrawer.getOneLetterSize());\n\n        OutDrawer outDrawer = null;\n        if (rect.right < min) { // left out\n            outDrawer = new OutDrawer(minSize);\n            outDrawer.leftOut(rect, screenH);\n        } else if (rect.bottom < min) {// top out\n            outDrawer = new OutDrawer(minSize);\n            outDrawer.topOut(rect, screenW);\n        } else if (rect.left > (screenW - min)) { // right out\n            outDrawer = new OutDrawer(minSize);\n            outDrawer.rightOut(rect, screenW, screenH);\n        } else if (rect.top > (screenH - min)) { // bottom out\n            outDrawer = new OutDrawer(minSize);\n            outDrawer.bottomOut(rect, screenW, screenH);\n        }\n\n        mDrawer = outDrawer == null ? inDrawer : outDrawer;\n    }\n\n    private boolean isLabelInside() {\n        return mDrawer instanceof InDrawer;\n    }\n\n    private float getMinSize(float sizeFactor) {\n        return 1.5f * sizeFactor;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/TextOverlay/Position.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder.TextOverlay;\n\nclass Position {\n    private float mX;\n    private float mY;\n\n    Position() {\n        mX = 0f;\n        mY = 0f;\n    }\n\n    void set(float x, float y) {\n        mX = x;\n        mY = y;\n    }\n\n    void offset(float x, float y) {\n        mX += x;\n        mY += y;\n    }\n\n    float getX() {\n        return mX;\n    }\n\n    float getY() {\n        return mY;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/TextOverlayTemplate.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder;\n\nimport androidx.annotation.NonNull;\nimport android.util.JsonReader;\nimport android.util.JsonToken;\nimport android.util.JsonWriter;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.io.OutputStream;\nimport java.io.OutputStreamWriter;\n\nimport om.sstvencoder.TextOverlay.IReader;\nimport om.sstvencoder.TextOverlay.IWriter;\nimport om.sstvencoder.TextOverlay.LabelCollection;\n\nclass TextOverlayTemplate {\n    private class LabelCollectionWriter implements IWriter {\n        private JsonWriter mWriter;\n\n        private LabelCollectionWriter(@NonNull JsonWriter writer) {\n            mWriter = writer;\n        }\n\n        @Override\n        public void beginRootObject() throws IOException {\n            mWriter.beginObject();\n        }\n\n        @Override\n        public void beginObject(@NonNull String name) throws IOException {\n            mWriter.name(name);\n            mWriter.beginObject();\n        }\n\n        @Override\n        public void endObject() throws IOException {\n            mWriter.endObject();\n        }\n\n        @Override\n        public void beginArray(@NonNull String name) throws IOException {\n            mWriter.name(name);\n            mWriter.beginArray();\n        }\n\n        @Override\n        public void endArray() throws IOException {\n            mWriter.endArray();\n        }\n\n        @Override\n        public void write(@NonNull String name, String value) throws IOException {\n            mWriter.name(name).value(value);\n        }\n\n        @Override\n        public void write(@NonNull String name, boolean value) throws IOException {\n            mWriter.name(name).value(value);\n        }\n\n        @Override\n        public void write(@NonNull String name, float value) throws IOException {\n            mWriter.name(name).value(value);\n        }\n\n        @Override\n        public void write(@NonNull String name, int value) throws IOException {\n            mWriter.name(name).value(value);\n        }\n    }\n\n    private class LabelCollectionReader implements IReader {\n        private JsonReader mReader;\n\n        private LabelCollectionReader(@NonNull JsonReader reader) {\n            mReader = reader;\n        }\n\n        @Override\n        public void beginRootObject() throws IOException {\n            mReader.beginObject();\n        }\n\n        @Override\n        public void beginObject() throws IOException {\n            mReader.nextName();\n            mReader.beginObject();\n        }\n\n        @Override\n        public void endObject() throws IOException {\n            mReader.endObject();\n        }\n\n        @Override\n        public void beginArray() throws IOException {\n            mReader.nextName();\n            mReader.beginArray();\n        }\n\n        @Override\n        public void endArray() throws IOException {\n            mReader.endArray();\n        }\n\n        @Override\n        public boolean hasNext() throws IOException {\n            return mReader.hasNext();\n        }\n\n        @Override\n        public String readString() throws IOException {\n            mReader.nextName();\n            if (mReader.peek() == JsonToken.NULL) {\n                mReader.nextNull();\n                return null;\n            }\n            return mReader.nextString();\n        }\n\n        @Override\n        public boolean readBoolean() throws IOException {\n            mReader.nextName();\n            return mReader.nextBoolean();\n        }\n\n        @Override\n        public float readFloat() throws IOException {\n            mReader.nextName();\n            return Float.valueOf(mReader.nextString());\n        }\n\n        @Override\n        public int readInt() throws IOException {\n            mReader.nextName();\n            return mReader.nextInt();\n        }\n    }\n\n    boolean load(@NonNull LabelCollection labels, File file) {\n        boolean loaded = false;\n        JsonReader jsonReader = null;\n        try {\n            InputStream in = new FileInputStream(file);\n            jsonReader = new JsonReader(new InputStreamReader(in, \"UTF-8\"));\n            loaded = labels.read(new LabelCollectionReader(jsonReader));\n        } catch (Exception ignore) {\n        } finally {\n            if (jsonReader != null) {\n                try {\n                    jsonReader.close();\n                } catch (Exception ignore) {\n                }\n            }\n        }\n        return loaded;\n    }\n\n    boolean save(@NonNull LabelCollection labels, File file) {\n        boolean saved = false;\n        JsonWriter jsonWriter = null;\n        try {\n            OutputStream out = new FileOutputStream(file);\n            jsonWriter = new JsonWriter(new OutputStreamWriter(out, \"UTF-8\"));\n            jsonWriter.setIndent(\" \");\n            labels.write(new LabelCollectionWriter(jsonWriter));\n            saved = true;\n        } catch (Exception ignore) {\n        } finally {\n            if (jsonWriter != null) {\n                try {\n                    jsonWriter.close();\n                } catch (Exception ignore) {\n                }\n            }\n        }\n        return saved;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Utility.java",
    "content": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage om.sstvencoder;\n\nimport android.content.Context;\nimport android.content.Intent;\nimport android.graphics.Rect;\n\nimport androidx.exifinterface.media.ExifInterface;\n\nimport android.graphics.Typeface;\nimport android.net.Uri;\nimport android.os.Build;\nimport android.os.Environment;\n\nimport androidx.annotation.NonNull;\nimport androidx.core.content.FileProvider;\n\nimport java.io.File;\nimport java.text.SimpleDateFormat;\nimport java.util.ArrayList;\nimport java.util.Date;\nimport java.util.List;\nimport java.util.Locale;\n\npublic final class Utility {\n    private static final String DIRECTORY_SYSTEM_FONTS = \"/system/fonts\";\n    private static final String DEFAULT_FONT_FAMILY = \"Default\";\n\n    @NonNull\n    static Rect getEmbeddedRect(int w, int h, int iw, int ih) {\n        Rect rect;\n\n        int ow = (9 * w) / 10;\n        int oh = (9 * h) / 10;\n\n        if (iw * oh < ow * ih) {\n            int right = (iw * oh) / ih;\n            rect = new Rect(0, 0, right, oh);\n            rect.offset((w - right) / 2, (h - oh) / 2);\n        } else {\n            int bottom = (ih * ow) / iw;\n            rect = new Rect(0, 0, ow, bottom);\n            rect.offset((w - ow) / 2, (h - bottom) / 2);\n        }\n        return rect;\n    }\n\n    static float getTextSizeFactor(int w, int h) {\n        return 0.1f * (Utility.getEmbeddedRect(w, h, 320, 240).height());\n    }\n\n    static String createMessage(Exception ex) {\n        StringBuilder sb = new StringBuilder();\n        sb.append(ex.getMessage());\n        sb.append(\"\\n\");\n        for (StackTraceElement el : ex.getStackTrace()) {\n            sb.append(\"\\n\");\n            sb.append(el.toString());\n        }\n        return sb.toString();\n    }\n\n    @NonNull\n    static Intent createEmailIntent(final String subject, final String text) {\n        Intent intent = new Intent(Intent.ACTION_SEND);\n        intent.setType(\"text/email\");\n        intent.putExtra(Intent.EXTRA_EMAIL, new String[]{\"olga.rgb@gmail.com\"});\n        intent.putExtra(Intent.EXTRA_SUBJECT, subject);\n        intent.putExtra(Intent.EXTRA_TEXT, text);\n        return intent;\n    }\n\n    static int convertToDegrees(int exifOrientation) {\n        switch (exifOrientation) {\n            case ExifInterface.ORIENTATION_ROTATE_90:\n                return 90;\n            case ExifInterface.ORIENTATION_ROTATE_180:\n                return 180;\n            case ExifInterface.ORIENTATION_ROTATE_270:\n                return 270;\n        }\n        return 0;\n    }\n\n    static Uri createImageUri(Context context) {\n        if (!isExternalStorageWritable())\n            return null;\n        File dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);\n        File file = new File(dir, createFileName() + \".jpg\");\n\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)\n            // API level 24 and higher: FileUriExposedException\n            return Uri.fromFile(file); // file:// URI\n        // API level 15: Camera crash\n        return FileProvider.getUriForFile(context, \"om.sstvencoder\", file); // content:// URI\n    }\n\n    static String createWaveFileName() {\n        return createFileName() + \".wav\";\n    }\n\n    private static String createFileName() {\n        return new SimpleDateFormat(\"yyyyMMdd_HHmmss\", Locale.US).format(new Date());\n    }\n\n    static boolean isExternalStorageWritable() {\n        String state = Environment.getExternalStorageState();\n        return Environment.MEDIA_MOUNTED.equals(state);\n    }\n\n    static List<String> getSystemFontFamilyList() {\n        List<String> fontFamilyNameList = new ArrayList<>();\n        File fontsDir = new File(DIRECTORY_SYSTEM_FONTS);\n\n        if (fontsDir.exists() && fontsDir.isDirectory()) {\n            File[] files = fontsDir.listFiles();\n            if (files != null) {\n                for (File file : files) {\n                    String fileName = file.getName();\n                    if (file.isFile() && isSupportedFontFileFormat(fileName)) {\n                        String fontFamilyName = getFontFamilyName(fileName);\n                        if (!fontFamilyNameList.contains(fontFamilyName))\n                            fontFamilyNameList.add(fontFamilyName);\n                    }\n                }\n            }\n        }\n\n        fontFamilyNameList.add(0, Utility.DEFAULT_FONT_FAMILY);\n        return fontFamilyNameList;\n    }\n\n    private static boolean isSupportedFontFileFormat(String fileName) {\n        return fileName.endsWith(\".ttf\") || fileName.endsWith(\".otf\");\n    }\n\n    private static String getFontFamilyName(String fileName) {\n        String fontFamilyName = fileName;\n        int lastIndex = fileName.length() - 1;\n\n        int charIndex = fileName.indexOf('-');\n        if (0 < charIndex && charIndex < lastIndex) {\n            fontFamilyName = fileName.substring(0, charIndex);\n        } else {\n            charIndex = fileName.lastIndexOf('.');\n            if (0 < charIndex && charIndex < lastIndex) {\n                fontFamilyName = fileName.substring(0, charIndex);\n            }\n        }\n        return fontFamilyName;\n    }\n\n    public static String getFontFilePath(String fontFamilyName, int style) {\n        List<String> fontFamilyFilePathList = getFontFamilyFilePathList(fontFamilyName);\n        String fontFilePath = fontFamilyFilePathList.get(0);\n\n        String styleString = getFontFileStyleString(style);\n        if (!styleString.isEmpty()) {\n            for (String path : fontFamilyFilePathList) {\n                if (path.contains(styleString)) {\n                    fontFilePath = path;\n                    break;\n                }\n            }\n        }\n        return fontFilePath;\n    }\n\n    private static List<String> getFontFamilyFilePathList(String fontFamilyName) {\n        List<String> fontFamilyFilePathList = new ArrayList<>();\n        File fontsDir = new File(DIRECTORY_SYSTEM_FONTS);\n\n        if (fontsDir.exists() && fontsDir.isDirectory()) {\n            File[] files = fontsDir.listFiles();\n            if (files != null) {\n                for (File file : files) {\n                    if (file.isFile()) {\n                        String path = file.getAbsolutePath();\n                        if (path.contains(fontFamilyName)) {\n                            fontFamilyFilePathList.add(path);\n                        }\n                    }\n                }\n            }\n        }\n        return fontFamilyFilePathList;\n    }\n\n    private static String getFontFileStyleString(int style) {\n        if (style == Typeface.NORMAL)\n            return \"-Regular\";\n        if (style == Typeface.BOLD_ITALIC)\n            return \"-BoldItalic\";\n        if (style == Typeface.BOLD)\n            return \"-Bold\";\n        if (style == Typeface.ITALIC)\n            return \"-Italic\";\n        return \"\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/res/layout/activity_edit_text.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ScrollView\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\"\n    tools:context=\".EditTextActivity\">\n\n    <TableLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:stretchColumns=\"1,4\">\n\n        <TableRow>\n\n            <TextView\n                android:text=\"@string/text\"/>\n\n            <EditText\n                android:id=\"@+id/edit_text\"\n                android:layout_span=\"4\"\n                android:imeOptions=\"actionDone|flagNoExtractUi\"\n                android:inputType=\"text\"\n                android:textSize=\"32sp\"/>\n\n        </TableRow>\n\n        <TableRow>\n\n            <TextView\n                android:text=\"@string/font\"/>\n\n            <Spinner\n                android:id=\"@+id/edit_font_family\"\n                android:layout_span=\"4\"/>\n\n        </TableRow>\n\n        <TableRow>\n\n            <TextView\n                android:id=\"@+id/text_italic\"\n                android:text=\"@string/italic\"/>\n\n            <CheckBox\n                android:id=\"@+id/edit_italic\"\n                android:onClick=\"onItalicClick\"\n                android:text=\"\"/>\n\n        </TableRow>\n\n        <TableRow>\n\n            <TextView\n                android:id=\"@+id/text_bold\"\n                android:text=\"@string/bold\"/>\n\n            <CheckBox\n                android:id=\"@+id/edit_bold\"\n                android:onClick=\"onBoldClick\"\n                android:text=\"\"/>\n\n            <CheckBox\n                android:id=\"@+id/edit_outline\"\n                android:onClick=\"onOutlineClick\"\n                android:text=\"\"/>\n\n            <TextView\n                android:id=\"@+id/text_outline\"\n                style=\"?android:attr/listSeparatorTextViewStyle\"\n                android:layout_span=\"2\"\n                android:clickable=\"true\"\n                android:focusable=\"true\"\n                android:onClick=\"onOutlineClick\"\n                android:text=\"@string/outline\"/>\n\n        </TableRow>\n\n        <TableRow>\n\n            <TextView\n                android:text=\"@string/size\"/>\n\n            <Spinner\n                android:id=\"@+id/edit_text_size\"\n                android:layout_width=\"50dp\"/>\n\n            <Space\n                android:layout_width=\"0dp\"/>\n\n            <TextView\n                android:id=\"@+id/text_outline_size\"\n                android:text=\"@string/outline_size\" />\n\n            <Spinner\n                android:id=\"@+id/edit_outline_size\"\n                android:layout_width=\"50dp\"/>\n\n        </TableRow>\n\n        <TableRow>\n\n            <TextView\n                android:text=\"@string/color\"/>\n\n            <LinearLayout\n                android:clickable=\"true\"\n                android:focusable=\"true\"\n                android:onClick=\"onColorClick\">\n\n                <View\n                    android:id=\"@+id/edit_color\"\n                    android:layout_width=\"24sp\"\n                    android:layout_height=\"24sp\"\n                    android:layout_margin=\"6sp\"\n                    android:background=\"@android:color/white\"\n                    android:clickable=\"true\"\n                    android:focusable=\"true\"\n                    android:onClick=\"onColorClick\"/>\n\n            </LinearLayout>\n\n            <Space\n                android:layout_width=\"0dp\"/>\n\n            <TextView\n                android:id=\"@+id/text_outline_color\"\n                android:text=\"@string/outline_color\" />\n\n            <LinearLayout\n                android:clickable=\"true\"\n                android:focusable=\"true\"\n                android:onClick=\"onOutlineColorClick\">\n\n                <View\n                    android:id=\"@+id/edit_outline_color\"\n                    android:layout_width=\"24sp\"\n                    android:layout_height=\"24sp\"\n                    android:layout_margin=\"6sp\"\n                    android:background=\"@android:color/white\"\n                    android:clickable=\"true\"\n                    android:focusable=\"true\"\n                    android:onClick=\"onOutlineColorClick\"/>\n\n            </LinearLayout>\n\n        </TableRow>\n\n    </TableLayout>\n\n</ScrollView>\n"
  },
  {
    "path": "app/src/main/res/layout/activity_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\".MainActivity\">\n\n    <om.sstvencoder.CropView\n        android:id=\"@+id/cropView\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:layout_alignParentBottom=\"true\"\n        android:layout_alignParentEnd=\"true\"\n        android:layout_alignParentLeft=\"true\"\n        android:layout_alignParentRight=\"true\"\n        android:layout_alignParentStart=\"true\"\n        android:layout_alignParentTop=\"true\"/>\n\n    <RelativeLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"vertical\">\n\n        <ProgressBar\n            android:id=\"@+id/progressBar\"\n            style=\"?android:attr/progressBarStyleHorizontal\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"horizontal\"/>\n\n        <TextView\n            android:id=\"@+id/progressBarText\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_below=\"@+id/progressBar\"/>\n\n        <ProgressBar\n            android:id=\"@+id/progressBar2\"\n            style=\"?android:attr/progressBarStyleHorizontal\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_below=\"@+id/progressBarText\"\n            android:orientation=\"horizontal\"/>\n\n        <TextView\n            android:id=\"@+id/progressBarText2\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_below=\"@+id/progressBar2\"/>\n\n    </RelativeLayout>\n\n</RelativeLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/fragment_color.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\">\n\n    <om.sstvencoder.ColorPalette.ColorPaletteView\n        android:id=\"@+id/select_color\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"236dp\"/>\n\n</RelativeLayout>"
  },
  {
    "path": "app/src/main/res/menu/menu_edit_text.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    tools:context=\".EditTextActivity\">\n    <item\n        android:id=\"@+id/action_done\"\n        android:icon=\"@mipmap/sym_keyboard_done_lxx_dark\"\n        android:title=\"@string/action_done\"\n        app:showAsAction=\"always\"/>\n</menu>\n"
  },
  {
    "path": "app/src/main/res/menu/menu_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    tools:context=\".MainActivity\">\n    <item\n        android:id=\"@+id/action_play\"\n        android:icon=\"@android:drawable/ic_media_play\"\n        android:title=\"@string/action_play\"\n        app:showAsAction=\"always\"/>\n    <item\n        android:id=\"@+id/action_stop\"\n        android:icon=\"@android:drawable/ic_menu_close_clear_cancel\"\n        android:title=\"@string/action_stop\"\n        app:showAsAction=\"always\"/>\n    <item\n        android:id=\"@+id/action_pick_picture\"\n        android:icon=\"@android:drawable/ic_menu_gallery\"\n        android:title=\"@string/action_pick_picture\"\n        app:showAsAction=\"always\"/>\n    <item\n        android:id=\"@+id/action_take_picture\"\n        android:icon=\"@android:drawable/ic_menu_camera\"\n        android:title=\"@string/action_take_picture\"\n        app:showAsAction=\"always\"/>\n    <item\n        android:id=\"@+id/action_save_wave\"\n        android:icon=\"@android:drawable/ic_menu_save\"\n        android:title=\"@string/action_save_wave\"\n        app:showAsAction=\"ifRoom\"/>\n    <item\n        android:id=\"@+id/action_transform\"\n        android:title=\"@string/action_transform\"\n        app:showAsAction=\"ifRoom\">\n        <menu>\n            <item\n                android:id=\"@+id/action_rotate\"\n                android:icon=\"@android:drawable/ic_menu_rotate\"\n                android:title=\"@string/action_rotate\"\n                app:showAsAction=\"ifRoom\"/>\n            <item\n                android:id=\"@+id/action_reset\"\n                android:icon=\"@android:drawable/ic_menu_revert\"\n                android:title=\"@string/action_reset\"\n                app:showAsAction=\"ifRoom\"/>\n        </menu>\n    </item>\n    <item\n        android:id=\"@+id/action_modes\"\n        android:title=\"@string/action_modes\"\n        app:showAsAction=\"ifRoom\">\n        <menu/>\n    </item>\n    <item\n        android:id=\"@+id/action_privacy_policy\"\n        android:title=\"@string/action_privacy_policy\"\n        app:showAsAction=\"never\">\n    </item>\n    <item\n        android:id=\"@+id/action_about\"\n        android:title=\"@string/action_about\"\n        app:showAsAction=\"never\">\n    </item>\n</menu>\n"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"app_name\" translatable=\"false\">SSTV Encoder</string>\n    <string name=\"action_pick_picture\">Pick Picture</string>\n    <string name=\"action_take_picture\">Take Picture</string>\n    <string name=\"action_save_wave\">Save as WAVE File</string>\n    <string name=\"action_stop\">Stop</string>\n    <string name=\"action_play\">Play</string>\n    <string name=\"action_transform\">Transform Image</string>\n    <string name=\"action_rotate\">Rotate</string>\n    <string name=\"action_reset\">Reset</string>\n    <string name=\"action_done\">Done</string>\n    <string name=\"action_modes\">Modes</string>\n    <string name=\"action_privacy_policy\">Privacy Policy</string>\n    <string name=\"action_about\">About SSTV Encoder</string>\n    <string name=\"action_about_text\">\n        SSTV Encoder %1$s\\nCopyright 2017 Olga Miller\n        \\n\\nSSTV Encoder sends images via Slow Scan Television (SSTV).\n        \\n\\nFor more info, see open source code: \\nhttps://github.com/olgamiller/SSTVEncoder2\n        \\n\\nDISCLAIMER:\\nTHE SOFTWARE IS PROVIDED \\\"AS IS\\\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n    </string>\n    <string name=\"load_img_err_title\">Image loading error</string>\n    <string name=\"load_img_orientation_err_title\">Image orientation error</string>\n    <string name=\"load_img_err_txt_unsupported\">Unsupported content.</string>\n    <string name=\"message_prev_img_not_loaded\">Previous image could not be loaded.</string>\n    <string name=\"message_no_camera\">Device has no camera.</string>\n    <string name=\"progressbar_message_sending\">Sending…</string>\n    <string name=\"progressbar_message_saving_to_file\">%1$s saving…</string>\n    <string name=\"another_activity_resolve_err\">Another activity could not be resolved.</string>\n    <string name=\"another_activity_start_err\">Another activity could not be started.</string>\n    <string name=\"btn_send_email\">Send Email</string>\n    <string name=\"btn_ok\">OK</string>\n    <string name=\"email_subject\">SSTV Encoder - Bug Report</string>\n    <string name=\"chooser_title\">Send Bug Report:</string>\n    <string name=\"bold\">Bold</string>\n    <string name=\"italic\">Italic</string>\n    <string name=\"outline\">Outline</string>\n    <string name=\"color\">Color</string>\n    <string name=\"outline_color\">Color</string>\n    <string name=\"size\">Size</string>\n    <string name=\"font\">Font</string>\n    <string name=\"font_default\">Default</string>\n    <string name=\"text\">Text</string>\n    <string name=\"font_size_small\">Small</string>\n    <string name=\"font_size_normal\">Normal</string>\n    <string name=\"font_size_large\">Large</string>\n    <string name=\"font_size_huge\">Huge</string>\n    <string name=\"outline_size\">Size</string>\n    <string name=\"outline_size_thin\">Thin</string>\n    <string name=\"outline_size_normal\">Normal</string>\n    <string name=\"outline_size_thick\">Thick</string>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/styles.xml",
    "content": "<resources>\n    <style name=\"AppTheme\" parent=\"Base.Theme.AppCompat\"/>\n</resources>"
  },
  {
    "path": "app/src/main/res/values-v35/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\t<style name=\"AppTheme\" parent=\"Base.Theme.AppCompat\">\n\t\t<item name=\"android:windowOptOutEdgeToEdgeEnforcement\">true</item>\n\t</style>\n</resources>"
  },
  {
    "path": "app/src/main/res/values-zh-rCN/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"action_pick_picture\">选择照片</string>\n    <string name=\"action_take_picture\">拍摄照片</string>\n    <string name=\"action_save_wave\">保存为波形(.wav)文件</string>\n    <string name=\"action_stop\">停止</string>\n    <string name=\"action_play\">播放</string>\n    <string name=\"action_transform\">更改图片</string>\n    <string name=\"action_rotate\">旋转</string>\n    <string name=\"action_reset\">重置</string>\n    <string name=\"action_done\">完成</string>\n    <string name=\"action_modes\">编码模式</string>\n    <string name=\"action_privacy_policy\">隐私政策</string>\n    <string name=\"action_about\">关于SSTV Encoder</string>\n    <string name=\"action_about_text\">\"\\n        SSTV Encoder %1$s\\n版权所有 2017 Olga Miller\\n        \\n\\nSSTV Encoder通过慢扫描电视/Slow Scan Television (SSTV)发送图片.\\n        \\n\\n要获取详细信息，请参阅本软件源码: \\nhttps://github.com/olgamiller/SSTVEncoder2\\n        \\n\\nDISCLAIMER:\\nTHE SOFTWARE IS PROVIDED \\\"AS IS\\\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\\n    \"</string>\n    <string name=\"load_img_err_title\">加载图像时出错</string>\n    <string name=\"load_img_orientation_err_title\">定位图像时出错</string>\n    <string name=\"load_img_err_txt_unsupported\">不受支持的内容</string>\n    <string name=\"message_prev_img_not_loaded\">先前所使用的图像未能被加载</string>\n    <string name=\"message_no_camera\">这个设备没有摄像头</string>\n    <string name=\"progressbar_message_sending\">发送…</string>\n    <string name=\"progressbar_message_saving_to_file\">%1$s 另存为文件…</string>\n    <string name=\"another_activity_resolve_err\">未能解析另一个活动</string>\n    <string name=\"another_activity_start_err\">另一个活动未能被启动</string>\n    <string name=\"btn_send_email\">发送邮件</string>\n    <string name=\"btn_ok\">好的</string>\n    <string name=\"email_subject\">SSTV Encoder - BUG反馈</string>\n    <string name=\"chooser_title\">发送BUG反馈</string>\n    <string name=\"bold\">粗体</string>\n    <string name=\"italic\">斜体</string>\n    <string name=\"outline\">描边</string>\n    <string name=\"color\">颜色</string>\n    <string name=\"outline_color\">描边颜色</string>\n    <string name=\"size\">大小</string>\n    <string name=\"font\">字体</string>\n    <string name=\"font_default\">默认字体</string>\n    <string name=\"text\">文本</string>\n    <string name=\"font_size_small\">小</string>\n    <string name=\"font_size_normal\">常规</string>\n    <string name=\"font_size_large\">大</string>\n    <string name=\"font_size_huge\">很大</string>\n    <string name=\"outline_size\">描边大小</string>\n    <string name=\"outline_size_thin\">细</string>\n    <string name=\"outline_size_normal\">常规</string>\n    <string name=\"outline_size_thick\">粗</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/xml/paths.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<paths>\n    <external-path\n        name=\"media\"\n        path=\".\"/>\n</paths>"
  },
  {
    "path": "build.gradle",
    "content": "buildscript {\n    repositories {\n        google()\n        mavenCentral()\n    }\n    dependencies {\n        classpath 'com.android.tools.build:gradle:8.7.3'\n    }\n}\n\nallprojects {\n    configurations.configureEach {\n        resolutionStrategy.eachDependency { details ->\n            if (details.requested.group == 'org.jetbrains.kotlin') {\n                details.useVersion \"1.8.22\"\n            }\n        }\n    }\n    repositories {\n        google()\n        mavenCentral()\n    }\n}\n\ntasks.register('clean', Delete) {\n    delete rootProject.layout.buildDirectory.get().asFile\n}\n"
  },
  {
    "path": "fastlane/metadata/android/en-US/full_description.txt",
    "content": "\n<h3>Modes</h3>\n\nSupported SSTV modes:\n<ul>\n    <li><b>Martin Modes</b>: Martin 1, Martin 2</li>\n    <li><b>PD Modes</b>: PD 50, PD 90, PD 120, PD 160, PD 180, PD 240, PD 290</li>\n    <li><b>Robot Modes</b>: Robot 36 Color, Robot 72 Color</li>\n    <li><b>Scottie Modes</b>: Scottie 1, Scottie 2, Scottie DX</li>\n    <li><b>Wraase Modes</b>: Wraase SC2 180</li>\n</ul>\nThe mode specifications are taken from the Dayton Paper, JL Barber, \"Proposal for SSTV Mode Specifications\", 2000:<br>\n<a href='http://www.barberdsp.com/downloads/Dayton%20Paper.pdf'>http://www.barberdsp.com/downloads/Dayton%20Paper.pdf</a>\n\n<h3>Image</h3>\n\nTo load an image:\n<ul>\n    <li>tap <b>\"Take Picture\"</b> or <b>\"Pick Picture\"</b> menu button, or\n    <li>use the <b>Share</b> option of an app like e.g. Gallery.\n</ul>\nTo keep the aspect ratio, black borders will be added if necessary.<br>\nOriginal image can be resend using another mode without reloading.<br>\nAfter image rotation or mode changing the image will be scaled to that mode's native size.<br>\nAfter closing the app the loaded image will not be stored.\n\n<h3>Text Overlay</h3>\n\nActions for working with text overlays:\n<ul>\n    <li>Single tap <b>to add</b> a text overlay.</li>\n    <li>Single tap on text overlay <b>to edit</b> it.</li>\n    <li>Long press <b>to move</b> text overlay.</li>\n    <li>Remove the text <b>to remove</b> a text overlay.</li>\n</ul>\nAfter closing the app all text overlays will be stored and reloaded when restarting.\n\n<h3>Menu</h3>\n\nAvailable menu options:\n<ul>\n    <li><b>\"Play\"</b>: Sends the image</li>\n    <li><b>\"Stop\"</b>: Stops the current sending and empties the queue</li>\n    <li><b>\"Pick Picture\"</b>: Opens an image viewer app to select a picture</li>\n    <li><b>\"Take Picture\"</b>: Starts a camera app to take a picture</li>\n    <li><b>\"Save as WAVE File\"</b>: Creates a wave file in the Music folder in SSTV Encoder album</li>\n    <li><b>\"Transform Image\"</b>:</li>\n    <ul>\n        <li><b>\"Rotate\"</b>: Rotates the image by 90 degrees</li>\n        <li><b>\"Reset\"</b>: Resets image rotation and scaling</li>\n    </ul>\n    <li><b>\"Modes\"</b>: Lists all supported modes</li>\n</ul>\n"
  },
  {
    "path": "fastlane/metadata/android/en-US/short_description.txt",
    "content": "Image encoder for Slow-Scan Television (SSTV) audio signals\n"
  },
  {
    "path": "fastlane/metadata/android/en-US/title.txt",
    "content": "SSTV Encoder \n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-8.9-bin.zip\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "gradle.properties",
    "content": "# Project-wide Gradle settings.\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will override*\n# any settings specified in this file.\n# For more details on how to configure your build environment visit\n# http://www.gradle.org/docs/current/userguide/build_environment.html\n# Specifies the JVM arguments used for the daemon process.\n# The setting is particularly useful for tweaking memory settings.\norg.gradle.jvmargs=-Xmx1536m\n# When configured, Gradle will run in incubating parallel mode.\n# This option should only be used with decoupled projects. More details, visit\n# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects\n# org.gradle.parallel=true\nandroid.useAndroidX=true\nandroid.enableJetifier=true\n\n"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env sh\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >/dev/null\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >/dev/null\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS=\"\"\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn () {\n    echo \"$*\"\n}\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\n  NONSTOP* )\n    nonstop=true\n    ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" -a \"$nonstop\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n    JAVACMD=`cygpath --unix \"$JAVACMD\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=$((i+1))\n    done\n    case $i in\n        (0) set -- ;;\n        (1) set -- \"$args0\" ;;\n        (2) set -- \"$args0\" \"$args1\" ;;\n        (3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        (4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        (5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        (6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        (7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        (8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        (9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Escape application args\nsave () {\n    for i do printf %s\\\\n \"$i\" | sed \"s/'/'\\\\\\\\''/g;1s/^/'/;\\$s/\\$/' \\\\\\\\/\" ; done\n    echo \" \"\n}\nAPP_ARGS=$(save \"$@\")\n\n# Collect all arguments for the java command, following the shell quoting and substitution rules\neval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS \"\\\"-Dorg.gradle.appname=$APP_BASE_NAME\\\"\" -classpath \"\\\"$CLASSPATH\\\"\" org.gradle.wrapper.GradleWrapperMain \"$APP_ARGS\"\n\n# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong\nif [ \"$(uname)\" = \"Darwin\" ] && [ \"$HOME\" = \"$PWD\" ]; then\n  cd \"$(dirname \"$0\")\"\nfi\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@if \"%DEBUG%\" == \"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif \"%ERRORLEVEL%\" == \"0\" goto init\r\n\r\necho.\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto init\r\n\r\necho.\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:init\r\n@rem Get command-line arguments, handling Windows variants\r\n\r\nif not \"%OS%\" == \"Windows_NT\" goto win9xME_args\r\n\r\n:win9xME_args\r\n@rem Slurp the command line arguments.\r\nset CMD_LINE_ARGS=\r\nset _SKIP=2\r\n\r\n:win9xME_args_slurp\r\nif \"x%~1\" == \"x\" goto execute\r\n\r\nset CMD_LINE_ARGS=%*\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\r\nexit /b 1\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "settings.gradle",
    "content": "include ':app'\n"
  }
]