chromium/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkOpener.java

// Copyright 2023 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.bookmarks;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.provider.Browser;
import android.text.format.DateUtils;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import org.chromium.base.IntentUtils;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.chrome.browser.ActivityUtils;
import org.chromium.chrome.browser.IntentHandler;
import org.chromium.chrome.browser.document.ChromeLauncherActivity;
import org.chromium.chrome.browser.tab.TabLaunchType;
import org.chromium.components.bookmarks.BookmarkId;
import org.chromium.components.bookmarks.BookmarkItem;
import org.chromium.components.bookmarks.BookmarkType;
import org.chromium.ui.base.PageTransition;

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

/** Consolidates logic about opening bookmarks. */
public class BookmarkOpener {
    private final BookmarkModel mModel;
    private final Context mContext;
    private final ComponentName mComponentName;

    /**
     * @param model The bookmark model, used to query for bookmark urls and type.
     * @param context The android context, used to build the intent to open bookmarks.
     * @param componentName The name of the parent component, can be null on tablets.
     */
    public BookmarkOpener(
            BookmarkModel model, Context context, @Nullable ComponentName componentName) {
        mModel = model;
        mContext = context;
        mComponentName = componentName;
    }

    /**
     * Open the given id in the current tab.
     *
     * @param bookmarkIds The bookmark id to open.
     * @param incognito Whether the bookmark should be opened in incognito mode.
     * @return Whether the bookmark id was successfully opened.
     */
    public boolean openBookmarkInCurrentTab(BookmarkId id, boolean incognito) {
        if (id == null) return false;
        BookmarkItem item = mModel.getBookmarkById(id);
        if (item == null) return false;
        maybeMarkReadingListItemAsRead(item);
        recordMetricsForOpenBookmarkInCurrentTab(item);

        Intent intent = createBasicOpenIntent(item, incognito);
        IntentHandler.startActivityForTrustedIntent(intent);
        return true;
    }

    /**
     * Open the given bookmarkIds in new tabs.
     *
     * @param bookmarkIds The bookmark ids to open.
     * @param incognito Whether the bookmarks should be opened in incognito mode.
     * @return Whether the bookmark ids were successfully opened.
     */
    public boolean openBookmarksInNewTabs(List<BookmarkId> bookmarkIds, boolean incognito) {
        if (bookmarkIds == null || bookmarkIds.size() == 0) return false;

        BookmarkItem firstItem = null;
        ArrayList<String> additionalUrls = new ArrayList<>();
        List<BookmarkItem> items = new ArrayList<>();
        for (BookmarkId id : bookmarkIds) {
            // Might need to check if this is a folder.
            if (id == null) continue;
            BookmarkItem item = mModel.getBookmarkById(id);
            if (item == null) continue;
            maybeMarkReadingListItemAsRead(item);

            if (firstItem == null) {
                firstItem = item;
            } else {
                additionalUrls.add(item.getUrl().getSpec());
            }

            // Collected for metrics to avoid additional JNI calls.
            items.add(item);
        }
        recordMetricsForOpenBookmarksInNewTabs(items);

        Intent intent = createBasicOpenIntent(firstItem, incognito);
        intent.putExtra(Browser.EXTRA_CREATE_NEW_TAB, true);
        intent.putExtra(IntentHandler.EXTRA_OPEN_NEW_INCOGNITO_TAB, incognito);
        intent.putExtra(IntentHandler.EXTRA_ADDITIONAL_URLS, additionalUrls);
        IntentHandler.startActivityForTrustedIntent(intent);
        return true;
    }

    private Intent createBasicOpenIntent(@NonNull BookmarkItem item, boolean incognito) {
        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(item.getUrl().getSpec()));
        intent.putExtra(
                Browser.EXTRA_APPLICATION_ID, mContext.getApplicationContext().getPackageName());
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.putExtra(IntentHandler.EXTRA_PAGE_TRANSITION_TYPE, PageTransition.AUTO_BOOKMARK);
        intent.putExtra(IntentHandler.EXTRA_PAGE_TRANSITION_BOOKMARK_ID, item.getId().toString());
        intent.putExtra(IntentHandler.EXTRA_INCOGNITO_MODE, incognito);

        if (mComponentName != null) {
            ActivityUtils.setNonAliasedComponentForMainBrowsingActivity(intent, mComponentName);
        } else {
            // If the bookmark manager is shown in a tab on a phone (rather than in a separate
            // activity) the component name may be null. Send the intent through
            // ChromeLauncherActivity instead to avoid crashing. See crbug.com/615012.
            intent.setClass(mContext.getApplicationContext(), ChromeLauncherActivity.class);
        }

        // Reading list has special back button behavior which brings the reading list back up
        // as the first back action when viewing an item. This is driven by the FROM_READING_LIST
        // TabLaunchType.
        if (item.getId().getType() == BookmarkType.READING_LIST) {
            IntentHandler.setTabLaunchType(intent, TabLaunchType.FROM_READING_LIST);
            intent.putExtra(Browser.EXTRA_CREATE_NEW_TAB, true);
            intent.putExtra(IntentHandler.EXTRA_OPEN_NEW_INCOGNITO_TAB, incognito);
        }

        IntentUtils.addTrustedIntentExtras(intent);
        return intent;
    }

    private void maybeMarkReadingListItemAsRead(@NonNull BookmarkItem item) {
        if (item.getId().getType() == BookmarkType.READING_LIST) {
            mModel.setReadStatusForReadingList(item.getId(), true);
        }
    }

    // Metrics

    private void recordMetricsForOpenBookmarkInCurrentTab(@NonNull BookmarkItem item) {
        RecordUserAction.record("MobileBookmarkManagerEntryOpened");
        recordTypeOpened(item, "Bookmarks.OpenBookmarkType");
        recordTimeSinceAdded(item, "Bookmarks.OpenBookmarkTimeInterval2.");
    }

    private void recordMetricsForOpenBookmarksInNewTabs(List<BookmarkItem> items) {
        RecordUserAction.record("MobileBookmarkManagerMultipleEntriesOpened");

        for (BookmarkItem item : items) {
            recordTypeOpened(item, "Bookmarks.MultipleOpened.OpenBookmarkType");
            recordTimeSinceAdded(item, "Bookmarks.MultipleOpened.OpenBookmarkTimeInterval2.");
        }
    }

    private String bookmarkTypeToHistogramSuffix(@BookmarkType int type) {
        switch (type) {
            case BookmarkType.NORMAL:
                return "Normal";
            case BookmarkType.PARTNER:
                return "Partner";
            case BookmarkType.READING_LIST:
                return "ReadingList";
        }
        assert false : "Unknown BookmarkType";
        return "";
    }

    private void recordTypeOpened(BookmarkItem item, String histogram) {
        RecordHistogram.recordEnumeratedHistogram(
                histogram, item.getId().getType(), BookmarkType.LAST + 1);
    }

    private void recordTimeSinceAdded(BookmarkItem item, String histogramPrefix) {
        RecordHistogram.recordCustomTimesHistogram(
                histogramPrefix + bookmarkTypeToHistogramSuffix(item.getId().getType()),
                System.currentTimeMillis() - item.getDateAdded(),
                1,
                DateUtils.DAY_IN_MILLIS * 30,
                50);
    }
}