IncorrectJpegMetadataQuirk.java

/*
 * Copyright 2023 The Android Open Source Project
 *
 * 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 androidx.camera.core.internal.compat.quirk;

import android.os.Build;

import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.camera.core.ImageProxy;
import androidx.camera.core.impl.Quirk;

import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;

/**
 * <p>QuirkSummary
 *     Bug Id: 309005680
 *     Description: Quirk required to check whether the captured JPEG image has incorrect metadata.
 *                  For example, Samsung A24 device has the problem and result in the captured
 *                  image can't be parsed and saved successfully.
 *     Device(s): Samsung Galaxy A24 device.
 */
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public final class IncorrectJpegMetadataQuirk implements Quirk {

    private static final Set<String> SAMSUNG_DEVICES = new HashSet<>(Arrays.asList(
            "A24" // Samsung Galaxy A24 series devices
    ));

    static boolean load() {
        return isSamsungProblematicDevice();
    }

    private static boolean isSamsungProblematicDevice() {
        return "Samsung".equalsIgnoreCase(Build.BRAND) && SAMSUNG_DEVICES.contains(
                Build.DEVICE.toUpperCase(Locale.US));
    }

    /**
     * Converts the image proxy to the byte array with correct JPEG metadata.
     *
     * <p>Some unexpected data exists in the head of the problematic JPEG images captured from
     * the Samsung A24 device. Removing those data can fix the JPEG images.
     */
    @NonNull
    public byte[] jpegImageToJpegByteArray(@NonNull ImageProxy imageProxy) {
        ByteBuffer byteBuffer = imageProxy.getPlanes()[0].getBuffer();
        byte[] bytes = new byte[byteBuffer.capacity()];
        byteBuffer.rewind();
        byteBuffer.get(bytes);

        int copyStartPos = 0;

        // Applies the solution only when the original JPEG data can't be correctly parsed to
        // find the SOS marker position.
        if (!canParseSosMarker(bytes)) {
            int secondFfd8Position = findSecondFfd8Position(bytes);
            if (secondFfd8Position != -1) {
                copyStartPos = secondFfd8Position;
            } else {
                return bytes;
            }
        }

        return Arrays.copyOfRange(bytes, copyStartPos, byteBuffer.limit());
    }

    /**
     * Returns whether the JFIF SOS marker can be correctly parsed from the input JPEG byte data.
     */
    private boolean canParseSosMarker(@NonNull byte[] bytes) {
        // Parses the JFIF segments from the start of the JPEG image data
        int markPosition = 0x2;
        while (true) {
            // Breaks the while-loop and return false if the mark byte can't be correctly found.
            if (markPosition + 4 > bytes.length || bytes[markPosition] != ((byte) 0xff)) {
                return false;
            }
            // Breaks the while-loop when finding the SOS (FF DA) mark
            if (bytes[markPosition] == ((byte) 0xff) && bytes[markPosition + 1] == ((byte) 0xda)) {
                return true;
            }
            int segmentLength =
                    ((bytes[markPosition + 2] & 0xff) << 8) | (bytes[markPosition + 3] & 0xff);
            markPosition += segmentLength + 2;
        }
    }

    /**
     * Returns the second FFD8 position.
     *
     * @param bytes the JPEG byte array data.
     * @return the second FFD8 position if it can be found. Otherwise, returns -1.
     */
    private int findSecondFfd8Position(@NonNull byte[] bytes) {
        // Starts from the position 2 to skip the first FFD8
        int position = 2;

        while (true) {
            if (position + 1 > bytes.length) {
                break;
            }
            // Find and return the second FFD8 position
            if (bytes[position] == ((byte) 0xff)
                    && bytes[position + 1] == ((byte) 0xd8)) {
                return position;
            }
            position++;
        }

        return -1;
    }
}