Full Code of olgamiller/SSTVEncoder2 for AI

master 94821b169700 cached
87 files
233.8 KB
57.4k tokens
579 symbols
1 requests
Download .txt
Showing preview only (259K chars total). Download the full file or copy to clipboard to get everything.
Repository: olgamiller/SSTVEncoder2
Branch: master
Commit: 94821b169700
Files: 87
Total size: 233.8 KB

Directory structure:
gitextract_y4ynicm8/

├── .gitignore
├── LICENSE
├── NOTICE
├── README.md
├── app/
│   ├── .gitignore
│   ├── build.gradle
│   ├── proguard-rules.pro
│   └── src/
│       └── main/
│           ├── AndroidManifest.xml
│           ├── java/
│           │   └── om/
│           │       └── sstvencoder/
│           │           ├── ColorFragment.java
│           │           ├── ColorPalette/
│           │           │   ├── ColorPaletteView.java
│           │           │   ├── GridColorPalette.java
│           │           │   └── IColorPalette.java
│           │           ├── CropView.java
│           │           ├── EditTextActivity.java
│           │           ├── Encoder.java
│           │           ├── FontFamilySet.java
│           │           ├── MainActivity.java
│           │           ├── MainActivityMessenger.java
│           │           ├── ModeInterfaces/
│           │           │   ├── IMode.java
│           │           │   ├── IModeInfo.java
│           │           │   └── ModeSize.java
│           │           ├── Modes/
│           │           │   ├── ImageFormats/
│           │           │   │   ├── NV21.java
│           │           │   │   ├── YUV440P.java
│           │           │   │   ├── YUY2.java
│           │           │   │   ├── YV12.java
│           │           │   │   ├── Yuv.java
│           │           │   │   ├── YuvConverter.java
│           │           │   │   ├── YuvFactory.java
│           │           │   │   └── YuvImageFormat.java
│           │           │   ├── Martin.java
│           │           │   ├── Martin1.java
│           │           │   ├── Martin2.java
│           │           │   ├── Mode.java
│           │           │   ├── ModeDescription.java
│           │           │   ├── ModeFactory.java
│           │           │   ├── ModeInfo.java
│           │           │   ├── PD.java
│           │           │   ├── PD120.java
│           │           │   ├── PD160.java
│           │           │   ├── PD180.java
│           │           │   ├── PD240.java
│           │           │   ├── PD290.java
│           │           │   ├── PD50.java
│           │           │   ├── PD90.java
│           │           │   ├── Robot36.java
│           │           │   ├── Robot72.java
│           │           │   ├── Scottie.java
│           │           │   ├── Scottie1.java
│           │           │   ├── Scottie2.java
│           │           │   ├── ScottieDX.java
│           │           │   └── Wraase.java
│           │           ├── Output/
│           │           │   ├── AudioOutput.java
│           │           │   ├── IOutput.java
│           │           │   ├── OutputFactory.java
│           │           │   ├── WaveFileOutput.java
│           │           │   └── WaveFileOutputContext.java
│           │           ├── ProgressBarWrapper.java
│           │           ├── Settings.java
│           │           ├── TextOverlay/
│           │           │   ├── IReader.java
│           │           │   ├── IWriter.java
│           │           │   ├── Label.java
│           │           │   ├── LabelCollection.java
│           │           │   ├── LabelContainer.java
│           │           │   ├── LabelPainter.java
│           │           │   └── Position.java
│           │           ├── TextOverlayTemplate.java
│           │           └── Utility.java
│           └── res/
│               ├── layout/
│               │   ├── activity_edit_text.xml
│               │   ├── activity_main.xml
│               │   └── fragment_color.xml
│               ├── menu/
│               │   ├── menu_edit_text.xml
│               │   └── menu_main.xml
│               ├── values/
│               │   ├── strings.xml
│               │   └── styles.xml
│               ├── values-v35/
│               │   └── styles.xml
│               ├── values-zh-rCN/
│               │   └── strings.xml
│               └── xml/
│                   └── paths.xml
├── build.gradle
├── fastlane/
│   └── metadata/
│       └── android/
│           └── en-US/
│               ├── full_description.txt
│               ├── short_description.txt
│               └── title.txt
├── gradle/
│   └── wrapper/
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
└── settings.gradle

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
*.iml
.gradle
/local.properties
/.idea
.DS_Store
/build
/captures
.externalNativeBuild


================================================
FILE: LICENSE
================================================

                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright [yyyy] [name of copyright owner]

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.


================================================
FILE: NOTICE
================================================
SSTV Encoder 2
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Mode specifications were taken from "Dayton Paper" of JL Barber:
http://www.barberdsp.com/downloads/Dayton%20Paper.pdf

sym_keyboard_done_lxx_dark.png icons were taken from
android.googlesource.com

ic_launcher.png were created using GIMP:
http://www.gimp.org/

SMPTE Color Bars image (CC BY-SA 3.0) was taken from Wikipedia:
http://en.wikipedia.org/wiki/SMPTE_color_bars#mediaviewer/File:SMPTE_Color_Bars.svg


================================================
FILE: README.md
================================================
![Icon](app/src/main/res/mipmap-xhdpi/ic_launcher.png)
# SSTV Encoder 2

Image encoder for Slow-Scan Television (SSTV) audio signals

### Modes

Supported SSTV modes:
* **Martin Modes**:  Martin 1, Martin 2  
* **PD Modes**:      PD 50, PD 90, PD 120, PD 160, PD 180, PD 240, PD 290   
* **Robot Modes**:   Robot 36 Color, Robot 72 Color  
* **Scottie Modes**: Scottie 1, Scottie 2, Scottie DX 
* **Wraase Modes**:  Wraase SC2 180

The mode specifications are taken from the Dayton Paper, JL Barber, "Proposal for SSTV Mode Specifications", 2000:  
http://www.barberdsp.com/downloads/Dayton%20Paper.pdf

### Image

To load an image:
* tap **"Take Picture"** or **"Pick Picture"** menu button, or  
* use the **Share** option of an app like e.g. Gallery.

To keep the aspect ratio, black borders will be added if necessary.  
Original image can be resend using another mode without reloading.  
After image rotation or mode changing the image will be scaled to that mode's native size.  
After closing the app the loaded image will not be stored.

### Text Overlay

Actions for working with text overlays:
* Single tap **to add** a text overlay.  
* Single tap on text overlay **to edit** it.  
* Long press **to move** text overlay.  
* Remove the text **to remove** a text overlay.

After closing the app all text overlays will be stored and reloaded when restarting.

### Menu

Available menu options:
* **"Play"**: Sends the image
* **"Stop"**: Stops the current sending and empties the queue
* **"Pick Picture"**: Opens an image viewer app to select a picture
* **"Take Picture"**: Starts a camera app to take a picture
* **"Save as WAVE File"**: Creates a wave file in the Music folder in SSTV Encoder album
* **"Transform Image"**:
  * **"Rotate"**: Rotates the image by 90 degrees
  * **"Reset"**: Resets image rotation and scaling
* **"Modes"**: Lists all supported modes

### Installation

The working app "SSTV Encoder" can be installed 

on Google Play:  
https://play.google.com/store/apps/details?id=om.sstvencoder

or on F-Droid:  
https://f-droid.org/packages/om.sstvencoder/

# SSTV Image Decoder

Open Source Code:  
https://github.com/xdsopl/robot36/tree/android

### Installation

The working app "Robot36 - SSTV Image Decoder" can be installed

on Google Play:  
https://play.google.com/store/apps/details?id=xdsopl.robot36

or on F-Droid:  
https://f-droid.org/packages/xdsopl.robot36/


================================================
FILE: app/.gitignore
================================================
/build


================================================
FILE: app/build.gradle
================================================
apply plugin: 'com.android.application'

android {
    compileSdk 35
    defaultConfig {
        applicationId "om.sstvencoder"
        minSdk 21
        targetSdk 35
        versionCode 34
        versionName "2.13"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    namespace 'om.sstvencoder'
    buildFeatures {
        buildConfig true
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.7.0'
    implementation "androidx.exifinterface:exifinterface:1.3.7"
}


================================================
FILE: app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /home/olga/Android/Sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
#   http://developer.android.com/guide/developing/tools/proguard.html

# Add any project specific keep options here:

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
#   public *;
#}


================================================
FILE: app/src/main/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <uses-permission
        android:name="android.permission.WRITE_EXTERNAL_STORAGE"
        android:maxSdkVersion="29"/>
    <uses-feature
        android:name="android.hardware.camera"
        android:required="false"/>
    <queries>
        <intent>
            <action android:name="android.intent.action.SEND"/>
            <data android:mimeType="image/*"/>
        </intent>
        <intent>
            <action android:name="android.media.action.IMAGE_CAPTURE"/>
        </intent>
        <intent>
            <action android:name="android.intent.action.PICK" />
            <data android:mimeType="image/*"/>
        </intent>
        <intent>
            <action android:name="android.intent.action.VIEW" />
            <category android:name="android.intent.category.BROWSABLE" />
            <data android:scheme="https" />
        </intent>
    </queries>
    <application
        android:requestLegacyExternalStorage="true"
        android:allowBackup="false"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme">
        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="om.sstvencoder"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/paths"/>
        </provider>

        <activity
            android:name=".MainActivity"
            android:configChanges="keyboardHidden|orientation|screenSize"
            android:label="@string/app_name"
            android:launchMode="singleTask"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.SEND"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <data android:mimeType="image/*"/>
            </intent-filter>
        </activity>

        <activity
            android:name=".EditTextActivity"
            android:configChanges="keyboardHidden|orientation|screenSize"
            android:windowSoftInputMode="stateVisible|adjustPan"/>

    </application>
</manifest>

================================================
FILE: app/src/main/java/om/sstvencoder/ColorFragment.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder;

import android.app.AlertDialog;
import android.app.Dialog;
import android.graphics.Color;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import android.view.View;

import java.util.ArrayList;
import java.util.List;

import om.sstvencoder.ColorPalette.ColorPaletteView;

public class ColorFragment extends DialogFragment
        implements ColorPaletteView.OnColorSelectedListener {

    public interface OnColorSelectedListener {
        void onColorSelected(DialogFragment fragment, int color);

        void onCancel(DialogFragment fragment);
    }

    private List<OnColorSelectedListener> mListeners;
    private int mTitle;
    private int mColor;

    public ColorFragment() {
        mListeners = new ArrayList<>();
        mTitle = R.string.color;
        mColor = Color.WHITE;
    }

    public void setTitle(int title) {
        mTitle = title;
    }

    public void setColor(int color) {
        mColor = color;
    }

    public void addOnColorSelectedListener(OnColorSelectedListener listener) {
        mListeners.add(listener);
    }

    @NonNull
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
        View view = getActivity().getLayoutInflater().inflate(R.layout.fragment_color, null);
        ColorPaletteView colorView = view.findViewById(R.id.select_color);
        colorView.setColor(mColor);
        colorView.addOnColorSelectedListener(this);
        builder.setTitle(mTitle);
        builder.setView(view);
        return builder.create();
    }

    @Override
    public void onColorChanged(View v, int color) {
    }

    @Override
    public void onColorSelected(View v, int color) {
        for (OnColorSelectedListener listener : mListeners)
            listener.onColorSelected(this, color);
        dismiss();
    }

    @Override
    public void onCancel(View v) {
        for (OnColorSelectedListener listener : mListeners)
            listener.onCancel(this);
        dismiss();
    }
}


================================================
FILE: app/src/main/java/om/sstvencoder/ColorPalette/ColorPaletteView.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.ColorPalette;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.PorterDuff;
import androidx.annotation.NonNull;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

import java.util.ArrayList;

public class ColorPaletteView extends View {

    public interface OnColorSelectedListener {
        void onColorChanged(View v, int color);

        void onColorSelected(View v, int color);

        void onCancel(View v);
    }

    private final ArrayList<OnColorSelectedListener> mListeners;
    private final IColorPalette mPalette;

    public ColorPaletteView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mListeners = new ArrayList<>();
        mPalette = new GridColorPalette(GridColorPalette.getStandardColors(),
                getResources().getDisplayMetrics().density);
    }

    public int getColor() {
        return mPalette.getSelectedColor();
    }

    public void setColor(int color) {
        mPalette.selectColor(color);
    }

    @Override
    protected void onSizeChanged(int w, int h, int old_w, int old_h) {
        super.onSizeChanged(w, h, old_w, old_h);
        mPalette.updateSize(w, h);
    }

    @Override
    protected void onDraw(@NonNull Canvas canvas) {
        canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
        mPalette.draw(canvas);
    }

    @Override
    public boolean onTouchEvent(@NonNull MotionEvent e) {
        boolean consumed = false;
        switch (e.getAction()) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_MOVE: {
                update(e.getX(), e.getY());
                consumed = true;
                break;
            }
            case MotionEvent.ACTION_UP: {
                float x = e.getX();
                float y = e.getY();
                if (getLeft() <= x && x <= getRight() && getTop() <= y && y <= getBottom())
                    colorSelectedCallback();
                else
                    cancelCallback();
                consumed = true;
                break;
            }
        }
        return consumed || super.onTouchEvent(e);
    }

    private void update(float x, float y) {
        if (mPalette.selectColor(x, y)) {
            invalidate();
            colorChangedCallback();
        }
    }

    public void addOnColorSelectedListener(OnColorSelectedListener listener) {
        mListeners.add(listener);
    }

    private void colorChangedCallback() {
        for (OnColorSelectedListener listener : mListeners) {
            listener.onColorChanged(this, mPalette.getSelectedColor());
        }
    }

    private void colorSelectedCallback() {
        for (OnColorSelectedListener listener : mListeners) {
            listener.onColorSelected(this, mPalette.getSelectedColor());
        }
    }

    private void cancelCallback() {
        for (OnColorSelectedListener listener : mListeners) {
            listener.onCancel(this);
        }
    }
}



================================================
FILE: app/src/main/java/om/sstvencoder/ColorPalette/GridColorPalette.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.ColorPalette;

import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;

class GridColorPalette implements IColorPalette {

    static int[] getStandardColors() {
        return new int[]{
                Color.BLACK,
                Color.GRAY,
                Color.LTGRAY,
                Color.WHITE,
                Color.YELLOW,
                Color.CYAN,
                Color.GREEN,
                Color.MAGENTA,
                Color.RED,
                Color.BLUE
        };
    }

    private final static float STROKE_WIDTH_FACTOR = 6f;
    private final static float BOX_SIZE_DP = 96f;
    private final static float SPACE_FACTOR = 6f;
    private final int[] mColorList;
    private final Paint mPaint;
    private final RectF mSelectedBounds;
    private final float mDisplayMetricsDensity;
    private int mColumns, mRows;
    private float mWidth, mHeight;
    private float mBoxSize, mSpace, mStrokeWidth, mCornerRadius;
    private int mSelectedColorIndex;
    private boolean mValid;

    GridColorPalette(int[] colorList, float displayMetricsDensity) {
        final float CORNER_RADIUS = 3f;
        mColorList = colorList;
        mDisplayMetricsDensity = displayMetricsDensity;
        mCornerRadius = CORNER_RADIUS * mDisplayMetricsDensity;
        mPaint = new Paint();
        setPaintStyleForBox();
        mSelectedBounds = new RectF();
        mSelectedColorIndex = 0;
        mValid = false;
    }

    @Override
    public void updateSize(float width, float height) {
        mValid = width > 0 && height > 0;

        if (mValid && (mWidth != width || mHeight != height)) {
            mWidth = width;
            mHeight = height;
            updateGrid();
            mStrokeWidth = mSpace / STROKE_WIDTH_FACTOR;
            setSelectedColor(mSelectedColorIndex);
        }
    }

    // The approximately same box size independently on resolution has the higher priority.
    // Thus the possible filling of the last row is not supported here.
    private void updateGrid() {
        int boxes = mColorList.length;
        mBoxSize = BOX_SIZE_DP * mDisplayMetricsDensity;
        mSpace = mBoxSize / SPACE_FACTOR;

        mColumns = min((int) ((mWidth - mSpace) / (mBoxSize + mSpace) + 0.5f), boxes);
        mRows = (boxes + mColumns - 1) / mColumns; // ceil
        updateBoxSizeAndSpace();

        while (mRows * (mBoxSize + mSpace) + mSpace > mHeight) {
            ++mColumns;
            mRows = (boxes + mColumns - 1) / mColumns;
            updateBoxSizeAndSpace();
        }
    }

    private int min(int a, int b) {
        return a <= b ? a : b;
    }

    // Fill out the whole width of the View.
    private void updateBoxSizeAndSpace() {
        // Set 'space = boxSize / spaceFactor' into
        // 'boxSize = (width - (columns + 1) * space ) / columns'
        // and solve for boxSize:
        mBoxSize = SPACE_FACTOR * mWidth / (1f + mColumns * (SPACE_FACTOR + 1f));
        mSpace = mBoxSize / SPACE_FACTOR;
    }

    @Override
    public void draw(Canvas canvas) {
        if (!mValid)
            return;

        float x = mSpace, y = mSpace;
        float maxX = mColumns * (mBoxSize + mSpace);
        for (int color : mColorList) {
            RectF rect = new RectF(x, y, x + mBoxSize, y + mBoxSize);
            mPaint.setColor(color);
            canvas.drawRoundRect(rect, mCornerRadius, mCornerRadius, mPaint);
            x += mBoxSize + mSpace;
            if (x > maxX) {
                x = mSpace;
                y += mBoxSize + mSpace;
            }
        }
        drawSelectedRect(canvas);
    }

    private void drawSelectedRect(Canvas canvas) {
        float padding = mSpace / 2f;
        float l = mSelectedBounds.left;
        float t = mSelectedBounds.top;
        float r = mSelectedBounds.right;
        float b = mSelectedBounds.bottom;
        RectF rect = new RectF(l - padding, t - padding, r + padding, b + padding);
        Paint.Style paintStyle = mPaint.getStyle();
        setPaintStyleForSelectedBox();
        canvas.drawRoundRect(rect, mCornerRadius, mCornerRadius, mPaint);
        mPaint.setStyle(paintStyle);
    }

    private void setPaintStyleForSelectedBox() {
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(mStrokeWidth);
        mPaint.setColor(Color.WHITE);
    }

    private void setPaintStyleForBox() {
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setAntiAlias(true);
    }

    @Override
    public int getSelectedColor() {
        return mColorList[mSelectedColorIndex];
    }

    @Override
    public boolean selectColor(float x, float y) {
        if (!mValid || mSelectedBounds.contains(x, y))
            return false;

        int column = (int) (x / (mBoxSize + mSpace));
        int row = (int) (y / (mBoxSize + mSpace));
        if (0 > row || row >= mRows || 0 > column || column >= mColumns)
            return false;

        int i = row * mColumns + column;
        if (i >= mColorList.length || i == mSelectedColorIndex)
            return false;

        float left = mSpace + column * (mBoxSize + mSpace);
        float top = mSpace + row * (mBoxSize + mSpace);
        if (left > x || x > left + mBoxSize || top > y || y > top + mBoxSize)
            return false;

        mSelectedBounds.set(left, top, left + mBoxSize, top + mBoxSize);
        mSelectedColorIndex = i;
        return true;
    }

    @Override
    public boolean selectColor(int color) {
        for (int i = 0; i < mColorList.length; ++i) {
            if (color == mColorList[i]) {
                if (mValid)
                    setSelectedColor(i);
                else
                    mSelectedColorIndex = i;
                return true;
            }
        }
        return false;
    }

    private void setSelectedColor(int i) {
        int row = i / mColumns;
        int column = i - row * mColumns;
        float x = mSpace + column * (mBoxSize + mSpace);
        float y = mSpace + row * (mBoxSize + mSpace);
        mSelectedBounds.set(x, y, x + mBoxSize, y + mBoxSize);
        mSelectedColorIndex = i;
    }
}


================================================
FILE: app/src/main/java/om/sstvencoder/ColorPalette/IColorPalette.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.ColorPalette;

import android.graphics.Canvas;

interface IColorPalette {
    void updateSize(float width, float height);

    void draw(Canvas canvas);

    int getSelectedColor();

    boolean selectColor(float x, float y);

    boolean selectColor(int color);
}


================================================
FILE: app/src/main/java/om/sstvencoder/CropView.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder;

import android.content.Context;
import android.content.ContextWrapper;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapRegionDecoder;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import androidx.annotation.NonNull;
import androidx.core.view.GestureDetectorCompat;
import androidx.appcompat.widget.AppCompatImageView;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;

import om.sstvencoder.ModeInterfaces.ModeSize;
import om.sstvencoder.TextOverlay.Label;
import om.sstvencoder.TextOverlay.LabelCollection;

public class CropView extends AppCompatImageView {
    private class GestureListener extends GestureDetector.SimpleOnGestureListener {
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            if (!mLongPress) {
                moveImage(distanceX, distanceY);
                return true;
            }
            return false;
        }

        @Override
        public void onLongPress(MotionEvent e) {
            mLongPress = false;
            if (!mInScale && mLabelCollection.moveLabelBegin(e.getX(), e.getY())) {
                invalidate();
                mLongPress = true;
            }
        }

        @Override
        public boolean onSingleTapConfirmed(MotionEvent e) {
            if (!mLongPress) {
                editLabelBegin(e.getX(), e.getY());
                return true;
            }
            return false;
        }
    }

    private class ScaleGestureListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
        @Override
        public boolean onScaleBegin(ScaleGestureDetector detector) {
            if (!mLongPress) {
                mInScale = true;
                return true;
            }
            return false;
        }

        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            scaleImage(detector.getScaleFactor());
            return true;
        }

        @Override
        public void onScaleEnd(ScaleGestureDetector detector) {
            mInScale = false;
        }
    }

    private GestureDetectorCompat mDetectorCompat;
    private ScaleGestureDetector mScaleDetector;
    private boolean mLongPress, mInScale;
    private ModeSize mModeSize;
    private final Paint mPaint, mRectPaint, mBorderPaint;
    private RectF mInputRect;
    private Rect mOutputRect;
    private BitmapRegionDecoder mRegionDecoder;
    private int mImageWidth, mImageHeight;
    private Bitmap mCacheBitmap;
    private boolean mSmallImage;
    private boolean mImageOK;
    private final Rect mCanvasDrawRect, mImageDrawRect;
    private int mOrientation;
    private Rect mCacheRect;
    private int mCacheSampleSize;
    private final BitmapFactory.Options mBitmapOptions;
    private LabelCollection mLabelCollection;

    public CropView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mDetectorCompat = new GestureDetectorCompat(getContext(), new GestureListener());
        mScaleDetector = new ScaleGestureDetector(getContext(), new ScaleGestureListener());

        mBitmapOptions = new BitmapFactory.Options();

        mPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
        mRectPaint = new Paint();
        mRectPaint.setStyle(Paint.Style.STROKE);
        mRectPaint.setStrokeWidth(1f);
        mBorderPaint = new Paint();
        mBorderPaint.setColor(Color.BLACK);

        mCanvasDrawRect = new Rect();
        mImageDrawRect = new Rect();
        mCacheRect = new Rect();
        mOutputRect = new Rect();

        mSmallImage = false;
        mImageOK = false;

        mLabelCollection = new LabelCollection();
    }

    public void setModeSize(ModeSize size) {
        mModeSize = size;
        mOutputRect = Utility.getEmbeddedRect(getWidth(), getHeight(), mModeSize.width(), mModeSize.height());
        if (mImageOK)
            resetInputRect();
        invalidate();
    }

    private void resetInputRect() {
        float iw = mModeSize.width();
        float ih = mModeSize.height();
        float ow = mImageWidth;
        float oh = mImageHeight;
        if (iw * oh > ow * ih) {
            float right = (iw * oh) / ih;
            mInputRect = new RectF(0f, 0f, right, oh);
            mInputRect.offset((ow - right) / 2f, 0f);
        } else {
            float bottom = (ih * ow) / iw;
            mInputRect = new RectF(0f, 0f, ow, bottom);
            mInputRect.offset(0f, (oh - bottom) / 2f);
        }
    }

    public void rotateImage(int orientation) {
        if (!mImageOK)
            return;
        mOrientation += orientation;
        mOrientation %= 360;
        if (orientation == 90 || orientation == 270) {
            int tmp = mImageWidth;
            mImageWidth = mImageHeight;
            mImageHeight = tmp;
        }
        resetInputRect();
        invalidate();
    }

    public void resetImage() {
        if (!mImageOK)
            return;
        if (mOrientation == 90 || mOrientation == 270) {
            int tmp = mImageWidth;
            mImageWidth = mImageHeight;
            mImageHeight = tmp;
        }
        mOrientation = 0;
        resetInputRect();
        invalidate();
    }

    public void setNoBitmap() {
        mImageOK = false;
        mOrientation = 0;
        recycle();
        invalidate();
    }

    public void setBitmap(@NonNull InputStream stream) throws Exception {
        mImageOK = false;
        mOrientation = 0;
        recycle();
        loadImage(stream);
        invalidate();
    }

    private void loadImage(InputStream stream) throws Exception {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        byte[] streamBytes = null;
        String errorMessage = null;
        try {
            int length = stream.available();
            if (length > 0) {
                streamBytes = new byte[length];
                if (length == stream.read(streamBytes, 0, streamBytes.length)) {
                    BitmapFactory.decodeByteArray(streamBytes, 0, streamBytes.length, options);
                }
                else
                    streamBytes = null;
            }
        } catch (Exception ex) {
            errorMessage = ex.getMessage();
            streamBytes = null;
        }

        mImageWidth = options.outWidth;
        mImageHeight = options.outHeight;

        if (streamBytes != null && mImageWidth > 0 && mImageHeight > 0) {
            mSmallImage = mImageWidth * mImageHeight < 1024 * 1024;
            if (mSmallImage) {
                mCacheBitmap = BitmapFactory.decodeByteArray(streamBytes, 0, streamBytes.length, null);
            } else {
                mRegionDecoder = BitmapRegionDecoder.newInstance(streamBytes, 0, streamBytes.length, true);
                mCacheRect.setEmpty();
            }
        }

        if (mCacheBitmap == null && mRegionDecoder == null) {
            String message = errorMessage;
            if (message == null) {
                message = "Stream could not be decoded.";
                if (mImageWidth > 0 && mImageHeight > 0) {
                    message += " Image size: " + mImageWidth + "x" + mImageHeight;
                }
            }
            throw new Exception(message);
        }

        mImageOK = true;
        resetInputRect();
    }

    private void recycle() {
        if (mRegionDecoder != null) {
            mRegionDecoder.recycle();
            mRegionDecoder = null;
        }
        if (mCacheBitmap != null) {
            mCacheBitmap.recycle();
            mCacheBitmap = null;
        }
    }

    public void scaleImage(float scaleFactor) {
        if (!mImageOK)
            return;
        float newW = mInputRect.width() / scaleFactor;
        float newH = mInputRect.height() / scaleFactor;
        float dx = 0.5f * (mInputRect.width() - newW);
        float dy = 0.5f * (mInputRect.height() - newH);
        float max = 2f * Math.max(mImageWidth, mImageHeight);
        if (Math.min(newW, newH) >= 4f && Math.max(newW, newH) <= max) {
            mInputRect.inset(dx, dy);
            invalidate();
        }
    }

    public void moveImage(float distanceX, float distanceY) {
        if (!mImageOK)
            return;
        float dx = (mInputRect.width() * distanceX) / mOutputRect.width();
        float dy = (mInputRect.height() * distanceY) / mOutputRect.height();
        float min_w = mInputRect.width() * 0.1f;
        float min_h = mInputRect.height() * 0.1f;
        dx = Math.max(min_w, mInputRect.right + dx) - mInputRect.right;
        dy = Math.max(min_h, mInputRect.bottom + dy) - mInputRect.bottom;
        dx = Math.min(mImageWidth - min_w, mInputRect.left + dx) - mInputRect.left;
        dy = Math.min(mImageHeight - min_h, mInputRect.top + dy) - mInputRect.top;
        mInputRect.offset(dx, dy);
        invalidate();
    }

    @Override
    public boolean onTouchEvent(@NonNull MotionEvent e) {
        boolean consumed = false;
        if (mLongPress) {
            switch (e.getAction()) {
                case MotionEvent.ACTION_MOVE:
                    mLabelCollection.moveLabel(e.getX(), e.getY());
                    invalidate();
                    consumed = true;
                    break;
                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_CANCEL:
                    mLabelCollection.moveLabelEnd();
                    invalidate();
                    mLongPress = false;
                    consumed = true;
                    break;
            }
        }
        consumed = mScaleDetector.onTouchEvent(e) || consumed;
        return mDetectorCompat.onTouchEvent(e) || consumed || super.onTouchEvent(e);
    }

    @Override
    protected void onSizeChanged(int w, int h, int old_w, int old_h) {
        super.onSizeChanged(w, h, old_w, old_h);
        if (mModeSize != null)
            mOutputRect = Utility.getEmbeddedRect(w, h, mModeSize.width(), mModeSize.height());
        mLabelCollection.update(w, h, Utility.getTextSizeFactor(w, h));
    }

    @Override
    protected void onDraw(@NonNull Canvas canvas) {
        if (mImageOK) {
            maximizeImageToCanvasRect();
            adjustCanvasAndImageRect(getWidth(), getHeight());
            canvas.drawRect(mOutputRect, mBorderPaint);
            drawBitmap(canvas);
        }
        mLabelCollection.draw(canvas);
        drawModeRect(canvas);
    }

    private void maximizeImageToCanvasRect() {
        float l = mOutputRect.left * mInputRect.width() / mOutputRect.width();
        float t = mOutputRect.top * mInputRect.height() / mOutputRect.height();
        float r = (mOutputRect.right - getWidth()) * mInputRect.width() / mOutputRect.width();
        float b = (mOutputRect.bottom - getHeight()) * mInputRect.height() / mOutputRect.height();
        mImageDrawRect.left = Math.round(mInputRect.left - l);
        mImageDrawRect.top = Math.round(mInputRect.top - t);
        mImageDrawRect.right = Math.round(mInputRect.right - r);
        mImageDrawRect.bottom = Math.round(mInputRect.bottom - b);
    }

    private void adjustCanvasAndImageRect(int width, int height) {
        mCanvasDrawRect.set(0, 0, width, height);
        if (mImageDrawRect.left < 0) {
            mCanvasDrawRect.left -= (mImageDrawRect.left * mCanvasDrawRect.width()) / mImageDrawRect.width();
            mImageDrawRect.left = 0;
        }
        if (mImageDrawRect.top < 0) {
            mCanvasDrawRect.top -= (mImageDrawRect.top * mCanvasDrawRect.height()) / mImageDrawRect.height();
            mImageDrawRect.top = 0;
        }
        if (mImageDrawRect.right > mImageWidth) {
            mCanvasDrawRect.right -= ((mImageDrawRect.right - mImageWidth) * mCanvasDrawRect.width()) / mImageDrawRect.width();
            mImageDrawRect.right = mImageWidth;
        }
        if (mImageDrawRect.bottom > mImageHeight) {
            mCanvasDrawRect.bottom -= ((mImageDrawRect.bottom - mImageHeight) * mCanvasDrawRect.height()) / mImageDrawRect.height();
            mImageDrawRect.bottom = mImageHeight;
        }
    }

    private void drawModeRect(Canvas canvas) {
        mRectPaint.setColor(Color.BLUE);
        canvas.drawRect(mOutputRect, mRectPaint);
        mRectPaint.setColor(Color.GREEN);
        drawRectInset(canvas, mOutputRect, -1);
        mRectPaint.setColor(Color.RED);
        drawRectInset(canvas, mOutputRect, -2);
    }

    private void drawRectInset(Canvas canvas, Rect rect, int inset) {
        canvas.drawRect(
                rect.left + inset,
                rect.top + inset,
                rect.right - inset,
                rect.bottom - inset, mRectPaint);
    }

    private Rect getIntRect(RectF rect) {
        return new Rect(
                Math.round(rect.left),
                Math.round(rect.top),
                Math.round(rect.right),
                Math.round(rect.bottom));
    }

    private int getSampleSize() {
        int sx = Math.round(mInputRect.width() / mModeSize.width());
        int sy = Math.round(mInputRect.height() / mModeSize.height());
        int scale = Math.max(1, Math.max(sx, sy));
        return Integer.highestOneBit(scale);
    }

    public Bitmap getBitmap() {
        Bitmap result = Bitmap.createBitmap(mModeSize.width(), mModeSize.height(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(result);
        canvas.drawColor(Color.BLACK);
        if (mImageOK) {
            mImageDrawRect.set(getIntRect(mInputRect));
            adjustCanvasAndImageRect(mModeSize.width(), mModeSize.height());
            drawBitmap(canvas);
        }
        mLabelCollection.draw(canvas, mOutputRect, new Rect(0, 0, mModeSize.width(), mModeSize.height()));
        return result;
    }

    private void drawBitmap(Canvas canvas) {
        canvas.save();
        canvas.rotate(mOrientation);
        rotateDrawRectangles();
        if (!mSmallImage) {
            updateCache();
            mImageDrawRect.offset(-mCacheRect.left, -mCacheRect.top);
            mImageDrawRect.left /= mCacheSampleSize;
            mImageDrawRect.top /= mCacheSampleSize;
            mImageDrawRect.right /= mCacheSampleSize;
            mImageDrawRect.bottom /= mCacheSampleSize;
        }
        canvas.drawBitmap(mCacheBitmap, mImageDrawRect, mCanvasDrawRect, mPaint);
        canvas.restore();
    }

    private void rotateDrawRectangles() {
        int w = mImageWidth;
        int h = mImageHeight;
        for (int i = 0; i < mOrientation / 90; ++i) {
            int tmp = w;
            w = h;
            h = tmp;
            mImageDrawRect.set(
                    mImageDrawRect.top,
                    h - mImageDrawRect.left,
                    mImageDrawRect.bottom,
                    h - mImageDrawRect.right);
            mCanvasDrawRect.set(
                    mCanvasDrawRect.top,
                    -mCanvasDrawRect.right,
                    mCanvasDrawRect.bottom,
                    -mCanvasDrawRect.left);
        }
        mImageDrawRect.sort();
    }

    private void updateCache() {
        int sampleSize = getSampleSize();
        if (sampleSize >= mCacheSampleSize && mCacheRect.contains(mImageDrawRect))
            return;

        if (mCacheBitmap != null)
            mCacheBitmap.recycle();

        int cacheWidth = mImageDrawRect.width();
        int cacheHeight = mImageDrawRect.height();
        while (cacheWidth * cacheHeight < (sampleSize * 1024 * sampleSize * 1024)) {
            cacheWidth += mImageDrawRect.width();
            cacheHeight += mImageDrawRect.height();
        }
        int left = ~(sampleSize - 1) & (mImageDrawRect.centerX() - cacheWidth / 2);
        int top = ~(sampleSize - 1) & (mImageDrawRect.centerY() - cacheHeight / 2);
        int right = ~(sampleSize - 1) & (mImageDrawRect.centerX() + cacheWidth / 2 + sampleSize - 1);
        int bottom = ~(sampleSize - 1) & (mImageDrawRect.centerY() + cacheHeight / 2 + sampleSize - 1);
        mCacheRect.set(
                Math.max(0, left),
                Math.max(0, top),
                Math.min(mRegionDecoder.getWidth(), right),
                Math.min(mRegionDecoder.getHeight(), bottom));
        mBitmapOptions.inSampleSize = mCacheSampleSize = sampleSize;
        mCacheBitmap = mRegionDecoder.decodeRegion(mCacheRect, mBitmapOptions);
    }

    private void editLabelBegin(float x, float y) {
        Label label = mLabelCollection.editLabelBegin(x, y);
        GetActivity().startEditTextActivity(label);
    }

    public void editLabelEnd(Label label) {
        mLabelCollection.editLabelEnd(label);
        invalidate();
    }

    public LabelCollection getLabels() {
        return mLabelCollection;
    }

    private MainActivity GetActivity() {
        MainActivity activity;
        Context context = getContext();
        if (!(context instanceof MainActivity))
            context = ((ContextWrapper) context).getBaseContext();
        activity = (MainActivity) context;
        return activity;
    }
}


================================================
FILE: app/src/main/java/om/sstvencoder/EditTextActivity.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder;

import android.content.Intent;
import android.graphics.Color;
import android.graphics.drawable.Drawable;

import androidx.annotation.ColorInt;
import androidx.fragment.app.DialogFragment;
import androidx.core.content.ContextCompat;
import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.Spinner;

import java.util.List;

import om.sstvencoder.TextOverlay.Label;

public class EditTextActivity extends AppCompatActivity
        implements AdapterView.OnItemSelectedListener, ColorFragment.OnColorSelectedListener {

    private enum EditColorMode {
        None,
        Text,
        Outline
    }

    public static final int REQUEST_CODE = 101;
    public static final String EXTRA = "EDIT_TEXT_EXTRA";
    private Label mLabel;
    private EditColorMode mEditColor;
    private List<String> mFontFamilyNameList;
    private CheckBox mEditItalic, mEditBold, mEditOutline;
    private int mClearTextButtonWidth;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_edit_text);
        mEditColor = EditColorMode.None;
        mEditBold = findViewById(R.id.edit_bold);
        mEditItalic = findViewById(R.id.edit_italic);
        mEditOutline = findViewById(R.id.edit_outline);
    }

    @Override
    protected void onStart() {
        super.onStart();
        mLabel = ((Label) getIntent().getSerializableExtra(EXTRA)).getClone();
        initText();
        initTextSizeSpinner(mLabel.getTextSize());
        mEditBold.setChecked(mLabel.getBold());
        mEditItalic.setChecked(mLabel.getItalic());
        initFontFamilySpinner(mLabel.getFamilyName());
        mEditOutline.setChecked(mLabel.getOutline());
        initOutlineSizeSpinner(mLabel.getOutlineSize());
        findViewById(R.id.edit_color).setBackgroundColor(mLabel.getForeColor());
        findViewById(R.id.edit_outline_color).setBackgroundColor(mLabel.getOutlineColor());
        enableOutline(mEditOutline.isChecked());
    }

    private void initText() {
        EditText editText = findViewById(R.id.edit_text);
        int clearTextIcon = android.R.drawable.ic_menu_close_clear_cancel;
        Drawable drawable = ContextCompat.getDrawable(this, clearTextIcon);
        editText.setText(mLabel.getText());
        editText.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null);
        mClearTextButtonWidth = 2 * drawable.getIntrinsicWidth();

        editText.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
            }

            @Override
            public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
                mLabel.setText(charSequence.toString());
            }

            @Override
            public void afterTextChanged(Editable editable) {
            }
        });

        editText.setOnTouchListener(new View.OnTouchListener() {
            private boolean mClear;

            @Override
            public boolean onTouch(View view, MotionEvent e) {
                switch (e.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        if (HitClearTextButton(view, e)) {
                            mClear = true;
                            return true;
                        }
                        break;
                    case MotionEvent.ACTION_MOVE:
                        if (!HitClearTextButton(view, e))
                            mClear = false;
                        break;
                    case MotionEvent.ACTION_UP:
                        if (HitClearTextButton(view, e) && mClear) {
                            ((EditText) view).setText("");
                            return true;
                        }
                        mClear = false;
                        break;
                    case MotionEvent.ACTION_CANCEL:
                        mClear = false;
                        break;
                }
                return false;
            }

            private boolean HitClearTextButton(View view, MotionEvent e) {
                int left = view.getRight() - mClearTextButtonWidth;
                return left < e.getX();
            }
        });
    }

    private void initFontFamilySpinner(String familyName) {
        Spinner spinner = findViewById(R.id.edit_font_family);
        spinner.setOnItemSelectedListener(this);
        mFontFamilyNameList = Utility.getSystemFontFamilyList();
        spinner.setAdapter(new ArrayAdapter<>(this,
                android.R.layout.simple_spinner_dropdown_item, mFontFamilyNameList));
        spinner.setSelection(mFontFamilyNameList.indexOf(familyName));
    }

    private void initTextSizeSpinner(float textSize) {
        Spinner spinner = findViewById(R.id.edit_text_size);
        spinner.setOnItemSelectedListener(this);
        String[] sizeList = new String[]
                {
                        getString(R.string.font_size_small),
                        getString(R.string.font_size_normal),
                        getString(R.string.font_size_large),
                        getString(R.string.font_size_huge)
                };
        spinner.setAdapter(new ArrayAdapter<>(this,
                android.R.layout.simple_spinner_dropdown_item, sizeList));
        spinner.setSelection(textSizeToPosition(textSize));
    }

    private void initOutlineSizeSpinner(float outlineSize) {
        Spinner spinner = findViewById(R.id.edit_outline_size);
        spinner.setOnItemSelectedListener(this);
        String[] sizeList = new String[]
                {
                        getString(R.string.outline_size_thin),
                        getString(R.string.outline_size_normal),
                        getString(R.string.outline_size_thick)
                };
        spinner.setAdapter(new ArrayAdapter<>(this,
                android.R.layout.simple_spinner_dropdown_item, sizeList));
        spinner.setSelection(outlineSizeToPosition(outlineSize));
    }

    private int textSizeToPosition(float textSize) {
        int position = (int) (textSize - 1f);
        if (0 <= position && position <= 3)
            return position;
        mLabel.setTextSize(Label.TEXT_SIZE_NORMAL);
        return 1;
    }

    private float positionToTextSize(int position) {
        return position + 1f;
    }

    private int outlineSizeToPosition(float outlineSize) {
        int position = (int) (outlineSize * 2f / Label.OUTLINE_SIZE_NORMAL - 1f);
        if (0 <= position && position <= 2)
            return position;
        mLabel.setOutlineSize(Label.OUTLINE_SIZE_NORMAL);
        return 1;
    }

    private float positionToOutlineSize(int position) {
        return Label.OUTLINE_SIZE_NORMAL * 0.5f * (position + 1f);
    }

    @Override
    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
        int parentId = parent.getId();
        if (parentId == R.id.edit_text_size) {
            mLabel.setTextSize(positionToTextSize(position));
        }
        else if (parentId == R.id.edit_outline_size) {
            mLabel.setOutlineSize(positionToOutlineSize(position));
        }
        else if (parentId == R.id.edit_font_family) {
            mLabel.setFamilyName(mFontFamilyNameList.get(position));
        }
    }

    private void enableOutline(boolean enabled) {
        findViewById(R.id.text_outline_size).setEnabled(enabled);
        findViewById(R.id.edit_outline_size).setEnabled(enabled);
        findViewById(R.id.text_outline_color).setEnabled(enabled);
        findViewById(R.id.edit_outline_color).setEnabled(enabled);
        @ColorInt
        int color = enabled ? mLabel.getOutlineColor() : Color.DKGRAY;
        findViewById(R.id.edit_outline_color).setBackgroundColor(color);
    }

    @Override
    public void onNothingSelected(AdapterView<?> parent) {
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_edit_text, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();
        if (id == R.id.action_done) {
            done();
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    public void onBoldClick(View view) {
        mLabel.setBold(mEditBold.isChecked());
    }

    public void onItalicClick(View view) {
        mLabel.setItalic(mEditItalic.isChecked());
    }

    public void onOutlineClick(View view) {
        if (view.getId() == R.id.text_outline)
            mEditOutline.setChecked(!mEditOutline.isChecked());
        boolean outline = mEditOutline.isChecked();
        mLabel.setOutline(outline);
        enableOutline(outline);
    }

    public void onColorClick(View view) {
        showColorDialog(R.string.color, mLabel.getForeColor());
        mEditColor = EditColorMode.Text;
    }

    public void onOutlineColorClick(View view) {
        if (mEditOutline.isChecked()) {
            showColorDialog(R.string.outline_color, mLabel.getOutlineColor());
            mEditColor = EditColorMode.Outline;
        }
    }

    private void showColorDialog(int title, int color) {
        ColorFragment fragment = new ColorFragment();
        fragment.setTitle(title);
        fragment.setColor(color);
        fragment.addOnColorSelectedListener(this);
        fragment.show(getSupportFragmentManager(), ColorFragment.class.getName());
    }

    @Override
    public void onColorSelected(DialogFragment fragment, int color) {
        switch (mEditColor) {
            case Text:
                mLabel.setForeColor(color);
                findViewById(R.id.edit_color).setBackgroundColor(color);
                break;
            case Outline:
                mLabel.setOutlineColor(color);
                findViewById(R.id.edit_outline_color).setBackgroundColor(color);
                break;
        }
        mEditColor = EditColorMode.None;
    }

    @Override
    public void onCancel(DialogFragment fragment) {
        mEditColor = EditColorMode.None;
    }

    private void done() {
        Intent intent = new Intent();
        intent.putExtra(EXTRA, mLabel);
        setResult(RESULT_OK, intent);
        finish();
    }
}


================================================
FILE: app/src/main/java/om/sstvencoder/Encoder.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder;

import android.graphics.Bitmap;

import java.util.LinkedList;
import java.util.List;

import om.sstvencoder.ModeInterfaces.IMode;
import om.sstvencoder.ModeInterfaces.IModeInfo;
import om.sstvencoder.Modes.ModeFactory;
import om.sstvencoder.Output.IOutput;
import om.sstvencoder.Output.OutputFactory;
import om.sstvencoder.Output.WaveFileOutputContext;

// Creates IMode instance
class Encoder {
    private final MainActivityMessenger mMessenger;
    private final Thread mThread;
    private Thread mSaveWaveThread;
    private final List<IMode> mQueue;
    private final ProgressBarWrapper mProgressBar, mProgressBar2;
    private boolean mQuit, mStop;
    private Class<?> mModeClass;

    Encoder(MainActivityMessenger messenger,
            ProgressBarWrapper progressBar, ProgressBarWrapper progressBar2) {
        mMessenger = messenger;
        mProgressBar = progressBar;
        mProgressBar2 = progressBar2;
        mQueue = new LinkedList<>();
        mQuit = false;
        mStop = false;
        mModeClass = ModeFactory.getDefaultMode();

        mThread = new Thread() {
            @Override
            public void run() {
                while (true) {
                    IMode mode;
                    synchronized (this) {
                        while (mQueue.isEmpty() && !mQuit) {
                            try {
                                wait();
                            } catch (Exception ignore) {
                            }
                        }
                        if (mQuit)
                            return;

                        mStop = false;
                        mode = mQueue.remove(0);
                    }
                    mode.init();
                    mProgressBar.begin(mode.getProcessCount(),
                            mMessenger.getString(R.string.progressbar_message_sending));

                    while (mode.process()) {
                        mProgressBar.step();

                        synchronized (this) {
                            if (mQuit || mStop)
                                break;
                        }
                    }
                    mode.finish(mStop);
                    mProgressBar.end();
                }
            }
        };
        mThread.start();
    }

    boolean setMode(String className) {
        try {
            mModeClass = Class.forName(className);
        } catch (Exception ignore) {
            return false;
        }
        return true;
    }

    IModeInfo getModeInfo() {
        return ModeFactory.getModeInfo(mModeClass);
    }

    IModeInfo[] getModeInfoList() {
        return ModeFactory.getModeInfoList();
    }

    void play(Bitmap bitmap) {
        IOutput output = OutputFactory.createOutputForSending();
        IMode mode = ModeFactory.CreateMode(mModeClass, bitmap, output);
        if (mode != null)
            enqueue(mode);
    }

    void save(Bitmap bitmap, WaveFileOutputContext context) {
        if (mSaveWaveThread != null && mSaveWaveThread.isAlive())
            return;
        IOutput output = OutputFactory.createOutputForSavingAsWave(context);
        IMode mode = ModeFactory.CreateMode(mModeClass, bitmap, output);
        if (mode != null)
            save(mode, context);
    }

    private void save(final IMode mode, final WaveFileOutputContext context) {
        mSaveWaveThread = new Thread() {
            @Override
            public void run() {
                mode.init();
                mProgressBar2.begin(mode.getProcessCount(),
                        mMessenger.getString(R.string.progressbar_message_saving_to_file, context.getFileName()));

                while (mode.process()) {
                    mProgressBar2.step();

                    synchronized (this) {
                        if (mQuit)
                            break;
                    }
                }
                mode.finish(mQuit);
                mProgressBar2.end();
                if (!mQuit)
                    mMessenger.carrySaveAsWaveIsDoneMessage(context);
            }
        };
        mSaveWaveThread.start();
    }

    void stop() {
        synchronized (mThread) {
            mStop = true;
            int size = mQueue.size();
            for (int i = 0; i < size; ++i)
                mQueue.remove(0).finish(true);
        }
    }

    private void enqueue(IMode mode) {
        synchronized (mThread) {
            mQueue.add(mode);
            mThread.notify();
        }
    }

    void destroy() {
        synchronized (mThread) {
            mQuit = true;
            mThread.notify();
        }
    }
}


================================================
FILE: app/src/main/java/om/sstvencoder/FontFamilySet.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder;

import androidx.annotation.NonNull;

import android.content.Context;
import android.util.Xml;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

class FontFamilySet {
    class FontFamily {
        String name;
        String displayName;
        boolean bold;
        boolean italic;
    }

    private final List<FontFamily> mFamilySet;
    private final Context mContext;

    FontFamilySet(Context context) {
        mContext = context;
        mFamilySet = new ArrayList<>();
        fillWithSystemFonts(mFamilySet);
        if (mFamilySet.size() == 0)
            mFamilySet.add(getDefaultFontFamily());
    }

    @NonNull
    private FontFamily getDefaultFontFamily() {
        FontFamily defaultFontFamily = new FontFamily();
        defaultFontFamily.name = null;
        defaultFontFamily.displayName = mContext.getString(R.string.font_default);
        defaultFontFamily.bold = true;
        defaultFontFamily.italic = true;
        return defaultFontFamily;
    }

    @NonNull
    FontFamily getFontFamily(String name) {
        if (name != null) {
            for (FontFamily fontFamily : mFamilySet) {
                if (name.equals(fontFamily.name))
                    return fontFamily;
            }
        }
        return mFamilySet.get(0);
    }

    @NonNull
    FontFamily getFontFamilyFromDisplayName(@NonNull String displayName) {
        for (FontFamily fontFamily : mFamilySet) {
            if (displayName.equals(fontFamily.displayName))
                return fontFamily;
        }
        return mFamilySet.get(0);
    }

    @NonNull
    List<String> getFontFamilyDisplayNameList() {
        List<String> names = new ArrayList<>();
        for (FontFamily fontFamily : mFamilySet)
            names.add(fontFamily.displayName);
        return names;
    }

    private void fillWithSystemFonts(@NonNull List<FontFamily> familySet) {
        File fontsFile = new File("/system/etc/system_fonts.xml");
        if (!fontsFile.exists()) {
            fontsFile = new File("/system/etc/fonts.xml");
            if (!fontsFile.exists())
                return;
        }
        InputStream in = null;
        try {
            in = new FileInputStream(fontsFile);
            XmlPullParser parser = Xml.newPullParser();
            parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
            parser.setInput(in, null);
            parser.next();
            if (parser.getEventType() == XmlPullParser.START_TAG && parser.getName().equals("familyset"))
                readFamilySet(parser, familySet);
        } catch (Exception ignore) {
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (Exception ignore) {
                }
            }
        }
    }

    private void readFamilySet(@NonNull XmlPullParser parser, @NonNull List<FontFamily> familySet)
            throws XmlPullParserException, IOException {
        while (parser.next() != XmlPullParser.END_TAG) {
            if (parser.getEventType() == XmlPullParser.START_TAG && parser.getName().equals("family")) {
                FontFamily fontFamily = readFamily(parser);
                if (fontFamily.displayName != null)
                    familySet.add(fontFamily);
            }
        }
    }

    @NonNull
    private FontFamily readFamily(@NonNull XmlPullParser parser)
            throws XmlPullParserException, IOException {
        FontFamily fontFamily = new FontFamily();

        while (parser.next() != XmlPullParser.END_TAG) {
            if (parser.getEventType() == XmlPullParser.START_TAG) {
                switch (parser.getName()) {
                    case "nameset":
                        readNameSet(parser, fontFamily);
                        break;
                    case "fileset":
                        readFileSet(parser, fontFamily);
                        break;
                }
            }
        }
        return fontFamily;
    }

    private void readNameSet(@NonNull XmlPullParser parser, @NonNull FontFamily fontFamily)
            throws XmlPullParserException, IOException {
        while (parser.next() != XmlPullParser.END_TAG) {
            if (parser.getEventType() == XmlPullParser.START_TAG && parser.getName().equals("name")) {
                if (fontFamily.name == null)
                    fontFamily.name = readText(parser);
                else {
                    // skip all other names
                    parser.next();
                    parser.next();
                }
            }
        }
    }

    private void readFileSet(@NonNull XmlPullParser parser, @NonNull FontFamily fontFamily)
            throws XmlPullParserException, IOException {
        while (parser.next() != XmlPullParser.END_TAG) {
            if (parser.getEventType() == XmlPullParser.START_TAG && parser.getName().equals("file"))
                parseDisplayNameAndStyle(readText(parser), fontFamily);
        }
    }

    private void parseDisplayNameAndStyle(String fontFileName, @NonNull FontFamily fontFamily) {
        // Example: RobotoCondensed-LightItalic.ttf
        // { "RobotoCondensed", "LightItalic" }
        String[] familyInfo = fontFileName.split("\\.")[0].split("-");
        String s = "";
        if (familyInfo.length > 1) {
            s = familyInfo[1];
            if (s.contains("Bold"))
                fontFamily.bold = true;
            if (s.contains("Italic"))
                fontFamily.italic = true;
        }
        if (fontFamily.displayName == null) {
            // "Light"
            s = s.replace("Regular", "").replace("Bold", "").replace("Italic", "");
            // "Roboto Condensed Light"
            fontFamily.displayName = (familyInfo[0] + s).replaceAll("(\\p{Ll})(\\p{Lu})", "$1 $2");
        }
    }

    private String readText(@NonNull XmlPullParser parser)
            throws IOException, XmlPullParserException {
        String text = "";
        if (parser.next() == XmlPullParser.TEXT) {
            text = parser.getText();
            parser.next();
        }
        return text;
    }
}


================================================
FILE: app/src/main/java/om/sstvencoder/MainActivity.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder;

import android.Manifest;
import android.app.AlertDialog;
import android.content.ContentResolver;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import androidx.exifinterface.media.ExifInterface;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.MediaStore;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.appcompat.app.AppCompatActivity;
import android.system.ErrnoException;
import android.system.OsConstants;
import android.view.Menu;
import android.view.MenuItem;
import android.view.SubMenu;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;

import java.io.InputStream;

import om.sstvencoder.ModeInterfaces.IModeInfo;
import om.sstvencoder.Output.WaveFileOutputContext;
import om.sstvencoder.TextOverlay.Label;

public class MainActivity extends AppCompatActivity {
    private static final String CLASS_NAME = "ClassName";
    private static final int REQUEST_LOAD_IMAGE_PERMISSION = 1;
    private static final int REQUEST_SAVE_WAVE_PERMISSION = 2;
    private static final int REQUEST_IMAGE_CAPTURE_PERMISSION = 3;
    private static final int REQUEST_PICK_IMAGE = 11;
    private static final int REQUEST_IMAGE_CAPTURE = 12;
    private Settings mSettings;
    private TextOverlayTemplate mTextOverlayTemplate;
    private CropView mCropView;
    private Encoder mEncoder;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mCropView = findViewById(R.id.cropView);
        mEncoder = new Encoder(new MainActivityMessenger(this), getProgressBar(), getProgressBar2());

        mSettings = new Settings(this);
        mSettings.load();

        mTextOverlayTemplate = new TextOverlayTemplate();
        mTextOverlayTemplate.load(mCropView.getLabels(), mSettings.getTextOverlayFile());

        setMode(mSettings.getModeClassName());
        loadImage(getIntent());
    }

    private ProgressBarWrapper getProgressBar() {
        ProgressBar progressBar = findViewById(R.id.progressBar);
        TextView progressBarText = findViewById(R.id.progressBarText);
        return new ProgressBarWrapper(progressBar, progressBarText);
    }

    private ProgressBarWrapper getProgressBar2() {
        ProgressBar progressBar = findViewById(R.id.progressBar2);
        TextView progressBarText = findViewById(R.id.progressBarText2);
        return new ProgressBarWrapper(progressBar, progressBarText);
    }

    @Override
    protected void onNewIntent(Intent intent) {
        loadImage(intent);
        super.onNewIntent(intent);
    }

    private void loadImage(Intent intent) {
        Uri uri = getImageUriFromIntent(intent);
        boolean verbose = true;
        if (uri == null) {
            // SecurityException in loadImage for Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
            // uri = mSettings.getImageUri();
            verbose = false;
        }
        loadImage(uri, verbose);
    }

    private Uri getImageUriFromIntent(Intent intent) {
        Uri uri = null;
        if (isIntentTypeValid(intent.getType()) && isIntentActionValid(intent.getAction())) {
            uri = intent.hasExtra(Intent.EXTRA_STREAM) ?
                    (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM) : intent.getData();
            if (uri == null) {
                String s = getString(R.string.load_img_err_txt_unsupported);
                showErrorMessage(getString(R.string.load_img_err_title), s, s + "\n\n" + intent);
            }
        }
        return uri;
    }

    // Set verbose to false for any Uri that might have expired (e.g. shared from browser).
    private boolean loadImage(Uri uri, boolean verbose) {
        boolean succeeded = false;
        ContentResolver resolver = getContentResolver();
        if (uri != null) {
            try {
                InputStream stream = resolver.openInputStream(uri);
                if (stream != null)
                    mCropView.setBitmap(stream);
                succeeded = true;
            } catch (Exception ex) { // e.g. FileNotFoundException, SecurityException
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && isPermissionException(ex)
                        && needsRequestReadPermission()) {
                    requestReadPermission(REQUEST_LOAD_IMAGE_PERMISSION);
                }
                else
                    showFileNotLoadedMessage(ex, verbose);
            }
        }
        if (succeeded) {
            mCropView.rotateImage(getOrientation(resolver, uri));
            mSettings.setImageUri(uri);
        }
        else
            setDefaultBitmap();
        return succeeded;
    }

    private void setDefaultBitmap() {
        try {
            mCropView.setBitmap(getResources().openRawResource(R.raw.smpte_color_bars));
        } catch (Exception ignore) {
            mCropView.setNoBitmap();
        }
        mSettings.setImageUri(null);
    }

    private boolean isIntentActionValid(String action) {
        return Intent.ACTION_SEND.equals(action);
    }

    private boolean isIntentTypeValid(String type) {
        return type != null && type.startsWith("image/");
    }

    private boolean isPermissionException(Exception ex) {
        return ex.getCause() instanceof ErrnoException
                && ((ErrnoException) ex.getCause()).errno == OsConstants.EACCES;
    }

    private boolean needsRequestReadPermission() {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
            return false;
        String permission = Manifest.permission.READ_EXTERNAL_STORAGE;
        int state = ContextCompat.checkSelfPermission(this, permission);
        return state != PackageManager.PERMISSION_GRANTED;
    }

    private boolean needsRequestWritePermission() {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M ||
                Build.VERSION.SDK_INT > Build.VERSION_CODES.Q)
            return false;
        String permission = Manifest.permission.WRITE_EXTERNAL_STORAGE;
        int state = ContextCompat.checkSelfPermission(this, permission);
        return state != PackageManager.PERMISSION_GRANTED;
    }

    private void requestReadPermission(int requestCode) {
        String[] permissions = new String[]{Manifest.permission.READ_EXTERNAL_STORAGE};
        ActivityCompat.requestPermissions(this, permissions, requestCode);
    }

    private void requestWritePermission(int requestCode) {
        String[] permissions = new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE};
        ActivityCompat.requestPermissions(this, permissions, requestCode);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode,
                                           @NonNull String[] permissions,
                                           @NonNull int[] grantResults) {
        switch (requestCode) {
            case REQUEST_LOAD_IMAGE_PERMISSION:
                if (permissionGranted(grantResults))
                    loadImage(mSettings.getImageUri(), false);
                else
                    setDefaultBitmap();
                break;
            case REQUEST_IMAGE_CAPTURE_PERMISSION:
                if (permissionGranted(grantResults))
                    dispatchTakePictureIntent();
                break;
            case REQUEST_SAVE_WAVE_PERMISSION:
                if (permissionGranted(grantResults))
                    save();
                break;
            default:
                break;
        }
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }

    private boolean permissionGranted(@NonNull int[] grantResults) {
        return grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
    }

    private void showFileNotLoadedMessage(Exception ex, boolean verbose) {
        String s;
        if (verbose)
            s = getString(R.string.load_img_err_title) + ": \n" + ex.getMessage();
        else
            s = getString(R.string.message_prev_img_not_loaded);
        Toast.makeText(this, s, Toast.LENGTH_LONG).show();
    }

    private void showErrorMessage(final String title, final String shortText, final String longText) {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle(title);
        builder.setMessage(shortText);
        builder.setNeutralButton(getString(R.string.btn_ok), null);
        builder.setPositiveButton(getString(R.string.btn_send_email), new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                String device = Build.MANUFACTURER + ", " + Build.BRAND + ", " + Build.MODEL + ", " + Build.VERSION.RELEASE;
                String text = longText + "\n\n" + BuildConfig.VERSION_NAME + ", " + device;
                Intent intent = Utility.createEmailIntent(getString(R.string.email_subject), text);
                startActivity(Intent.createChooser(intent, getString(R.string.chooser_title)));
            }
        });
        builder.show();
    }

    private void showOrientationErrorMessage(Uri uri, Exception ex) {
        String title = getString(R.string.load_img_orientation_err_title);
        String longText = title + "\n\n" + Utility.createMessage(ex) + "\n\n" + uri;
        showErrorMessage(title, ex.getMessage(), longText);
    }

    public int getOrientation(ContentResolver resolver, Uri uri) {
        int orientation = 0;
        try {
            Cursor cursor = resolver.query(uri,
                    new String[]{MediaStore.Images.ImageColumns.ORIENTATION},
                    null, null, null);
            if (cursor.moveToFirst())
                orientation = cursor.getInt(0);
            cursor.close();
        } catch (Exception ignore) {
            orientation = getExifOrientation(resolver, uri);
        }
        return orientation;
    }

    private int getExifOrientation(ContentResolver resolver, Uri uri) {
        int orientation = 0;
        InputStream in = null;
        try {
            in = resolver.openInputStream(uri);
            int orientationAttribute = (new ExifInterface(in)).getAttributeInt(ExifInterface.TAG_ORIENTATION, 0);
            orientation = Utility.convertToDegrees(orientationAttribute);
        } catch (Exception ex) {
            showOrientationErrorMessage(uri, ex);
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (Exception ignore) {
                }
            }
        }
        return orientation;
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_main, menu);
        createModesMenu(menu);
        return true;
    }

    private void createModesMenu(Menu menu) {
        SubMenu modesSubMenu = menu.findItem(R.id.action_modes).getSubMenu();
        IModeInfo[] modeInfoList = mEncoder.getModeInfoList();
        for (IModeInfo modeInfo : modeInfoList) {
            MenuItem item = modesSubMenu.add(modeInfo.getModeName());
            Intent intent = new Intent();
            intent.putExtra(CLASS_NAME, modeInfo.getModeClassName());
            item.setIntent(intent);
        }
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();
        if (id == R.id.action_pick_picture) {
            dispatchPickPictureIntent();
        }
        else if (id == R.id.action_take_picture) {
            takePicture();
        }
        else if (id == R.id.action_save_wave) {
            if (needsRequestWritePermission())
                requestWritePermission(REQUEST_SAVE_WAVE_PERMISSION);
            else
                save();
        }
        else if (id == R.id.action_play) {
            play();
        }
        else if (id == R.id.action_stop) {
            mEncoder.stop();
        }
        else if (id == R.id.action_rotate) {
            mCropView.rotateImage(90);
        }
        else if (id == R.id.action_reset) {
            mCropView.resetImage();
        }
        else if (id == R.id.action_privacy_policy) {
            openLinkInBrowser("https://sites.google.com/view/olgamiller/sstvencoder/privacypolicy/");
        }
        else if (id == R.id.action_about) {
            showTextPage(getString(R.string.action_about), getString(R.string.action_about_text, BuildConfig.VERSION_NAME));
        }
        else if (id != R.id.action_modes && id != R.id.action_transform) {
            String className = item.getIntent().getStringExtra(CLASS_NAME);
            setMode(className);
        }
        return true;
    }

    private void openLinkInBrowser(String link) {
        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(link));
        if (intent.resolveActivity(getPackageManager()) != null) {
            startActivity(intent);
        }
        else {
            showErrorMessage(getString(R.string.another_activity_start_err), link, "");
        }
    }

    private void showTextPage(String title, String message) {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle(title);
        builder.setMessage(message);
        builder.setNeutralButton(R.string.btn_ok, null);
        builder.show();
    }

    private void setMode(String modeClassName) {
        if (mEncoder.setMode(modeClassName)) {
            IModeInfo modeInfo = mEncoder.getModeInfo();
            mCropView.setModeSize(modeInfo.getModeSize());
            setTitle(modeInfo.getModeName());
            mSettings.setModeClassName(modeClassName);
        }
    }

    private void takePicture() {
        if (!hasCamera()) {
            Toast.makeText(this, getString(R.string.message_no_camera), Toast.LENGTH_LONG).show();
            return;
        }
        if (needsRequestWritePermission())
            requestWritePermission(REQUEST_IMAGE_CAPTURE_PERMISSION);
        else
            dispatchTakePictureIntent();
    }

    private boolean hasCamera() {
        return getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
    }

    public void startEditTextActivity(@NonNull Label label) {
        Intent intent = new Intent(this, EditTextActivity.class);
        intent.putExtra(EditTextActivity.EXTRA, label);
        tryToStartActivityForResult(intent, EditTextActivity.REQUEST_CODE);
    }

    private void dispatchTakePictureIntent() {
        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        Uri uri = Utility.createImageUri(this);
        if (uri != null) {
            mSettings.setImageUri(uri);
            intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
            tryToStartActivityForResult(intent, REQUEST_IMAGE_CAPTURE);
        }
    }

    private void dispatchPickPictureIntent() {
        Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
        tryToStartActivityForResult(intent, REQUEST_PICK_IMAGE);
    }

    private void tryToStartActivityForResult(Intent intent, int requestCode) {
        if (intent.resolveActivity(getPackageManager()) == null) {
            Toast.makeText(this, R.string.another_activity_resolve_err, Toast.LENGTH_LONG).show();
            return;
        }
        try {
            startActivityForResult(intent, requestCode);
        } catch (Exception ignore) {
            Toast.makeText(this, R.string.another_activity_start_err, Toast.LENGTH_LONG).show();
        }
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        switch (requestCode) {
            case EditTextActivity.REQUEST_CODE:
                Label label = null;
                if (resultCode == RESULT_OK && data != null)
                    label = (Label) data.getSerializableExtra(EditTextActivity.EXTRA);
                mCropView.editLabelEnd(label);
                break;
            case REQUEST_IMAGE_CAPTURE:
                if (resultCode == RESULT_OK) {
                    Uri uri = mSettings.getImageUri();
                    if (loadImage(uri, true))
                        addImageToGallery(uri);
                }
                break;
            case REQUEST_PICK_IMAGE:
                if (resultCode == RESULT_OK && data != null)
                    loadImage(data.getData(), true);
                break;
            default:
                super.onActivityResult(requestCode, resultCode, data);
                break;
        }
    }

    private void addImageToGallery(Uri uri) {
        Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
        intent.setData(uri);
        sendBroadcast(intent);
    }

    private void play() {
        mEncoder.play(mCropView.getBitmap());
    }

    private void save() {
        if (Utility.isExternalStorageWritable()) {
            WaveFileOutputContext context
                    = new WaveFileOutputContext(getContentResolver(), Utility.createWaveFileName());
            mEncoder.save(mCropView.getBitmap(), context);
        }
    }

    public void completeSaving(WaveFileOutputContext context) {
        context.update();
    }

    @Override
    protected void onPause() {
        super.onPause();
        mSettings.save();
        mTextOverlayTemplate.save(mCropView.getLabels(), mSettings.getTextOverlayFile());
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mEncoder.destroy();
    }
}

================================================
FILE: app/src/main/java/om/sstvencoder/MainActivityMessenger.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder;

import android.os.Handler;

import om.sstvencoder.Output.WaveFileOutputContext;

class MainActivityMessenger {
    private final MainActivity mMainActivity;
    private final Handler mHandler;

    MainActivityMessenger(MainActivity activity) {
        mMainActivity = activity;
        mHandler = new Handler();
    }

    void carrySaveAsWaveIsDoneMessage(final WaveFileOutputContext context) {
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                mMainActivity.completeSaving(context);
            }
        });
    }

    public String getString(int resId, Object... formatArgs) {
        return mMainActivity.getString(resId, formatArgs);
    }
}


================================================
FILE: app/src/main/java/om/sstvencoder/ModeInterfaces/IMode.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.ModeInterfaces;

public interface IMode {
    void init();

    int getProcessCount();

    boolean process();

    void finish(boolean cancel);
}


================================================
FILE: app/src/main/java/om/sstvencoder/ModeInterfaces/IModeInfo.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.ModeInterfaces;

public interface IModeInfo {
    String getModeName();

    String getModeClassName();

    ModeSize getModeSize();
}


================================================
FILE: app/src/main/java/om/sstvencoder/ModeInterfaces/ModeSize.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.ModeInterfaces;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ModeSize {
    int width();

    int height();
}

================================================
FILE: app/src/main/java/om/sstvencoder/Modes/ImageFormats/NV21.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.Modes.ImageFormats;

import android.graphics.Bitmap;

class NV21 extends Yuv {
    NV21(Bitmap bitmap) {
        super(bitmap);
    }

    protected void convertBitmapToYuv(Bitmap bitmap) {
        mYuv = new byte[(3 * mWidth * mHeight) / 2];
        int pos = 0;

        for (int h = 0; h < mHeight; ++h)
            for (int w = 0; w < mWidth; ++w)
                mYuv[pos++] = (byte) YuvConverter.convertToY(bitmap.getPixel(w, h));

        for (int h = 0; h < mHeight; h += 2) {
            for (int w = 0; w < mWidth; w += 2) {
                int v0 = YuvConverter.convertToV(bitmap.getPixel(w, h));
                int v1 = YuvConverter.convertToV(bitmap.getPixel(w + 1, h));
                int v2 = YuvConverter.convertToV(bitmap.getPixel(w, h + 1));
                int v3 = YuvConverter.convertToV(bitmap.getPixel(w + 1, h + 1));
                mYuv[pos++] = (byte) ((v0 + v1 + v2 + v3) / 4);
                int u0 = YuvConverter.convertToU(bitmap.getPixel(w, h));
                int u1 = YuvConverter.convertToU(bitmap.getPixel(w + 1, h));
                int u2 = YuvConverter.convertToU(bitmap.getPixel(w, h + 1));
                int u3 = YuvConverter.convertToU(bitmap.getPixel(w + 1, h + 1));
                mYuv[pos++] = (byte) ((u0 + u1 + u2 + u3) / 4);
            }
        }
    }

    public int getY(int x, int y) {
        return 255 & mYuv[mWidth * y + x];
    }

    public int getU(int x, int y) {
        return 255 & mYuv[mWidth * mHeight + mWidth * (y >> 1) + (x | 1)];
    }

    public int getV(int x, int y) {
        return 255 & mYuv[mWidth * mHeight + mWidth * (y >> 1) + (x & ~1)];
    }
}


================================================
FILE: app/src/main/java/om/sstvencoder/Modes/ImageFormats/YUV440P.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.Modes.ImageFormats;

import android.graphics.Bitmap;

class YUV440P extends Yuv {
    YUV440P(Bitmap bitmap) {
        super(bitmap);
    }

    protected void convertBitmapToYuv(Bitmap bitmap) {
        mYuv = new byte[2 * mWidth * mHeight];
        int pos = 0;

        for (int h = 0; h < mHeight; ++h)
            for (int w = 0; w < mWidth; ++w)
                mYuv[pos++] = (byte) YuvConverter.convertToY(bitmap.getPixel(w, h));

        for (int h = 0; h < mHeight; h += 2) {
            for (int w = 0; w < mWidth; ++w) {
                int u0 = YuvConverter.convertToU(bitmap.getPixel(w, h));
                int u1 = YuvConverter.convertToU(bitmap.getPixel(w, h + 1));
                mYuv[pos++] = (byte) ((u0 + u1) / 2);
            }
        }

        for (int h = 0; h < mHeight; h += 2) {
            for (int w = 0; w < mWidth; ++w) {
                int v0 = YuvConverter.convertToV(bitmap.getPixel(w, h));
                int v1 = YuvConverter.convertToV(bitmap.getPixel(w, h + 1));
                mYuv[pos++] = (byte) ((v0 + v1) / 2);
            }
        }
    }

    public int getY(int x, int y) {
        return 255 & mYuv[mWidth * y + x];
    }

    public int getU(int x, int y) {
        return 255 & mYuv[mWidth * mHeight + mWidth * (y >> 1) + x];
    }

    public int getV(int x, int y) {
        return 255 & mYuv[((3 * mWidth * mHeight) >> 1) + mWidth * (y >> 1) + x];
    }
}


================================================
FILE: app/src/main/java/om/sstvencoder/Modes/ImageFormats/YUY2.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.Modes.ImageFormats;

import android.graphics.Bitmap;

class YUY2 extends Yuv {
    YUY2(Bitmap bitmap) {
        super(bitmap);
    }

    protected void convertBitmapToYuv(Bitmap bitmap) {
        mYuv = new byte[2 * mWidth * mHeight];

        for (int pos = 0, h = 0; h < mHeight; ++h) {
            for (int w = 0; w < mWidth; w += 2) {
                mYuv[pos++] = (byte) YuvConverter.convertToY(bitmap.getPixel(w, h));
                int u0 = YuvConverter.convertToU(bitmap.getPixel(w, h));
                int u1 = YuvConverter.convertToU(bitmap.getPixel(w + 1, h));
                mYuv[pos++] = (byte) ((u0 + u1) / 2);
                mYuv[pos++] = (byte) YuvConverter.convertToY(bitmap.getPixel(w + 1, h));
                int v0 = YuvConverter.convertToV(bitmap.getPixel(w, h));
                int v1 = YuvConverter.convertToV(bitmap.getPixel(w + 1, h));
                mYuv[pos++] = (byte) ((v0 + v1) / 2);
            }
        }
    }

    public int getY(int x, int y) {
        return 255 & mYuv[2 * mWidth * y + 2 * x];
    }

    public int getU(int x, int y) {
        return 255 & mYuv[2 * mWidth * y + (((x & ~1) << 1) | 1)];
    }

    public int getV(int x, int y) {
        return 255 & mYuv[2 * mWidth * y + ((x << 1) | 3)];
    }
}


================================================
FILE: app/src/main/java/om/sstvencoder/Modes/ImageFormats/YV12.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.Modes.ImageFormats;

import android.graphics.Bitmap;

class YV12 extends Yuv {
    YV12(Bitmap bitmap) {
        super(bitmap);
    }

    protected void convertBitmapToYuv(Bitmap bitmap) {
        mYuv = new byte[(3 * mWidth * mHeight) / 2];
        int pos = 0;

        for (int h = 0; h < mHeight; ++h)
            for (int w = 0; w < mWidth; ++w)
                mYuv[pos++] = (byte) YuvConverter.convertToY(bitmap.getPixel(w, h));

        for (int h = 0; h < mHeight; h += 2) {
            for (int w = 0; w < mWidth; w += 2) {
                int u0 = YuvConverter.convertToU(bitmap.getPixel(w, h));
                int u1 = YuvConverter.convertToU(bitmap.getPixel(w + 1, h));
                int u2 = YuvConverter.convertToU(bitmap.getPixel(w, h + 1));
                int u3 = YuvConverter.convertToU(bitmap.getPixel(w + 1, h + 1));
                mYuv[pos++] = (byte) ((u0 + u1 + u2 + u3) / 4);
            }
        }

        for (int h = 0; h < mHeight; h += 2) {
            for (int w = 0; w < mWidth; w += 2) {
                int v0 = YuvConverter.convertToV(bitmap.getPixel(w, h));
                int v1 = YuvConverter.convertToV(bitmap.getPixel(w + 1, h));
                int v2 = YuvConverter.convertToV(bitmap.getPixel(w, h + 1));
                int v3 = YuvConverter.convertToV(bitmap.getPixel(w + 1, h + 1));
                mYuv[pos++] = (byte) ((v0 + v1 + v2 + v3) / 4);
            }
        }
    }

    public int getY(int x, int y) {
        return 255 & mYuv[mWidth * y + x];
    }

    public int getU(int x, int y) {
        return 255 & mYuv[mWidth * mHeight + (mWidth >> 1) * (y >> 1) + (x >> 1)];
    }

    public int getV(int x, int y) {
        return 255 & mYuv[((5 * mWidth * mHeight) >> 2) + (mWidth >> 1) * (y >> 1) + (x >> 1)];
    }
}


================================================
FILE: app/src/main/java/om/sstvencoder/Modes/ImageFormats/Yuv.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.Modes.ImageFormats;

import android.graphics.Bitmap;

public abstract class Yuv {
    protected byte[] mYuv;
    final int mWidth;
    final int mHeight;

    Yuv(Bitmap bitmap) {
        mWidth = bitmap.getWidth();
        mHeight = bitmap.getHeight();
        convertBitmapToYuv(bitmap);
    }

    protected abstract void convertBitmapToYuv(Bitmap bitmap);

    public int getWidth() {
        return mWidth;
    }

    public int getHeight() {
        return mHeight;
    }

    public abstract int getY(int x, int y);

    public abstract int getU(int x, int y);

    public abstract int getV(int x, int y);
}


================================================
FILE: app/src/main/java/om/sstvencoder/Modes/ImageFormats/YuvConverter.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.Modes.ImageFormats;

import android.graphics.Color;

final class YuvConverter {
    static int convertToY(int color) {
        double R = Color.red(color);
        double G = Color.green(color);
        double B = Color.blue(color);
        return clamp(16.0 + (.003906 * ((65.738 * R) + (129.057 * G) + (25.064 * B))));
    }

    static int convertToU(int color) {
        double R = Color.red(color);
        double G = Color.green(color);
        double B = Color.blue(color);
        return clamp(128.0 + (.003906 * ((-37.945 * R) + (-74.494 * G) + (112.439 * B))));
    }

    static int convertToV(int color) {
        double R = Color.red(color);
        double G = Color.green(color);
        double B = Color.blue(color);
        return clamp(128.0 + (.003906 * ((112.439 * R) + (-94.154 * G) + (-18.285 * B))));
    }

    private static int clamp(double value) {
        return value < 0.0 ? 0 : (value > 255.0 ? 255 : (int) value);
    }
}


================================================
FILE: app/src/main/java/om/sstvencoder/Modes/ImageFormats/YuvFactory.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.Modes.ImageFormats;

import android.graphics.Bitmap;

public final class YuvFactory {
    public static Yuv createYuv(Bitmap bitmap, int format) {
        switch (format) {
            case YuvImageFormat.YV12:
                return new YV12(bitmap);
            case YuvImageFormat.NV21:
                return new NV21(bitmap);
            case YuvImageFormat.YUY2:
                return new YUY2(bitmap);
            case YuvImageFormat.YUV440P:
                return new YUV440P(bitmap);
            default:
                throw new IllegalArgumentException("Only support YV12, NV21, YUY2 and YUV440P");
        }
    }
}


================================================
FILE: app/src/main/java/om/sstvencoder/Modes/ImageFormats/YuvImageFormat.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.Modes.ImageFormats;

import android.graphics.ImageFormat;

public class YuvImageFormat extends ImageFormat {
    public static final int YUV440P = 0x50303434;
}


================================================
FILE: app/src/main/java/om/sstvencoder/Modes/Martin.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.Modes;

import android.graphics.Bitmap;
import android.graphics.Color;

import om.sstvencoder.Output.IOutput;

abstract class Martin extends Mode {
    private final int mSyncPulseSamples;
    private final double mSyncPulseFrequency;

    private final int mSyncPorchSamples;
    private final double mSyncPorchFrequency;

    private final int mSeparatorSamples;
    private final double mSeparatorFrequency;

    protected double mColorScanDurationMs;
    protected int mColorScanSamples;

    Martin(Bitmap bitmap, IOutput output) {
        super(bitmap, output);

        mSyncPulseSamples = convertMsToSamples(4.862);
        mSyncPulseFrequency = 1200.0;

        mSyncPorchSamples = convertMsToSamples(0.572);
        mSyncPorchFrequency = 1500.0;

        mSeparatorSamples = convertMsToSamples(0.572);
        mSeparatorFrequency = 1500.0;
    }

    protected int getTransmissionSamples() {
        int lineSamples = mSyncPulseSamples + mSyncPorchSamples
                + 3 * (mSeparatorSamples + mColorScanSamples);
        return mBitmap.getHeight() * lineSamples;
    }

    protected void writeEncodedLine() {
        addSyncPulse();
        addSyncPorch();
        addGreenScan(mLine);
        addSeparator();
        addBlueScan(mLine);
        addSeparator();
        addRedScan(mLine);
        addSeparator();
    }

    private void addSyncPulse() {
        for (int i = 0; i < mSyncPulseSamples; ++i)
            setTone(mSyncPulseFrequency);
    }

    private void addSyncPorch() {
        for (int i = 0; i < mSyncPorchSamples; ++i)
            setTone(mSyncPorchFrequency);
    }

    private void addSeparator() {
        for (int i = 0; i < mSeparatorSamples; ++i)
            setTone(mSeparatorFrequency);
    }

    private void addGreenScan(int y) {
        for (int i = 0; i < mColorScanSamples; ++i)
            setColorTone(Color.green(getColor(i, y)));
    }

    private void addBlueScan(int y) {
        for (int i = 0; i < mColorScanSamples; ++i)
            setColorTone(Color.blue(getColor(i, y)));
    }

    private void addRedScan(int y) {
        for (int i = 0; i < mColorScanSamples; ++i)
            setColorTone(Color.red(getColor(i, y)));
    }

    private int getColor(int colorScanSample, int y) {
        int x = colorScanSample * mBitmap.getWidth() / mColorScanSamples;
        return mBitmap.getPixel(x, y);
    }
}


================================================
FILE: app/src/main/java/om/sstvencoder/Modes/Martin1.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.Modes;

import android.graphics.Bitmap;

import om.sstvencoder.ModeInterfaces.ModeSize;
import om.sstvencoder.Output.IOutput;

@ModeSize(width = 320, height = 256)
@ModeDescription(name = Martin1.Name)
class Martin1 extends Martin {
    public static final String Name = "Martin 1";

    Martin1(Bitmap bitmap, IOutput output) {
        super(bitmap, output);
        mVISCode = 44;
        mColorScanDurationMs = 146.432;
        mColorScanSamples = convertMsToSamples(mColorScanDurationMs);
    }
}


================================================
FILE: app/src/main/java/om/sstvencoder/Modes/Martin2.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.Modes;

import android.graphics.Bitmap;

import om.sstvencoder.ModeInterfaces.ModeSize;
import om.sstvencoder.Output.IOutput;

@ModeSize(width = 320, height = 256)
@ModeDescription(name = Martin2.Name)
class Martin2 extends Martin {
    public static final String Name = "Martin 2";

    Martin2(Bitmap bitmap, IOutput output) {
        super(bitmap, output);
        mVISCode = 40;
        mColorScanDurationMs = 73.216;
        mColorScanSamples = convertMsToSamples(mColorScanDurationMs);
    }
}


================================================
FILE: app/src/main/java/om/sstvencoder/Modes/Mode.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.Modes;

import android.graphics.Bitmap;

import om.sstvencoder.ModeInterfaces.IMode;
import om.sstvencoder.Output.IOutput;

abstract class Mode implements IMode {
    protected Bitmap mBitmap;
    protected int mVISCode;
    protected int mLine;
    private IOutput mOutput;
    private double mSampleRate;
    private double mRunningIntegral;

    protected Mode(Bitmap bitmap, IOutput output) {
        mOutput = output;
        mSampleRate = mOutput.getSampleRate();
        mBitmap = bitmap;
    }

    @Override
    public void init() {
        mRunningIntegral = 0.0;
        mLine = 0;
        mOutput.init(getTotalSamples());
        writeCalibrationHeader();
    }

    @Override
    public int getProcessCount() {
        return mBitmap.getHeight();
    }

    @Override
    public boolean process() {
        if (mLine >= mBitmap.getHeight())
            return false;

        writeEncodedLine();
        ++mLine;
        return true;
    }

    // Note that also Bitmap will be recycled here
    @Override
    public void finish(boolean cancel) {
        mOutput.finish(cancel);
        destroyBitmap();
    }

    private int getTotalSamples() {
        return getHeaderSamples() + getTransmissionSamples();
    }

    private int getHeaderSamples() {
        return 2 * convertMsToSamples(300.0)
                + convertMsToSamples(10.0)
                + 10 * convertMsToSamples(30.0);
    }

    protected abstract int getTransmissionSamples();

    private void writeCalibrationHeader() {
        int leaderToneSamples = convertMsToSamples(300.0);
        double leaderToneFrequency = 1900.0;

        int breakSamples = convertMsToSamples(10.0);
        double breakFrequency = 1200.0;

        int visBitSamples = convertMsToSamples(30.0);
        double visBitSSFrequency = 1200.0;
        double[] visBitFrequency = new double[]{1300.0, 1100.0};

        for (int i = 0; i < leaderToneSamples; ++i)
            setTone(leaderToneFrequency);

        for (int i = 0; i < breakSamples; ++i)
            setTone(breakFrequency);

        for (int i = 0; i < leaderToneSamples; ++i)
            setTone(leaderToneFrequency);

        for (int i = 0; i < visBitSamples; ++i)
            setTone(visBitSSFrequency);

        int parity = 0;
        for (int pos = 0; pos < 7; ++pos) {
            int bit = (mVISCode >> pos) & 1;
            parity ^= bit;
            for (int i = 0; i < visBitSamples; ++i)
                setTone(visBitFrequency[bit]);
        }

        for (int i = 0; i < visBitSamples; ++i)
            setTone(visBitFrequency[parity]);

        for (int i = 0; i < visBitSamples; ++i)
            setTone(visBitSSFrequency);
    }

    protected abstract void writeEncodedLine();

    protected int convertMsToSamples(double durationMs) {
        return (int) Math.round(durationMs * mSampleRate / 1000.0);
    }

    protected void setTone(double frequency) {
        mRunningIntegral += 2.0 * frequency * Math.PI / mSampleRate;
        mRunningIntegral %= 2.0 * Math.PI;
        mOutput.write(Math.sin(mRunningIntegral));
    }

    protected void setColorTone(int color) {
        double blackFrequency = 1500.0;
        double whiteFrequency = 2300.0;
        setTone(color * (whiteFrequency - blackFrequency) / 255.0 + blackFrequency);
    }

    private void destroyBitmap() {
        if (mBitmap != null && !mBitmap.isRecycled()) {
            mBitmap.recycle();
            mBitmap = null;
        }
    }
}


================================================
FILE: app/src/main/java/om/sstvencoder/Modes/ModeDescription.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.Modes;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@interface ModeDescription {
    String name();
}


================================================
FILE: app/src/main/java/om/sstvencoder/Modes/ModeFactory.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.Modes;

import android.graphics.Bitmap;

import java.lang.reflect.Constructor;

import om.sstvencoder.ModeInterfaces.IMode;
import om.sstvencoder.ModeInterfaces.IModeInfo;
import om.sstvencoder.ModeInterfaces.ModeSize;
import om.sstvencoder.Output.IOutput;

public final class ModeFactory {
    public static Class<?> getDefaultMode() {
        return Robot36.class;
    }

    public static String getDefaultModeClassName() {
        return (new ModeInfo(getDefaultMode())).getModeClassName();
    }

    public static IModeInfo[] getModeInfoList() {
        return new IModeInfo[]{
                new ModeInfo(Martin1.class), new ModeInfo(Martin2.class),
                new ModeInfo(PD50.class), new ModeInfo(PD90.class), new ModeInfo(PD120.class),
                new ModeInfo(PD160.class), new ModeInfo(PD180.class),
                new ModeInfo(PD240.class), new ModeInfo(PD290.class),
                new ModeInfo(Scottie1.class), new ModeInfo(Scottie2.class), new ModeInfo(ScottieDX.class),
                new ModeInfo(Robot36.class), new ModeInfo(Robot72.class),
                new ModeInfo(Wraase.class)
        };
    }

    public static IModeInfo getModeInfo(Class<?> modeClass) {
        if (!isModeClassValid(modeClass))
            return null;

        return new ModeInfo(modeClass);
    }

    public static IMode CreateMode(Class<?> modeClass, Bitmap bitmap, IOutput output) {
        Mode mode = null;

        if (bitmap != null && output != null && isModeClassValid(modeClass)) {
            ModeSize size = modeClass.getAnnotation(ModeSize.class);

            if (bitmap.getWidth() == size.width() && bitmap.getHeight() == size.height()) {
                try {
                    Constructor constructor = modeClass.getDeclaredConstructor(Bitmap.class, IOutput.class);
                    mode = (Mode) constructor.newInstance(bitmap, output);
                } catch (Exception ignore) {
                }
            }
        }

        return mode;
    }

    private static boolean isModeClassValid(Class<?> modeClass) {
        return Mode.class.isAssignableFrom(modeClass) &&
                modeClass.isAnnotationPresent(ModeSize.class) &&
                modeClass.isAnnotationPresent(ModeDescription.class);
    }
}


================================================
FILE: app/src/main/java/om/sstvencoder/Modes/ModeInfo.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.Modes;

import om.sstvencoder.ModeInterfaces.IModeInfo;
import om.sstvencoder.ModeInterfaces.ModeSize;

class ModeInfo implements IModeInfo {
    private final Class<?> mModeClass;

    ModeInfo(Class<?> modeClass) {
        mModeClass = modeClass;
    }

    public String getModeName() {
        return mModeClass.getAnnotation(ModeDescription.class).name();
    }

    public String getModeClassName() {
        return mModeClass.getName();
    }

    public ModeSize getModeSize() {
        return mModeClass.getAnnotation(ModeSize.class);
    }
}


================================================
FILE: app/src/main/java/om/sstvencoder/Modes/PD.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.Modes;

import android.graphics.Bitmap;

import om.sstvencoder.Modes.ImageFormats.Yuv;
import om.sstvencoder.Modes.ImageFormats.YuvFactory;
import om.sstvencoder.Modes.ImageFormats.YuvImageFormat;
import om.sstvencoder.Output.IOutput;

abstract class PD extends Mode {
    private final Yuv mYuv;

    private final int mSyncPulseSamples;
    private final double mSyncPulseFrequency;

    private final int mPorchSamples;
    private final double mPorchFrequency;

    protected double mColorScanDurationMs;
    protected int mColorScanSamples;

    PD(Bitmap bitmap, IOutput output) {
        super(bitmap, output);

        mYuv = YuvFactory.createYuv(mBitmap, YuvImageFormat.YUV440P);

        mSyncPulseSamples = convertMsToSamples(20.0);
        mSyncPulseFrequency = 1200.0;

        mPorchSamples = convertMsToSamples(2.08);
        mPorchFrequency = 1500.0;
    }

    protected int getTransmissionSamples() {
        int lineSamples = mSyncPulseSamples + mPorchSamples + 4 * mColorScanSamples;
        return mBitmap.getHeight() / 2 * lineSamples;
    }

    @Override
    public int getProcessCount() {
        return mBitmap.getHeight() / 2;
    }

    protected void writeEncodedLine() {
        addSyncPulse();
        addPorch();
        addYScan(mLine);
        addVScan(mLine);
        addUScan(mLine);
        addYScan(++mLine);
    }

    private void addSyncPulse() {
        for (int i = 0; i < mSyncPulseSamples; ++i)
            setTone(mSyncPulseFrequency);
    }

    private void addPorch() {
        for (int i = 0; i < mPorchSamples; ++i)
            setTone(mPorchFrequency);
    }

    private void addYScan(int y) {
        for (int i = 0; i < mColorScanSamples; ++i)
            setColorTone(mYuv.getY((i * mYuv.getWidth()) / mColorScanSamples, y));
    }

    private void addUScan(int y) {
        for (int i = 0; i < mColorScanSamples; ++i)
            setColorTone(mYuv.getU((i * mYuv.getWidth()) / mColorScanSamples, y));
    }

    private void addVScan(int y) {
        for (int i = 0; i < mColorScanSamples; ++i)
            setColorTone(mYuv.getV((i * mYuv.getWidth()) / mColorScanSamples, y));
    }
}


================================================
FILE: app/src/main/java/om/sstvencoder/Modes/PD120.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.Modes;

import android.graphics.Bitmap;

import om.sstvencoder.ModeInterfaces.ModeSize;
import om.sstvencoder.Output.IOutput;

@ModeSize(width = 640, height = 496)
@ModeDescription(name = PD120.Name)
class PD120 extends PD {
    public static final String Name = "PD 120";

    PD120(Bitmap bitmap, IOutput output) {
        super(bitmap, output);
        mVISCode = 95;
        mColorScanDurationMs = 121.6;
        mColorScanSamples = convertMsToSamples(mColorScanDurationMs);
    }
}



================================================
FILE: app/src/main/java/om/sstvencoder/Modes/PD160.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.Modes;

import android.graphics.Bitmap;

import om.sstvencoder.ModeInterfaces.ModeSize;
import om.sstvencoder.Output.IOutput;

@ModeSize(width = 512, height = 400)
@ModeDescription(name = PD160.Name)
class PD160 extends PD {
    public static final String Name = "PD 160";

    PD160(Bitmap bitmap, IOutput output) {
        super(bitmap, output);
        mVISCode = 98;
        mColorScanDurationMs = 195.584;
        mColorScanSamples = convertMsToSamples(mColorScanDurationMs);
    }
}



================================================
FILE: app/src/main/java/om/sstvencoder/Modes/PD180.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.Modes;

import android.graphics.Bitmap;

import om.sstvencoder.ModeInterfaces.ModeSize;
import om.sstvencoder.Output.IOutput;

@ModeSize(width = 640, height = 496)
@ModeDescription(name = PD180.Name)
class PD180 extends PD {
    public static final String Name = "PD 180";

    PD180(Bitmap bitmap, IOutput output) {
        super(bitmap, output);
        mVISCode = 96;
        mColorScanDurationMs = 183.04;
        mColorScanSamples = convertMsToSamples(mColorScanDurationMs);
    }
}



================================================
FILE: app/src/main/java/om/sstvencoder/Modes/PD240.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.Modes;

import android.graphics.Bitmap;

import om.sstvencoder.ModeInterfaces.ModeSize;
import om.sstvencoder.Output.IOutput;

@ModeSize(width = 640, height = 496)
@ModeDescription(name = PD240.Name)
class PD240 extends PD {
    public static final String Name = "PD 240";

    PD240(Bitmap bitmap, IOutput output) {
        super(bitmap, output);
        mVISCode = 97;
        mColorScanDurationMs = 244.48;
        mColorScanSamples = convertMsToSamples(mColorScanDurationMs);
    }
}



================================================
FILE: app/src/main/java/om/sstvencoder/Modes/PD290.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.Modes;

import android.graphics.Bitmap;

import om.sstvencoder.ModeInterfaces.ModeSize;
import om.sstvencoder.Output.IOutput;

@ModeSize(width = 800, height = 616)
@ModeDescription(name = PD290.Name)
class PD290 extends PD {
    public static final String Name = "PD 290";

    PD290(Bitmap bitmap, IOutput output) {
        super(bitmap, output);
        mVISCode = 94;
        mColorScanDurationMs = 228.8;
        mColorScanSamples = convertMsToSamples(mColorScanDurationMs);
    }
}



================================================
FILE: app/src/main/java/om/sstvencoder/Modes/PD50.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.Modes;

import android.graphics.Bitmap;

import om.sstvencoder.ModeInterfaces.ModeSize;
import om.sstvencoder.Output.IOutput;

@ModeSize(width = 320, height = 256)
@ModeDescription(name = PD50.Name)
class PD50 extends PD {
    public static final String Name = "PD 50";

    PD50(Bitmap bitmap, IOutput output) {
        super(bitmap, output);
        mVISCode = 93;
        mColorScanDurationMs = 91.52;
        mColorScanSamples = convertMsToSamples(mColorScanDurationMs);
    }
}



================================================
FILE: app/src/main/java/om/sstvencoder/Modes/PD90.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.Modes;

import android.graphics.Bitmap;

import om.sstvencoder.ModeInterfaces.ModeSize;
import om.sstvencoder.Output.IOutput;

@ModeSize(width = 320, height = 256)
@ModeDescription(name = PD90.Name)
class PD90 extends PD {
    public static final String Name = "PD 90";

    PD90(Bitmap bitmap, IOutput output) {
        super(bitmap, output);
        mVISCode = 99;
        mColorScanDurationMs = 170.24;
        mColorScanSamples = convertMsToSamples(mColorScanDurationMs);
    }
}



================================================
FILE: app/src/main/java/om/sstvencoder/Modes/Robot36.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.Modes;

import android.graphics.Bitmap;

import om.sstvencoder.Modes.ImageFormats.Yuv;
import om.sstvencoder.Modes.ImageFormats.YuvFactory;
import om.sstvencoder.Modes.ImageFormats.YuvImageFormat;
import om.sstvencoder.ModeInterfaces.ModeSize;
import om.sstvencoder.Output.IOutput;

@ModeSize(width = 320, height = 240)
@ModeDescription(name = Robot36.Name)
class Robot36 extends Mode {
    public static final String Name = "Robot 36";

    private final Yuv mYuv;

    private final int mLumaScanSamples;
    private final int mChrominanceScanSamples;

    private final int mSyncPulseSamples;
    private final double mSyncPulseFrequency;

    private final int mSyncPorchSamples;
    private final double mSyncPorchFrequency;

    private final int mPorchSamples;
    private final double mPorchFrequency;

    private final int mSeparatorSamples;
    private final double mEvenSeparatorFrequency;
    private final double mOddSeparatorFrequency;

    Robot36(Bitmap bitmap, IOutput output) {
        super(bitmap, output);

        mYuv = YuvFactory.createYuv(mBitmap, YuvImageFormat.NV21);
        mVISCode = 8;

        mLumaScanSamples = convertMsToSamples(88.0);
        mChrominanceScanSamples = convertMsToSamples(44.0);

        mSyncPulseSamples = convertMsToSamples(9.0);
        mSyncPulseFrequency = 1200.0;

        mSyncPorchSamples = convertMsToSamples(3.0);
        mSyncPorchFrequency = 1500.0;

        mPorchSamples = convertMsToSamples(1.5);
        mPorchFrequency = 1900.0;

        mSeparatorSamples = convertMsToSamples(4.5);
        mEvenSeparatorFrequency = 1500.0;
        mOddSeparatorFrequency = 2300.0;
    }

    protected int getTransmissionSamples() {
        int lineSamples = mSyncPulseSamples + mSyncPorchSamples
                + mLumaScanSamples + mSeparatorSamples
                + mPorchSamples + mChrominanceScanSamples;
        return mBitmap.getHeight() * lineSamples;
    }

    protected void writeEncodedLine() {
        addSyncPulse();
        addSyncPorch();
        addYScan(mLine);

        if (mLine % 2 == 0) {
            addSeparator(mEvenSeparatorFrequency);
            addPorch();
            addVScan(mLine);
        } else {
            addSeparator(mOddSeparatorFrequency);
            addPorch();
            addUScan(mLine);
        }
    }

    private void addSyncPulse() {
        for (int i = 0; i < mSyncPulseSamples; ++i)
            setTone(mSyncPulseFrequency);
    }

    private void addSyncPorch() {
        for (int i = 0; i < mSyncPorchSamples; ++i)
            setTone(mSyncPorchFrequency);
    }

    private void addSeparator(double separatorFrequency) {
        for (int i = 0; i < mSeparatorSamples; ++i)
            setTone(separatorFrequency);
    }

    private void addPorch() {
        for (int i = 0; i < mPorchSamples; ++i)
            setTone(mPorchFrequency);
    }

    private void addYScan(int y) {
        for (int i = 0; i < mLumaScanSamples; ++i)
            setColorTone(mYuv.getY((i * mYuv.getWidth()) / mLumaScanSamples, y));
    }

    private void addUScan(int y) {
        for (int i = 0; i < mChrominanceScanSamples; ++i)
            setColorTone(mYuv.getU((i * mYuv.getWidth()) / mChrominanceScanSamples, y));
    }

    private void addVScan(int y) {
        for (int i = 0; i < mChrominanceScanSamples; ++i)
            setColorTone(mYuv.getV((i * mYuv.getWidth()) / mChrominanceScanSamples, y));
    }
}



================================================
FILE: app/src/main/java/om/sstvencoder/Modes/Robot72.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.Modes;

import android.graphics.Bitmap;

import om.sstvencoder.Modes.ImageFormats.Yuv;
import om.sstvencoder.Modes.ImageFormats.YuvFactory;
import om.sstvencoder.Modes.ImageFormats.YuvImageFormat;
import om.sstvencoder.ModeInterfaces.ModeSize;
import om.sstvencoder.Output.IOutput;

@ModeSize(width = 320, height = 240)
@ModeDescription(name = Robot72.Name)
class Robot72 extends Mode {
    public static final String Name = "Robot 72";

    private final Yuv mYuv;

    private final int mLumaScanSamples;
    private final int mChrominanceScanSamples;

    private final int mSyncPulseSamples;
    private final double mSyncPulseFrequency;

    private final int mSyncPorchSamples;
    private final double mSyncPorchFrequency;

    private final int mPorchSamples;
    private final double mPorchFrequency;

    private final int mSeparatorSamples;
    private final double mFirstSeparatorFrequency;
    private final double mSecondSeparatorFrequency;

    Robot72(Bitmap bitmap, IOutput output) {
        super(bitmap, output);

        mYuv = YuvFactory.createYuv(mBitmap, YuvImageFormat.YUY2);
        mVISCode = 12;

        mLumaScanSamples = convertMsToSamples(138.0);
        mChrominanceScanSamples = convertMsToSamples(69.0);

        mSyncPulseSamples = convertMsToSamples(9.0);
        mSyncPulseFrequency = 1200.0;

        mSyncPorchSamples = convertMsToSamples(3.0);
        mSyncPorchFrequency = 1500.0;

        mPorchSamples = convertMsToSamples(1.5);
        mPorchFrequency = 1900.0;

        mSeparatorSamples = convertMsToSamples(4.5);
        mFirstSeparatorFrequency = 1500.0;
        mSecondSeparatorFrequency = 2300.0;
    }

    protected int getTransmissionSamples() {
        int lineSamples = mSyncPulseSamples + mSyncPorchSamples + mLumaScanSamples
                + 2 * (mSeparatorSamples + mPorchSamples + mChrominanceScanSamples);
        return mBitmap.getHeight() * lineSamples;
    }

    protected void writeEncodedLine() {
        addSyncPulse();
        addSyncPorch();
        addYScan(mLine);
        addSeparator(mFirstSeparatorFrequency);
        addPorch();
        addVScan(mLine);
        addSeparator(mSecondSeparatorFrequency);
        addPorch();
        addUScan(mLine);
    }

    private void addSyncPulse() {
        for (int i = 0; i < mSyncPulseSamples; ++i)
            setTone(mSyncPulseFrequency);
    }

    private void addSyncPorch() {
        for (int i = 0; i < mSyncPorchSamples; ++i)
            setTone(mSyncPorchFrequency);
    }

    private void addSeparator(double separatorFrequency) {
        for (int i = 0; i < mSeparatorSamples; ++i)
            setTone(separatorFrequency);
    }

    private void addPorch() {
        for (int i = 0; i < mPorchSamples; ++i)
            setTone(mPorchFrequency);
    }

    private void addYScan(int y) {
        for (int i = 0; i < mLumaScanSamples; ++i)
            setColorTone(mYuv.getY((i * mYuv.getWidth()) / mLumaScanSamples, y));
    }

    private void addUScan(int y) {
        for (int i = 0; i < mChrominanceScanSamples; ++i)
            setColorTone(mYuv.getU((i * mYuv.getWidth()) / mChrominanceScanSamples, y));
    }

    private void addVScan(int y) {
        for (int i = 0; i < mChrominanceScanSamples; ++i)
            setColorTone(mYuv.getV((i * mYuv.getWidth()) / mChrominanceScanSamples, y));
    }
}


================================================
FILE: app/src/main/java/om/sstvencoder/Modes/Scottie.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.Modes;

import android.graphics.Bitmap;
import android.graphics.Color;

import om.sstvencoder.Output.IOutput;

abstract class Scottie extends Mode {
    private final int mSyncPulseSamples;
    private final double mSyncPulseFrequency;

    private final int mSyncPorchSamples;
    private final double mSyncPorchFrequency;

    private final int mSeparatorSamples;
    private final double mSeparatorFrequency;

    protected double mColorScanDurationMs;
    protected int mColorScanSamples;

    Scottie(Bitmap bitmap, IOutput output) {
        super(bitmap, output);

        mSyncPulseSamples = convertMsToSamples(9.0);
        mSyncPulseFrequency = 1200.0;

        mSyncPorchSamples = convertMsToSamples(1.5);
        mSyncPorchFrequency = 1500.0;

        mSeparatorSamples = convertMsToSamples(1.5);
        mSeparatorFrequency = 1500.0;
    }

    protected int getTransmissionSamples() {
        int lineSamples = 2 * mSeparatorSamples + 3 * mColorScanSamples +
                mSyncPulseSamples + mSyncPorchSamples;
        return mSyncPulseSamples + mBitmap.getHeight() * lineSamples;
    }

    protected void writeEncodedLine() {
        if (mLine == 0)
            addSyncPulse();

        addSeparator();
        addGreenScan(mLine);
        addSeparator();
        addBlueScan(mLine);
        addSyncPulse();
        addSyncPorch();
        addRedScan(mLine);
    }

    private void addSyncPulse() {
        for (int i = 0; i < mSyncPulseSamples; ++i)
            setTone(mSyncPulseFrequency);
    }

    private void addSyncPorch() {
        for (int i = 0; i < mSyncPorchSamples; ++i)
            setTone(mSyncPorchFrequency);
    }

    private void addSeparator() {
        for (int i = 0; i < mSeparatorSamples; ++i)
            setTone(mSeparatorFrequency);
    }

    private void addGreenScan(int y) {
        for (int i = 0; i < mColorScanSamples; ++i)
            setColorTone(Color.green(getColor(i, y)));
    }

    private void addBlueScan(int y) {
        for (int i = 0; i < mColorScanSamples; ++i)
            setColorTone(Color.blue(getColor(i, y)));
    }

    private void addRedScan(int y) {
        for (int i = 0; i < mColorScanSamples; ++i)
            setColorTone(Color.red(getColor(i, y)));
    }

    private int getColor(int colorScanSample, int y) {
        int x = colorScanSample * mBitmap.getWidth() / mColorScanSamples;
        return mBitmap.getPixel(x, y);
    }
}


================================================
FILE: app/src/main/java/om/sstvencoder/Modes/Scottie1.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.Modes;

import android.graphics.Bitmap;

import om.sstvencoder.ModeInterfaces.ModeSize;
import om.sstvencoder.Output.IOutput;

@ModeSize(width = 320, height = 256)
@ModeDescription(name = Scottie1.Name)
class Scottie1 extends Scottie {
    public static final String Name = "Scottie 1";

    Scottie1(Bitmap bitmap, IOutput output) {
        super(bitmap, output);
        mVISCode = 60;
        mColorScanDurationMs = 138.24;
        mColorScanSamples = convertMsToSamples(mColorScanDurationMs);
    }
}


================================================
FILE: app/src/main/java/om/sstvencoder/Modes/Scottie2.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.Modes;

import android.graphics.Bitmap;

import om.sstvencoder.ModeInterfaces.ModeSize;
import om.sstvencoder.Output.IOutput;

@ModeSize(width = 320, height = 256)
@ModeDescription(name = Scottie2.Name)
class Scottie2 extends Scottie {
    public static final String Name = "Scottie 2";

    Scottie2(Bitmap bitmap, IOutput output){
        super(bitmap, output);
        mVISCode = 56;
        mColorScanDurationMs = 88.064;
        mColorScanSamples = convertMsToSamples(mColorScanDurationMs);
    }
}


================================================
FILE: app/src/main/java/om/sstvencoder/Modes/ScottieDX.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.Modes;

import android.graphics.Bitmap;

import om.sstvencoder.ModeInterfaces.ModeSize;
import om.sstvencoder.Output.IOutput;

@ModeSize(width = 320, height = 256)
@ModeDescription(name = ScottieDX.Name)
class ScottieDX extends Scottie {
    public static final String Name = "Scottie DX";

    ScottieDX(Bitmap bitmap, IOutput output) {
        super(bitmap, output);
        mVISCode = 76;
        mColorScanDurationMs = 345.6;
        mColorScanSamples = convertMsToSamples(mColorScanDurationMs);
    }
}


================================================
FILE: app/src/main/java/om/sstvencoder/Modes/Wraase.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.Modes;

import android.graphics.Bitmap;
import android.graphics.Color;

import om.sstvencoder.ModeInterfaces.ModeSize;
import om.sstvencoder.Output.IOutput;

@ModeSize(width = 320, height = 256)
@ModeDescription(name = Wraase.Name)
class Wraase extends Mode {
    public static final String Name = "Wraase SC2 180";

    private final int mSyncPulseSamples;
    private final double mSyncPulseFrequency;

    private final int mPorchSamples;
    private final double mPorchFrequency;

    private final int mColorScanSamples;

    Wraase(Bitmap bitmap, IOutput output) {
        super(bitmap, output);

        mVISCode = 55;
        mColorScanSamples = convertMsToSamples(235.0);

        mSyncPulseSamples = convertMsToSamples(5.5225);
        mSyncPulseFrequency = 1200.0;

        mPorchSamples = convertMsToSamples(0.5);
        mPorchFrequency = 1500.0;
    }

    protected int getTransmissionSamples() {
        int lineSamples = mSyncPulseSamples + mPorchSamples + 3 * mColorScanSamples;
        return mBitmap.getHeight() * lineSamples;
    }

    protected void writeEncodedLine() {
        addSyncPulse();
        addPorch();
        addRedScan(mLine);
        addGreenScan(mLine);
        addBlueScan(mLine);
    }

    private void addSyncPulse() {
        for (int i = 0; i < mSyncPulseSamples; ++i)
            setTone(mSyncPulseFrequency);
    }

    private void addPorch() {
        for (int i = 0; i < mPorchSamples; ++i)
            setTone(mPorchFrequency);
    }

    private void addRedScan(int y) {
        for (int i = 0; i < mColorScanSamples; ++i)
            setColorTone(Color.red(getColor(i, y)));
    }

    private void addGreenScan(int y) {
        for (int i = 0; i < mColorScanSamples; ++i)
            setColorTone(Color.green(getColor(i, y)));
    }

    private void addBlueScan(int y) {
        for (int i = 0; i < mColorScanSamples; ++i)
            setColorTone(Color.blue(getColor(i, y)));
    }

    private int getColor(int colorScanSample, int y) {
        int x = colorScanSample * mBitmap.getWidth() / mColorScanSamples;
        return mBitmap.getPixel(x, y);
    }
}


================================================
FILE: app/src/main/java/om/sstvencoder/Output/AudioOutput.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.Output;

import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;

class AudioOutput implements IOutput {
    private final double mSampleRate;
    private short[] mAudioBuffer;
    private AudioTrack mAudioTrack;
    private int mBufferPos;

    AudioOutput(double sampleRate) {
        mSampleRate = sampleRate;
        mBufferPos = 0;
    }

    @Override
    public void init(int samples) {
        mAudioBuffer = new short[(5 * (int) mSampleRate) / 2]; // 2.5 seconds of buffer
        mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
                (int) mSampleRate, AudioFormat.CHANNEL_OUT_MONO,
                AudioFormat.ENCODING_PCM_16BIT, mAudioBuffer.length * 2,
                AudioTrack.MODE_STREAM);
        mAudioTrack.play();
    }

    @Override
    public double getSampleRate() {
        return mSampleRate;
    }

    @Override
    public void write(double value) {
        if (mBufferPos == mAudioBuffer.length) {
            mAudioTrack.write(mAudioBuffer, 0, mAudioBuffer.length);
            mBufferPos = 0;
        }

        mAudioBuffer[mBufferPos++] = (short) (value * Short.MAX_VALUE);
    }

    @Override
    public void finish(boolean cancel) {
        if (mAudioTrack != null) {
            if (!cancel)
                drainBuffer();
            mAudioTrack.stop();
            mAudioTrack.release();
            mAudioTrack = null;
            mAudioBuffer = null;
        }
    }

    private void drainBuffer() {
        // The second run makes sure that the previous buffer indeed got played
        for (int i = 0; i < 2; ++i) {
            while (mBufferPos < mAudioBuffer.length)
                mAudioBuffer[mBufferPos++] = 0;
            mAudioTrack.write(mAudioBuffer, 0, mAudioBuffer.length);
            mBufferPos = 0;
        }
    }
}


================================================
FILE: app/src/main/java/om/sstvencoder/Output/IOutput.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.Output;

public interface IOutput {
    double getSampleRate();

    void init(int samples);

    void write(double value);

    void finish(boolean cancel);
}


================================================
FILE: app/src/main/java/om/sstvencoder/Output/OutputFactory.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.Output;

public final class OutputFactory {

    public static IOutput createOutputForSending() {
        double sampleRate = 44100.0;
        return new AudioOutput(sampleRate);
    }

    public static IOutput createOutputForSavingAsWave(WaveFileOutputContext context) {
        double sampleRate = 44100.0;
        return new WaveFileOutput(context, sampleRate);
    }
}


================================================
FILE: app/src/main/java/om/sstvencoder/Output/WaveFileOutput.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.Output;

import java.io.BufferedOutputStream;

class WaveFileOutput implements IOutput {
    private final double mSampleRate;
    private WaveFileOutputContext mContext;
    private BufferedOutputStream mOutputStream;
    private int mSamples, mWrittenSamples;

    WaveFileOutput(WaveFileOutputContext context, double sampleRate) {
        mContext = context;
        mSampleRate = sampleRate;
    }

    public void init(int samples) {
        int offset = (int) ((0.01 * mSampleRate) / 2.0);
        mSamples = samples + 2 * offset;
        mWrittenSamples = 0;
        InitOutputStream();
        writeHeader();
        padWithZeros(offset);
    }

    private void writeHeader() {
        try {
            int numChannels = 1; // mono
            int bitsPerSample = Short.SIZE;
            int blockAlign = numChannels * bitsPerSample / Byte.SIZE;
            int subchunk2Size = mSamples * blockAlign;

            mOutputStream.write("RIFF".getBytes()); // ChunkID
            mOutputStream.write(toLittleEndian(36 + subchunk2Size)); // ChunkSize
            mOutputStream.write("WAVE".getBytes()); // Format

            mOutputStream.write("fmt ".getBytes()); // Subchunk1ID
            mOutputStream.write(toLittleEndian(16)); // Subchunk1Size
            mOutputStream.write(toLittleEndian((short) 1)); // AudioFormat
            mOutputStream.write(toLittleEndian((short) numChannels)); // NumChannels
            mOutputStream.write(toLittleEndian((int) mSampleRate)); // SampleRate
            mOutputStream.write(toLittleEndian((int) mSampleRate * blockAlign)); // ByteRate
            mOutputStream.write(toLittleEndian((short) blockAlign)); // BlockAlign
            mOutputStream.write(toLittleEndian((short) bitsPerSample)); // BitsPerSample

            mOutputStream.write("data".getBytes()); // Subchunk2ID
            mOutputStream.write(toLittleEndian(subchunk2Size)); // Subchunk2Size
        } catch (Exception ignore) {
        }
    }

    private void InitOutputStream() {
        try {
            mOutputStream = new BufferedOutputStream(mContext.getOutputStream());
        } catch (Exception ignore) {
        }
    }

    @Override
    public double getSampleRate() {
        return mSampleRate;
    }

    @Override
    public void write(double value) {
        short tmp = (short) (value * Short.MAX_VALUE);
        ++mWrittenSamples;
        try {
            mOutputStream.write(toLittleEndian(tmp));
        } catch (Exception ignore) {
        }
    }

    @Override
    public void finish(boolean cancel) {
        if (!cancel)
            padWithZeros(mSamples);

        try {
            mOutputStream.close();
            mOutputStream = null;
        } catch (Exception ignore) {
        }

        if (cancel)
            mContext.deleteFile();
    }

    private void padWithZeros(int count) {
        try {
            while (mWrittenSamples++ < count)
                mOutputStream.write(toLittleEndian((short) 0));
        } catch (Exception ignore) {
        }
    }

    private byte[] toLittleEndian(int value) {
        byte[] buffer = new byte[4];
        buffer[0] = (byte) (value & 255);
        buffer[1] = (byte) ((value >> 8) & 255);
        buffer[2] = (byte) ((value >> 16) & 255);
        buffer[3] = (byte) ((value >> 24) & 255);
        return buffer;
    }

    private byte[] toLittleEndian(short value) {
        byte[] buffer = new byte[2];
        buffer[0] = (byte) (value & 255);
        buffer[1] = (byte) ((value >> 8) & 255);
        return buffer;
    }
}


================================================
FILE: app/src/main/java/om/sstvencoder/Output/WaveFileOutputContext.java
================================================
/*
Copyright 2020 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.Output;

import android.content.ContentResolver;
import android.content.ContentValues;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.MediaStore;

import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;

public class WaveFileOutputContext {
    private ContentResolver mContentResolver;
    private String mFileName;
    private File mFile;
    private Uri mUri;
    private ContentValues mValues;

    public WaveFileOutputContext(ContentResolver contentResolver, String fileName) {
        mContentResolver = contentResolver;
        mFileName = fileName;
        mValues = getContentValues(fileName);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
            mUri = mContentResolver.insert(MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), mValues);
        else
            mFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC), mFileName);
    }
    
    private ContentValues getContentValues(String fileName) {
        ContentValues values = new ContentValues();
        values.put(MediaStore.Audio.Media.MIME_TYPE, "audio/wav");
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            values.put(MediaStore.Audio.Media.DISPLAY_NAME, fileName);
            values.put(MediaStore.Audio.Media.RELATIVE_PATH, (new File(Environment.DIRECTORY_MUSIC, "SSTV Encoder")).getPath());
            values.put(MediaStore.Audio.Media.IS_PENDING, 1);
        } else {
            values.put(MediaStore.Audio.Media.ALBUM, "SSTV Encoder");
            values.put(MediaStore.Audio.Media.TITLE, fileName);
            values.put(MediaStore.Audio.Media.IS_MUSIC, true);
        }
        return values;
    }

    public String getFileName() {
        return mFileName;
    }

    public OutputStream getOutputStream() {
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                return mContentResolver.openOutputStream(mUri);
            } else
                return new FileOutputStream(mFile);
        } catch (Exception ignore) {
        }
        return null;
    }

    public void update() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            if (mUri != null && mValues != null) {
                mValues.clear();
                mValues.put(MediaStore.Audio.Media.IS_PENDING, 0);
                mContentResolver.update(mUri, mValues, null, null);
            }
        } else {
            if (mFile != null && mValues != null) {
                mValues.put(MediaStore.Audio.Media.DATA, mFile.toString());
                mUri = mContentResolver.insert(MediaStore.Audio.Media.getContentUriForPath(mFile.getAbsolutePath()), mValues);
            }
        }
    }

    public void deleteFile() {
        try {
            if (mFile == null)
                mFile = new File(mUri.getPath());
            mFile.delete();
        } catch (Exception ignore) {
        }
    }
}


================================================
FILE: app/src/main/java/om/sstvencoder/ProgressBarWrapper.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder;

import android.os.Handler;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.TextView;

class ProgressBarWrapper {
    private final ProgressBar mProgressBar;
    private final TextView mText;
    private final Handler mHandler;
    private final int mSteps;
    private int mLastStep;
    private int mPosition, mMaxPosition;

    ProgressBarWrapper(ProgressBar progressBar, TextView text) {
        mProgressBar = progressBar;
        mProgressBar.setVisibility(View.GONE);
        mText = text;
        mText.setVisibility(View.GONE);
        mHandler = new Handler();
        mSteps = 10;
    }

    private void startProgressBar(final int max, final String text) {
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                mProgressBar.setMax(max);
                mProgressBar.setProgress(0);
                mProgressBar.setVisibility(View.VISIBLE);
                if (text != null) {
                    mText.setText(text);
                    mText.setVisibility(View.VISIBLE);
                }
            }
        });
    }

    private void stepProgressBar(final int progress) {
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                mProgressBar.setProgress(progress);
            }
        });
    }

    private void endProgressBar() {
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                mProgressBar.setVisibility(View.GONE);
                mText.setVisibility(View.GONE);
            }
        });
    }

    void begin(int max, String text) {
        mLastStep = 0;
        mPosition = 0;
        mMaxPosition = max;
        startProgressBar(mSteps, text);
    }

    void step() {
        ++mPosition;
        int newStep = (mSteps * mPosition + mMaxPosition / 2) / mMaxPosition;
        if (newStep != mLastStep) {
            stepProgressBar(newStep);
            mLastStep = newStep;
        }
    }

    void end() {
        endProgressBar();
    }
}


================================================
FILE: app/src/main/java/om/sstvencoder/Settings.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder;

import android.content.Context;
import android.net.Uri;
import android.util.JsonReader;
import android.util.JsonToken;
import android.util.JsonWriter;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;

import om.sstvencoder.Modes.ModeFactory;

class Settings {
    private final static String VERSION = "version";
    private final static String IMAGE_URI = "image_uri";
    private final static String TEXT_OVERLAY_PATH = "text_overlay_path";
    private final static String MODE_CLASS_NAME = "mode_class_name";
    private final int mVersion;
    private final String mFileName;
    private Context mContext;
    private String mModeClassName;
    private String mImageUri;
    private String mTextOverlayPath;

    private Settings() {
        mVersion = 1;
        mFileName = "settings.json";
        mModeClassName = ModeFactory.getDefaultModeClassName();
    }

    Settings(Context context) {
        this();
        mContext = context;
    }

    boolean load() {
        boolean loaded = false;
        JsonReader reader = null;
        try {
            InputStream in = new FileInputStream(getFile());
            reader = new JsonReader(new InputStreamReader(in, "UTF-8"));
            read(reader);
            loaded = true;
        } catch (Exception ignore) {
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (Exception ignore) {
                }
            }
        }
        return loaded;
    }

    boolean save() {
        boolean saved = false;
        JsonWriter writer = null;
        try {
            OutputStream out = new FileOutputStream(getFile());
            writer = new JsonWriter(new OutputStreamWriter(out, "UTF-8"));
            writer.setIndent(" ");
            write(writer);
            saved = true;
        } catch (Exception ignore) {
        } finally {
            if (writer != null) {
                try {
                    writer.close();
                } catch (Exception ignore) {
                }
            }
        }
        return saved;
    }

    void setModeClassName(String modeClassName) {
        mModeClassName = modeClassName;
    }

    String getModeClassName() {
        return mModeClassName;
    }

    void setImageUri(Uri uri) {
        mImageUri = uri == null ? null : uri.toString();
    }

    Uri getImageUri() {
        if (mImageUri == null)
            return null;
        return Uri.parse(mImageUri);
    }

    File getTextOverlayFile() {
        if (mTextOverlayPath == null)
            mTextOverlayPath = new File(mContext.getFilesDir(), "text_overlay.json").getPath();
        return new File(mTextOverlayPath);
    }

    private File getFile() {
        return new File(mContext.getFilesDir(), mFileName);
    }

    private void write(JsonWriter writer) throws IOException {
        writer.beginObject();
        {
            writeVersion(writer);
            writeModeClassName(writer);
            writeImageUri(writer);
            writeTextOverlayPath(writer);
        }
        writer.endObject();
    }

    private void writeVersion(JsonWriter writer) throws IOException {
        writer.name(VERSION).value(mVersion);
    }

    private void writeModeClassName(JsonWriter writer) throws IOException {
        writer.name(MODE_CLASS_NAME).value(mModeClassName);
    }

    private void writeImageUri(JsonWriter writer) throws IOException {
        writer.name(IMAGE_URI).value(mImageUri);
    }

    private void writeTextOverlayPath(JsonWriter writer) throws IOException {
        writer.name(TEXT_OVERLAY_PATH).value(mTextOverlayPath);
    }

    private void read(JsonReader reader) throws IOException {
        reader.beginObject();
        {
            if (readVersion(reader) == mVersion) {
                readModeClassName(reader);
                readImageUri(reader);
                readTextOverlayPath(reader);
            }
        }
        reader.endObject();
    }

    private int readVersion(JsonReader reader) throws IOException {
        reader.nextName();
        return reader.nextInt();
    }

    private void readModeClassName(JsonReader reader) throws IOException {
        reader.nextName();
        mModeClassName = reader.nextString();
    }

    private void readImageUri(JsonReader reader) throws IOException {
        reader.nextName();
        if (reader.peek() == JsonToken.NULL) {
            reader.nextNull();
            mImageUri = null;
        } else
            mImageUri = reader.nextString();
    }

    private void readTextOverlayPath(JsonReader reader) throws IOException {
        reader.nextName();
        mTextOverlayPath = reader.nextString();
    }
}


================================================
FILE: app/src/main/java/om/sstvencoder/TextOverlay/IReader.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.TextOverlay;

import java.io.IOException;

public interface IReader {
    void beginRootObject() throws IOException;

    void beginObject() throws IOException;

    void endObject() throws IOException;

    void beginArray() throws IOException;

    void endArray() throws IOException;

    boolean hasNext() throws IOException;

    String readString() throws IOException;

    boolean readBoolean() throws IOException;

    float readFloat() throws IOException;

    int readInt() throws IOException;
}


================================================
FILE: app/src/main/java/om/sstvencoder/TextOverlay/IWriter.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.TextOverlay;

import androidx.annotation.NonNull;

import java.io.IOException;

public interface IWriter {
    void beginRootObject() throws IOException;

    void beginObject(@NonNull String name) throws IOException;

    void endObject() throws IOException;

    void beginArray(@NonNull String name) throws IOException;

    void endArray() throws IOException;

    void write(@NonNull String name, String value) throws IOException;

    void write(@NonNull String name, boolean value) throws IOException;

    void write(@NonNull String name, float value) throws IOException;

    void write(@NonNull String name, int value) throws IOException;
}


================================================
FILE: app/src/main/java/om/sstvencoder/TextOverlay/Label.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.TextOverlay;

import android.graphics.Color;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

public class Label implements Serializable {
    public static final float TEXT_SIZE_NORMAL = 2f;
    public static final float OUTLINE_SIZE_NORMAL = 0.05f;
    private String mText;
    private float mTextSize, mOutlineSize;
    private String mFamilyName;
    private boolean mBold, mItalic, mOutline;
    private int mForeColor, mBackColor, mOutlineColor;

    public Label() {
        mText = "";
        mTextSize = TEXT_SIZE_NORMAL;
        mFamilyName = null;
        mBold = true;
        mItalic = false;
        mForeColor = Color.BLACK;
        mBackColor = Color.TRANSPARENT;
        mOutline = true;
        mOutlineSize = OUTLINE_SIZE_NORMAL;
        mOutlineColor = Color.WHITE;
    }

    public String getText() {
        return mText;
    }

    public void setText(String text) {
        if (text != null)
            mText = text;
    }

    public float getTextSize() {
        return mTextSize;
    }

    public void setTextSize(float size) {
        if (size > 0f)
            mTextSize = size;
    }

    public String getFamilyName() {
        return mFamilyName;
    }

    public void setFamilyName(String familyName) {
        mFamilyName = familyName;
    }

    public boolean getBold() {
        return mBold;
    }

    public void setBold(boolean bold) {
        mBold = bold;
    }

    public boolean getItalic() {
        return mItalic;
    }

    public void setItalic(boolean italic) {
        mItalic = italic;
    }

    public int getForeColor() {
        return mForeColor;
    }

    public void setForeColor(int color) {
        mForeColor = color;
    }

    public int getBackColor() {
        return mBackColor;
    }

    public void setBackColor(int color) {
        mBackColor = color;
    }

    public boolean getOutline() {
        return mOutline;
    }

    public void setOutline(boolean outline) {
        mOutline = outline;
    }

    public float getOutlineSize() {
        return mOutlineSize;
    }

    public void setOutlineSize(float size) {
        mOutlineSize = size;
    }

    public int getOutlineColor() {
        return mOutlineColor;
    }

    public void setOutlineColor(int color) {
        mOutlineColor = color;
    }

    public Label getClone() {
        Label clone = new Label();
        try {
            for (Field field : getClass().getDeclaredFields()) {
                if (!Modifier.isFinal(field.getModifiers()))
                    field.set(clone, field.get(this));
            }
        } catch (Exception ignore) {
        }
        return clone;
    }
}


================================================
FILE: app/src/main/java/om/sstvencoder/TextOverlay/LabelCollection.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.TextOverlay;

import android.graphics.Canvas;
import android.graphics.Rect;
import androidx.annotation.NonNull;

import java.io.IOException;
import java.util.LinkedList;
import java.util.List;

public class LabelCollection {
    private class Size {
        private float mW, mH;

        Size(float w, float h) {
            mW = w;
            mH = h;
        }

        float width() {
            return mW;
        }

        float height() {
            return mH;
        }
    }

    private final int mVersion;
    private final List<LabelContainer> mLabels;
    private Size mScreenSize;
    private float mTextSizeFactor;
    private LabelContainer mActiveLabel, mEditLabel;
    private float mPreviousX, mPreviousY;

    public LabelCollection() {
        mVersion = 1;
        mLabels = new LinkedList<>();
        mPreviousX = 0f;
        mPreviousY = 0f;
    }

    public void update(float w, float h, float textSizeFactor) {
        if (mScreenSize != null) {
            float x = (w - mScreenSize.width()) / 2f;
            float y = (h - mScreenSize.height()) / 2f;
            for (LabelContainer label : mLabels)
                label.offset(x, y);
        }
        mScreenSize = new Size(w, h);
        mTextSizeFactor = textSizeFactor;
        for (LabelContainer label : mLabels)
            label.update(mTextSizeFactor, w, h);
    }

    public void draw(Canvas canvas) {
        for (LabelContainer label : mLabels)
            label.draw(canvas);
        if (mActiveLabel != null)
            mActiveLabel.drawActive(canvas);
    }

    public void draw(Canvas canvas, Rect src, Rect dst) {
        for (LabelContainer label : mLabels)
            label.draw(canvas, src, dst);
    }

    public boolean moveLabelBegin(float x, float y) {
        mActiveLabel = find(x, y);
        if (mActiveLabel == null)
            return false;
        mLabels.remove(mActiveLabel);
        mPreviousX = x;
        mPreviousY = y;
        mActiveLabel.jumpInside(mTextSizeFactor, mScreenSize.width(), mScreenSize.height());
        return true;
    }

    public void moveLabel(float x, float y) {
        mActiveLabel.offset(x - mPreviousX, y - mPreviousY);
        mActiveLabel.update(mTextSizeFactor, mScreenSize.width(), mScreenSize.height());
        mPreviousX = x;
        mPreviousY = y;
    }

    public void moveLabelEnd() {
        mLabels.add(mActiveLabel);
        mActiveLabel = null;
        mPreviousX = 0f;
        mPreviousY = 0f;
    }

    public Label editLabelBegin(float x, float y) {
        mEditLabel = find(x, y);
        if (mEditLabel == null) {
            mEditLabel = new LabelContainer(new Label());
            mEditLabel.offset(x, y);
        }
        return mEditLabel.getContent();
    }

    public void editLabelEnd(Label label) {
        if (mEditLabel != null && label != null) {
            if ("".equals(label.getText().trim())) {
                if (mLabels.contains(mEditLabel))
                    mLabels.remove(mEditLabel);
            } else {
                if (!mLabels.contains(mEditLabel))
                    mLabels.add(mEditLabel);
                mEditLabel.setContent(label);
                mEditLabel.update(mTextSizeFactor, mScreenSize.width(), mScreenSize.height());
            }
        }
        mEditLabel = null;
    }

    private LabelContainer find(float x, float y) {
        for (LabelContainer label : mLabels) {
            if (label.contains(x, y))
                return label;
        }
        return null;
    }

    private void add(LabelContainer label) {
        if (mLabels.size() == 0)
            mLabels.add(label);
        else
            mLabels.add(0, label);
    }

    public void write(@NonNull IWriter writer) throws IOException {
        writer.beginRootObject();
        {
            writer.write("version", mVersion);
            writer.write("width", mScreenSize.width());
            writer.write("height", mScreenSize.height());
            writer.write("factor", mTextSizeFactor);
            writer.beginArray("labels");
            {
                for (LabelContainer label : mLabels)
                    label.write(writer);
            }
            writer.endArray();
        }
        writer.endObject();
    }

    public boolean read(@NonNull IReader reader) throws IOException {
        reader.beginRootObject();
        {
            if (reader.readInt() != mVersion)
                return false;

            float w = reader.readFloat();
            float h = reader.readFloat();
            float textSizeFactor = reader.readFloat();
            reader.beginArray();
            {
                while (reader.hasNext()) {
                    LabelContainer label = new LabelContainer(new Label());
                    label.read(reader);
                    add(label);
                }
            }
            reader.endArray();
            update(w, h, textSizeFactor);
        }
        reader.endObject();
        return true;
    }
}


================================================
FILE: app/src/main/java/om/sstvencoder/TextOverlay/LabelContainer.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.TextOverlay;

import android.graphics.Canvas;
import android.graphics.Rect;
import androidx.annotation.NonNull;

import java.io.IOException;

class LabelContainer {
    private Label mLabel;
    private LabelPainter mPainter;
    private Position mPosition; // left-bottom corner

    LabelContainer(@NonNull Label label) {
        mLabel = label;
        mPainter = new LabelPainter(label);
        mPosition = new Position();
    }

    boolean contains(float x, float y) {
        return mPainter.getBounds().contains(x, y);
    }

    void draw(Canvas canvas) {
        mPainter.draw(canvas);
    }

    void drawActive(Canvas canvas) {
        mPainter.drawActive(canvas);
    }

    void draw(Canvas canvas, Rect src, Rect dst) {
        mPainter.draw(canvas, src, dst);
    }

    void jumpInside(float textSizeFactor, float screenW, float screenH) {
        mPainter.moveLabelInside(textSizeFactor, screenW, screenH, mPosition);
    }

    void offset(float x, float y) {
        mPosition.offset(x, y);
    }

    void update(float textSizeFactor, float screenW, float screenH) {
        mPainter.update(textSizeFactor, screenW, screenH, mPosition);
    }

    Label getContent() {
        return mLabel;
    }

    void setContent(@NonNull Label label) {
        mLabel = label;
        mPainter.setLabel(label);
    }

    void write(IWriter writer) throws IOException {
        writer.beginRootObject();
        {
            writer.write("position_x", mPosition.getX());
            writer.write("position_y", mPosition.getY());
            writer.beginObject("label");
            {
                writeLabel(writer, mLabel);
            }
            writer.endObject();
        }
        writer.endObject();
    }

    void read(IReader reader) throws IOException {
        reader.beginRootObject();
        {
            mPosition.set(reader.readFloat(), reader.readFloat());
            reader.beginObject();
            {
                readLabel(reader, mLabel);
            }
            reader.endObject();
        }
        reader.endObject();
    }

    private void writeLabel(IWriter writer, Label label) throws IOException {
        writer.write("text", label.getText());
        writer.write("text_size", label.getTextSize());
        writer.write("family_name", label.getFamilyName());
        writer.write("bold", label.getBold());
        writer.write("italic", label.getItalic());
        writer.write("fore_color", label.getForeColor());
        writer.write("back_color", label.getBackColor());
        writer.write("outline", label.getOutline());
        writer.write("outline_size", label.getOutlineSize());
        writer.write("outline_color", label.getOutlineColor());
    }

    private void readLabel(IReader reader, Label label) throws IOException {
        label.setText(reader.readString());
        label.setTextSize(reader.readFloat());
        label.setFamilyName(reader.readString());
        label.setBold(reader.readBoolean());
        label.setItalic(reader.readBoolean());
        label.setForeColor(reader.readInt());
        label.setBackColor(reader.readInt());
        label.setOutline(reader.readBoolean());
        label.setOutlineSize(reader.readFloat());
        label.setOutlineColor(reader.readInt());
    }
}



================================================
FILE: app/src/main/java/om/sstvencoder/TextOverlay/LabelPainter.java
================================================
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package om.sstvencoder.TextOverlay;

import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Typeface;

import androidx.annotation.NonNull;
import om.sstvencoder.Utility;

class LabelPainter {
    private interface IDrawer {
        void draw(Canvas canvas);

        void drawShadow(Canvas canvas);

        void draw(Canvas canvas, Rect src, Rect dst);

        RectF getBounds();
    }

    private class InDrawer implements IDrawer {
        private float mSizeFactor;
        private float mX, mY;

        private InDrawer(float sizeFactor, float x, float y) {
            mSizeFactor = sizeFactor;
            setPosition(x, y);
            setPaintSettings(mSizeFactor);
        }

        @Override
        public void draw(Canvas canvas) {
            drawOutline(canvas, mX, mY);
            canvas.drawText(mLabel.getText(), mX, mY, mPaint);
        }

        @Override
        public void drawShadow(Canvas canvas) {
            RectF bounds = new RectF(getBounds());
            float rx = 10f;
            float ry = 10f;
            mPaint.setStrokeWidth(0f);

            mPaint.setColor(Color.LTGRAY);
            mPaint.setAlpha(100);
            mPaint.setStyle(Paint.Style.FILL);
            canvas.drawRoundRect(bounds, rx, ry, mPaint);

            mPaint.setAlpha(255);
            mPaint.setStyle(Paint.Style.STROKE);

            mPaint.setColor(Color.BLUE);
            canvas.drawRoundRect(bounds, rx, ry, mPaint);

            mPaint.setColor(Color.GREEN);
            bounds.inset(-1f, -1f);
            canvas.drawRoundRect(bounds, rx, ry, mPaint);

            mPaint.setColor(Color.RED);
            bounds.inset(-1f, -1f);
            canvas.drawRoundRect(bounds, rx, ry, mPaint);

            setPaintSettings(mSizeFactor);
        }

        @Override
        public void draw(Canvas canvas, Rect src, Rect dst) {
            float factor = (dst.height() / (float) src.height());
            float x = (mX - src.left) * factor;
            float y = (mY - src.top) * factor;
            setSizePaintSettings(factor * mSizeFactor);
            drawOutline(canvas, x, y);
            canvas.drawText(mLabel.getText(), x, y, mPaint);
            setSizePaintSettings(mSizeFactor);
        }

        @Override
        public RectF getBounds() {
            RectF bounds = new RectF(getTextBounds());
            bounds.offset(mX, mY);
            if (mLabel.getOutline()) {
                float inset = mLabel.getOutlineSize() * mPaint.getTextSize();
                bounds.inset(-inset, -inset);
            }
            return bounds;
        }

        private void setPosition(float x, float y) {
            mX = x;
            mY = y;
        }

        private float getOneLetterSize() {
            Rect bounds = new Rect();
            mPaint.getTextBounds("M", 0, 1, bounds);
            return bounds.width();
        }

        private Rect getTextBounds() {
            Rect bounds = new Rect();
            String text = mLabel.getText();
            mPaint.getTextBounds(text, 0, text.length(), bounds);
            return bounds;
        }

        private void drawOutline(Canvas canvas, float x, float y) {
            if (mLabel.getOutline()) {
                setOutlinePaintSettings();
                canvas.drawText(mLabel.getText(), x, y, mPaint);
                setTextPaintSettings();
            }
        }

        private void setPaintSettings(float sizeFactor) {
            mPaint.setAlpha(255);
            try {
                mPaint.setTypeface(createTypeface());
            } catch (Exception ignore) {
            }
            setTextPaintSettings();
            
Download .txt
gitextract_y4ynicm8/

├── .gitignore
├── LICENSE
├── NOTICE
├── README.md
├── app/
│   ├── .gitignore
│   ├── build.gradle
│   ├── proguard-rules.pro
│   └── src/
│       └── main/
│           ├── AndroidManifest.xml
│           ├── java/
│           │   └── om/
│           │       └── sstvencoder/
│           │           ├── ColorFragment.java
│           │           ├── ColorPalette/
│           │           │   ├── ColorPaletteView.java
│           │           │   ├── GridColorPalette.java
│           │           │   └── IColorPalette.java
│           │           ├── CropView.java
│           │           ├── EditTextActivity.java
│           │           ├── Encoder.java
│           │           ├── FontFamilySet.java
│           │           ├── MainActivity.java
│           │           ├── MainActivityMessenger.java
│           │           ├── ModeInterfaces/
│           │           │   ├── IMode.java
│           │           │   ├── IModeInfo.java
│           │           │   └── ModeSize.java
│           │           ├── Modes/
│           │           │   ├── ImageFormats/
│           │           │   │   ├── NV21.java
│           │           │   │   ├── YUV440P.java
│           │           │   │   ├── YUY2.java
│           │           │   │   ├── YV12.java
│           │           │   │   ├── Yuv.java
│           │           │   │   ├── YuvConverter.java
│           │           │   │   ├── YuvFactory.java
│           │           │   │   └── YuvImageFormat.java
│           │           │   ├── Martin.java
│           │           │   ├── Martin1.java
│           │           │   ├── Martin2.java
│           │           │   ├── Mode.java
│           │           │   ├── ModeDescription.java
│           │           │   ├── ModeFactory.java
│           │           │   ├── ModeInfo.java
│           │           │   ├── PD.java
│           │           │   ├── PD120.java
│           │           │   ├── PD160.java
│           │           │   ├── PD180.java
│           │           │   ├── PD240.java
│           │           │   ├── PD290.java
│           │           │   ├── PD50.java
│           │           │   ├── PD90.java
│           │           │   ├── Robot36.java
│           │           │   ├── Robot72.java
│           │           │   ├── Scottie.java
│           │           │   ├── Scottie1.java
│           │           │   ├── Scottie2.java
│           │           │   ├── ScottieDX.java
│           │           │   └── Wraase.java
│           │           ├── Output/
│           │           │   ├── AudioOutput.java
│           │           │   ├── IOutput.java
│           │           │   ├── OutputFactory.java
│           │           │   ├── WaveFileOutput.java
│           │           │   └── WaveFileOutputContext.java
│           │           ├── ProgressBarWrapper.java
│           │           ├── Settings.java
│           │           ├── TextOverlay/
│           │           │   ├── IReader.java
│           │           │   ├── IWriter.java
│           │           │   ├── Label.java
│           │           │   ├── LabelCollection.java
│           │           │   ├── LabelContainer.java
│           │           │   ├── LabelPainter.java
│           │           │   └── Position.java
│           │           ├── TextOverlayTemplate.java
│           │           └── Utility.java
│           └── res/
│               ├── layout/
│               │   ├── activity_edit_text.xml
│               │   ├── activity_main.xml
│               │   └── fragment_color.xml
│               ├── menu/
│               │   ├── menu_edit_text.xml
│               │   └── menu_main.xml
│               ├── values/
│               │   ├── strings.xml
│               │   └── styles.xml
│               ├── values-v35/
│               │   └── styles.xml
│               ├── values-zh-rCN/
│               │   └── strings.xml
│               └── xml/
│                   └── paths.xml
├── build.gradle
├── fastlane/
│   └── metadata/
│       └── android/
│           └── en-US/
│               ├── full_description.txt
│               ├── short_description.txt
│               └── title.txt
├── gradle/
│   └── wrapper/
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
Download .txt
SYMBOL INDEX (579 symbols across 57 files)

FILE: app/src/main/java/om/sstvencoder/ColorFragment.java
  class ColorFragment (line 31) | public class ColorFragment extends DialogFragment
    type OnColorSelectedListener (line 34) | public interface OnColorSelectedListener {
      method onColorSelected (line 35) | void onColorSelected(DialogFragment fragment, int color);
      method onCancel (line 37) | void onCancel(DialogFragment fragment);
    method ColorFragment (line 44) | public ColorFragment() {
    method setTitle (line 50) | public void setTitle(int title) {
    method setColor (line 54) | public void setColor(int color) {
    method addOnColorSelectedListener (line 58) | public void addOnColorSelectedListener(OnColorSelectedListener listene...
    method onCreateDialog (line 62) | @NonNull
    method onColorChanged (line 75) | @Override
    method onColorSelected (line 79) | @Override
    method onCancel (line 86) | @Override

FILE: app/src/main/java/om/sstvencoder/ColorPalette/ColorPaletteView.java
  class ColorPaletteView (line 29) | public class ColorPaletteView extends View {
    type OnColorSelectedListener (line 31) | public interface OnColorSelectedListener {
      method onColorChanged (line 32) | void onColorChanged(View v, int color);
      method onColorSelected (line 34) | void onColorSelected(View v, int color);
      method onCancel (line 36) | void onCancel(View v);
    method ColorPaletteView (line 42) | public ColorPaletteView(Context context, AttributeSet attrs) {
    method getColor (line 49) | public int getColor() {
    method setColor (line 53) | public void setColor(int color) {
    method onSizeChanged (line 57) | @Override
    method onDraw (line 63) | @Override
    method onTouchEvent (line 69) | @Override
    method update (line 93) | private void update(float x, float y) {
    method addOnColorSelectedListener (line 100) | public void addOnColorSelectedListener(OnColorSelectedListener listene...
    method colorChangedCallback (line 104) | private void colorChangedCallback() {
    method colorSelectedCallback (line 110) | private void colorSelectedCallback() {
    method cancelCallback (line 116) | private void cancelCallback() {

FILE: app/src/main/java/om/sstvencoder/ColorPalette/GridColorPalette.java
  class GridColorPalette (line 23) | class GridColorPalette implements IColorPalette {
    method getStandardColors (line 25) | static int[] getStandardColors() {
    method GridColorPalette (line 53) | GridColorPalette(int[] colorList, float displayMetricsDensity) {
    method updateSize (line 65) | @Override
    method updateGrid (line 80) | private void updateGrid() {
    method min (line 96) | private int min(int a, int b) {
    method updateBoxSizeAndSpace (line 101) | private void updateBoxSizeAndSpace() {
    method draw (line 109) | @Override
    method drawSelectedRect (line 129) | private void drawSelectedRect(Canvas canvas) {
    method setPaintStyleForSelectedBox (line 142) | private void setPaintStyleForSelectedBox() {
    method setPaintStyleForBox (line 148) | private void setPaintStyleForBox() {
    method getSelectedColor (line 153) | @Override
    method selectColor (line 158) | @Override
    method selectColor (line 182) | @Override
    method setSelectedColor (line 196) | private void setSelectedColor(int i) {

FILE: app/src/main/java/om/sstvencoder/ColorPalette/IColorPalette.java
  type IColorPalette (line 20) | interface IColorPalette {
    method updateSize (line 21) | void updateSize(float width, float height);
    method draw (line 23) | void draw(Canvas canvas);
    method getSelectedColor (line 25) | int getSelectedColor();
    method selectColor (line 27) | boolean selectColor(float x, float y);
    method selectColor (line 29) | boolean selectColor(int color);

FILE: app/src/main/java/om/sstvencoder/CropView.java
  class CropView (line 44) | public class CropView extends AppCompatImageView {
    class GestureListener (line 45) | private class GestureListener extends GestureDetector.SimpleOnGestureL...
      method onScroll (line 46) | @Override
      method onLongPress (line 55) | @Override
      method onSingleTapConfirmed (line 64) | @Override
    class ScaleGestureListener (line 74) | private class ScaleGestureListener extends ScaleGestureDetector.Simple...
      method onScaleBegin (line 75) | @Override
      method onScale (line 84) | @Override
      method onScaleEnd (line 90) | @Override
    method CropView (line 115) | public CropView(Context context, AttributeSet attrs) {
    method setModeSize (line 140) | public void setModeSize(ModeSize size) {
    method resetInputRect (line 148) | private void resetInputRect() {
    method rotateImage (line 164) | public void rotateImage(int orientation) {
    method resetImage (line 178) | public void resetImage() {
    method setNoBitmap (line 191) | public void setNoBitmap() {
    method setBitmap (line 198) | public void setBitmap(@NonNull InputStream stream) throws Exception {
    method loadImage (line 206) | private void loadImage(InputStream stream) throws Exception {
    method recycle (line 254) | private void recycle() {
    method scaleImage (line 265) | public void scaleImage(float scaleFactor) {
    method moveImage (line 279) | public void moveImage(float distanceX, float distanceY) {
    method onTouchEvent (line 294) | @Override
    method onSizeChanged (line 317) | @Override
    method onDraw (line 325) | @Override
    method maximizeImageToCanvasRect (line 337) | private void maximizeImageToCanvasRect() {
    method adjustCanvasAndImageRect (line 348) | private void adjustCanvasAndImageRect(int width, int height) {
    method drawModeRect (line 368) | private void drawModeRect(Canvas canvas) {
    method drawRectInset (line 377) | private void drawRectInset(Canvas canvas, Rect rect, int inset) {
    method getIntRect (line 385) | private Rect getIntRect(RectF rect) {
    method getSampleSize (line 393) | private int getSampleSize() {
    method getBitmap (line 400) | public Bitmap getBitmap() {
    method drawBitmap (line 413) | private void drawBitmap(Canvas canvas) {
    method rotateDrawRectangles (line 429) | private void rotateDrawRectangles() {
    method updateCache (line 450) | private void updateCache() {
    method editLabelBegin (line 477) | private void editLabelBegin(float x, float y) {
    method editLabelEnd (line 482) | public void editLabelEnd(Label label) {
    method getLabels (line 487) | public LabelCollection getLabels() {
    method GetActivity (line 491) | private MainActivity GetActivity() {

FILE: app/src/main/java/om/sstvencoder/EditTextActivity.java
  class EditTextActivity (line 44) | public class EditTextActivity extends AppCompatActivity
    type EditColorMode (line 47) | private enum EditColorMode {
    method onCreate (line 61) | @Override
    method onStart (line 71) | @Override
    method initText (line 87) | private void initText() {
    method initFontFamilySpinner (line 147) | private void initFontFamilySpinner(String familyName) {
    method initTextSizeSpinner (line 156) | private void initTextSizeSpinner(float textSize) {
    method initOutlineSizeSpinner (line 171) | private void initOutlineSizeSpinner(float outlineSize) {
    method textSizeToPosition (line 185) | private int textSizeToPosition(float textSize) {
    method positionToTextSize (line 193) | private float positionToTextSize(int position) {
    method outlineSizeToPosition (line 197) | private int outlineSizeToPosition(float outlineSize) {
    method positionToOutlineSize (line 205) | private float positionToOutlineSize(int position) {
    method onItemSelected (line 209) | @Override
    method enableOutline (line 223) | private void enableOutline(boolean enabled) {
    method onNothingSelected (line 233) | @Override
    method onCreateOptionsMenu (line 237) | @Override
    method onOptionsItemSelected (line 243) | @Override
    method onBoldClick (line 253) | public void onBoldClick(View view) {
    method onItalicClick (line 257) | public void onItalicClick(View view) {
    method onOutlineClick (line 261) | public void onOutlineClick(View view) {
    method onColorClick (line 269) | public void onColorClick(View view) {
    method onOutlineColorClick (line 274) | public void onOutlineColorClick(View view) {
    method showColorDialog (line 281) | private void showColorDialog(int title, int color) {
    method onColorSelected (line 289) | @Override
    method onCancel (line 304) | @Override
    method done (line 309) | private void done() {

FILE: app/src/main/java/om/sstvencoder/Encoder.java
  class Encoder (line 31) | class Encoder {
    method Encoder (line 40) | Encoder(MainActivityMessenger messenger,
    method setMode (line 88) | boolean setMode(String className) {
    method getModeInfo (line 97) | IModeInfo getModeInfo() {
    method getModeInfoList (line 101) | IModeInfo[] getModeInfoList() {
    method play (line 105) | void play(Bitmap bitmap) {
    method save (line 112) | void save(Bitmap bitmap, WaveFileOutputContext context) {
    method save (line 121) | private void save(final IMode mode, final WaveFileOutputContext contex...
    method stop (line 146) | void stop() {
    method enqueue (line 155) | private void enqueue(IMode mode) {
    method destroy (line 162) | void destroy() {

FILE: app/src/main/java/om/sstvencoder/FontFamilySet.java
  class FontFamilySet (line 33) | class FontFamilySet {
    class FontFamily (line 34) | class FontFamily {
    method FontFamilySet (line 44) | FontFamilySet(Context context) {
    method getDefaultFontFamily (line 52) | @NonNull
    method getFontFamily (line 62) | @NonNull
    method getFontFamilyFromDisplayName (line 73) | @NonNull
    method getFontFamilyDisplayNameList (line 82) | @NonNull
    method fillWithSystemFonts (line 90) | private void fillWithSystemFonts(@NonNull List<FontFamily> familySet) {
    method readFamilySet (line 117) | private void readFamilySet(@NonNull XmlPullParser parser, @NonNull Lis...
    method readFamily (line 128) | @NonNull
    method readNameSet (line 148) | private void readNameSet(@NonNull XmlPullParser parser, @NonNull FontF...
    method readFileSet (line 163) | private void readFileSet(@NonNull XmlPullParser parser, @NonNull FontF...
    method parseDisplayNameAndStyle (line 171) | private void parseDisplayNameAndStyle(String fontFileName, @NonNull Fo...
    method readText (line 191) | private String readText(@NonNull XmlPullParser parser)

FILE: app/src/main/java/om/sstvencoder/MainActivity.java
  class MainActivity (line 49) | public class MainActivity extends AppCompatActivity {
    method onCreate (line 61) | @Override
    method getProgressBar (line 78) | private ProgressBarWrapper getProgressBar() {
    method getProgressBar2 (line 84) | private ProgressBarWrapper getProgressBar2() {
    method onNewIntent (line 90) | @Override
    method loadImage (line 96) | private void loadImage(Intent intent) {
    method getImageUriFromIntent (line 107) | private Uri getImageUriFromIntent(Intent intent) {
    method loadImage (line 121) | private boolean loadImage(Uri uri, boolean verbose) {
    method setDefaultBitmap (line 148) | private void setDefaultBitmap() {
    method isIntentActionValid (line 157) | private boolean isIntentActionValid(String action) {
    method isIntentTypeValid (line 161) | private boolean isIntentTypeValid(String type) {
    method isPermissionException (line 165) | private boolean isPermissionException(Exception ex) {
    method needsRequestReadPermission (line 170) | private boolean needsRequestReadPermission() {
    method needsRequestWritePermission (line 178) | private boolean needsRequestWritePermission() {
    method requestReadPermission (line 187) | private void requestReadPermission(int requestCode) {
    method requestWritePermission (line 192) | private void requestWritePermission(int requestCode) {
    method onRequestPermissionsResult (line 197) | @Override
    method permissionGranted (line 222) | private boolean permissionGranted(@NonNull int[] grantResults) {
    method showFileNotLoadedMessage (line 226) | private void showFileNotLoadedMessage(Exception ex, boolean verbose) {
    method showErrorMessage (line 235) | private void showErrorMessage(final String title, final String shortTe...
    method showOrientationErrorMessage (line 252) | private void showOrientationErrorMessage(Uri uri, Exception ex) {
    method getOrientation (line 258) | public int getOrientation(ContentResolver resolver, Uri uri) {
    method getExifOrientation (line 273) | private int getExifOrientation(ContentResolver resolver, Uri uri) {
    method onCreateOptionsMenu (line 293) | @Override
    method createModesMenu (line 300) | private void createModesMenu(Menu menu) {
    method onOptionsItemSelected (line 311) | @Override
    method openLinkInBrowser (line 351) | private void openLinkInBrowser(String link) {
    method showTextPage (line 361) | private void showTextPage(String title, String message) {
    method setMode (line 369) | private void setMode(String modeClassName) {
    method takePicture (line 378) | private void takePicture() {
    method hasCamera (line 389) | private boolean hasCamera() {
    method startEditTextActivity (line 393) | public void startEditTextActivity(@NonNull Label label) {
    method dispatchTakePictureIntent (line 399) | private void dispatchTakePictureIntent() {
    method dispatchPickPictureIntent (line 409) | private void dispatchPickPictureIntent() {
    method tryToStartActivityForResult (line 414) | private void tryToStartActivityForResult(Intent intent, int requestCod...
    method onActivityResult (line 426) | @Override
    method addImageToGallery (line 452) | private void addImageToGallery(Uri uri) {
    method play (line 458) | private void play() {
    method save (line 462) | private void save() {
    method completeSaving (line 470) | public void completeSaving(WaveFileOutputContext context) {
    method onPause (line 474) | @Override
    method onDestroy (line 481) | @Override

FILE: app/src/main/java/om/sstvencoder/MainActivityMessenger.java
  class MainActivityMessenger (line 22) | class MainActivityMessenger {
    method MainActivityMessenger (line 26) | MainActivityMessenger(MainActivity activity) {
    method carrySaveAsWaveIsDoneMessage (line 31) | void carrySaveAsWaveIsDoneMessage(final WaveFileOutputContext context) {
    method getString (line 40) | public String getString(int resId, Object... formatArgs) {

FILE: app/src/main/java/om/sstvencoder/ModeInterfaces/IMode.java
  type IMode (line 18) | public interface IMode {
    method init (line 19) | void init();
    method getProcessCount (line 21) | int getProcessCount();
    method process (line 23) | boolean process();
    method finish (line 25) | void finish(boolean cancel);

FILE: app/src/main/java/om/sstvencoder/ModeInterfaces/IModeInfo.java
  type IModeInfo (line 18) | public interface IModeInfo {
    method getModeName (line 19) | String getModeName();
    method getModeClassName (line 21) | String getModeClassName();
    method getModeSize (line 23) | ModeSize getModeSize();

FILE: app/src/main/java/om/sstvencoder/Modes/ImageFormats/NV21.java
  class NV21 (line 20) | class NV21 extends Yuv {
    method NV21 (line 21) | NV21(Bitmap bitmap) {
    method convertBitmapToYuv (line 25) | protected void convertBitmapToYuv(Bitmap bitmap) {
    method getY (line 49) | public int getY(int x, int y) {
    method getU (line 53) | public int getU(int x, int y) {
    method getV (line 57) | public int getV(int x, int y) {

FILE: app/src/main/java/om/sstvencoder/Modes/ImageFormats/YUV440P.java
  class YUV440P (line 20) | class YUV440P extends Yuv {
    method YUV440P (line 21) | YUV440P(Bitmap bitmap) {
    method convertBitmapToYuv (line 25) | protected void convertBitmapToYuv(Bitmap bitmap) {
    method getY (line 50) | public int getY(int x, int y) {
    method getU (line 54) | public int getU(int x, int y) {
    method getV (line 58) | public int getV(int x, int y) {

FILE: app/src/main/java/om/sstvencoder/Modes/ImageFormats/YUY2.java
  class YUY2 (line 20) | class YUY2 extends Yuv {
    method YUY2 (line 21) | YUY2(Bitmap bitmap) {
    method convertBitmapToYuv (line 25) | protected void convertBitmapToYuv(Bitmap bitmap) {
    method getY (line 42) | public int getY(int x, int y) {
    method getU (line 46) | public int getU(int x, int y) {
    method getV (line 50) | public int getV(int x, int y) {

FILE: app/src/main/java/om/sstvencoder/Modes/ImageFormats/YV12.java
  class YV12 (line 20) | class YV12 extends Yuv {
    method YV12 (line 21) | YV12(Bitmap bitmap) {
    method convertBitmapToYuv (line 25) | protected void convertBitmapToYuv(Bitmap bitmap) {
    method getY (line 54) | public int getY(int x, int y) {
    method getU (line 58) | public int getU(int x, int y) {
    method getV (line 62) | public int getV(int x, int y) {

FILE: app/src/main/java/om/sstvencoder/Modes/ImageFormats/Yuv.java
  class Yuv (line 20) | public abstract class Yuv {
    method Yuv (line 25) | Yuv(Bitmap bitmap) {
    method convertBitmapToYuv (line 31) | protected abstract void convertBitmapToYuv(Bitmap bitmap);
    method getWidth (line 33) | public int getWidth() {
    method getHeight (line 37) | public int getHeight() {
    method getY (line 41) | public abstract int getY(int x, int y);
    method getU (line 43) | public abstract int getU(int x, int y);
    method getV (line 45) | public abstract int getV(int x, int y);

FILE: app/src/main/java/om/sstvencoder/Modes/ImageFormats/YuvConverter.java
  class YuvConverter (line 20) | final class YuvConverter {
    method convertToY (line 21) | static int convertToY(int color) {
    method convertToU (line 28) | static int convertToU(int color) {
    method convertToV (line 35) | static int convertToV(int color) {
    method clamp (line 42) | private static int clamp(double value) {

FILE: app/src/main/java/om/sstvencoder/Modes/ImageFormats/YuvFactory.java
  class YuvFactory (line 20) | public final class YuvFactory {
    method createYuv (line 21) | public static Yuv createYuv(Bitmap bitmap, int format) {

FILE: app/src/main/java/om/sstvencoder/Modes/ImageFormats/YuvImageFormat.java
  class YuvImageFormat (line 20) | public class YuvImageFormat extends ImageFormat {

FILE: app/src/main/java/om/sstvencoder/Modes/Martin.java
  class Martin (line 23) | abstract class Martin extends Mode {
    method Martin (line 36) | Martin(Bitmap bitmap, IOutput output) {
    method getTransmissionSamples (line 49) | protected int getTransmissionSamples() {
    method writeEncodedLine (line 55) | protected void writeEncodedLine() {
    method addSyncPulse (line 66) | private void addSyncPulse() {
    method addSyncPorch (line 71) | private void addSyncPorch() {
    method addSeparator (line 76) | private void addSeparator() {
    method addGreenScan (line 81) | private void addGreenScan(int y) {
    method addBlueScan (line 86) | private void addBlueScan(int y) {
    method addRedScan (line 91) | private void addRedScan(int y) {
    method getColor (line 96) | private int getColor(int colorScanSample, int y) {

FILE: app/src/main/java/om/sstvencoder/Modes/Martin1.java
  class Martin1 (line 23) | @ModeSize(width = 320, height = 256)
    method Martin1 (line 28) | Martin1(Bitmap bitmap, IOutput output) {

FILE: app/src/main/java/om/sstvencoder/Modes/Martin2.java
  class Martin2 (line 23) | @ModeSize(width = 320, height = 256)
    method Martin2 (line 28) | Martin2(Bitmap bitmap, IOutput output) {

FILE: app/src/main/java/om/sstvencoder/Modes/Mode.java
  class Mode (line 23) | abstract class Mode implements IMode {
    method Mode (line 31) | protected Mode(Bitmap bitmap, IOutput output) {
    method init (line 37) | @Override
    method getProcessCount (line 45) | @Override
    method process (line 50) | @Override
    method finish (line 61) | @Override
    method getTotalSamples (line 67) | private int getTotalSamples() {
    method getHeaderSamples (line 71) | private int getHeaderSamples() {
    method getTransmissionSamples (line 77) | protected abstract int getTransmissionSamples();
    method writeCalibrationHeader (line 79) | private void writeCalibrationHeader() {
    method writeEncodedLine (line 117) | protected abstract void writeEncodedLine();
    method convertMsToSamples (line 119) | protected int convertMsToSamples(double durationMs) {
    method setTone (line 123) | protected void setTone(double frequency) {
    method setColorTone (line 129) | protected void setColorTone(int color) {
    method destroyBitmap (line 135) | private void destroyBitmap() {

FILE: app/src/main/java/om/sstvencoder/Modes/ModeFactory.java
  class ModeFactory (line 27) | public final class ModeFactory {
    method getDefaultMode (line 28) | public static Class<?> getDefaultMode() {
    method getDefaultModeClassName (line 32) | public static String getDefaultModeClassName() {
    method getModeInfoList (line 36) | public static IModeInfo[] getModeInfoList() {
    method getModeInfo (line 48) | public static IModeInfo getModeInfo(Class<?> modeClass) {
    method CreateMode (line 55) | public static IMode CreateMode(Class<?> modeClass, Bitmap bitmap, IOut...
    method isModeClassValid (line 73) | private static boolean isModeClassValid(Class<?> modeClass) {

FILE: app/src/main/java/om/sstvencoder/Modes/ModeInfo.java
  class ModeInfo (line 21) | class ModeInfo implements IModeInfo {
    method ModeInfo (line 24) | ModeInfo(Class<?> modeClass) {
    method getModeName (line 28) | public String getModeName() {
    method getModeClassName (line 32) | public String getModeClassName() {
    method getModeSize (line 36) | public ModeSize getModeSize() {

FILE: app/src/main/java/om/sstvencoder/Modes/PD.java
  class PD (line 25) | abstract class PD extends Mode {
    method PD (line 37) | PD(Bitmap bitmap, IOutput output) {
    method getTransmissionSamples (line 49) | protected int getTransmissionSamples() {
    method getProcessCount (line 54) | @Override
    method writeEncodedLine (line 59) | protected void writeEncodedLine() {
    method addSyncPulse (line 68) | private void addSyncPulse() {
    method addPorch (line 73) | private void addPorch() {
    method addYScan (line 78) | private void addYScan(int y) {
    method addUScan (line 83) | private void addUScan(int y) {
    method addVScan (line 88) | private void addVScan(int y) {

FILE: app/src/main/java/om/sstvencoder/Modes/PD120.java
  class PD120 (line 23) | @ModeSize(width = 640, height = 496)
    method PD120 (line 28) | PD120(Bitmap bitmap, IOutput output) {

FILE: app/src/main/java/om/sstvencoder/Modes/PD160.java
  class PD160 (line 23) | @ModeSize(width = 512, height = 400)
    method PD160 (line 28) | PD160(Bitmap bitmap, IOutput output) {

FILE: app/src/main/java/om/sstvencoder/Modes/PD180.java
  class PD180 (line 23) | @ModeSize(width = 640, height = 496)
    method PD180 (line 28) | PD180(Bitmap bitmap, IOutput output) {

FILE: app/src/main/java/om/sstvencoder/Modes/PD240.java
  class PD240 (line 23) | @ModeSize(width = 640, height = 496)
    method PD240 (line 28) | PD240(Bitmap bitmap, IOutput output) {

FILE: app/src/main/java/om/sstvencoder/Modes/PD290.java
  class PD290 (line 23) | @ModeSize(width = 800, height = 616)
    method PD290 (line 28) | PD290(Bitmap bitmap, IOutput output) {

FILE: app/src/main/java/om/sstvencoder/Modes/PD50.java
  class PD50 (line 23) | @ModeSize(width = 320, height = 256)
    method PD50 (line 28) | PD50(Bitmap bitmap, IOutput output) {

FILE: app/src/main/java/om/sstvencoder/Modes/PD90.java
  class PD90 (line 23) | @ModeSize(width = 320, height = 256)
    method PD90 (line 28) | PD90(Bitmap bitmap, IOutput output) {

FILE: app/src/main/java/om/sstvencoder/Modes/Robot36.java
  class Robot36 (line 26) | @ModeSize(width = 320, height = 240)
    method Robot36 (line 49) | Robot36(Bitmap bitmap, IOutput output) {
    method getTransmissionSamples (line 72) | protected int getTransmissionSamples() {
    method writeEncodedLine (line 79) | protected void writeEncodedLine() {
    method addSyncPulse (line 95) | private void addSyncPulse() {
    method addSyncPorch (line 100) | private void addSyncPorch() {
    method addSeparator (line 105) | private void addSeparator(double separatorFrequency) {
    method addPorch (line 110) | private void addPorch() {
    method addYScan (line 115) | private void addYScan(int y) {
    method addUScan (line 120) | private void addUScan(int y) {
    method addVScan (line 125) | private void addVScan(int y) {

FILE: app/src/main/java/om/sstvencoder/Modes/Robot72.java
  class Robot72 (line 26) | @ModeSize(width = 320, height = 240)
    method Robot72 (line 49) | Robot72(Bitmap bitmap, IOutput output) {
    method getTransmissionSamples (line 72) | protected int getTransmissionSamples() {
    method writeEncodedLine (line 78) | protected void writeEncodedLine() {
    method addSyncPulse (line 90) | private void addSyncPulse() {
    method addSyncPorch (line 95) | private void addSyncPorch() {
    method addSeparator (line 100) | private void addSeparator(double separatorFrequency) {
    method addPorch (line 105) | private void addPorch() {
    method addYScan (line 110) | private void addYScan(int y) {
    method addUScan (line 115) | private void addUScan(int y) {
    method addVScan (line 120) | private void addVScan(int y) {

FILE: app/src/main/java/om/sstvencoder/Modes/Scottie.java
  class Scottie (line 23) | abstract class Scottie extends Mode {
    method Scottie (line 36) | Scottie(Bitmap bitmap, IOutput output) {
    method getTransmissionSamples (line 49) | protected int getTransmissionSamples() {
    method writeEncodedLine (line 55) | protected void writeEncodedLine() {
    method addSyncPulse (line 68) | private void addSyncPulse() {
    method addSyncPorch (line 73) | private void addSyncPorch() {
    method addSeparator (line 78) | private void addSeparator() {
    method addGreenScan (line 83) | private void addGreenScan(int y) {
    method addBlueScan (line 88) | private void addBlueScan(int y) {
    method addRedScan (line 93) | private void addRedScan(int y) {
    method getColor (line 98) | private int getColor(int colorScanSample, int y) {

FILE: app/src/main/java/om/sstvencoder/Modes/Scottie1.java
  class Scottie1 (line 23) | @ModeSize(width = 320, height = 256)
    method Scottie1 (line 28) | Scottie1(Bitmap bitmap, IOutput output) {

FILE: app/src/main/java/om/sstvencoder/Modes/Scottie2.java
  class Scottie2 (line 23) | @ModeSize(width = 320, height = 256)
    method Scottie2 (line 28) | Scottie2(Bitmap bitmap, IOutput output){

FILE: app/src/main/java/om/sstvencoder/Modes/ScottieDX.java
  class ScottieDX (line 23) | @ModeSize(width = 320, height = 256)
    method ScottieDX (line 28) | ScottieDX(Bitmap bitmap, IOutput output) {

FILE: app/src/main/java/om/sstvencoder/Modes/Wraase.java
  class Wraase (line 24) | @ModeSize(width = 320, height = 256)
    method Wraase (line 37) | Wraase(Bitmap bitmap, IOutput output) {
    method getTransmissionSamples (line 50) | protected int getTransmissionSamples() {
    method writeEncodedLine (line 55) | protected void writeEncodedLine() {
    method addSyncPulse (line 63) | private void addSyncPulse() {
    method addPorch (line 68) | private void addPorch() {
    method addRedScan (line 73) | private void addRedScan(int y) {
    method addGreenScan (line 78) | private void addGreenScan(int y) {
    method addBlueScan (line 83) | private void addBlueScan(int y) {
    method getColor (line 88) | private int getColor(int colorScanSample, int y) {

FILE: app/src/main/java/om/sstvencoder/Output/AudioOutput.java
  class AudioOutput (line 22) | class AudioOutput implements IOutput {
    method AudioOutput (line 28) | AudioOutput(double sampleRate) {
    method init (line 33) | @Override
    method getSampleRate (line 43) | @Override
    method write (line 48) | @Override
    method finish (line 58) | @Override
    method drainBuffer (line 70) | private void drainBuffer() {

FILE: app/src/main/java/om/sstvencoder/Output/IOutput.java
  type IOutput (line 18) | public interface IOutput {
    method getSampleRate (line 19) | double getSampleRate();
    method init (line 21) | void init(int samples);
    method write (line 23) | void write(double value);
    method finish (line 25) | void finish(boolean cancel);

FILE: app/src/main/java/om/sstvencoder/Output/OutputFactory.java
  class OutputFactory (line 18) | public final class OutputFactory {
    method createOutputForSending (line 20) | public static IOutput createOutputForSending() {
    method createOutputForSavingAsWave (line 25) | public static IOutput createOutputForSavingAsWave(WaveFileOutputContex...

FILE: app/src/main/java/om/sstvencoder/Output/WaveFileOutput.java
  class WaveFileOutput (line 20) | class WaveFileOutput implements IOutput {
    method WaveFileOutput (line 26) | WaveFileOutput(WaveFileOutputContext context, double sampleRate) {
    method init (line 31) | public void init(int samples) {
    method writeHeader (line 40) | private void writeHeader() {
    method InitOutputStream (line 66) | private void InitOutputStream() {
    method getSampleRate (line 73) | @Override
    method write (line 78) | @Override
    method finish (line 88) | @Override
    method padWithZeros (line 103) | private void padWithZeros(int count) {
    method toLittleEndian (line 111) | private byte[] toLittleEndian(int value) {
    method toLittleEndian (line 120) | private byte[] toLittleEndian(short value) {

FILE: app/src/main/java/om/sstvencoder/Output/WaveFileOutputContext.java
  class WaveFileOutputContext (line 29) | public class WaveFileOutputContext {
    method WaveFileOutputContext (line 36) | public WaveFileOutputContext(ContentResolver contentResolver, String f...
    method getContentValues (line 46) | private ContentValues getContentValues(String fileName) {
    method getFileName (line 61) | public String getFileName() {
    method getOutputStream (line 65) | public OutputStream getOutputStream() {
    method update (line 76) | public void update() {
    method deleteFile (line 91) | public void deleteFile() {

FILE: app/src/main/java/om/sstvencoder/ProgressBarWrapper.java
  class ProgressBarWrapper (line 23) | class ProgressBarWrapper {
    method ProgressBarWrapper (line 31) | ProgressBarWrapper(ProgressBar progressBar, TextView text) {
    method startProgressBar (line 40) | private void startProgressBar(final int max, final String text) {
    method stepProgressBar (line 55) | private void stepProgressBar(final int progress) {
    method endProgressBar (line 64) | private void endProgressBar() {
    method begin (line 74) | void begin(int max, String text) {
    method step (line 81) | void step() {
    method end (line 90) | void end() {

FILE: app/src/main/java/om/sstvencoder/Settings.java
  class Settings (line 35) | class Settings {
    method Settings (line 47) | private Settings() {
    method Settings (line 53) | Settings(Context context) {
    method load (line 58) | boolean load() {
    method save (line 78) | boolean save() {
    method setModeClassName (line 99) | void setModeClassName(String modeClassName) {
    method getModeClassName (line 103) | String getModeClassName() {
    method setImageUri (line 107) | void setImageUri(Uri uri) {
    method getImageUri (line 111) | Uri getImageUri() {
    method getTextOverlayFile (line 117) | File getTextOverlayFile() {
    method getFile (line 123) | private File getFile() {
    method write (line 127) | private void write(JsonWriter writer) throws IOException {
    method writeVersion (line 138) | private void writeVersion(JsonWriter writer) throws IOException {
    method writeModeClassName (line 142) | private void writeModeClassName(JsonWriter writer) throws IOException {
    method writeImageUri (line 146) | private void writeImageUri(JsonWriter writer) throws IOException {
    method writeTextOverlayPath (line 150) | private void writeTextOverlayPath(JsonWriter writer) throws IOException {
    method read (line 154) | private void read(JsonReader reader) throws IOException {
    method readVersion (line 166) | private int readVersion(JsonReader reader) throws IOException {
    method readModeClassName (line 171) | private void readModeClassName(JsonReader reader) throws IOException {
    method readImageUri (line 176) | private void readImageUri(JsonReader reader) throws IOException {
    method readTextOverlayPath (line 185) | private void readTextOverlayPath(JsonReader reader) throws IOException {

FILE: app/src/main/java/om/sstvencoder/TextOverlay/IReader.java
  type IReader (line 20) | public interface IReader {
    method beginRootObject (line 21) | void beginRootObject() throws IOException;
    method beginObject (line 23) | void beginObject() throws IOException;
    method endObject (line 25) | void endObject() throws IOException;
    method beginArray (line 27) | void beginArray() throws IOException;
    method endArray (line 29) | void endArray() throws IOException;
    method hasNext (line 31) | boolean hasNext() throws IOException;
    method readString (line 33) | String readString() throws IOException;
    method readBoolean (line 35) | boolean readBoolean() throws IOException;
    method readFloat (line 37) | float readFloat() throws IOException;
    method readInt (line 39) | int readInt() throws IOException;

FILE: app/src/main/java/om/sstvencoder/TextOverlay/IWriter.java
  type IWriter (line 22) | public interface IWriter {
    method beginRootObject (line 23) | void beginRootObject() throws IOException;
    method beginObject (line 25) | void beginObject(@NonNull String name) throws IOException;
    method endObject (line 27) | void endObject() throws IOException;
    method beginArray (line 29) | void beginArray(@NonNull String name) throws IOException;
    method endArray (line 31) | void endArray() throws IOException;
    method write (line 33) | void write(@NonNull String name, String value) throws IOException;
    method write (line 35) | void write(@NonNull String name, boolean value) throws IOException;
    method write (line 37) | void write(@NonNull String name, float value) throws IOException;
    method write (line 39) | void write(@NonNull String name, int value) throws IOException;

FILE: app/src/main/java/om/sstvencoder/TextOverlay/Label.java
  class Label (line 24) | public class Label implements Serializable {
    method Label (line 33) | public Label() {
    method getText (line 46) | public String getText() {
    method setText (line 50) | public void setText(String text) {
    method getTextSize (line 55) | public float getTextSize() {
    method setTextSize (line 59) | public void setTextSize(float size) {
    method getFamilyName (line 64) | public String getFamilyName() {
    method setFamilyName (line 68) | public void setFamilyName(String familyName) {
    method getBold (line 72) | public boolean getBold() {
    method setBold (line 76) | public void setBold(boolean bold) {
    method getItalic (line 80) | public boolean getItalic() {
    method setItalic (line 84) | public void setItalic(boolean italic) {
    method getForeColor (line 88) | public int getForeColor() {
    method setForeColor (line 92) | public void setForeColor(int color) {
    method getBackColor (line 96) | public int getBackColor() {
    method setBackColor (line 100) | public void setBackColor(int color) {
    method getOutline (line 104) | public boolean getOutline() {
    method setOutline (line 108) | public void setOutline(boolean outline) {
    method getOutlineSize (line 112) | public float getOutlineSize() {
    method setOutlineSize (line 116) | public void setOutlineSize(float size) {
    method getOutlineColor (line 120) | public int getOutlineColor() {
    method setOutlineColor (line 124) | public void setOutlineColor(int color) {
    method getClone (line 128) | public Label getClone() {

FILE: app/src/main/java/om/sstvencoder/TextOverlay/LabelCollection.java
  class LabelCollection (line 26) | public class LabelCollection {
    class Size (line 27) | private class Size {
      method Size (line 30) | Size(float w, float h) {
      method width (line 35) | float width() {
      method height (line 39) | float height() {
    method LabelCollection (line 51) | public LabelCollection() {
    method update (line 58) | public void update(float w, float h, float textSizeFactor) {
    method draw (line 71) | public void draw(Canvas canvas) {
    method draw (line 78) | public void draw(Canvas canvas, Rect src, Rect dst) {
    method moveLabelBegin (line 83) | public boolean moveLabelBegin(float x, float y) {
    method moveLabel (line 94) | public void moveLabel(float x, float y) {
    method moveLabelEnd (line 101) | public void moveLabelEnd() {
    method editLabelBegin (line 108) | public Label editLabelBegin(float x, float y) {
    method editLabelEnd (line 117) | public void editLabelEnd(Label label) {
    method find (line 132) | private LabelContainer find(float x, float y) {
    method add (line 140) | private void add(LabelContainer label) {
    method write (line 147) | public void write(@NonNull IWriter writer) throws IOException {
    method read (line 164) | public boolean read(@NonNull IReader reader) throws IOException {

FILE: app/src/main/java/om/sstvencoder/TextOverlay/LabelContainer.java
  class LabelContainer (line 24) | class LabelContainer {
    method LabelContainer (line 29) | LabelContainer(@NonNull Label label) {
    method contains (line 35) | boolean contains(float x, float y) {
    method draw (line 39) | void draw(Canvas canvas) {
    method drawActive (line 43) | void drawActive(Canvas canvas) {
    method draw (line 47) | void draw(Canvas canvas, Rect src, Rect dst) {
    method jumpInside (line 51) | void jumpInside(float textSizeFactor, float screenW, float screenH) {
    method offset (line 55) | void offset(float x, float y) {
    method update (line 59) | void update(float textSizeFactor, float screenW, float screenH) {
    method getContent (line 63) | Label getContent() {
    method setContent (line 67) | void setContent(@NonNull Label label) {
    method write (line 72) | void write(IWriter writer) throws IOException {
    method read (line 86) | void read(IReader reader) throws IOException {
    method writeLabel (line 99) | private void writeLabel(IWriter writer, Label label) throws IOException {
    method readLabel (line 112) | private void readLabel(IReader reader, Label label) throws IOException {

FILE: app/src/main/java/om/sstvencoder/TextOverlay/LabelPainter.java
  class LabelPainter (line 29) | class LabelPainter {
    type IDrawer (line 30) | private interface IDrawer {
      method draw (line 31) | void draw(Canvas canvas);
      method drawShadow (line 33) | void drawShadow(Canvas canvas);
      method draw (line 35) | void draw(Canvas canvas, Rect src, Rect dst);
      method getBounds (line 37) | RectF getBounds();
    class InDrawer (line 40) | private class InDrawer implements IDrawer {
      method InDrawer (line 44) | private InDrawer(float sizeFactor, float x, float y) {
      method draw (line 50) | @Override
      method drawShadow (line 56) | @Override
      method draw (line 85) | @Override
      method getBounds (line 96) | @Override
      method setPosition (line 107) | private void setPosition(float x, float y) {
      method getOneLetterSize (line 112) | private float getOneLetterSize() {
      method getTextBounds (line 118) | private Rect getTextBounds() {
      method drawOutline (line 125) | private void drawOutline(Canvas canvas, float x, float y) {
      method setPaintSettings (line 133) | private void setPaintSettings(float sizeFactor) {
      method setOutlinePaintSettings (line 143) | private void setOutlinePaintSettings() {
      method setTextPaintSettings (line 148) | private void setTextPaintSettings() {
      method setSizePaintSettings (line 153) | private void setSizePaintSettings(float sizeFactor) {
      method createTypeface (line 159) | private Typeface createTypeface() {
    class OutDrawer (line 178) | private class OutDrawer implements IDrawer {
      method OutDrawer (line 183) | private OutDrawer(float min) {
      method leftOut (line 189) | private void leftOut(RectF rect, float screenH) {
      method topOut (line 196) | private void topOut(RectF rect, float screenW) {
      method rightOut (line 203) | private void rightOut(RectF rect, float screenW, float screenH) {
      method bottomOut (line 210) | private void bottomOut(RectF rect, float screenW, float screenH) {
      method getLeftAlignedTriangle (line 217) | private Path getLeftAlignedTriangle(float x, float y, float r) {
      method getTopAlignedTriangle (line 226) | private Path getTopAlignedTriangle(float x, float y, float r) {
      method getRightAlignedTriangle (line 235) | private Path getRightAlignedTriangle(float x, float y, float r) {
      method getBottomAlignedTriangle (line 244) | private Path getBottomAlignedTriangle(float x, float y, float r) {
      method draw (line 253) | @Override
      method draw (line 264) | @Override
      method drawShadow (line 268) | @Override
      method getBounds (line 288) | @Override
    method LabelPainter (line 298) | LabelPainter(@NonNull Label label) {
    method draw (line 304) | void draw(Canvas canvas) {
    method drawActive (line 308) | void drawActive(Canvas canvas) {
    method draw (line 313) | void draw(Canvas canvas, Rect src, Rect dst) {
    method getBounds (line 317) | RectF getBounds() {
    method setLabel (line 321) | void setLabel(@NonNull Label label) {
    method moveLabelInside (line 325) | void moveLabelInside(float sizeFactor, float screenW, float screenH, P...
    method update (line 349) | void update(float sizeFactor, float screenW, float screenH, Position p...
    method isLabelInside (line 373) | private boolean isLabelInside() {
    method getMinSize (line 377) | private float getMinSize(float sizeFactor) {

FILE: app/src/main/java/om/sstvencoder/TextOverlay/Position.java
  class Position (line 18) | class Position {
    method Position (line 22) | Position() {
    method set (line 27) | void set(float x, float y) {
    method offset (line 32) | void offset(float x, float y) {
    method getX (line 37) | float getX() {
    method getY (line 41) | float getY() {

FILE: app/src/main/java/om/sstvencoder/TextOverlayTemplate.java
  class TextOverlayTemplate (line 36) | class TextOverlayTemplate {
    class LabelCollectionWriter (line 37) | private class LabelCollectionWriter implements IWriter {
      method LabelCollectionWriter (line 40) | private LabelCollectionWriter(@NonNull JsonWriter writer) {
      method beginRootObject (line 44) | @Override
      method beginObject (line 49) | @Override
      method endObject (line 55) | @Override
      method beginArray (line 60) | @Override
      method endArray (line 66) | @Override
      method write (line 71) | @Override
      method write (line 76) | @Override
      method write (line 81) | @Override
      method write (line 86) | @Override
    class LabelCollectionReader (line 92) | private class LabelCollectionReader implements IReader {
      method LabelCollectionReader (line 95) | private LabelCollectionReader(@NonNull JsonReader reader) {
      method beginRootObject (line 99) | @Override
      method beginObject (line 104) | @Override
      method endObject (line 110) | @Override
      method beginArray (line 115) | @Override
      method endArray (line 121) | @Override
      method hasNext (line 126) | @Override
      method readString (line 131) | @Override
      method readBoolean (line 141) | @Override
      method readFloat (line 147) | @Override
      method readInt (line 153) | @Override
    method load (line 160) | boolean load(@NonNull LabelCollection labels, File file) {
    method save (line 179) | boolean save(@NonNull LabelCollection labels, File file) {

FILE: app/src/main/java/om/sstvencoder/Utility.java
  class Utility (line 39) | public final class Utility {
    method getEmbeddedRect (line 43) | @NonNull
    method getTextSizeFactor (line 62) | static float getTextSizeFactor(int w, int h) {
    method createMessage (line 66) | static String createMessage(Exception ex) {
    method createEmailIntent (line 77) | @NonNull
    method convertToDegrees (line 87) | static int convertToDegrees(int exifOrientation) {
    method createImageUri (line 99) | static Uri createImageUri(Context context) {
    method createWaveFileName (line 112) | static String createWaveFileName() {
    method createFileName (line 116) | private static String createFileName() {
    method isExternalStorageWritable (line 120) | static boolean isExternalStorageWritable() {
    method getSystemFontFamilyList (line 125) | static List<String> getSystemFontFamilyList() {
    method isSupportedFontFileFormat (line 147) | private static boolean isSupportedFontFileFormat(String fileName) {
    method getFontFamilyName (line 151) | private static String getFontFamilyName(String fileName) {
    method getFontFilePath (line 167) | public static String getFontFilePath(String fontFamilyName, int style) {
    method getFontFamilyFilePathList (line 183) | private static List<String> getFontFamilyFilePathList(String fontFamil...
    method getFontFileStyleString (line 203) | private static String getFontFileStyleString(int style) {
Condensed preview — 87 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (255K chars).
[
  {
    "path": ".gitignore",
    "chars": 87,
    "preview": "*.iml\n.gradle\n/local.properties\n/.idea\n.DS_Store\n/build\n/captures\n.externalNativeBuild\n"
  },
  {
    "path": "LICENSE",
    "chars": 11358,
    "preview": "\n                                 Apache License\n                           Version 2.0, January 2004\n                  "
  },
  {
    "path": "NOTICE",
    "chars": 474,
    "preview": "SSTV Encoder 2\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nMode specifications were taken from \"Dayton Paper\" of JL"
  },
  {
    "path": "README.md",
    "chars": 2407,
    "preview": "![Icon](app/src/main/res/mipmap-xhdpi/ic_launcher.png)\n# SSTV Encoder 2\n\nImage encoder for Slow-Scan Television (SSTV) a"
  },
  {
    "path": "app/.gitignore",
    "chars": 7,
    "preview": "/build\n"
  },
  {
    "path": "app/build.gradle",
    "chars": 684,
    "preview": "apply plugin: 'com.android.application'\n\nandroid {\n    compileSdk 35\n    defaultConfig {\n        applicationId \"om.sstve"
  },
  {
    "path": "app/proguard-rules.pro",
    "chars": 653,
    "preview": "# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in /h"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "chars": 2550,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <uses-"
  },
  {
    "path": "app/src/main/java/om/sstvencoder/ColorFragment.java",
    "chars": 2695,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/ColorPalette/ColorPaletteView.java",
    "chars": 3651,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/ColorPalette/GridColorPalette.java",
    "chars": 6751,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/ColorPalette/IColorPalette.java",
    "chars": 867,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/CropView.java",
    "chars": 17912,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/EditTextActivity.java",
    "chars": 11300,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Encoder.java",
    "chars": 5221,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/FontFamilySet.java",
    "chars": 6897,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/MainActivity.java",
    "chars": 18309,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/MainActivityMessenger.java",
    "chars": 1312,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/ModeInterfaces/IMode.java",
    "chars": 749,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/ModeInterfaces/IModeInfo.java",
    "chars": 737,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/ModeInterfaces/ModeSize.java",
    "chars": 911,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/ImageFormats/NV21.java",
    "chars": 2236,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/ImageFormats/YUV440P.java",
    "chars": 2016,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/ImageFormats/YUY2.java",
    "chars": 1864,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/ImageFormats/YV12.java",
    "chars": 2386,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/ImageFormats/Yuv.java",
    "chars": 1217,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/ImageFormats/YuvConverter.java",
    "chars": 1555,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/ImageFormats/YuvFactory.java",
    "chars": 1233,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/ImageFormats/YuvImageFormat.java",
    "chars": 763,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/Martin.java",
    "chars": 2973,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/Martin1.java",
    "chars": 1103,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/Martin2.java",
    "chars": 1102,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/Mode.java",
    "chars": 4060,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/ModeDescription.java",
    "chars": 886,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/ModeFactory.java",
    "chars": 2858,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/ModeInfo.java",
    "chars": 1154,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/PD.java",
    "chars": 2746,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/PD120.java",
    "chars": 1090,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/PD160.java",
    "chars": 1092,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/PD180.java",
    "chars": 1091,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/PD240.java",
    "chars": 1091,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/PD290.java",
    "chars": 1090,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/PD50.java",
    "chars": 1086,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/PD90.java",
    "chars": 1087,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/Robot36.java",
    "chars": 4018,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/Robot72.java",
    "chars": 3939,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/Scottie.java",
    "chars": 3020,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/Scottie1.java",
    "chars": 1107,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/Scottie2.java",
    "chars": 1106,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/ScottieDX.java",
    "chars": 1110,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Modes/Wraase.java",
    "chars": 2718,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Output/AudioOutput.java",
    "chars": 2443,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Output/IOutput.java",
    "chars": 762,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Output/OutputFactory.java",
    "chars": 976,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Output/WaveFileOutput.java",
    "chars": 4138,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Output/WaveFileOutputContext.java",
    "chars": 3612,
    "preview": "/*\nCopyright 2020 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/ProgressBarWrapper.java",
    "chars": 2678,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Settings.java",
    "chars": 5493,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/TextOverlay/IReader.java",
    "chars": 1108,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/TextOverlay/IWriter.java",
    "chars": 1253,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/TextOverlay/Label.java",
    "chars": 3296,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/TextOverlay/LabelCollection.java",
    "chars": 5586,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/TextOverlay/LabelContainer.java",
    "chars": 3877,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/TextOverlay/LabelPainter.java",
    "chars": 12440,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/TextOverlay/Position.java",
    "chars": 976,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/TextOverlayTemplate.java",
    "chars": 5742,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/java/om/sstvencoder/Utility.java",
    "chars": 7335,
    "preview": "/*\nCopyright 2017 Olga Miller <olga.rgb@gmail.com>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
  },
  {
    "path": "app/src/main/res/layout/activity_edit_text.xml",
    "chars": 4295,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ScrollView\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xm"
  },
  {
    "path": "app/src/main/res/layout/activity_main.xml",
    "chars": 1940,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  "
  },
  {
    "path": "app/src/main/res/layout/fragment_color.xml",
    "chars": 412,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n      "
  },
  {
    "path": "app/src/main/res/menu/menu_edit_text.xml",
    "chars": 448,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:ap"
  },
  {
    "path": "app/src/main/res/menu/menu_main.xml",
    "chars": 2319,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:ap"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "chars": 3286,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"app_name\" translatable=\"false\">SSTV Encoder</string"
  },
  {
    "path": "app/src/main/res/values/styles.xml",
    "chars": 83,
    "preview": "<resources>\n    <style name=\"AppTheme\" parent=\"Base.Theme.AppCompat\"/>\n</resources>"
  },
  {
    "path": "app/src/main/res/values-v35/styles.xml",
    "chars": 197,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\t<style name=\"AppTheme\" parent=\"Base.Theme.AppCompat\">\n\t\t<item name=\""
  },
  {
    "path": "app/src/main/res/values-zh-rCN/strings.xml",
    "chars": 2926,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"action_pick_picture\">选择照片</string>\n    <string name"
  },
  {
    "path": "app/src/main/res/xml/paths.xml",
    "chars": 114,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<paths>\n    <external-path\n        name=\"media\"\n        path=\".\"/>\n</paths>"
  },
  {
    "path": "build.gradle",
    "chars": 573,
    "preview": "buildscript {\n    repositories {\n        google()\n        mavenCentral()\n    }\n    dependencies {\n        classpath 'com"
  },
  {
    "path": "fastlane/metadata/android/en-US/full_description.txt",
    "chars": 2155,
    "preview": "\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>"
  },
  {
    "path": "fastlane/metadata/android/en-US/short_description.txt",
    "chars": 60,
    "preview": "Image encoder for Slow-Scan Television (SSTV) audio signals\n"
  },
  {
    "path": "fastlane/metadata/android/en-US/title.txt",
    "chars": 14,
    "preview": "SSTV Encoder \n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "chars": 200,
    "preview": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributi"
  },
  {
    "path": "gradle.properties",
    "chars": 780,
    "preview": "# Project-wide Gradle settings.\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will ov"
  },
  {
    "path": "gradlew",
    "chars": 5296,
    "preview": "#!/usr/bin/env sh\n\n##############################################################################\n##\n##  Gradle start up"
  },
  {
    "path": "gradlew.bat",
    "chars": 2260,
    "preview": "@if \"%DEBUG%\" == \"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@r"
  },
  {
    "path": "settings.gradle",
    "chars": 15,
    "preview": "include ':app'\n"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the olgamiller/SSTVEncoder2 GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 87 files (233.8 KB), approximately 57.4k tokens, and a symbol index with 579 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!