Android Java App Helpers

  • Overview
  • Activity base
  • Android actions helper
  • App internal storage helper
  • PDF DIN-A4 creator

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!

Next steps

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;
    }

}

Not found what you’re looking for?

Questions, concerns, suggestions for improvement, criticism?

We are looking forward to hearing what you have to say!

happy guy working with the library simple java fx application base from wedasoft.