chromium/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebApkDataProvider.java

// Copyright 2022 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.webapps;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;

import androidx.annotation.NonNull;

import org.jni_zero.CalledByNative;

import org.chromium.base.ContextUtils;
import org.chromium.base.ResettersForTesting;
import org.chromium.chrome.browser.ActivityUtils;
import org.chromium.chrome.browser.browserservices.intents.BitmapHelper;
import org.chromium.chrome.browser.browserservices.intents.BrowserServicesIntentDataProvider;
import org.chromium.chrome.browser.browserservices.intents.WebappInfo;
import org.chromium.chrome.browser.customtabs.CustomTabActivity;
import org.chromium.components.embedder_support.util.Origin;
import org.chromium.components.webapk.lib.client.WebApkValidator;
import org.chromium.components.webapps.WebApkDetailsForDefaultOfflinePage;
import org.chromium.content_public.browser.WebContents;
import org.chromium.webapk.lib.common.WebApkConstants;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/** Provides access to more detail about webapks. */
public class WebApkDataProvider {
    // Contains the details to return for an offline app when testing.
    private static WebappInfo sWebappInfoForTesting;

    // Keeps track of the data needed for the custom offline page data.
    private static class OfflineData {
        private @NonNull String mName;
        private @NonNull String mIcon;
    }

    public static void setWebappInfoForTesting(WebappInfo webappInfo) {
        sWebappInfoForTesting = webappInfo;
        ResettersForTesting.register(() -> sWebappInfoForTesting = null);
    }

    public static WebappInfo getPartialWebappInfo(String url) {
        if (sWebappInfoForTesting != null) return sWebappInfoForTesting;

        Context appContext = ContextUtils.getApplicationContext();
        String packageName = WebApkValidator.queryFirstWebApkPackage(appContext, url);
        return WebappInfo.create(
                WebApkIntentDataProviderFactory.create(
                        new Intent(),
                        packageName,
                        "",
                        WebApkConstants.ShortcutSource.UNKNOWN,
                        /* forceNavigation= */ false,
                        /* canUseSplashFromContentProvider= */ false,
                        /* shareData= */ null,
                        /* shareDataActivityClassName= */ null));
    }

    private static OfflineData getOfflinePageInfoForPwa(String url) {
        WebappInfo webAppInfo = getPartialWebappInfo(url);
        if (webAppInfo == null) return null;

        OfflineData result = new OfflineData();
        result.mName = webAppInfo.shortName();
        // Encoding the image is marked as a slow method, but this call is intentional,
        // we're encoding only a single small icon (the app icon) and the code path is
        // triggered only when the device is offline. We therefore shouldn't bother
        // jumping through hoops to make it fast.
        result.mIcon = webAppInfo.icon().encoded();

        return result;
    }

    private static OfflineData getOfflinePageInfoForTwa(CustomTabActivity customTabActivity) {
        BrowserServicesIntentDataProvider dataProvider = customTabActivity.getIntentDataProvider();
        if (dataProvider == null) return null;

        String clientPackageName = dataProvider.getClientPackageName();
        if (clientPackageName == null) return null;

        OfflineData result = new OfflineData();
        PackageManager packageManager = ContextUtils.getApplicationContext().getPackageManager();
        try {
            result.mName =
                    packageManager
                            .getApplicationLabel(
                                    packageManager.getApplicationInfo(
                                            clientPackageName, PackageManager.GET_META_DATA))
                            .toString();
            Drawable d = packageManager.getApplicationIcon(clientPackageName);
            Bitmap bitmap =
                    Bitmap.createBitmap(
                            d.getIntrinsicWidth(), d.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
            Canvas canvas = new Canvas(bitmap);
            d.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
            d.draw(canvas);
            result.mIcon = BitmapHelper.encodeBitmapAsString(bitmap);
        } catch (PackageManager.NameNotFoundException e) {
            return null;
        }

        return result;
    }

    private static boolean isWithinScope(String url, CustomTabActivity customTabActivity) {
        BrowserServicesIntentDataProvider dataProvider = customTabActivity.getIntentDataProvider();
        Set<Origin> origins = new HashSet<>();
        origins.add(Origin.create(dataProvider.getUrlToLoad()));

        List<String> additionalOrigins = dataProvider.getTrustedWebActivityAdditionalOrigins();
        if (additionalOrigins != null) {
            for (String origin : additionalOrigins) {
                origins.add(Origin.create(origin));
            }
        }

        return origins.contains(Origin.create(url));
    }

    @CalledByNative
    public static String[] getOfflinePageInfo(int[] fields, String url, WebContents webContents) {
        Activity activity = ActivityUtils.getActivityFromWebContents(webContents);

        OfflineData offlineData = null;
        if (activity instanceof CustomTabActivity) {
            CustomTabActivity customTabActivity = (CustomTabActivity) activity;
            if (isWithinScope(url, customTabActivity)) {
                offlineData = getOfflinePageInfoForTwa(customTabActivity);
            }
        } else {
            offlineData = getOfflinePageInfoForPwa(url);
        }

        if (offlineData == null) return null;

        List<String> fieldValues = new ArrayList<String>();
        for (int field : fields) {
            switch (field) {
                case WebApkDetailsForDefaultOfflinePage.SHORT_NAME:
                    fieldValues.add(offlineData.mName);
                    break;
                case WebApkDetailsForDefaultOfflinePage.ICON:
                    fieldValues.add("data:image/png;base64," + offlineData.mIcon);
                    break;
                default:
                    fieldValues.add("No such field: " + field);
                    break;
            }
        }
        return fieldValues.toArray(new String[0]);
    }
}