chromium/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebApkSyncService.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.webapps;

import android.graphics.Bitmap;
import android.text.TextUtils;

import androidx.annotation.NonNull;

import org.jni_zero.CalledByNative;
import org.jni_zero.JNINamespace;
import org.jni_zero.JniType;
import org.jni_zero.NativeMethods;

import org.chromium.chrome.browser.browserservices.intents.BrowserServicesIntentDataProvider;
import org.chromium.chrome.browser.browserservices.intents.WebappIcon;
import org.chromium.chrome.browser.browserservices.intents.WebappInfo;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.components.sync.protocol.WebApkIconInfo;
import org.chromium.components.sync.protocol.WebApkSpecifics;

import java.util.ArrayList;
import java.util.List;

/** Static class to update WebAPK data to sync. */
@JNINamespace("webapk")
public class WebApkSyncService {
    private static final long UNIX_OFFSET_MICROS = 11644473600000000L;

    /** Called with update result. */
    public static interface PwaRestorableListCallback {
        @CalledByNative("PwaRestorableListCallback")
        public void onRestorableAppsAvailable(
                boolean success,
                @NonNull String[] appIds,
                @NonNull String[] appNames,
                @NonNull int[] lastUsedInDays,
                @NonNull List<Bitmap> icons);
    }

    static void onWebApkUsed(
            BrowserServicesIntentDataProvider intendDataProvider,
            WebappDataStorage storage,
            boolean isInstall) {
        WebApkSpecifics specifics =
                getWebApkSpecifics(WebappInfo.create(intendDataProvider), storage);
        if (specifics != null) {
            WebApkSyncServiceJni.get().onWebApkUsed(specifics.toByteArray(), isInstall);
        }
    }

    static void onWebApkUninstalled(String manifestId) {
        WebApkSyncServiceJni.get().onWebApkUninstalled(manifestId);
    }

    static WebApkSpecifics getWebApkSpecifics(WebappInfo webApkInfo, WebappDataStorage storage) {
        if (webApkInfo == null || !webApkInfo.isForWebApk()) {
            return null;
        }

        WebApkSpecifics.Builder webApkSpecificsBuilder = WebApkSpecifics.newBuilder();

        if (TextUtils.isEmpty(webApkInfo.manifestId())) {
            return null;
        }
        webApkSpecificsBuilder.setManifestId(webApkInfo.manifestId());

        if (!TextUtils.isEmpty(webApkInfo.manifestStartUrl())) {
            webApkSpecificsBuilder.setStartUrl(webApkInfo.manifestStartUrl());
        }

        if (!TextUtils.isEmpty(webApkInfo.name())) {
            webApkSpecificsBuilder.setName(webApkInfo.name());
        } else if (!TextUtils.isEmpty(webApkInfo.shortName())) {
            webApkSpecificsBuilder.setName(webApkInfo.shortName());
        }

        if (webApkInfo.hasValidToolbarColor()) {
            webApkSpecificsBuilder.setThemeColor((int) webApkInfo.toolbarColor());
        }

        if (!TextUtils.isEmpty(webApkInfo.scopeUrl())) {
            webApkSpecificsBuilder.setScope(webApkInfo.scopeUrl());
        }

        if (webApkInfo.shellApkVersion() >= WebappIcon.ICON_WITH_URL_AND_HASH_SHELL_VERSION) {
            String iconUrl = webApkInfo.icon().iconUrl();
            if (!TextUtils.isEmpty(iconUrl)) {
                WebApkIconInfo iconInfo =
                        WebApkIconInfo.newBuilder()
                                .setUrl(iconUrl)
                                .setPurpose(
                                        webApkInfo.isIconAdaptive()
                                                ? WebApkIconInfo.Purpose.MASKABLE
                                                : WebApkIconInfo.Purpose.ANY)
                                .build();
                webApkSpecificsBuilder.addIconInfos(iconInfo);
            }
        }
        for (String iconUrl : webApkInfo.iconUrlToMurmur2HashMap().keySet()) {
            if (!TextUtils.isEmpty(iconUrl)) {
                webApkSpecificsBuilder.addIconInfos(
                        WebApkIconInfo.newBuilder().setUrl(iconUrl).build());
            }
        }

        webApkSpecificsBuilder.setLastUsedTimeWindowsEpochMicros(
                toMicrosecondsSinceWindowsEpoch(storage.getLastUsedTimeMs()));

        return webApkSpecificsBuilder.build();
    }

    static void removeOldWebAPKsFromSync(long currentTimeMsSinceUnixEpoch) {
        WebApkSyncServiceJni.get().removeOldWebAPKsFromSync(currentTimeMsSinceUnixEpoch);
    }

    private static long toMicrosecondsSinceWindowsEpoch(long timeInMills) {
        return timeInMills * 1000 + UNIX_OFFSET_MICROS;
    }

    public static void fetchRestorableApps(Profile profile, PwaRestorableListCallback callback) {
        WebApkSyncServiceJni.get().fetchRestorableApps(profile, callback);
    }

    @CalledByNative
    private static List<Bitmap> createBitmapList() {
        return new ArrayList<Bitmap>();
    }

    @CalledByNative
    private static void addToBitmapList(List<Bitmap> bitmaps, Bitmap bitmap) {
        bitmaps.add(bitmap);
    }

    @NativeMethods
    interface Natives {
        void onWebApkUsed(byte[] webApkSpecifics, boolean isInstall);

        void onWebApkUninstalled(@JniType("std::string") String manifestId);

        void removeOldWebAPKsFromSync(long currentTimeMsSinceUnixEpoch);

        void fetchRestorableApps(
                @JniType("Profile*") Profile profile, PwaRestorableListCallback callback);
    }
}