Android Java App Helpers

About the project

A library that provides several functions for developing Android (API 33) apps with Java. The functions may work with later API versions too, but there is no guarantee.

Features

  • Activity base: A simple super class for Activities. Display progress-, info- and error dialogs with ease and make profit of some preconfigurations.
  • Android actions helper: Provides several functions for interacting with native elements like the camera or the gallery. Don’t bother how to implement this yourself.
  • App internal storage helper: Use this simple class to interact with the app internal storage without any effort. Execute CRUD operations and more.
  • PDF DIN-A4 creator: A builder class for creating PDF files in DIN-A4 format the easy way. Add text and image block, define padding and more!
Java
import android.app.AlertDialog;
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.util.Log;
import android.view.MenuItem;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.Toast;

import androidx.activity.OnBackPressedCallback;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.app.NavUtils;

import java.util.Objects;

/**
 * <b style="color: red;">This class is designed to work with Android 33.</b>
 *
 * @author David Weber
 */
public abstract class ActivityBase extends AppCompatActivity {

    protected AppCompatActivity currentActivity;
    protected Context context;
    private AlertDialog progressAlertDialog;

    protected void onCreate(
            Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        this.currentActivity = this;
        this.context = currentActivity.getApplicationContext();
        this.getOnBackPressedDispatcher().addCallback(new OnBackPressedCallback(true) {
            @Override
            public void handleOnBackPressed() {
                if (NavUtils.getParentActivityName(currentActivity) != null) {
                    NavUtils.navigateUpFromSameTask(ActivityBase.this); // is needed!
                } else {
                    Log.v("ActivityBase", "Activity has no parent activity.");
                    finish(); // close app
                }
            }
        });
        createProgressDialog();
    }

    public void displayProgressDialog() {
        if (!progressAlertDialog.isShowing()) {
            progressAlertDialog.show();
        }
    }

    public void hideProgressDialog() {
        progressAlertDialog.dismiss();
    }

    private void createProgressDialog() {
        LinearLayout vLayout = new LinearLayout(currentActivity);
        vLayout.setOrientation(LinearLayout.VERTICAL);
        vLayout.setPadding(50, 50, 50, 50);
        vLayout.addView(new ProgressBar(currentActivity, null, android.R.attr.progressBarStyleLarge));

        progressAlertDialog = new AlertDialog.Builder(currentActivity)
                .setCancelable(false)
                .setView(vLayout)
                .create();
    }

    public void displayToast(
            Context _this,
            String message) {

        Toast.makeText(_this, message, Toast.LENGTH_SHORT).show();
    }

    private AlertDialog createAlertDialog(
            Context _this,
            String title,
            String message,
            Drawable icon) {

        AlertDialog alert = new AlertDialog.Builder(_this).create();
        alert.setTitle(title);
        alert.setMessage(message);
        alert.setIcon(icon);
        alert.setButton(AlertDialog.BUTTON_NEUTRAL, "OK", (dialog, which) -> alert.dismiss());
        return alert;
    }

    public void displayInfoDialog(
            Context _this,
            String message) {

        Drawable drawable = AppCompatResources.getDrawable(_this, android.R.drawable.ic_dialog_info);
        Objects.requireNonNull(drawable).setTint(Color.DKGRAY);
        createAlertDialog(_this, " ", message, drawable).show();
    }

    public void displayErrorDialog(
            Context _this,
            String message) {

        Drawable drawable = AppCompatResources.getDrawable(_this, android.R.drawable.ic_dialog_alert);
        Objects.requireNonNull(drawable).setTint(Color.DKGRAY);
        createAlertDialog(_this, " ", message, drawable).show();
    }

    public void checkOrUncheckMenuItem(
            MenuItem menuItem) {

        menuItem.setChecked(!menuItem.isChecked());
    }

}
Java
import android.app.Activity;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.util.Log;

import androidx.activity.ComponentActivity;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.FileProvider;

import com.wedasoft.personalcookbook.BuildConfig; // edit this line

import java.io.File;
import java.io.IOException;
import java.time.Instant;

/**
 * <b style="color: red;">This class is designed to work with Android 33.</b>
 * <br>
 * This are all used permissions. Enable them only if you need them.
 * <pre>
 * {@code
 *     <!--    <uses-feature-->
 *     <!--        android:name="android.hardware.camera"-->
 *     <!--        android:required="false" />-->
 *
 *     <uses-permission android:name="android.permission.INTERNET" />
 *     <uses-permission android:name="android.permission.READ_PHONE_STATE" />
 *     <uses-permission android:name="android.permission.BLUETOOTH" />
 *     <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
 *     <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
 *     <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
 *     <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
 *     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
 *     <uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
 *     <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
 *     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
 *     <uses-permission android:name="android.permission.RECORD_AUDIO" />
 *     <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
 *     <!--    <uses-permission android:name="android.permission.CAMERA" />-->
 *     <!--    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />-->
 *     <!--    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />-->
 *     <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
 *     <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
 *     <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
 * }
 * </pre>
 */
public class AndroidActionsHelper {

    public static class ImagePicker2 {
        private ActivityResultLauncher<Intent> resultLauncher;
        private ImagePicker2Callback callbackAfterImageIsPicked;

        private ImagePicker2() {
        }

        /**
         * Creates and registers the <code>ImagePicker2</code> and the needed <code>ResultLauncher</code>.<br>
         * Note: Should be used directly in <code>onCreate()</code>.
         *
         * @param activity The current activity.
         */
        public static ImagePicker2 createAndRegisterInOnCreateOfActivity(AppCompatActivity activity) {
            ImagePicker2 imagePicker = new ImagePicker2();
            imagePicker.resultLauncher = activity.registerForActivityResult(
                    new ActivityResultContracts.StartActivityForResult(),
                    activityResult -> {
                        if (activityResult.getResultCode() == Activity.RESULT_OK) {
                            if (activityResult.getData() != null) {
                                Uri uri;
                                try {
                                    uri = Uri.parse(activityResult.getData().getDataString());
                                    Log.v("ImagePicker2", "Used Uri ='" + uri + "'.");
                                    imagePicker.callbackAfterImageIsPicked.run(uri);
                                } catch (IOException e) {
                                    Log.v("ImagePicker2", "Couldn't execute the ImagePicker2 correctly.");
                                    e.printStackTrace();
                                }
                            }
                        }
                    });
            return imagePicker;
        }

        /**
         * Picks an image.
         *
         * @param callbackAfterImageIsPicked The callback that is executed after the image is picked. It contains the URI to the picked image.
         * @throws Exception If the <code>ImagePicker2</code> is not created and registered before or if the callback is null.
         */
        public void pickImage(ImagePicker2Callback callbackAfterImageIsPicked) throws Exception {
            if (resultLauncher == null) {
                throw new Exception("The ImagePicker2 is not created and registered yet. The resultLauncher is null.");
            }
            if (callbackAfterImageIsPicked == null) {
                throw new Exception("The ImagePicker2Callback must not be null");
            }
            this.callbackAfterImageIsPicked = callbackAfterImageIsPicked;
            Intent intent = new Intent();
            intent.setType("image/*");
            intent.setAction(Intent.ACTION_GET_CONTENT);
            resultLauncher.launch(intent);
        }

        public interface ImagePicker2Callback {
            void run(Uri uriOfPickedImage) throws IOException;
        }
    }

    /**
     * <b>The following steps are required for using the {@code PictureTaker}:</b><br><br>
     *
     * <b>1. Add in the {@code <application>} tag in the manifest:</b><br>
     * <pre>
     * {@code
     * <provider
     *     android:name="androidx.core.content.FileProvider"
     *     android:authorities="${applicationId}.provider"
     *     android:exported="false"
     *     android:grantUriPermissions="true">
     *     <meta-data
     *         android:name="android.support.FILE_PROVIDER_PATHS"
     *         android:resource="@xml/provider_paths" />
     * </provider>
     * }
     * </pre>
     * <b>2. Add the file {@code res/xml/provider_paths.xml} with this content:</b><br>
     * <pre>
     * {@code
     * <?xml version="1.0" encoding="utf-8"?>
     * <paths xmlns:android="http://schemas.android.com/apk/res/android">
     *     <external-path
     *         name="external_files"
     *         path="." />
     *     <files-path
     *         name="internal_files"
     *         path="." />
     * </paths>
     * }
     * </pre>
     */
    public static class PictureTaker {

        private ActivityResultLauncher<Uri> resultLauncher;
        private File takenPictureFile;
        private Uri takenPictureContentUri;
        private PictureTakerCallback callbackAfterPictureIsTaken;

        private PictureTaker() {
        }

        /**
         * Creates and registers the <code>PictureTaker</code> and the needed <code>ResultLauncher</code>.<br>
         * Note: Should be used directly in <code>onCreate()</code>.<br>
         * Note: The taken picture is saved only temporary and is deleted after the callback is executed.
         *
         * @param activity The current activity.
         */
        public static PictureTaker createAndRegisterInOnCreateOfActivity(AppCompatActivity activity) {
            PictureTaker pictureTaker = new PictureTaker();
            pictureTaker.takenPictureFile = new File(activity.getFilesDir(), "tmp_ImageFromPictureTaker_" + Instant.now().toEpochMilli());
            pictureTaker.takenPictureContentUri = FileProvider.getUriForFile(
                    activity.getApplicationContext(),
                    BuildConfig.APPLICATION_ID + ".provider",
                    pictureTaker.takenPictureFile);
            pictureTaker.resultLauncher = activity.registerForActivityResult(
                    new ActivityResultContracts.TakePicture(),
                    pictureTakenSuccess -> {
                        if (pictureTakenSuccess) {
                            pictureTaker.callbackAfterPictureIsTaken.run(pictureTaker.takenPictureContentUri);
                            //noinspection ResultOfMethodCallIgnored
                            pictureTaker.takenPictureFile.delete();
                        }
                    });
            return pictureTaker;
        }

        /**
         * Takes a picture.
         *
         * @param callbackAfterPictureIsTaken The callback that is executed after the picture is taken. It contains the URI to the taken picture.
         * @throws Exception If the <code>PictureTaker</code> is not created and registered before or if the callback is null.
         */
        public void takePicture(PictureTakerCallback callbackAfterPictureIsTaken) throws Exception {
            if (resultLauncher == null) {
                throw new Exception("The PictureTaker is not created and registered yet. The resultLauncher is null.");
            }
            if (callbackAfterPictureIsTaken == null) {
                throw new Exception("The PictureTakerCallback must not be null");
            }
            this.callbackAfterPictureIsTaken = callbackAfterPictureIsTaken;
            resultLauncher.launch(takenPictureContentUri);
        }

        public interface PictureTakerCallback {
            void run(Uri takenPictureContentUri);
        }

    }

    @SuppressWarnings("unused")
    public static class PictureThumbnailTaker {

        private ActivityResultLauncher<Void> resultLauncher;
        private PictureThumbnailTakerCallback callbackAfterPictureThumbnailIsTaken;

        private PictureThumbnailTaker() {
        }

        /**
         * Creates and registers the <code>PictureThumbnailTaker</code> and the needed <code>ResultLauncher</code>.<br>
         * Note: Should be used directly in <code>onCreate()</code>.
         *
         * @param activity The current activity.
         */
        public static PictureThumbnailTaker createPictureThumbnailTakerResultLauncher(ComponentActivity activity) {
            PictureThumbnailTaker pictureThumbnailTaker = new PictureThumbnailTaker();
            pictureThumbnailTaker.resultLauncher = activity.registerForActivityResult(
                    new ActivityResultContracts.TakePicturePreview(),
                    bitmapOfTakenPictureThumbnail -> {
                        if (bitmapOfTakenPictureThumbnail != null) {
                            pictureThumbnailTaker.callbackAfterPictureThumbnailIsTaken.run(bitmapOfTakenPictureThumbnail);
                        }
                    });
            return pictureThumbnailTaker;
        }

        /**
         * Takes a picture as thumbnail.
         *
         * @param callbackAfterPictureThumbnailIsTaken The callback that is executed after the picture thumbnail is taken. It provides the Bitmap of the taken thumbnail.
         * @throws Exception If the <code>PictureThumbnailTaker</code> is not created and registered before or if the callback is null.
         */
        public void takePictureThumbnail(PictureThumbnailTakerCallback callbackAfterPictureThumbnailIsTaken) throws Exception {
            if (resultLauncher == null) {
                throw new Exception("The PictureThumbnailTaker is not created and registered yet. The resultLauncher is null.");
            }
            if (callbackAfterPictureThumbnailIsTaken == null) {
                throw new Exception("The PictureThumbnailTakerCallback must not be null.");
            }
            this.callbackAfterPictureThumbnailIsTaken = callbackAfterPictureThumbnailIsTaken;
            resultLauncher.launch(null);
        }

        public interface PictureThumbnailTakerCallback {
            void run(Bitmap bitmapOfTakenPictureThumbnail);
        }

    }

}
Java
import android.content.Context;
import android.graphics.Bitmap;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.List;

/**
 * <b style="color: red;">This class is designed to work with Android 33.</b>
 * <br>
 * The private app internal storage files are located in the directory <b><code>getFilesDir()</code></b>.
 * <br>
 * <b>Alle files are stored and loaded in UTF-8.</b>
 *
 * @author David Weber
 */
public class AppInternalStorageHelper {

    /**
     * Checks if a file exists in the <b>app internal storage</b>.
     *
     * @return True if the file exists.
     */
    public static boolean fileExists(
            Context context,
            String filename) {

        return new File(context.getFilesDir(), filename).exists();
    }

    /**
     * Gets the path of the file with the given file name in the <b>app internal storage</b>.
     *
     * @return The file path of the file as String if the file exists, otherwise null.
     */
    public static String getPathOfFile(
            Context context,
            String filename) {

        File file = new File(context.getFilesDir(), filename);
        return file.exists() ? file.getPath() : null;
    }

    /**
     * Creates and writes a file into the <b>app internal storage</b>.
     *
     * @return The created file.
     */
    public static File createFile(
            Context context,
            String filename,
            String fileContentAsString,
            boolean overwriteExistingFile)
            throws IOException {

        File file = new File(context.getFilesDir(), filename);
        if (overwriteExistingFile) {
            Files.deleteIfExists(file.toPath());
        }

        return Files.write(file.toPath(), fileContentAsString.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE).toFile();
    }

    /**
     * Creates and writes a file into the <b>app internal storage</b>.
     */
    public static void createFile(
            Context context,
            String filename,
            Bitmap fileContentAsBitmap,
            Bitmap.CompressFormat fileFormat)
            throws IOException {

        File file = new File(context.getFilesDir(), filename);
        OutputStream os = new BufferedOutputStream(new FileOutputStream(file));
        fileContentAsBitmap.compress(fileFormat, 100, os);
        os.close();
    }

    /**
     * Reads the content of a file from the <b>app internal storage</b> into bytes.
     */
    public static byte[] readFileToBytes(
            Context context,
            String filename)
            throws IOException {

        File file = new File(context.getFilesDir(), filename);
        return Files.readAllBytes(file.toPath());
    }

    /**
     * Reads the content of a file from the <b>app internal storage</b> into a <code>String</code>.
     */
    public static String readFileToString(
            Context context,
            String filename)
            throws IOException {

        return new String(readFileToBytes(context, filename), StandardCharsets.UTF_8);
    }

    /**
     * Reads the content of a file from the <b>app internal storage</b> into a <code>List</code> of lines.
     */
    public static List<String> readFileToLines(
            Context context,
            String filename)
            throws IOException {

        File file = new File(context.getFilesDir(), filename);
        return Files.readAllLines(file.toPath(), StandardCharsets.UTF_8);
    }

    /**
     * Deletes all files in the complete <b>app internal storage</b> whose file names begin with the given prefix.
     */
    public static void deleteFilesWithPrefix(
            Context context,
            String filePrefix) {

        ArrayList<File> filesToDelete = new ArrayList<>(AppInternalStorageHelper.findFilesWithPrefix(
                context.getFilesDir(),
                filePrefix));
        for (File file : filesToDelete) {
            //noinspection ResultOfMethodCallIgnored
            file.delete();
        }
    }

    /**
     * Finds all files in the complete <b>app internal storage</b> whose file names begin with the given prefix.
     */
    public static ArrayList<File> findFilesWithPrefix(
            File dir,
            String filePrefix) {

        ArrayList<File> filesToReturn = new ArrayList<>();
        File[] listedFiles = dir.listFiles();
        if (listedFiles != null) {
            for (File listedFile : listedFiles) {
                if (listedFile.isDirectory()) {
                    findFilesWithPrefix(listedFile, filePrefix);
                } else {
                    if (listedFile.getName().startsWith(filePrefix)) {
                        filesToReturn.add(listedFile);
                    }
                }
            }
        }
        return filesToReturn;
    }

}
Java
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.pdf.PdfDocument;
import android.graphics.pdf.PdfDocument.Page;
import android.graphics.pdf.PdfDocument.PageInfo;
import android.graphics.text.LineBreaker;
import android.os.Environment;
import android.text.Layout;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.util.Log;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * <b style="color: red;">This class is designed to work with Android 33.</b>
 *
 * @author David Weber
 */
public class PdfDinA4Creator {
    private static class ContentBlock<Type> {
        public Type type;
        public Integer prefWidth;
        public Integer prefHeight;

        public ContentBlock(Type type) {
            this.type = type;
        }

        public ContentBlock(Type type, Integer prefWidth, Integer prefHeight) {
            this.type = type;
            this.prefWidth = prefWidth;
            this.prefHeight = prefHeight;
        }
    }

    private static final double ONE_CM_AS_INCH = 0.39370079;
    public static final int SIZE_OF_ONE_CM = (int) (ONE_CM_AS_INCH * 72);
    private final int widthDinA4 = (int) (8.27d * 72); // inches to points
    private final int heightDinA4 = (int) (11.69d * 72); // inches to points

    private int paddingLeftAndRight;
    private final List<ContentBlock<?>> contentBlocks;

    public PdfDinA4Creator() {
        contentBlocks = new ArrayList<>();
    }

    /**
     * Sets the left and right padding of the document.
     *
     * @param paddingLeftAndRight The used padding for left and right.
     * @return This PdfDinA4Creator.
     */
    public PdfDinA4Creator setPaddingLeftAndRight(int paddingLeftAndRight) {
        this.paddingLeftAndRight = paddingLeftAndRight;
        return this;
    }

    /**
     * Adds the given content as {@link ContentBlock} to the PDF.<br>
     * A {@link ContentBlock} inserts a line break after its end.<br>
     * The inserted content is left aligned and breaks when the document end is reached its line.
     *
     * @param content The content to add.
     * @return This PdfDinA4Creator.
     */
    public PdfDinA4Creator addContentBlock(CharSequence content) {
        if (content == null) {
            throw new NullPointerException("Content must not be null!");
        }
        TextPaint textPaint = new TextPaint();
        textPaint.setColor(Color.BLACK);
        textPaint.setTextSize(12);
        StaticLayout staticLayout = StaticLayout.Builder.obtain(
                        content,
                        0,
                        content.length(),
                        textPaint,
                        getDinA4WidthMinusPaddingLeftAndRight())
                .setText(content)
                .setAlignment(Layout.Alignment.ALIGN_NORMAL)
                .setLineSpacing(0.0f, 1.0f)
                .setIncludePad(false)
                .setBreakStrategy(LineBreaker.BREAK_STRATEGY_SIMPLE)
                .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE)
                .build();
        contentBlocks.add(new ContentBlock<>(staticLayout));
        return this;
    }

    /**
     * Adds the given content as {@link ContentBlock} to the PDF.<br>
     * A {@link ContentBlock} inserts a line break after its end.<br>
     * By defining the arguments "prefWidth" and "prefHeight", the content is scaled by respecting its width-height-aspect-ratio.
     * If the width or height is bigger than the max available size, the content is scaled to the max available size.
     *
     * @param content    The content to add.
     * @param prefWidth  The preferred width to scale the content to.
     * @param prefHeight The preferred height to scale the content to.
     * @return This PdfDinA4Creator.
     */
    public PdfDinA4Creator addContentBlock(Bitmap content, Integer prefWidth, Integer prefHeight) {
        if (content == null) {
            throw new NullPointerException("Content must not be null!");
        }

        if (content.getHeight() > content.getWidth()) {
            content = rescaleBitmapByPrefWidth(content, prefWidth);
            content = rescaleBitmapByPrefHeight(content, prefHeight);
        } else {
            content = rescaleBitmapByPrefHeight(content, prefHeight);
            content = rescaleBitmapByPrefWidth(content, prefWidth);
        }

        if (content.getHeight() > heightDinA4) {
            throw new RuntimeException("The Bitmap is to big to fit on a single DIN A4 sheet. Either implement logic here or don't allow it.");
        }
        if (content.getWidth() > widthDinA4) {
            throw new RuntimeException("The Bitmap is to big to fit on a single DIN A4 sheet. Either implement logic here or don't allow it.");
        }

        contentBlocks.add(new ContentBlock<>(content, prefWidth, prefHeight));
        return this;
    }

    /**
     * Adds the given content as {@link ContentBlock} to the PDF.<br>
     * A {@link ContentBlock} inserts a line break after its end.<br>
     * By defining the arguments "prefWidth" and "prefHeight", the content is scaled by respecting its width-height-aspect-ratio.
     * If the width or height is bigger than the max available size, the content is scaled to the max available size.
     *
     * @param content    The content to add.
     * @param prefWidth  The preferred width to scale the content to.
     * @param prefHeight The preferred height to scale the content to.
     * @return This PdfDinA4Creator.
     */
    public PdfDinA4Creator addContentBlock(Drawable content, Integer prefWidth, Integer prefHeight) {
        if (content == null) {
            throw new NullPointerException("Content must not be null!");
        }
        return addContentBlock(convertDrawableToBitmap(content), prefWidth, prefHeight);
    }

    /**
     * Creates the PDF-Document.
     *
     * @return The created PDF-Document.
     */
    public PdfDocument createAndGet() {
        PdfDocument document = new PdfDocument();
        Page currentPage = initNextPage(document, null, widthDinA4, heightDinA4);

        int currentCoordinateSystemStartY = 0;
        for (ContentBlock<?> contentBlock : contentBlocks) {
            if (contentBlock.type instanceof StaticLayout) {
                StaticLayout layout = (StaticLayout) contentBlock.type;
                if (layout.getHeight() > heightDinA4) {
                    throw new RuntimeException("The StaticLayout representing is to big to fit on a single DIN A4 sheet. Please split it into several ContentBlocks.");
                }
                if (currentCoordinateSystemStartY + layout.getHeight() > heightDinA4) { // layout passt nicht mehr komplett auf die aktuelle Seite drauf
                    currentPage = initNextPage(document, currentPage, widthDinA4, heightDinA4);
                    currentCoordinateSystemStartY = 0;
                }

                currentPage.getCanvas().translate(paddingLeftAndRight, 0);
                layout.draw(currentPage.getCanvas());
                currentPage.getCanvas().translate(-paddingLeftAndRight, layout.getHeight());
                currentCoordinateSystemStartY += layout.getHeight();
                continue;
            }
            if (contentBlock.type instanceof Bitmap) {
                Bitmap bitmap = (Bitmap) contentBlock.type;

                Paint paint = new Paint();
                paint.setAntiAlias(true);
                paint.setFilterBitmap(true);
                paint.setDither(true);

                if (currentCoordinateSystemStartY + bitmap.getHeight() > heightDinA4) { // layout passt nicht mehr komplett auf die aktuelle Seite drauf
                    currentPage = initNextPage(document, currentPage, widthDinA4, heightDinA4);
                    currentCoordinateSystemStartY = 0;
                }

                currentPage.getCanvas().translate(paddingLeftAndRight, 0);
                currentPage.getCanvas().drawBitmap(bitmap, 0, 0, paint);
                currentPage.getCanvas().translate(-paddingLeftAndRight, bitmap.getHeight());
                currentCoordinateSystemStartY += bitmap.getHeight();
            }
        }
        document.finishPage(currentPage);
        return document;
    }

    public static void savePdfIntoDownloadsDirectory(String fileName, PdfDocument pdfDocument) throws IOException {
        String filePathInDownloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath() + "/" + fileName;
        File file = new File(filePathInDownloadsDir);
        pdfDocument.writeTo(new FileOutputStream(file));
        pdfDocument.close();
    }

    @SuppressWarnings("PointlessArithmeticExpression")
    private int getDinA4HeightMinusPaddingTopAndBottom() {
        return heightDinA4 - 0 * 2;
    }

    private int getDinA4WidthMinusPaddingLeftAndRight() {
        return widthDinA4 - paddingLeftAndRight * 2;
    }

    @SuppressWarnings("SameParameterValue")
    private Page initNextPage(PdfDocument document, Page currentPage, int width, int height) {
        if (currentPage != null) {
            document.finishPage(currentPage);
        }
        Page nextPage = document.startPage(new PageInfo.Builder(width, height, document.getPages().size() + 1).create());
        nextPage.getCanvas().save();
        return nextPage;
    }

    private Bitmap convertDrawableToBitmap(Drawable drawable) {
        if (drawable instanceof BitmapDrawable) {
            return ((BitmapDrawable) drawable).getBitmap();
        }

        Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
        drawable.draw(canvas);

        return bitmap;
    }

    private Bitmap scaleProportionalToNewHeight(Bitmap bitmap, int newHeight) {
        float aspectRatio = bitmap.getWidth() / (float) bitmap.getHeight();
        int width = Math.round(newHeight * aspectRatio);
        return Bitmap.createScaledBitmap(bitmap, width, newHeight, true);
    }

    private Bitmap scaleProportionalToNewWidth(Bitmap bitmap, int newWidth) {
        float aspectRatio = bitmap.getWidth() / (float) bitmap.getHeight();
        int height = Math.round(newWidth / aspectRatio);
        return Bitmap.createScaledBitmap(bitmap, newWidth, height, true);
    }

    private Bitmap rescaleBitmapByPrefHeight(Bitmap bitmap, Integer prefHeight) {
        if (prefHeight == null) {
            if (bitmap.getHeight() > getDinA4HeightMinusPaddingTopAndBottom()) {
                bitmap = scaleProportionalToNewHeight(bitmap, getDinA4HeightMinusPaddingTopAndBottom());
            }
        } else {
            if (prefHeight > getDinA4HeightMinusPaddingTopAndBottom()) {
                Log.w("PdfDinA4Creator", "contentBlock.prefHeight is not null, but it is bigger than the available space. Using the max. available space.");
                bitmap = scaleProportionalToNewHeight(bitmap, getDinA4HeightMinusPaddingTopAndBottom());
            } else {
                bitmap = scaleProportionalToNewHeight(bitmap, prefHeight);
            }
        }
        return bitmap;
    }

    private Bitmap rescaleBitmapByPrefWidth(Bitmap bitmap, Integer prefWidth) {
        if (prefWidth == null) {
            if (bitmap.getWidth() > getDinA4WidthMinusPaddingLeftAndRight()) {
                bitmap = scaleProportionalToNewWidth(bitmap, getDinA4WidthMinusPaddingLeftAndRight());
            }
        } else {
            if (prefWidth > getDinA4WidthMinusPaddingLeftAndRight()) {
                Log.w("PdfDinA4Creator", "contentBlock.prefWidth is not null, but it is bigger than the available space. Using the max. available space.");
                bitmap = scaleProportionalToNewWidth(bitmap, getDinA4WidthMinusPaddingLeftAndRight());
            } else {
                bitmap = scaleProportionalToNewWidth(bitmap, prefWidth);
            }
        }
        return bitmap;
    }

}