chromium/content/public/android/javatests/src/org/chromium/content/browser/font/AndroidFontLookupImplTest.java

// Copyright 2020 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.content.browser.font;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.AdditionalMatchers.aryEq;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.ArgumentMatchers.notNull;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;

import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.AssetFileDescriptor;
import android.net.Uri;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.test.mock.MockContentProvider;

import androidx.core.provider.FontRequest;
import androidx.core.provider.FontsContractCompat.Columns;
import androidx.core.provider.FontsContractCompat.FontFamilyResult;
import androidx.core.provider.FontsContractCompat.FontInfo;
import androidx.test.filters.SmallTest;

import com.google.common.collect.ImmutableMap;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.stubbing.OngoingStubbing;

import org.chromium.base.test.BaseJUnit4ClassRunner;
import org.chromium.base.test.util.AdvancedMockContext;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.HistogramWatcher;
import org.chromium.blink.mojom.AndroidFontLookup;
import org.chromium.blink.mojom.AndroidFontLookup.FetchAllFontFiles_Response;
import org.chromium.blink.mojom.AndroidFontLookup.GetUniqueNameLookupTable_Response;
import org.chromium.blink.mojom.AndroidFontLookup.MatchLocalFontByUniqueName_Response;
import org.chromium.content_public.browser.test.NativeLibraryTestUtils;
import org.chromium.mojo.MojoTestRule;
import org.chromium.mojo_base.mojom.ReadOnlyFile;

import java.io.IOException;
import java.util.Map;

/** Tests the {@link AndroidFontLookup} implementation. */
@RunWith(BaseJUnit4ClassRunner.class)
public final class AndroidFontLookupImplTest {
    private static final String FULL_FONT_NAME_1 = "foo";
    private static final String FONT_QUERY_1 = "name=Foo&weight=400";
    private static final String FULL_FONT_NAME_2 = "bar";
    private static final String FONT_QUERY_2 = "name=Bar&weight=400";
    private static final String FULL_FONT_NAME_3 = "bar bold";
    private static final String FONT_QUERY_3 = "name=Bar&weight=700";
    private static final String AUTHORITY = "com.google.android.gms.fonts";
    private static final Uri URI = Uri.parse("content://com.google.android.gms.fonts/123");
    private static final Uri URI2 = Uri.parse("content://com.google.android.gms.fonts/456");
    private static final int FD = 42;
    private static final int FD2 = 43;
    private static final long RUN_LOOP_TIMEOUT_MS = 50;

    @Rule public MojoTestRule mMojoTestRule = new MojoTestRule(MojoTestRule.MojoCore.INITIALIZE);

    @Mock private FontsContractWrapper mMockFontsContractWrapper;
    @Mock private ParcelFileDescriptor mMockFileDescriptor;
    @Mock private ParcelFileDescriptor mMockFileDescriptor2;
    @Mock private ParcelFileDescriptor mMockCachedFileDescriptor;
    @Mock private ParcelFileDescriptor mMockCachedFileDescriptor2;
    @Mock private ParcelFileDescriptor mMockDuplicateFileDescriptor;
    @Mock private ParcelFileDescriptor mMockDuplicateFileDescriptor2;
    private AdvancedMockContext mMockContext;

    @Mock private GetUniqueNameLookupTable_Response mGetUniqueNameLookupTableCallback;
    @Mock private MatchLocalFontByUniqueName_Response mMatchLocalFontByUniqueNameCallback;
    @Mock private FetchAllFontFiles_Response mFetchAllFontFilesCallback;

    @Captor private ArgumentCaptor<Map<String, ReadOnlyFile>> mFontMapCaptor;

    private AndroidFontLookupImpl mAndroidFontLookup;

    @Before
    public void setUp() throws IOException {
        initMocks(this);
        mMockContext = new AdvancedMockContext();

        NativeLibraryTestUtils.loadNativeLibraryNoBrowserProcess();

        when(mMockFileDescriptor.dup()).thenReturn(mMockCachedFileDescriptor);
        when(mMockFileDescriptor2.dup()).thenReturn(mMockCachedFileDescriptor2);
        when(mMockCachedFileDescriptor.dup()).thenReturn(mMockDuplicateFileDescriptor);
        when(mMockCachedFileDescriptor2.dup()).thenReturn(mMockDuplicateFileDescriptor2);
        when(mMockDuplicateFileDescriptor.detachFd()).thenReturn(FD);
        when(mMockDuplicateFileDescriptor2.detachFd()).thenReturn(FD2);
        mMockContext
                .getMockContentResolver()
                .addProvider(
                        AUTHORITY,
                        new MockContentProvider(mMockContext) {
                            @Override
                            public AssetFileDescriptor openTypedAssetFile(
                                    Uri url, String mimeType, Bundle opts) {
                                if (url.equals(URI)) {
                                    return new AssetFileDescriptor(mMockFileDescriptor, 0, -1);
                                } else if (url.equals(URI2)) {
                                    return new AssetFileDescriptor(mMockFileDescriptor2, 0, -1);
                                } else {
                                    return null;
                                }
                            }
                        });

        Map<String, String> fullFontNameToQuery =
                ImmutableMap.of(
                        FULL_FONT_NAME_1,
                        FONT_QUERY_1,
                        FULL_FONT_NAME_2,
                        FONT_QUERY_2,
                        FULL_FONT_NAME_3,
                        FONT_QUERY_3);

        mAndroidFontLookup =
                new AndroidFontLookupImpl(
                        mMockContext, mMockFontsContractWrapper, fullFontNameToQuery);
    }

    @SmallTest
    @Test
    public void testGetUniqueNameLookupTable_Available() {
        String[] expected = new String[] {FULL_FONT_NAME_2, FULL_FONT_NAME_3, FULL_FONT_NAME_1};

        mAndroidFontLookup.getUniqueNameLookupTable(mGetUniqueNameLookupTableCallback);

        mMojoTestRule.runLoop(RUN_LOOP_TIMEOUT_MS);
        verify(mGetUniqueNameLookupTableCallback).call(aryEq(expected));
    }

    @SmallTest
    @Test
    public void testFetchAllFontFiles_Available() throws NameNotFoundException {
        FontInfo fontInfo = new FontInfo(URI, 0, 400, false, Columns.RESULT_CODE_OK);
        FontFamilyResult result =
                new FontFamilyResult(FontFamilyResult.STATUS_OK, new FontInfo[] {fontInfo});
        whenFetchFontsWith(FONT_QUERY_1).thenReturn(result);
        whenFetchFontsWith(FONT_QUERY_2).thenReturn(result);
        whenFetchFontsWith(FONT_QUERY_3).thenReturn(result);

        HistogramWatcher watcher =
                HistogramWatcher.newSingleRecordWatcher(
                        AndroidFontLookupImpl.FETCH_ALL_FONT_FILES_HISTOGRAM);
        mAndroidFontLookup.fetchAllFontFiles(mFetchAllFontFilesCallback);
        watcher.pollInstrumentationThreadUntilSatisfied();

        mMojoTestRule.runLoop(RUN_LOOP_TIMEOUT_MS);
        verify(mFetchAllFontFilesCallback, timeout(CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL))
                .call(mFontMapCaptor.capture());

        Map<String, ReadOnlyFile> response = mFontMapCaptor.getValue();
        assertEquals(3, response.size());
        assertNotNull(response.get(FULL_FONT_NAME_1));
        assertNotNull(response.get(FULL_FONT_NAME_2));
        assertNotNull(response.get(FULL_FONT_NAME_3));
    }

    @SmallTest
    @Test
    public void testFetchAllFontFiles_OneNotAvailable() throws NameNotFoundException {
        FontInfo fontInfo = new FontInfo(URI, 0, 400, false, Columns.RESULT_CODE_OK);
        FontFamilyResult result =
                new FontFamilyResult(FontFamilyResult.STATUS_OK, new FontInfo[] {fontInfo});
        whenFetchFontsWith(FONT_QUERY_1).thenReturn(result);
        whenFetchFontsWith(FONT_QUERY_2).thenReturn(result);
        whenFetchFontsWith(FONT_QUERY_3)
                .thenReturn(
                        new FontFamilyResult(
                                FontFamilyResult.STATUS_UNEXPECTED_DATA_PROVIDED, null));

        HistogramWatcher watcher =
                HistogramWatcher.newSingleRecordWatcher(
                        AndroidFontLookupImpl.FETCH_ALL_FONT_FILES_HISTOGRAM);
        mAndroidFontLookup.fetchAllFontFiles(mFetchAllFontFilesCallback);
        watcher.pollInstrumentationThreadUntilSatisfied();

        mMojoTestRule.runLoop(RUN_LOOP_TIMEOUT_MS);
        verify(mFetchAllFontFilesCallback, timeout(CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL))
                .call(mFontMapCaptor.capture());

        Map<String, ReadOnlyFile> response = mFontMapCaptor.getValue();
        assertEquals(2, response.size());
        assertNotNull(response.get(FULL_FONT_NAME_1));
        assertNotNull(response.get(FULL_FONT_NAME_2));

        // Verify the font was removed from the lookup table.
        mAndroidFontLookup.getUniqueNameLookupTable(mGetUniqueNameLookupTableCallback);
        mMojoTestRule.runLoop(RUN_LOOP_TIMEOUT_MS);
        verify(mGetUniqueNameLookupTableCallback)
                .call(aryEq(new String[] {FULL_FONT_NAME_2, FULL_FONT_NAME_1}));
    }

    @SmallTest
    @Test
    public void testFetchAllFontFiles_Empty() throws NameNotFoundException {
        mAndroidFontLookup =
                new AndroidFontLookupImpl(
                        mMockContext, mMockFontsContractWrapper, ImmutableMap.of());

        HistogramWatcher watcher =
                HistogramWatcher.newSingleRecordWatcher(
                        AndroidFontLookupImpl.FETCH_ALL_FONT_FILES_HISTOGRAM);
        mAndroidFontLookup.fetchAllFontFiles(mFetchAllFontFilesCallback);
        watcher.pollInstrumentationThreadUntilSatisfied();

        mMojoTestRule.runLoop(RUN_LOOP_TIMEOUT_MS);
        verify(mFetchAllFontFilesCallback, timeout(CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL))
                .call(mFontMapCaptor.capture());

        Map<String, ReadOnlyFile> response = mFontMapCaptor.getValue();
        assertTrue(response.isEmpty());
    }

    @SmallTest
    @Test
    public void testGetUniqueNameLookupTable_MultipleFonts() throws NameNotFoundException {
        // All 3 fonts should be found in results.
        mAndroidFontLookup.getUniqueNameLookupTable(mGetUniqueNameLookupTableCallback);
        mMojoTestRule.runLoop(RUN_LOOP_TIMEOUT_MS);
        verify(mGetUniqueNameLookupTableCallback)
                .call(aryEq(new String[] {FULL_FONT_NAME_2, FULL_FONT_NAME_3, FULL_FONT_NAME_1}));

        // Bar Bold is not available.
        FontFamilyResult result3 =
                new FontFamilyResult(FontFamilyResult.STATUS_OK, new FontInfo[0]);
        whenFetchFontsWith(FONT_QUERY_3).thenReturn(result3);
        mAndroidFontLookup.matchLocalFontByUniqueName(
                FULL_FONT_NAME_3, mMatchLocalFontByUniqueNameCallback);
        mMojoTestRule.runLoop(RUN_LOOP_TIMEOUT_MS);
        verify(
                        mMatchLocalFontByUniqueNameCallback,
                        timeout(CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL))
                .call(isNull());

        // Bar Bold should now be excluded from list.
        mAndroidFontLookup.getUniqueNameLookupTable(mGetUniqueNameLookupTableCallback);
        mMojoTestRule.runLoop(RUN_LOOP_TIMEOUT_MS);
        verify(mGetUniqueNameLookupTableCallback)
                .call(aryEq(new String[] {FULL_FONT_NAME_2, FULL_FONT_NAME_1}));
    }

    @SmallTest
    @Test
    public void testMatchLocalFontByUniqueName_UnsupportedFontName() {
        HistogramWatcher watcher =
                HistogramWatcher.newSingleRecordWatcher(
                        AndroidFontLookupImpl.MATCH_LOCAL_FONT_BY_UNIQUE_NAME_HISTOGRAM);
        mAndroidFontLookup.matchLocalFontByUniqueName("baz", mMatchLocalFontByUniqueNameCallback);
        watcher.pollInstrumentationThreadUntilSatisfied();

        mMojoTestRule.runLoop(RUN_LOOP_TIMEOUT_MS);
        verify(
                        mMatchLocalFontByUniqueNameCallback,
                        timeout(CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL))
                .call(isNull());
    }

    @SmallTest
    @Test
    public void testMatchLocalFontByUniqueName_BadResultStatus() throws NameNotFoundException {
        FontFamilyResult result =
                new FontFamilyResult(FontFamilyResult.STATUS_UNEXPECTED_DATA_PROVIDED, null);
        whenFetchFontsWith(FONT_QUERY_1).thenReturn(result);

        HistogramWatcher watcher =
                HistogramWatcher.newSingleRecordWatcher(
                        AndroidFontLookupImpl.MATCH_LOCAL_FONT_BY_UNIQUE_NAME_HISTOGRAM);
        mAndroidFontLookup.matchLocalFontByUniqueName(
                FULL_FONT_NAME_1, mMatchLocalFontByUniqueNameCallback);
        watcher.pollInstrumentationThreadUntilSatisfied();

        mMojoTestRule.runLoop(RUN_LOOP_TIMEOUT_MS);
        verify(
                        mMatchLocalFontByUniqueNameCallback,
                        timeout(CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL))
                .call(isNull());
    }

    @SmallTest
    @Test
    public void testMatchLocalFontByUniqueName_EmptyResults() throws NameNotFoundException {
        FontFamilyResult result = new FontFamilyResult(FontFamilyResult.STATUS_OK, new FontInfo[0]);
        whenFetchFontsWith(FONT_QUERY_1).thenReturn(result);

        HistogramWatcher watcher =
                HistogramWatcher.newSingleRecordWatcher(
                        AndroidFontLookupImpl.MATCH_LOCAL_FONT_BY_UNIQUE_NAME_HISTOGRAM);
        mAndroidFontLookup.matchLocalFontByUniqueName(
                FULL_FONT_NAME_1, mMatchLocalFontByUniqueNameCallback);
        watcher.pollInstrumentationThreadUntilSatisfied();

        mMojoTestRule.runLoop(RUN_LOOP_TIMEOUT_MS);
        verify(
                        mMatchLocalFontByUniqueNameCallback,
                        timeout(CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL))
                .call(isNull());
    }

    @SmallTest
    @Test
    public void testMatchLocalFontByUniqueName_BadFontInfoStatus() throws NameNotFoundException {
        FontInfo fontInfo = new FontInfo(URI, 0, 400, false, Columns.RESULT_CODE_FONT_NOT_FOUND);
        FontFamilyResult result =
                new FontFamilyResult(FontFamilyResult.STATUS_OK, new FontInfo[] {fontInfo});
        whenFetchFontsWith(FONT_QUERY_1).thenReturn(result);

        HistogramWatcher watcher =
                HistogramWatcher.newSingleRecordWatcher(
                        AndroidFontLookupImpl.MATCH_LOCAL_FONT_BY_UNIQUE_NAME_HISTOGRAM);
        mAndroidFontLookup.matchLocalFontByUniqueName(
                FULL_FONT_NAME_1, mMatchLocalFontByUniqueNameCallback);
        watcher.pollInstrumentationThreadUntilSatisfied();

        mMojoTestRule.runLoop(RUN_LOOP_TIMEOUT_MS);
        verify(
                        mMatchLocalFontByUniqueNameCallback,
                        timeout(CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL))
                .call(isNull());
    }

    @SmallTest
    @Test
    public void testMatchLocalFontByUniqueName_Throws() throws NameNotFoundException {
        whenFetchFontsWith(FONT_QUERY_1).thenThrow(new NameNotFoundException());

        HistogramWatcher watcher =
                HistogramWatcher.newSingleRecordWatcher(
                        AndroidFontLookupImpl.MATCH_LOCAL_FONT_BY_UNIQUE_NAME_HISTOGRAM);
        mAndroidFontLookup.matchLocalFontByUniqueName(
                FULL_FONT_NAME_1, mMatchLocalFontByUniqueNameCallback);
        watcher.pollInstrumentationThreadUntilSatisfied();

        mMojoTestRule.runLoop(RUN_LOOP_TIMEOUT_MS);
        verify(
                        mMatchLocalFontByUniqueNameCallback,
                        timeout(CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL))
                .call(isNull());
    }

    @SmallTest
    @Test
    public void testMatchLocalFontByUniqueName_NoRetry() throws NameNotFoundException {
        // Request font and fail.
        whenFetchFontsWith(FONT_QUERY_1).thenThrow(new NameNotFoundException());

        {
            HistogramWatcher watcher =
                    HistogramWatcher.newSingleRecordWatcher(
                            AndroidFontLookupImpl.MATCH_LOCAL_FONT_BY_UNIQUE_NAME_HISTOGRAM);
            mAndroidFontLookup.matchLocalFontByUniqueName(
                    FULL_FONT_NAME_1, mMatchLocalFontByUniqueNameCallback);
            watcher.pollInstrumentationThreadUntilSatisfied();
        }
        mMojoTestRule.runLoop(RUN_LOOP_TIMEOUT_MS);
        verify(
                        mMatchLocalFontByUniqueNameCallback,
                        timeout(CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL))
                .call(isNull());

        // Second request should early out with FAILED_AVOID_RETRY.
        {
            HistogramWatcher watcher =
                    HistogramWatcher.newSingleRecordWatcher(
                            AndroidFontLookupImpl.MATCH_LOCAL_FONT_BY_UNIQUE_NAME_HISTOGRAM);
            mAndroidFontLookup.matchLocalFontByUniqueName(
                    FULL_FONT_NAME_1, mMatchLocalFontByUniqueNameCallback);
            watcher.pollInstrumentationThreadUntilSatisfied();
        }
        mMojoTestRule.runLoop(RUN_LOOP_TIMEOUT_MS);
        verify(
                        mMatchLocalFontByUniqueNameCallback,
                        timeout(CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL).times(2))
                .call(isNull());
    }

    @SmallTest
    @Test
    public void testMatchLocalFontByUniqueName_Success() throws NameNotFoundException {
        FontInfo fontInfo = new FontInfo(URI, 0, 400, false, Columns.RESULT_CODE_OK);
        FontFamilyResult result =
                new FontFamilyResult(FontFamilyResult.STATUS_OK, new FontInfo[] {fontInfo});
        whenFetchFontsWith(FONT_QUERY_1).thenReturn(result);

        HistogramWatcher watcher =
                HistogramWatcher.newSingleRecordWatcher(
                        AndroidFontLookupImpl.MATCH_LOCAL_FONT_BY_UNIQUE_NAME_HISTOGRAM);
        mAndroidFontLookup.matchLocalFontByUniqueName(
                FULL_FONT_NAME_1, mMatchLocalFontByUniqueNameCallback);
        watcher.pollInstrumentationThreadUntilSatisfied();

        mMojoTestRule.runLoop(RUN_LOOP_TIMEOUT_MS);
        verify(
                        mMatchLocalFontByUniqueNameCallback,
                        timeout(CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL))
                .call(notNull());
    }

    private OngoingStubbing<FontFamilyResult> whenFetchFontsWith(String query)
            throws NameNotFoundException {
        return when(
                mMockFontsContractWrapper.fetchFonts(
                        eq(mMockContext),
                        isNull(),
                        argThat((FontRequest r) -> r.getQuery().equals(query))));
    }
}