chromium/chrome/browser/ui/android/pdf/java/src/org/chromium/chrome/browser/pdf/PdfUtils.java

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.pdf;

import android.net.Uri;
import android.text.TextUtils;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.os.BuildCompat;

import org.jni_zero.CalledByNative;

import org.chromium.base.ContentUriUtils;
import org.chromium.base.Log;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.chrome.browser.ui.native_page.NativePage;
import org.chromium.chrome.browser.util.ChromeFileProvider;
import org.chromium.components.embedder_support.util.UrlConstants;
import org.chromium.content_public.browser.ContentFeatureList;
import org.chromium.content_public.browser.ContentFeatureMap;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.url.GURL;

import java.io.File;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.Objects;

/** Utilities for inline pdf support. */
public class PdfUtils {
    @IntDef({
        PdfPageType.NONE,
        PdfPageType.TRANSIENT_SECURE,
        PdfPageType.LOCAL,
        PdfPageType.TRANSIENT_INSECURE
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface PdfPageType {
        int NONE = 0;
        int TRANSIENT_SECURE = 1;
        int LOCAL = 2;
        int TRANSIENT_INSECURE = 3;
    }

    private static final String TAG = "PdfUtils";
    private static final String PARAM_ANDROID_INLINE_PDF_IN_INCOGNITO = "inline_pdf_in_incognito";
    private static boolean sShouldOpenPdfInlineForTesting;

    /**
     * Determines whether the navigation is to a pdf file.
     *
     * @param url The url of the navigation.
     * @param params The LoadUrlParams which might be null.
     * @return Whether the navigation is to a pdf file.
     */
    public static boolean isPdfNavigation(String url, @Nullable LoadUrlParams params) {
        String scheme = getDecodedScheme(url);
        if (scheme == null) {
            return false;
        }

        if (scheme.equals(UrlConstants.FILE_SCHEME) || scheme.equals(UrlConstants.CONTENT_SCHEME)) {
            return true;
        }
        if (scheme.equals(UrlConstants.HTTP_SCHEME) || scheme.equals(UrlConstants.HTTPS_SCHEME)) {
            return params != null && params.getIsPdf();
        }
        return false;
    }

    /**
     * Determines whether the navigation is to a permanent downloaded pdf file.
     *
     * @param url The url of the navigation.
     * @return Whether the navigation is to a permanent downloaded pdf file.
     */
    public static boolean isDownloadedPdf(String url) {
        String scheme = getDecodedScheme(url);
        if (scheme == null) {
            return false;
        }

        return scheme.equals(UrlConstants.FILE_SCHEME)
                || scheme.equals(UrlConstants.CONTENT_SCHEME);
    }

    private static String getDecodedScheme(String url) {
        String decodedUrl = PdfUtils.decodePdfPageUrl(url);
        if (decodedUrl == null) {
            return null;
        }
        Uri uri = Uri.parse(decodedUrl);
        return uri.getScheme();
    }

    /**
     * Determines whether to open pdf inline.
     *
     * @param isIncognito Whether the current page is in an incognito mode.
     * @return Whether to open pdf inline.
     */
    @CalledByNative
    public static boolean shouldOpenPdfInline(boolean isIncognito) {
        if (sShouldOpenPdfInlineForTesting) return true;
        if (!ContentFeatureMap.isEnabled(ContentFeatureList.ANDROID_OPEN_PDF_INLINE)) {
            return false;
        }
        if (isIncognito
                && !ContentFeatureMap.getInstance()
                        .getFieldTrialParamByFeatureAsBoolean(
                                ContentFeatureList.ANDROID_OPEN_PDF_INLINE,
                                PARAM_ANDROID_INLINE_PDF_IN_INCOGNITO,
                                false)) {
            return false;
        }
        // TODO(https://crbug.com/337674493): Check if pdf viewer is available on pre-V devices.
        return BuildCompat.isAtLeastV();
    }

    /**
     * Retrieve pdf specific information from NativePage.
     *
     * @param nativePage The NativePage being used to retrieve pdf information.
     * @return Pdf information including filename, filepath etc.
     */
    public static PdfInfo getPdfInfo(NativePage nativePage) {
        if (nativePage == null || !nativePage.isPdf()) {
            return null;
        }
        return new PdfInfo(
                nativePage.getTitle(),
                nativePage.getCanonicalFilepath(),
                nativePage.isDownloadSafe());
    }

    static String getFileNameFromUrl(String url, String defaultTitle) {
        Uri uri = Uri.parse(url);
        String scheme = uri.getScheme();
        assert scheme != null;
        assert scheme.equals(UrlConstants.HTTP_SCHEME)
                || scheme.equals(UrlConstants.HTTPS_SCHEME)
                || scheme.equals(UrlConstants.CONTENT_SCHEME)
                || scheme.equals(UrlConstants.FILE_SCHEME);
        String fileName = defaultTitle;
        if (scheme.equals(UrlConstants.CONTENT_SCHEME)) {
            String displayName = ContentUriUtils.maybeGetDisplayName(url);
            if (!TextUtils.isEmpty(displayName)) {
                fileName = displayName;
            }
        } else if (scheme.equals(UrlConstants.FILE_SCHEME)) {
            if (uri.getPath() != null) {
                File file = new File(uri.getPath());
                if (!file.getName().isEmpty()) {
                    fileName = file.getName();
                }
            }
        }
        return fileName;
    }

    static String getFilePathFromUrl(String url) {
        GURL gurl = new GURL(url);
        if (getPdfPageTypeInternal(gurl, false) == PdfPageType.LOCAL) {
            return url;
        }
        return null;
    }

    /** Return the type of the pdf page. */
    public static @PdfPageType int getPdfPageType(NativePage pdfPage) {
        if (pdfPage == null || !pdfPage.isPdf()) {
            return PdfPageType.NONE;
        }
        assert pdfPage instanceof PdfPage;
        GURL url = new GURL(pdfPage.getUrl());
        return getPdfPageTypeInternal(url, pdfPage.isDownloadSafe());
    }

    private static @PdfPageType int getPdfPageTypeInternal(GURL url, boolean isDownloadSafe) {
        // The url may be encoded. Try to decode first.
        String scheme = getDecodedScheme(url.getSpec());
        // Get scheme from url directly if fail to decode.
        if (scheme == null) {
            scheme = url.getScheme();
        }

        if (scheme == null) {
            return PdfPageType.NONE;
        }
        if (scheme.equals(UrlConstants.HTTP_SCHEME) || scheme.equals(UrlConstants.HTTPS_SCHEME)) {
            return isDownloadSafe ? PdfPageType.TRANSIENT_SECURE : PdfPageType.TRANSIENT_INSECURE;
        }
        if (scheme.equals(UrlConstants.CONTENT_SCHEME) || scheme.equals(UrlConstants.FILE_SCHEME)) {
            return PdfPageType.LOCAL;
        }
        return PdfPageType.NONE;
    }

    static void setShouldOpenPdfInlineForTesting(boolean shouldOpenPdfInlineForTesting) {
        sShouldOpenPdfInlineForTesting = shouldOpenPdfInlineForTesting;
    }

    static Uri getUriFromFilePath(@NonNull String pdfFilePath) {
        Uri uri = Uri.parse(pdfFilePath);
        String scheme = uri.getScheme();
        try {
            if (UrlConstants.CONTENT_SCHEME.equals(scheme)) {
                return uri;
            } else if (UrlConstants.FILE_SCHEME.equals(scheme)) {
                File file = new File(Objects.requireNonNull(uri.getPath()));
                return ChromeFileProvider.generateUri(file);
            } else {
                File file = new File(pdfFilePath);
                return ChromeFileProvider.generateUri(file);
            }
        } catch (Exception e) {
            Log.e(TAG, "Couldn't generate Uri: " + e);
            return null;
        }
    }

    /**
     * Record boolean histogram Android.Pdf.IsFrozenWhenDisplayed.
     *
     * @param nativePage When the native page is a pdf page, record whether it is frozen before the
     *     tab is displayed.
     */
    public static void recordIsPdfFrozen(NativePage nativePage) {
        if (nativePage == null) {
            return;
        }
        if (!nativePage.isPdf()) {
            return;
        }
        RecordHistogram.recordBooleanHistogram(
                "Android.Pdf.IsFrozenWhenDisplayed", nativePage.isFrozen());
    }

    /**
     * Encode the download url and generate the pdf page url.
     *
     * @param downloadUrl The url which is interpreted as download.
     * @return The pdf page url including the encoded downloadUrl.
     */
    public static String encodePdfPageUrl(String downloadUrl) {
        try {
            String pdfPageUrl =
                    UrlConstants.PDF_URL
                            + UrlConstants.PDF_URL_PARAM
                            + URLEncoder.encode(downloadUrl, "UTF-8");
            recordIsPdfDownloadUrlEncoded(true);
            return pdfPageUrl;
        } catch (java.io.UnsupportedEncodingException e) {
            recordIsPdfDownloadUrlEncoded(false);
            Log.e(TAG, "Unsupported encoding: " + e.getMessage());
            return null;
        }
    }

    /**
     * Decode the pdf page url.
     *
     * @param originalUrl The url to be decoded.
     * @return the decoded download url; or null if the original url is not a pdf page url.
     */
    public static String decodePdfPageUrl(String originalUrl) {
        if (!originalUrl.startsWith(UrlConstants.PDF_URL)) {
            return null;
        }
        Uri uri = Uri.parse(originalUrl);
        String encodedUrl = uri.getQueryParameter(UrlConstants.PDF_URL_QUERY_PARAM);
        try {
            String decodedUrl = URLDecoder.decode(encodedUrl, "UTF-8");
            recordIsPdfDownloadUrlDecoded(true);
            return decodedUrl;
        } catch (java.io.UnsupportedEncodingException e) {
            recordIsPdfDownloadUrlDecoded(false);
            Log.e(TAG, "Unsupported encoding: " + e.getMessage());
            return null;
        }
    }

    private static void recordIsPdfDownloadUrlEncoded(boolean encodeResult) {
        RecordHistogram.recordBooleanHistogram("Android.Pdf.DownloadUrlEncoded", encodeResult);
    }

    private static void recordIsPdfDownloadUrlDecoded(boolean decodeResult) {
        RecordHistogram.recordBooleanHistogram("Android.Pdf.DownloadUrlDecoded", decodeResult);
    }
}