chromium/chrome/android/javatests/src/org/chromium/chrome/browser/download/DownloadTestRule.java

// Copyright 2017 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.download;

import android.app.DownloadManager;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.text.TextUtils;

import org.junit.Assert;

import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.chrome.browser.download.items.OfflineContentAggregatorFactory;
import org.chromium.chrome.browser.profiles.ProfileKey;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.components.download.DownloadCollectionBridge;
import org.chromium.components.offline_items_collection.ContentId;
import org.chromium.components.offline_items_collection.OfflineContentProvider;
import org.chromium.components.offline_items_collection.OfflineItem;
import org.chromium.components.offline_items_collection.OfflineItemState;
import org.chromium.components.offline_items_collection.UpdateDelta;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * Custom TestRule for tests that need to download a file.
 *
 * <p>This has to be a base class because some classes (like BrowserEvent) are exposed only to
 * children of ChromeActivityTestCaseBase. It is a very broken approach to sharing but the only
 * other option is to refactor the ChromeActivityTestCaseBase implementation and all of our test
 * cases.
 */
public class DownloadTestRule extends ChromeTabbedActivityTestRule {
    private static final String TAG = "DownloadTestBase";
    private static final File DOWNLOAD_DIRECTORY =
            Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
    public static final long UPDATE_DELAY_MILLIS = 1000;

    private final CustomMainActivityStart mActivityStart;
    private List<DownloadItem> mAllDownloads;

    public DownloadTestRule(CustomMainActivityStart action) {
        mActivityStart = action;
    }

    /**
     * Checks if a file has downloaded. Is agnostic to the mechanism by which the file has
     * downloaded.
     *
     * @param fileName Expected file name. Path is built by appending filename to the system
     *     downloads path.
     * @param expectedContents Expected contents of the file, or null if the contents should not be
     *     checked.
     */
    public boolean hasDownloaded(String fileName, String expectedContents) {
        try {
            File downloadedFile = getDownloadedPath(fileName);
            if (!downloadedFile.exists()) {
                return false;
            }
            if (expectedContents != null) {
                checkFileContents(downloadedFile.getAbsolutePath(), expectedContents);
            }
            return true;
        } catch (IOException e) {
            Assert.fail("IOException when opening file " + fileName);
            return false;
        }
    }

    /**
     * Check the download exists in DownloadManager by matching the local file path.
     *
     * @param fileName Expected file name. Path is built by appending filename to the system
     *     downloads path.
     * @param expectedContents Expected contents of the file, or null if the contents should not be
     *     checked.
     */
    public boolean hasDownload(String fileName, String expectedContents) throws IOException {
        File downloadedFile = getDownloadedPath(fileName);
        if (!downloadedFile.exists()) {
            return false;
        }

        DownloadManager manager =
                (DownloadManager) getActivity().getSystemService(Context.DOWNLOAD_SERVICE);
        Cursor cursor = manager.query(new DownloadManager.Query());

        cursor.moveToFirst();
        boolean result = false;
        while (!cursor.isAfterLast()) {
            if (fileName.equals(getTitleFromCursor(cursor))) {
                if (expectedContents != null) {
                    checkFileContents(downloadedFile.getAbsolutePath(), expectedContents);
                }
                result = true;
                break;
            }
            cursor.moveToNext();
        }
        cursor.close();
        return result;
    }

    private static File getDownloadedPath(String fileName) {
        File downloadedFile = new File(DOWNLOAD_DIRECTORY, fileName);
        if (!downloadedFile.exists()) {
            Log.d(TAG, "The file " + fileName + " does not exist");
        }
        return downloadedFile;
    }

    private static void checkFileContents(String fullPath, String expectedContents)
            throws IOException {
        FileInputStream stream = new FileInputStream(new File(fullPath));
        byte[] data = new byte[ApiCompatibilityUtils.getBytesUtf8(expectedContents).length];
        try {
            Assert.assertEquals(stream.read(data), data.length);
            String contents = new String(data);
            Assert.assertEquals(expectedContents, contents);
        } finally {
            stream.close();
        }
    }

    /** Delete all download entries in DownloadManager and delete the corresponding files. */
    private void cleanUpAllDownloads() {
        DownloadManager manager =
                (DownloadManager) getActivity().getSystemService(Context.DOWNLOAD_SERVICE);
        Cursor cursor = manager.query(new DownloadManager.Query());
        int idColumnIndex = cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ID);
        cursor.moveToFirst();
        while (!cursor.isAfterLast()) {
            long id = cursor.getLong(idColumnIndex);
            String fileName = getTitleFromCursor(cursor);
            manager.remove(id);

            // manager.remove does not remove downloaded file.
            if (!TextUtils.isEmpty(fileName)) {
                File localFile = new File(DOWNLOAD_DIRECTORY, fileName);
                if (localFile.exists()) {
                    localFile.delete();
                }
            }

            cursor.moveToNext();
        }
        cursor.close();
    }

    /**
     * Retrieve the title of the download from a DownloadManager cursor, the title should correspond
     * to the filename of the downloaded file, unless the title has been set explicitly.
     */
    private String getTitleFromCursor(Cursor cursor) {
        return cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_TITLE));
    }

    private String mLastDownloadFilePath;
    private CallbackHelper mHttpDownloadFinished = new CallbackHelper();
    private TestDownloadManagerServiceObserver mDownloadManagerServiceObserver;

    public int getChromeDownloadCallCount() {
        return mHttpDownloadFinished.getCallCount();
    }

    protected void resetCallbackHelper() {
        mHttpDownloadFinished = new CallbackHelper();
    }

    public boolean waitForChromeDownloadToFinish(int currentCallCount) {
        boolean eventReceived = true;
        try {
            mHttpDownloadFinished.waitForCallback(currentCallCount, 1, 10, TimeUnit.SECONDS);
        } catch (TimeoutException e) {
            eventReceived = false;
        }
        return eventReceived;
    }

    public List<DownloadItem> getAllDownloads() {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    DownloadManagerService.getDownloadManagerService().getAllDownloads(null);
                });
        return mAllDownloads;
    }

    private class TestDownloadManagerServiceObserver
            implements DownloadManagerService.DownloadObserver {
        @Override
        public void onAllDownloadsRetrieved(final List<DownloadItem> list, ProfileKey profileKey) {
            mAllDownloads = list;
        }

        @Override
        public void onDownloadItemCreated(DownloadItem item) {}

        @Override
        public void onDownloadItemRemoved(String guid) {}

        @Override
        public void onAddOrReplaceDownloadSharedPreferenceEntry(ContentId id) {}

        @Override
        public void onDownloadItemUpdated(DownloadItem item) {}

        @Override
        public void broadcastDownloadSuccessful(DownloadInfo downloadInfo) {
            mLastDownloadFilePath = downloadInfo.getFilePath();
            mHttpDownloadFinished.notifyCalled();
        }
    }

    private class TestDownloadBackendObserver implements OfflineContentProvider.Observer {
        @Override
        public void onItemsAdded(List<OfflineItem> items) {}

        @Override
        public void onItemRemoved(ContentId id) {}

        @Override
        public void onItemUpdated(OfflineItem item, UpdateDelta updateDelta) {
            if (item.state == OfflineItemState.COMPLETE) {
                mLastDownloadFilePath = item.filePath;
                mHttpDownloadFinished.notifyCalled();
            }
        }
    }

    @Override
    protected void before() throws Throwable {
        super.before();
        mActivityStart.customMainActivityStart();

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    DownloadDialogBridge.setPromptForDownloadAndroid(
                            getActivity().getProfileProviderSupplier().get().getOriginalProfile(),
                            DownloadPromptStatus.DONT_SHOW);
                });

        cleanUpAllDownloads();

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mDownloadManagerServiceObserver = new TestDownloadManagerServiceObserver();
                    DownloadManagerService.getDownloadManagerService()
                            .addDownloadObserver(mDownloadManagerServiceObserver);
                    OfflineContentAggregatorFactory.get()
                            .addObserver(new TestDownloadBackendObserver());
                });
    }

    @Override
    protected void after() {
        cleanUpAllDownloads();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    DownloadManagerService.getDownloadManagerService()
                            .removeDownloadObserver(mDownloadManagerServiceObserver);
                });
        super.after();
    }

    public void deleteFilesInDownloadDirectory(String... filenames) {
        for (String filename : filenames) deleteFile(filename);
    }

    private void deleteFile(String fileName) {
        // Delete file path on pre Q.
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
            final File fileToDelete = new File(DOWNLOAD_DIRECTORY, fileName);
            if (fileToDelete.exists()) {
                Assert.assertTrue(
                        "Could not delete file that would block this test", fileToDelete.delete());
            }
            return;
        }

        // Delete content URI starting from Q.
        Uri uri = DownloadCollectionBridge.getDownloadUriForFileName(fileName);
        if (uri == null) {
            Log.e(TAG, "Can't find URI of file for deletion: %s on Android P+.", fileName);
            return;
        }
        DownloadCollectionBridge.deleteIntermediateUri(uri.toString());
    }

    /**
     * Interface for Download tests to define actions that starts the activity.
     *
     * <p>This method will be called in DownloadTestRule's setUp process, which means it would
     * happen before Test class' own setUp() call
     */
    public interface CustomMainActivityStart {
        void customMainActivityStart() throws InterruptedException;
    }
}