<file_sep>package com.android.providers.downloads;

import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import android.app.job.JobParameters;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.net.NetworkInfo.DetailedState;
import android.os.Bundle;

import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.List;

import javax.net.ssl.SSLContext;

public class FakeSystemFacade implements SystemFacade {
    long mTimeMillis = 0;
    Integer mActiveNetworkType = ConnectivityManager.TYPE_WIFI;
    boolean mIsRoaming = false;
    boolean mIsMetered = false;
    long mMaxBytesOverMobile = Long.MAX_VALUE;
    long mRecommendedMaxBytesOverMobile = Long.MAX_VALUE;
    List<Intent> mBroadcastsSent = new ArrayList<Intent>();
    Bundle mLastBroadcastOptions;
    boolean mCleartextTrafficPermitted = true;
    private boolean mReturnActualTime = false;
    private SSLContext mSSLContext = null;

    public void setUp() {
        mTimeMillis = 0;
        mActiveNetworkType = ConnectivityManager.TYPE_WIFI;
        mIsRoaming = false;
        mIsMetered = false;
        mMaxBytesOverMobile = Long.MAX_VALUE;
        mRecommendedMaxBytesOverMobile = Long.MAX_VALUE;
        mBroadcastsSent.clear();
        mLastBroadcastOptions = null;
        mReturnActualTime = false;
        try {
            mSSLContext = SSLContext.getDefault();
        } catch (GeneralSecurityException e) {
            throw new RuntimeException(e);
        }
    }

    void incrementTimeMillis(long delta) {
        mTimeMillis += delta;
    }

    @Override
    public long currentTimeMillis() {
        if (mReturnActualTime) {
            return System.currentTimeMillis();
        }
        return mTimeMillis;
    }

    @Override
    public Network getNetwork(JobParameters params) {
        if (mActiveNetworkType == null) {
            return null;
        } else {
            final Network network = mock(Network.class);
            try {
                when(network.openConnection(any())).then(new Answer<URLConnection>() {
                    @Override
                    public URLConnection answer(InvocationOnMock invocation) throws Throwable {
                        final URL url = (URL) invocation.getArguments()[0];
                        return url.openConnection();
                    }
                });
            } catch (IOException ignored) {
            }
            return network;
        }
    }

    @Override
    public NetworkCapabilities getNetworkCapabilities(Network network) {
        if (mActiveNetworkType == null) {
            return null;
        } else {
            final NetworkCapabilities.Builder builder = new NetworkCapabilities.Builder();
            if (!mIsMetered) builder.addCapability(NET_CAPABILITY_NOT_METERED);
            if (!mIsRoaming) builder.addCapability(NET_CAPABILITY_NOT_ROAMING);
            return builder.build();
        }
    }

    @Override
    public long getMaxBytesOverMobile() {
        return mMaxBytesOverMobile;
    }

    @Override
    public long getRecommendedMaxBytesOverMobile() {
        return mRecommendedMaxBytesOverMobile;
    }

    @Override
    public void sendBroadcast(Intent intent) {
        mBroadcastsSent.add(intent);
        mLastBroadcastOptions = null;
    }

    @Override
    public void sendBroadcast(Intent intent, String receiverPermission, Bundle options) {
        mBroadcastsSent.add(intent);
        mLastBroadcastOptions = options;
    }

    @Override
    public boolean userOwnsPackage(int uid, String pckg) throws NameNotFoundException {
        return true;
    }

    @Override
    public boolean isCleartextTrafficPermitted(String packageName, String hostname) {
        return mCleartextTrafficPermitted;
    }

    @Override
    public SSLContext getSSLContextForPackage(Context context, String pckg) {
        return mSSLContext;
    }

    public void setSSLContext(SSLContext context) {
        mSSLContext = context;
    }

    public void setReturnActualTime(boolean flag) {
        mReturnActualTime = flag;
    }
}
<file_sep>/*
 * Copyright (C) 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.providers.downloads;

import static android.provider.BaseColumns._ID;
import static android.provider.Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI;
import static android.provider.Downloads.Impl.COLUMN_DESTINATION;
import static android.provider.Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI;
import static android.provider.Downloads.Impl.COLUMN_MEDIASTORE_URI;
import static android.provider.Downloads.Impl.COLUMN_MEDIA_SCANNED;
import static android.provider.Downloads.Impl.DESTINATION_EXTERNAL;
import static android.provider.Downloads.Impl.DESTINATION_FILE_URI;
import static android.provider.Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD;
import static android.provider.Downloads.Impl.MEDIA_SCANNED;
import static android.provider.Downloads.Impl._DATA;

import static com.android.providers.downloads.Constants.MEDIA_SCAN_TRIGGER_JOB_ID;

import android.app.job.JobInfo;
import android.app.job.JobParameters;
import android.app.job.JobScheduler;
import android.app.job.JobService;
import android.content.ComponentName;
import android.content.ContentProviderClient;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Environment;
import android.provider.Downloads;
import android.provider.MediaStore;
import android.util.Log;

import java.io.File;

/**
 * Job to update MediaProvider with all the downloads and force mediascan on them.
 */
public class MediaScanTriggerJob extends JobService {
    private volatile boolean mJobStopped;

    @Override
    public boolean onStartJob(JobParameters parameters) {
        if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            // External storage is not available yet, so defer this job to a later time.
            jobFinished(parameters, true /* reschedule */);
            return false;
        }
        Helpers.getAsyncHandler().post(() -> {
            final String selection = _DATA + " IS NOT NULL"
                    + " AND (" + COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI + "=1"
                    + " OR " + COLUMN_MEDIA_SCANNED + "=" + MEDIA_SCANNED + ")"
                    + " AND (" + COLUMN_DESTINATION + "=" + DESTINATION_EXTERNAL
                    + " OR " + COLUMN_DESTINATION + "=" + DESTINATION_FILE_URI
                    + " OR " + COLUMN_DESTINATION + "=" + DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD
                    + ")";
            try (ContentProviderClient cpc
                    = getContentResolver().acquireContentProviderClient(Downloads.Impl.AUTHORITY);
                 ContentProviderClient mediaProviderClient
                    = getContentResolver().acquireContentProviderClient(MediaStore.AUTHORITY)) {
                final DownloadProvider downloadProvider
                        = ((DownloadProvider) cpc.getLocalContentProvider());
                try (Cursor cursor = downloadProvider.query(ALL_DOWNLOADS_CONTENT_URI,
                        null, selection, null, null)) {

                    final DownloadInfo.Reader reader
                            = new DownloadInfo.Reader(getContentResolver(), cursor);
                    final DownloadInfo info = new DownloadInfo(MediaScanTriggerJob.this);
                    while (cursor.moveToNext()) {
                        if (mJobStopped) {
                            return;
                        }
                        reader.updateFromDatabase(info);
                        // This indicates that this entry has been handled already (perhaps when
                        // this job ran earlier and got preempted), so skip.
                        if (info.mMediaStoreUri != null) {
                            continue;
                        }
                        final ContentValues mediaValues;
                        try {
                            mediaValues = downloadProvider.convertToMediaProviderValues(info);
                        } catch (IllegalArgumentException e) {
                            Log.e(Constants.TAG,
                                    "Error getting media content values from " + info, e);
                            continue;
                        }
                        // Overriding size column value to 0 for forcing the mediascan
                        // later (to address http://b/138419471).
                        mediaValues.put(MediaStore.Files.FileColumns.SIZE, 0);
                        downloadProvider.updateMediaProvider(mediaProviderClient, mediaValues);

                        final Uri mediaStoreUri = Helpers.triggerMediaScan(mediaProviderClient,
                                new File(info.mFileName));
                        if (mediaStoreUri != null) {
                            final ContentValues downloadValues = new ContentValues();
                            downloadValues.put(COLUMN_MEDIASTORE_URI, mediaStoreUri.toString());
                            downloadProvider.update(ALL_DOWNLOADS_CONTENT_URI,
                                    downloadValues, _ID + "=" + info.mId, null);
                        }
                    }
                }
            }
            jobFinished(parameters, false /* reschedule */);
        });
        return true;
    }

    @Override
    public boolean onStopJob(JobParameters parameters) {
        mJobStopped = true;
        return true;
    }

    public static void schedule(Context context) {
        final JobScheduler scheduler = context.getSystemService(JobScheduler.class);
        final JobInfo job = new JobInfo.Builder(MEDIA_SCAN_TRIGGER_JOB_ID,
                new ComponentName(context, MediaScanTriggerJob.class))
                        .setRequiresCharging(true)
                        .setRequiresDeviceIdle(true)
                        .build();
        scheduler.schedule(job);
    }
}
<file_sep>/*
 * Copyright (C) 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.providers.downloads;

import android.content.ContentUris;
import android.net.Uri;
import android.provider.MediaStore;

public class MediaStoreDownloadsHelper {

    private static final String MEDIASTORE_DOWNLOAD_FILE_PREFIX = "msf:";
    private static final String MEDIASTORE_DOWNLOAD_DIR_PREFIX = "msd:";

    public static String getDocIdForMediaStoreDownload(long id, boolean isDir) {
        return (isDir ? MEDIASTORE_DOWNLOAD_DIR_PREFIX : MEDIASTORE_DOWNLOAD_FILE_PREFIX) + id;
    }

    public static boolean isMediaStoreDownload(String docId) {
        return docId != null && (docId.startsWith(MEDIASTORE_DOWNLOAD_FILE_PREFIX)
                || docId.startsWith(MEDIASTORE_DOWNLOAD_DIR_PREFIX));
    }

    public static long getMediaStoreId(String docId) {
        return Long.parseLong(getMediaStoreIdString(docId));
    }


    public static String getMediaStoreIdString(String docId) {
        final int index = docId.indexOf(":");
        return docId.substring(index + 1);
    }

    public static boolean isMediaStoreDownloadDir(String docId) {
        return docId != null && docId.startsWith(MEDIASTORE_DOWNLOAD_DIR_PREFIX);
    }

    /**
     * The returned uri always appends external volume {@link MediaStore#VOLUME_EXTERNAL}.
     * It doesn't consider the item is located on second volume. It can't be used to update
     * or insert.
     * @param docId the doc id
     * @return external uri for query
     */
    public static Uri getMediaStoreUriForQuery(String docId) {
        return getMediaStoreUri(MediaStore.VOLUME_EXTERNAL, docId);
    }

    public static Uri getMediaStoreUri(String volume, String docId) {
        return MediaStore.Downloads.getContentUri(volume, getMediaStoreId(docId));
    }
}
<file_sep>/*
 * Copyright (C) 2012 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.providers.downloads;

import static android.app.DownloadManager.COLUMN_LOCAL_FILENAME;
import static android.app.DownloadManager.COLUMN_MEDIA_TYPE;
import static android.app.DownloadManager.COLUMN_URI;
import static android.provider.Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI;

import static com.android.providers.downloads.Constants.TAG;

import android.app.DownloadManager;
import android.content.ActivityNotFoundException;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.Process;
import android.provider.DocumentsContract;
import android.provider.Downloads.Impl.RequestHeaders;
import android.provider.MediaStore;
import android.util.Log;

import java.io.File;

public class OpenHelper {
    /**
     * Build and start an {@link Intent} to view the download with given ID,
     * handling subtleties around installing packages.
     */
    public static boolean startViewIntent(Context context, long id, int intentFlags) {
        final Intent intent = OpenHelper.buildViewIntent(context, id);
        if (intent == null) {
            Log.w(TAG, "No intent built for " + id);
            return false;
        }

        intent.addFlags(intentFlags);
        return startViewIntent(context, intent);
    }

    public static boolean startViewIntent(Context context, Intent intent) {
        try {
            context.startActivity(intent);
            return true;
        } catch (ActivityNotFoundException e) {
            Log.w(TAG, "Failed to start " + intent + ": " + e);
            return false;
        }
    }

    public static Intent buildViewIntentForMediaStoreDownload(Context context,
            Uri documentUri) {
        final long mediaStoreId = MediaStoreDownloadsHelper.getMediaStoreId(
                DocumentsContract.getDocumentId(documentUri));
        final Uri queryUri = ContentUris.withAppendedId(
                MediaStore.Downloads.EXTERNAL_CONTENT_URI, mediaStoreId);
        try (Cursor cursor = context.getContentResolver().query(
                queryUri, null, null, null)) {
            if (cursor.moveToFirst()) {
                final String mimeType = cursor.getString(
                        cursor.getColumnIndex(MediaStore.Downloads.MIME_TYPE));

                final Intent intent = new Intent(Intent.ACTION_VIEW);
                intent.setDataAndType(documentUri, mimeType);
                intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
                        | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

                if ("application/vnd.android.package-archive".equals(mimeType)) {
                    // Also splice in details about where it came from
                    intent.putExtra(Intent.EXTRA_ORIGINATING_URI,
                            getCursorUri(cursor, MediaStore.Downloads.DOWNLOAD_URI));
                    intent.putExtra(Intent.EXTRA_REFERRER,
                            getCursorUri(cursor, MediaStore.Downloads.REFERER_URI));
                    final String ownerPackageName = getCursorString(cursor,
                            MediaStore.Downloads.OWNER_PACKAGE_NAME);
                    final int ownerUid = getPackageUid(context, ownerPackageName);
                    if (ownerUid > 0) {
                        intent.putExtra(Intent.EXTRA_ORIGINATING_UID, ownerUid);
                    }
                }
                return intent;
            }
        }
        return null;
    }

    /**
     * Build an {@link Intent} to view the download with given ID, handling
     * subtleties around installing packages.
     */
    private static Intent buildViewIntent(Context context, long id) {
        final DownloadManager downManager = (DownloadManager) context.getSystemService(
                Context.DOWNLOAD_SERVICE);
        downManager.setAccessAllDownloads(true);
        downManager.setAccessFilename(true);

        final Cursor cursor = downManager.query(new DownloadManager.Query().setFilterById(id));
        try {
            if (!cursor.moveToFirst()) {
                return null;
            }

            final File file = getCursorFile(cursor, COLUMN_LOCAL_FILENAME);
            String mimeType = getCursorString(cursor, COLUMN_MEDIA_TYPE);
            mimeType = DownloadDrmHelper.getOriginalMimeType(context, file, mimeType);

            final Uri documentUri = DocumentsContract.buildDocumentUri(
                    Constants.STORAGE_AUTHORITY, String.valueOf(id));

            final Intent intent = new Intent(Intent.ACTION_VIEW);
            intent.setDataAndType(documentUri, mimeType);
            intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
                    | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

            if ("application/vnd.android.package-archive".equals(mimeType)) {
                // Also splice in details about where it came from
                final Uri remoteUri = getCursorUri(cursor, COLUMN_URI);
                intent.putExtra(Intent.EXTRA_ORIGINATING_URI, remoteUri);
                intent.putExtra(Intent.EXTRA_REFERRER, getRefererUri(context, id));
                intent.putExtra(Intent.EXTRA_ORIGINATING_UID, getOriginatingUid(context, id));
            }

            return intent;
        } finally {
            cursor.close();
        }
    }

    private static Uri getRefererUri(Context context, long id) {
        final Uri headersUri = Uri.withAppendedPath(
                ContentUris.withAppendedId(ALL_DOWNLOADS_CONTENT_URI, id),
                RequestHeaders.URI_SEGMENT);
        final Cursor headers = context.getContentResolver()
                .query(headersUri, null, null, null, null);
        try {
            while (headers.moveToNext()) {
                final String header = getCursorString(headers, RequestHeaders.COLUMN_HEADER);
                if ("Referer".equalsIgnoreCase(header)) {
                    return getCursorUri(headers, RequestHeaders.COLUMN_VALUE);
                }
            }
        } finally {
            headers.close();
        }
        return null;
    }

    private static int getOriginatingUid(Context context, long id) {
        final Uri uri = ContentUris.withAppendedId(ALL_DOWNLOADS_CONTENT_URI, id);
        final Cursor cursor = context.getContentResolver().query(uri, new String[]{Constants.UID},
                null, null, null);
        if (cursor != null) {
            try {
                if (cursor.moveToFirst()) {
                    final int uid = cursor.getInt(cursor.getColumnIndexOrThrow(Constants.UID));
                    if (uid != Process.myUid()) {
                        return uid;
                    }
                }
            } finally {
                cursor.close();
            }
        }
        return PackageInstaller.SessionParams.UID_UNKNOWN;
    }

    private static int getPackageUid(Context context, String packageName) {
        if (packageName == null) {
            return -1;
        }
        try {
            return context.getPackageManager().getPackageUid(packageName, 0);
        } catch (PackageManager.NameNotFoundException e) {
            Log.e(TAG, "Couldn't get uid for " + packageName, e);
            return -1;
        }
    }

    private static String getCursorString(Cursor cursor, String column) {
        return cursor.getString(cursor.getColumnIndexOrThrow(column));
    }

    private static Uri getCursorUri(Cursor cursor, String column) {
        final String uriString = cursor.getString(cursor.getColumnIndexOrThrow(column));
        return uriString == null ? null : Uri.parse(uriString);
    }

    private static File getCursorFile(Cursor cursor, String column) {
        return new File(cursor.getString(cursor.getColumnIndexOrThrow(column)));
    }
}
<file_sep>/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.providers.downloads.ui;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.app.DownloadManager;
import android.app.DownloadManager.Query;
import android.app.FragmentManager;
import android.content.ContentUris;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.DocumentsContract;
import android.util.Log;
import android.widget.Toast;

import com.android.providers.downloads.Constants;
import com.android.providers.downloads.MediaStoreDownloadsHelper;
import com.android.providers.downloads.OpenHelper;
import com.android.providers.downloads.RawDocumentsHelper;

import libcore.io.IoUtils;

/**
 * Intercept all download clicks to provide special behavior. For example,
 * PackageInstaller really wants raw file paths.
 */
public class TrampolineActivity extends Activity {
    private static final String TAG_PAUSED = "paused";
    private static final String TAG_FAILED = "failed";

    private static final String KEY_ID = "id";
    private static final String KEY_REASON = "reason";
    private static final String KEY_SIZE = "size";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Uri documentUri = getIntent().getData();
        if (RawDocumentsHelper.isRawDocId(DocumentsContract.getDocumentId(documentUri))) {
            if (!RawDocumentsHelper.startViewIntent(this, documentUri)) {
                Toast.makeText(this, R.string.download_no_application_title, Toast.LENGTH_SHORT)
                        .show();
            }
            finish();
            return;
        }

        if (MediaStoreDownloadsHelper.isMediaStoreDownload(
                DocumentsContract.getDocumentId(documentUri))) {
            final Intent intent = OpenHelper.buildViewIntentForMediaStoreDownload(
                    this, documentUri);
            if (intent == null || !OpenHelper.startViewIntent(this, intent)) {
                Toast.makeText(this, R.string.download_no_application_title, Toast.LENGTH_SHORT)
                        .show();
            }
            finish();
            return;
        }

        final long id = ContentUris.parseId(documentUri);
        final DownloadManager dm = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
        dm.setAccessAllDownloads(true);

        final int status;
        final int reason;
        final long size;

        final Cursor cursor = dm.query(new Query().setFilterById(id));
        try {
            if (cursor.moveToFirst()) {
                status = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS));
                reason = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_REASON));
                size = cursor.getLong(
                        cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
            } else {
                Toast.makeText(this, R.string.dialog_file_missing_body, Toast.LENGTH_SHORT).show();
                finish();
                return;
            }
        } finally {
            IoUtils.closeQuietly(cursor);
        }

        Log.d(Constants.TAG, "Found " + id + " with status " + status + ", reason " + reason);
        switch (status) {
            case DownloadManager.STATUS_PENDING:
            case DownloadManager.STATUS_RUNNING:
                sendRunningDownloadClickedBroadcast(id);
                finish();
                break;

            case DownloadManager.STATUS_PAUSED:
                if (reason == DownloadManager.PAUSED_QUEUED_FOR_WIFI) {
                    PausedDialogFragment.show(getFragmentManager(), id, size);
                } else {
                    sendRunningDownloadClickedBroadcast(id);
                    finish();
                }
                break;

            case DownloadManager.STATUS_SUCCESSFUL:
                if (!OpenHelper.startViewIntent(this, id, 0)) {
                    Toast.makeText(this, R.string.download_no_application_title, Toast.LENGTH_SHORT)
                            .show();
                }
                finish();
                break;

            case DownloadManager.STATUS_FAILED:
                FailedDialogFragment.show(getFragmentManager(), id, reason);
                break;
        }
    }

    private void sendRunningDownloadClickedBroadcast(long id) {
        final Intent intent = new Intent(Constants.ACTION_LIST);
        intent.setPackage(Constants.PROVIDER_PACKAGE_NAME);
        intent.putExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS, new long[] { id });
        sendBroadcast(intent);
    }

    public static class PausedDialogFragment extends DialogFragment {
        public static void show(FragmentManager fm, long id, long size) {
            final PausedDialogFragment dialog = new PausedDialogFragment();
            final Bundle args = new Bundle();
            args.putLong(KEY_ID, id);
            args.putLong(KEY_SIZE, size);
            dialog.setArguments(args);
            dialog.show(fm, TAG_PAUSED);
        }

        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            final Context context = getActivity();

            final DownloadManager dm = (DownloadManager) context.getSystemService(
                    Context.DOWNLOAD_SERVICE);
            dm.setAccessAllDownloads(true);

            final long id = getArguments().getLong(KEY_ID);
            final long size = getArguments().getLong(KEY_SIZE);

            final AlertDialog.Builder builder = new AlertDialog.Builder(
                    context, android.R.style.Theme_DeviceDefault_Light_Dialog_Alert);
            builder.setTitle(R.string.dialog_title_queued_body);
            builder.setMessage(R.string.dialog_queued_body);

            final Long maxSize = DownloadManager.getMaxBytesOverMobile(context);
            if (maxSize != null && size > maxSize) {
                // When we have a max size, we have no choice
                builder.setPositiveButton(R.string.keep_queued_download, null);
            } else {
                // Give user the choice of starting now
                builder.setPositiveButton(R.string.start_now_download,
                        new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                dm.forceDownload(id);
                            }
                        });
            }

            builder.setNegativeButton(
                    R.string.remove_download, new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            dm.remove(id);
                        }
                    });

            return builder.create();
        }

        @Override
        public void onDismiss(DialogInterface dialog) {
            super.onDismiss(dialog);
            final Activity activity = getActivity();
            if (activity != null) {
                activity.finish();
            }
        }
    }

    public static class FailedDialogFragment extends DialogFragment {
        public static void show(FragmentManager fm, long id, int reason) {
            final FailedDialogFragment dialog = new FailedDialogFragment();
            final Bundle args = new Bundle();
            args.putLong(KEY_ID, id);
            args.putInt(KEY_REASON, reason);
            dialog.setArguments(args);
            dialog.show(fm, TAG_FAILED);
        }

        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            final Context context = getActivity();

            final DownloadManager dm = (DownloadManager) context.getSystemService(
                    Context.DOWNLOAD_SERVICE);
            dm.setAccessAllDownloads(true);

            final long id = getArguments().getLong(KEY_ID);
            final int reason = getArguments().getInt(KEY_REASON);

            final AlertDialog.Builder builder = new AlertDialog.Builder(
                    context, android.R.style.Theme_DeviceDefault_Light_Dialog_Alert);
            builder.setTitle(R.string.dialog_title_not_available);

            switch (reason) {
                case DownloadManager.ERROR_FILE_ALREADY_EXISTS:
                    builder.setMessage(R.string.dialog_file_already_exists);
                    break;
                case DownloadManager.ERROR_INSUFFICIENT_SPACE:
                    builder.setMessage(R.string.dialog_insufficient_space_on_cache);
                    break;
                case DownloadManager.ERROR_DEVICE_NOT_FOUND:
                    builder.setMessage(R.string.dialog_media_not_found);
                    break;
                case DownloadManager.ERROR_CANNOT_RESUME:
                    builder.setMessage(R.string.dialog_cannot_resume);
                    break;
                default:
                    builder.setMessage(R.string.dialog_failed_body);
            }

            builder.setNegativeButton(
                    R.string.delete_download, new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            dm.remove(id);
                        }
                    });

            builder.setPositiveButton(
                    R.string.retry_download, new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            dm.restartDownload(id);
                        }
                    });

            return builder.create();
        }

        @Override
        public void onDismiss(DialogInterface dialog) {
            super.onDismiss(dialog);
            final Activity activity = getActivity();
            if (activity != null) {
                activity.finish();
            }
        }
    }
}
<file_sep>/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.providers.downloads;

import static com.android.providers.downloads.MediaStoreDownloadsHelper.getDocIdForMediaStoreDownload;
import static com.android.providers.downloads.MediaStoreDownloadsHelper.getMediaStoreIdString;
import static com.android.providers.downloads.MediaStoreDownloadsHelper.getMediaStoreUriForQuery;
import static com.android.providers.downloads.MediaStoreDownloadsHelper.isMediaStoreDownload;
import static com.android.providers.downloads.MediaStoreDownloadsHelper.isMediaStoreDownloadDir;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.DownloadManager;
import android.app.DownloadManager.Query;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.content.UriPermission;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.MatrixCursor.RowBuilder;
import android.media.MediaFile;
import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.Environment;
import android.os.FileObserver;
import android.os.FileUtils;
import android.os.ParcelFileDescriptor;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsContract.Path;
import android.provider.DocumentsContract.Root;
import android.provider.Downloads;
import android.provider.MediaStore;
import android.provider.MediaStore.DownloadColumns;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.content.FileSystemProvider;

import libcore.io.IoUtils;

import java.io.File;
import java.io.FileNotFoundException;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;

/**
 * Presents files located in {@link Environment#DIRECTORY_DOWNLOADS} and contents from
 * {@link DownloadManager}. {@link DownloadManager} contents include active downloads and completed
 * downloads added by other applications using
 * {@link DownloadManager#addCompletedDownload(String, String, boolean, String, String, long, boolean, boolean, Uri, Uri)}
 * .
 */
public class DownloadStorageProvider extends FileSystemProvider {
    private static final String TAG = "DownloadStorageProvider";
    private static final boolean DEBUG = false;

    private static final String AUTHORITY = Constants.STORAGE_AUTHORITY;
    private static final String DOC_ID_ROOT = Constants.STORAGE_ROOT_ID;

    private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
            Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON,
            Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID, Root.COLUMN_QUERY_ARGS
    };

    private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
            Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME,
            Document.COLUMN_SUMMARY, Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS,
            Document.COLUMN_SIZE,
    };

    private DownloadManager mDm;

    private static final int NO_LIMIT = -1;

    @Override
    public boolean onCreate() {
        super.onCreate(DEFAULT_DOCUMENT_PROJECTION);
        mDm = (DownloadManager) getContext().getSystemService(Context.DOWNLOAD_SERVICE);
        mDm.setAccessAllDownloads(true);
        mDm.setAccessFilename(true);

        return true;
    }

    private static String[] resolveRootProjection(String[] projection) {
        return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
    }

    private static String[] resolveDocumentProjection(String[] projection) {
        return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION;
    }

    private void copyNotificationUri(@NonNull MatrixCursor result, @NonNull Cursor cursor) {
        final List<Uri> notifyUris = cursor.getNotificationUris();
        if (notifyUris != null) {
            result.setNotificationUris(getContext().getContentResolver(), notifyUris);
        }
    }

    /**
     * Called by {@link DownloadProvider} when deleting a row in the {@link DownloadManager}
     * database.
     */
    static void onDownloadProviderDelete(Context context, long id) {
        final Uri uri = DocumentsContract.buildDocumentUri(AUTHORITY, Long.toString(id));
        context.revokeUriPermission(uri, ~0);
    }

    static void onMediaProviderDownloadsDelete(Context context, long[] ids, String[] mimeTypes) {
        for (int i = 0; i < ids.length; ++i) {
            final boolean isDir = mimeTypes[i] == null;
            final Uri uri = DocumentsContract.buildDocumentUri(AUTHORITY,
                    MediaStoreDownloadsHelper.getDocIdForMediaStoreDownload(ids[i], isDir));
            context.revokeUriPermission(uri, ~0);
        }
    }

    static void revokeAllMediaStoreUriPermissions(Context context) {
        final List<UriPermission> uriPermissions =
                context.getContentResolver().getOutgoingUriPermissions();
        final int size = uriPermissions.size();
        final StringBuilder sb = new StringBuilder("Revoking permissions for uris: ");
        for (int i = 0; i < size; ++i) {
            final Uri uri = uriPermissions.get(i).getUri();
            if (AUTHORITY.equals(uri.getAuthority())
                    && isMediaStoreDownload(DocumentsContract.getDocumentId(uri))) {
                context.revokeUriPermission(uri, ~0);
                sb.append(uri + ",");
            }
        }
        Log.d(TAG, sb.toString());
    }

    @Override
    public Cursor queryRoots(String[] projection) throws FileNotFoundException {
        // It's possible that the folder does not exist on disk, so we will create the folder if
        // that is the case. If user decides to delete the folder later, then it's OK to fail on
        // subsequent queries.
        getPublicDownloadsDirectory().mkdirs();

        final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
        final RowBuilder row = result.newRow();
        row.add(Root.COLUMN_ROOT_ID, DOC_ID_ROOT);
        row.add(Root.COLUMN_FLAGS, Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_RECENTS
                | Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_SEARCH);
        row.add(Root.COLUMN_ICON, R.mipmap.ic_launcher_download);
        row.add(Root.COLUMN_TITLE, getContext().getString(R.string.root_downloads));
        row.add(Root.COLUMN_DOCUMENT_ID, DOC_ID_ROOT);
        row.add(Root.COLUMN_QUERY_ARGS, SUPPORTED_QUERY_ARGS);
        return result;
    }

    @Override
    public Path findDocumentPath(@Nullable String parentDocId, String docId) throws FileNotFoundException {

        // parentDocId is null if the client is asking for the path to the root of a doc tree.
        // Don't share root information with those who shouldn't know it.
        final String rootId = (parentDocId == null) ? DOC_ID_ROOT : null;

        if (parentDocId == null) {
            parentDocId = DOC_ID_ROOT;
        }

        final File parent = getFileForDocId(parentDocId);

        final File doc = getFileForDocId(docId);

        return new Path(rootId, findDocumentPath(parent, doc));
    }

    /**
     * Calls on {@link FileSystemProvider#createDocument(String, String, String)}, and then creates
     * a new database entry in {@link DownloadManager} if it is not a raw file and not a folder.
     */
    @Override
    public String createDocument(String parentDocId, String mimeType, String displayName)
            throws FileNotFoundException {
        // Delegate to real provider
        final long token = Binder.clearCallingIdentity();
        try {
            String newDocumentId = super.createDocument(parentDocId, mimeType, displayName);
            if (!Document.MIME_TYPE_DIR.equals(mimeType)
                    && !RawDocumentsHelper.isRawDocId(parentDocId)
                    && !isMediaStoreDownload(parentDocId)) {
                File newFile = getFileForDocId(newDocumentId);
                newDocumentId = Long.toString(mDm.addCompletedDownload(
                        newFile.getName(), newFile.getName(), true, mimeType,
                        newFile.getAbsolutePath(), 0L,
                        false, true));
            }
            return newDocumentId;
        } finally {
            Binder.restoreCallingIdentity(token);
        }
    }

    @Override
    public void deleteDocument(String docId) throws FileNotFoundException {
        // Delegate to real provider
        final long token = Binder.clearCallingIdentity();
        try {
            if (RawDocumentsHelper.isRawDocId(docId) || isMediaStoreDownload(docId)) {
                super.deleteDocument(docId);
                return;
            }

            if (mDm.remove(Long.parseLong(docId)) != 1) {
                throw new IllegalStateException("Failed to delete " + docId);
            }
        } finally {
            Binder.restoreCallingIdentity(token);
        }
    }

    @Override
    public String renameDocument(String docId, String displayName)
            throws FileNotFoundException {
        final long token = Binder.clearCallingIdentity();

        try {
            if (RawDocumentsHelper.isRawDocId(docId)
                    || isMediaStoreDownloadDir(docId)) {
                return super.renameDocument(docId, displayName);
            }

            displayName = FileUtils.buildValidFatFilename(displayName);
            if (isMediaStoreDownload(docId)) {
                return renameMediaStoreDownload(docId, displayName);
            } else {
                final long id = Long.parseLong(docId);
                if (!mDm.rename(getContext(), id, displayName)) {
                    throw new IllegalStateException(
                            "Failed to rename to " + displayName + " in downloadsManager");
                }
            }
            return null;
        } finally {
            Binder.restoreCallingIdentity(token);
        }
    }

    @Override
    public Cursor queryDocument(String docId, String[] projection) throws FileNotFoundException {
        // Delegate to real provider
        final long token = Binder.clearCallingIdentity();
        Cursor cursor = null;
        try {
            if (RawDocumentsHelper.isRawDocId(docId)) {
                return super.queryDocument(docId, projection);
            }

            final DownloadsCursor result = new DownloadsCursor(projection,
                    getContext().getContentResolver());

            if (DOC_ID_ROOT.equals(docId)) {
                includeDefaultDocument(result);
            } else if (isMediaStoreDownload(docId)) {
                cursor = getContext().getContentResolver().query(getMediaStoreUriForQuery(docId),
                        null, null, null);
                copyNotificationUri(result, cursor);
                if (cursor.moveToFirst()) {
                    includeDownloadFromMediaStore(result, cursor, null /* filePaths */,
                            false /* shouldExcludeMedia */);
                }
            } else {
                cursor = mDm.query(new Query().setFilterById(Long.parseLong(docId)));
                copyNotificationUri(result, cursor);
                if (cursor.moveToFirst()) {
                    // We don't know if this queryDocument() call is from Downloads (manage)
                    // or Files. Safely assume it's Files.
                    includeDownloadFromCursor(result, cursor, null /* filePaths */,
                            null /* queryArgs */);
                }
            }
            result.start();
            return result;
        } finally {
            IoUtils.closeQuietly(cursor);
            Binder.restoreCallingIdentity(token);
        }
    }

    @Override
    public Cursor queryChildDocuments(String parentDocId, String[] projection, String sortOrder)
            throws FileNotFoundException {
        return queryChildDocuments(parentDocId, projection, sortOrder, false);
    }

    @Override
    public Cursor queryChildDocumentsForManage(
            String parentDocId, String[] projection, String sortOrder)
            throws FileNotFoundException {
        return queryChildDocuments(parentDocId, projection, sortOrder, true);
    }

    private Cursor queryChildDocuments(String parentDocId, String[] projection,
            String sortOrder, boolean manage) throws FileNotFoundException {

        // Delegate to real provider
        final long token = Binder.clearCallingIdentity();
        Cursor cursor = null;
        try {
            if (RawDocumentsHelper.isRawDocId(parentDocId)) {
                return super.queryChildDocuments(parentDocId, projection, sortOrder);
            }

            final DownloadsCursor result = new DownloadsCursor(projection,
                    getContext().getContentResolver());
            final ArrayList<Uri> notificationUris = new ArrayList<>();
            if (isMediaStoreDownloadDir(parentDocId)) {
                includeDownloadsFromMediaStore(result, null /* queryArgs */,
                        null /* filePaths */, notificationUris,
                        getMediaStoreIdString(parentDocId), NO_LIMIT, manage);
            } else {
                assert (DOC_ID_ROOT.equals(parentDocId));
                if (manage) {
                    cursor = mDm.query(
                            new DownloadManager.Query().setOnlyIncludeVisibleInDownloadsUi(true));
                } else {
                    cursor = mDm.query(
                            new DownloadManager.Query().setOnlyIncludeVisibleInDownloadsUi(true)
                                    .setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL));
                }
                final Set<String> filePaths = new HashSet<>();
                while (cursor.moveToNext()) {
                    includeDownloadFromCursor(result, cursor, filePaths, null /* queryArgs */);
                }
                notificationUris.add(cursor.getNotificationUri());
                includeDownloadsFromMediaStore(result, null /* queryArgs */,
                        filePaths, notificationUris,
                        null /* parentId */, NO_LIMIT, manage);
                includeFilesFromSharedStorage(result, filePaths, null);
            }
            result.setNotificationUris(getContext().getContentResolver(), notificationUris);
            result.start();
            return result;
        } finally {
            IoUtils.closeQuietly(cursor);
            Binder.restoreCallingIdentity(token);
        }
    }

    @Override
    public Cursor queryRecentDocuments(String rootId, String[] projection,
            @Nullable Bundle queryArgs, @Nullable CancellationSignal signal)
            throws FileNotFoundException {
        final DownloadsCursor result =
                new DownloadsCursor(projection, getContext().getContentResolver());

        // Delegate to real provider
        final long token = Binder.clearCallingIdentity();

        int limit = 12;
        if (queryArgs != null) {
            limit = queryArgs.getInt(ContentResolver.QUERY_ARG_LIMIT, -1);

            if (limit < 0) {
                // Use default value, and no QUERY_ARG* is honored.
                limit = 12;
            } else {
                // We are honoring the QUERY_ARG_LIMIT.
                Bundle extras = new Bundle();
                result.setExtras(extras);
                extras.putStringArray(ContentResolver.EXTRA_HONORED_ARGS, new String[]{
                        ContentResolver.QUERY_ARG_LIMIT
                });
            }
        }

        Cursor cursor = null;
        final ArrayList<Uri> notificationUris = new ArrayList<>();
        try {
            cursor = mDm.query(new DownloadManager.Query().setOnlyIncludeVisibleInDownloadsUi(true)
                    .setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL));
            final Set<String> filePaths = new HashSet<>();
            while (cursor.moveToNext() && result.getCount() < limit) {
                final String mimeType = cursor.getString(
                        cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIA_TYPE));
                final String uri = cursor.getString(
                        cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIAPROVIDER_URI));

                // Skip images, videos and documents that have been inserted into the MediaStore so
                // we don't duplicate them in the recent list. The audio root of
                // MediaDocumentsProvider doesn't support recent, we add it into recent list.
                if (mimeType == null || (MediaFile.isImageMimeType(mimeType)
                        || MediaFile.isVideoMimeType(mimeType) || MediaFile.isDocumentMimeType(
                        mimeType)) && !TextUtils.isEmpty(uri)) {
                    continue;
                }
                includeDownloadFromCursor(result, cursor, filePaths,
                        null /* queryArgs */);
            }
            notificationUris.add(cursor.getNotificationUri());

            // Skip media files that have been inserted into the MediaStore so we
            // don't duplicate them in the recent list.
            final Bundle args = new Bundle();
            args.putBoolean(DocumentsContract.QUERY_ARG_EXCLUDE_MEDIA, true);

            includeDownloadsFromMediaStore(result, args, filePaths,
                    notificationUris, null /* parentId */, (limit - result.getCount()),
                    false /* includePending */);
        } finally {
            IoUtils.closeQuietly(cursor);
            Binder.restoreCallingIdentity(token);
        }

        result.setNotificationUris(getContext().getContentResolver(), notificationUris);
        result.start();
        return result;
    }

    @Override
    public Cursor querySearchDocuments(String rootId, String[] projection, Bundle queryArgs)
            throws FileNotFoundException {

        final DownloadsCursor result =
                new DownloadsCursor(projection, getContext().getContentResolver());
        final ArrayList<Uri> notificationUris = new ArrayList<>();

        // Delegate to real provider
        final long token = Binder.clearCallingIdentity();
        Cursor cursor = null;
        try {
            cursor = mDm.query(new DownloadManager.Query().setOnlyIncludeVisibleInDownloadsUi(true)
                    .setFilterByString(DocumentsContract.getSearchDocumentsQuery(queryArgs)));
            final Set<String> filePaths = new HashSet<>();
            while (cursor.moveToNext()) {
                includeDownloadFromCursor(result, cursor, filePaths, queryArgs);
            }
            notificationUris.add(cursor.getNotificationUri());
            includeDownloadsFromMediaStore(result, queryArgs, filePaths,
                    notificationUris, null /* parentId */, NO_LIMIT, true /* includePending */);

            includeSearchFilesFromSharedStorage(result, projection, filePaths, queryArgs);
        } finally {
            IoUtils.closeQuietly(cursor);
            Binder.restoreCallingIdentity(token);
        }

        final String[] handledQueryArgs = DocumentsContract.getHandledQueryArguments(queryArgs);
        if (handledQueryArgs.length > 0) {
            final Bundle extras = new Bundle();
            extras.putStringArray(ContentResolver.EXTRA_HONORED_ARGS, handledQueryArgs);
            result.setExtras(extras);
        }

        result.setNotificationUris(getContext().getContentResolver(), notificationUris);
        result.start();
        return result;
    }

    private void includeSearchFilesFromSharedStorage(DownloadsCursor result,
            String[] projection, Set<String> filePaths,
            Bundle queryArgs) throws FileNotFoundException {
        final File downloadDir = getPublicDownloadsDirectory();
        try (Cursor rawFilesCursor = super.querySearchDocuments(downloadDir,
                projection, filePaths, queryArgs)) {

            final boolean shouldExcludeMedia = queryArgs.getBoolean(
                    DocumentsContract.QUERY_ARG_EXCLUDE_MEDIA, false /* defaultValue */);
            while (rawFilesCursor.moveToNext()) {
                final String mimeType = rawFilesCursor.getString(
                        rawFilesCursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE));
                // When the value of shouldExcludeMedia is true, don't add media files into
                // the result to avoid duplicated files. MediaScanner will scan the files
                // into MediaStore. If the behavior is changed, we need to add the files back.
                if (!shouldExcludeMedia || !isMediaMimeType(mimeType)) {
                    String docId = rawFilesCursor.getString(
                            rawFilesCursor.getColumnIndexOrThrow(Document.COLUMN_DOCUMENT_ID));
                    File rawFile = getFileForDocId(docId);
                    includeFileFromSharedStorage(result, rawFile);
                }
            }
        }
    }

    @Override
    public String getDocumentType(String docId) throws FileNotFoundException {
        // Delegate to real provider
        final long token = Binder.clearCallingIdentity();
        try {
            if (RawDocumentsHelper.isRawDocId(docId)) {
                return super.getDocumentType(docId);
            }

            final ContentResolver resolver = getContext().getContentResolver();
            final Uri contentUri;
            if (isMediaStoreDownload(docId)) {
                contentUri = getMediaStoreUriForQuery(docId);
            } else {
                final long id = Long.parseLong(docId);
                contentUri = mDm.getDownloadUri(id);
            }
            return resolver.getType(contentUri);
        } finally {
            Binder.restoreCallingIdentity(token);
        }
    }

    @Override
    public ParcelFileDescriptor openDocument(String docId, String mode, CancellationSignal signal)
            throws FileNotFoundException {
        // Delegate to real provider
        final long token = Binder.clearCallingIdentity();
        try {
            if (RawDocumentsHelper.isRawDocId(docId)) {
                return super.openDocument(docId, mode, signal);
            }

            final ContentResolver resolver = getContext().getContentResolver();
            final Uri contentUri;
            if (isMediaStoreDownload(docId)) {
                contentUri = getMediaStoreUriForQuery(docId);
            } else {
                final long id = Long.parseLong(docId);
                contentUri = mDm.getDownloadUri(id);
            }
            return resolver.openFileDescriptor(contentUri, mode, signal);
        } finally {
            Binder.restoreCallingIdentity(token);
        }
    }

    @Override
    protected File getFileForDocId(String docId, boolean visible) throws FileNotFoundException {
        if (RawDocumentsHelper.isRawDocId(docId)) {
            return new File(RawDocumentsHelper.getAbsoluteFilePath(docId));
        }

        if (isMediaStoreDownload(docId)) {
            return getFileForMediaStoreDownload(docId);
        }

        if (DOC_ID_ROOT.equals(docId)) {
            return getPublicDownloadsDirectory();
        }

        final long token = Binder.clearCallingIdentity();
        Cursor cursor = null;
        String localFilePath = null;
        try {
            cursor = mDm.query(new Query().setFilterById(Long.parseLong(docId)));
            if (cursor.moveToFirst()) {
                localFilePath = cursor.getString(
                        cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_FILENAME));
            }
        } finally {
            IoUtils.closeQuietly(cursor);
            Binder.restoreCallingIdentity(token);
        }

        if (localFilePath == null) {
            throw new IllegalStateException("File has no filepath. Could not be found.");
        }
        return new File(localFilePath);
    }

    @Override
    protected String getDocIdForFile(File file) throws FileNotFoundException {
        return RawDocumentsHelper.getDocIdForFile(file);
    }

    @Override
    protected Uri buildNotificationUri(String docId) {
        return DocumentsContract.buildChildDocumentsUri(AUTHORITY, docId);
    }

    private static boolean isMediaMimeType(String mimeType) {
        return MediaFile.isImageMimeType(mimeType) || MediaFile.isVideoMimeType(mimeType)
                || MediaFile.isAudioMimeType(mimeType) || MediaFile.isDocumentMimeType(mimeType);
    }

    private void includeDefaultDocument(MatrixCursor result) {
        final RowBuilder row = result.newRow();
        row.add(Document.COLUMN_DOCUMENT_ID, DOC_ID_ROOT);
        // We have the same display name as our root :)
        row.add(Document.COLUMN_DISPLAY_NAME,
                getContext().getString(R.string.root_downloads));
        row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
        row.add(Document.COLUMN_FLAGS,
                Document.FLAG_DIR_PREFERS_LAST_MODIFIED | Document.FLAG_DIR_SUPPORTS_CREATE);
    }

    /**
     * Adds the entry from the cursor to the result only if the entry is valid. That is,
     * if the file exists in the file system.
     */
    private void includeDownloadFromCursor(MatrixCursor result, Cursor cursor,
            Set<String> filePaths, Bundle queryArgs) {
        final long id = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ID));
        final String docId = String.valueOf(id);

        final String displayName = cursor.getString(
                cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TITLE));
        String summary = cursor.getString(
                cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_DESCRIPTION));
        String mimeType = cursor.getString(
                cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIA_TYPE));
        if (mimeType == null) {
            // Provide fake MIME type so it's openable
            mimeType = "vnd.android.document/file";
        }

        if (queryArgs != null) {
            final boolean shouldExcludeMedia = queryArgs.getBoolean(
                    DocumentsContract.QUERY_ARG_EXCLUDE_MEDIA, false /* defaultValue */);
            if (shouldExcludeMedia) {
                final String uri = cursor.getString(
                        cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIAPROVIDER_URI));

                // Skip media files that have been inserted into the MediaStore so we
                // don't duplicate them in the search list.
                if (isMediaMimeType(mimeType) && !TextUtils.isEmpty(uri)) {
                    return;
                }
            }
        }

        // size could be -1 which indicates that download hasn't started.
        final long size = cursor.getLong(
                cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));

        String localFilePath = cursor.getString(
                cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_FILENAME));

        int extraFlags = Document.FLAG_PARTIAL;
        final int status = cursor.getInt(
                cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS));
        switch (status) {
            case DownloadManager.STATUS_SUCCESSFUL:
                // Verify that the document still exists in external storage. This is necessary
                // because files can be deleted from the file system without their entry being
                // removed from DownloadsManager.
                if (localFilePath == null || !new File(localFilePath).exists()) {
                    return;
                }
                extraFlags = Document.FLAG_SUPPORTS_RENAME;  // only successful is non-partial
                break;
            case DownloadManager.STATUS_PAUSED:
                summary = getContext().getString(R.string.download_queued);
                break;
            case DownloadManager.STATUS_PENDING:
                summary = getContext().getString(R.string.download_queued);
                break;
            case DownloadManager.STATUS_RUNNING:
                final long progress = cursor.getLong(cursor.getColumnIndexOrThrow(
                        DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
                if (size > 0) {
                    String percent =
                            NumberFormat.getPercentInstance().format((double) progress / size);
                    summary = getContext().getString(R.string.download_running_percent, percent);
                } else {
                    summary = getContext().getString(R.string.download_running);
                }
                break;
            case DownloadManager.STATUS_FAILED:
            default:
                summary = getContext().getString(R.string.download_error);
                break;
        }

        final long lastModified = cursor.getLong(
                cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP));

        if (!DocumentsContract.matchSearchQueryArguments(queryArgs, displayName, mimeType,
                lastModified, size)) {
            return;
        }

        includeDownload(result, docId, displayName, summary, size, mimeType,
                lastModified, extraFlags, status == DownloadManager.STATUS_RUNNING);
        if (filePaths != null && localFilePath != null) {
            filePaths.add(localFilePath);
        }
    }

    private void includeDownload(MatrixCursor result,
            String docId, String displayName, String summary, long size,
            String mimeType, long lastModifiedMs, int extraFlags, boolean isPending) {

        int flags = Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_WRITE | extraFlags;
        if (mimeType.startsWith("image/")) {
            flags |= Document.FLAG_SUPPORTS_THUMBNAIL;
        }

        if (typeSupportsMetadata(mimeType)) {
            flags |= Document.FLAG_SUPPORTS_METADATA;
        }

        final RowBuilder row = result.newRow();
        row.add(Document.COLUMN_DOCUMENT_ID, docId);
        row.add(Document.COLUMN_DISPLAY_NAME, displayName);
        row.add(Document.COLUMN_SUMMARY, summary);
        row.add(Document.COLUMN_SIZE, size == -1 ? null : size);
        row.add(Document.COLUMN_MIME_TYPE, mimeType);
        row.add(Document.COLUMN_FLAGS, flags);
        // Incomplete downloads get a null timestamp.  This prevents thrashy UI when a bunch of
        // active downloads get sorted by mod time.
        if (!isPending) {
            row.add(Document.COLUMN_LAST_MODIFIED, lastModifiedMs);
        }
    }

    /**
     * Takes all the top-level files from the Downloads directory and adds them to the result.
     *
     * @param result cursor containing all documents to be returned by queryChildDocuments or
     *            queryChildDocumentsForManage.
     * @param downloadedFilePaths The absolute file paths of all the files in the result Cursor.
     * @param searchString query used to filter out unwanted results.
     */
    private void includeFilesFromSharedStorage(DownloadsCursor result,
            Set<String> downloadedFilePaths, @Nullable String searchString)
            throws FileNotFoundException {
        final File downloadsDir = getPublicDownloadsDirectory();
        // Add every file from the Downloads directory to the result cursor. Ignore files that
        // were in the supplied downloaded file paths.
        for (File file : FileUtils.listFilesOrEmpty(downloadsDir)) {
            boolean inResultsAlready = downloadedFilePaths.contains(file.getAbsolutePath());
            boolean containsQuery = searchString == null || file.getName().contains(
                    searchString);
            if (!inResultsAlready && containsQuery) {
                includeFileFromSharedStorage(result, file);
            }
        }
    }

    /**
     * Adds a file to the result cursor. It uses a combination of {@code #RAW_PREFIX} and its
     * absolute file path for its id. Directories are not to be included.
     *
     * @param result cursor containing all documents to be returned by queryChildDocuments or
     *            queryChildDocumentsForManage.
     * @param file file to be included in the result cursor.
     */
    private void includeFileFromSharedStorage(MatrixCursor result, File file)
            throws FileNotFoundException {
        includeFile(result, null, file);
    }

    private static File getPublicDownloadsDirectory() {
        return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
    }

    private String renameMediaStoreDownload(String docId, String displayName) {
        final File before = getFileForMediaStoreDownload(docId);
        final File after = new File(before.getParentFile(), displayName);

        if (after.exists()) {
            throw new IllegalStateException("Already exists " + after);
        }
        if (!before.renameTo(after)) {
            throw new IllegalStateException("Failed to rename from " + before + " to " + after);
        }

        final String noMedia = ".nomedia";
        // Scan the file to update the database
        // For file, check whether the file is renamed to .nomedia. If yes, to scan the parent
        // directory to update all files in the directory. We don't consider the case of renaming
        // .nomedia file. We don't show .nomedia file.
        if (!after.isDirectory() && displayName.toLowerCase(Locale.ROOT).endsWith(noMedia)) {
            final Uri newUri = MediaStore.scanFile(getContext().getContentResolver(),
                    after.getParentFile());
            // the file will not show in the list, return the parent docId to avoid not finding
            // the detail for the file.
            return getDocIdForMediaStoreDownloadUri(newUri, true /* isDir */);
        }
        // update the database for the old file
        MediaStore.scanFile(getContext().getContentResolver(), before);
        // Update tne database for the new file and get the new uri
        final Uri newUri = MediaStore.scanFile(getContext().getContentResolver(), after);
        return getDocIdForMediaStoreDownloadUri(newUri, after.isDirectory());
    }

    private static String getDocIdForMediaStoreDownloadUri(Uri uri, boolean isDir) {
        if (uri != null) {
            return getDocIdForMediaStoreDownload(Long.parseLong(uri.getLastPathSegment()), isDir);
        }
        return null;
    }

    private File getFileForMediaStoreDownload(String docId) {
        final Uri mediaStoreUri = getMediaStoreUriForQuery(docId);
        final long token = Binder.clearCallingIdentity();
        try (Cursor cursor = queryForSingleItem(mediaStoreUri,
                new String[] { DownloadColumns.DATA }, null, null, null)) {
            final String filePath = cursor.getString(0);
            if (filePath == null) {
                throw new IllegalStateException("Missing _data for " + mediaStoreUri);
            }
            return new File(filePath);
        } catch (FileNotFoundException e) {
            throw new IllegalStateException(e);
        } finally {
            Binder.restoreCallingIdentity(token);
        }
    }

    private Pair<String, String> getRelativePathAndDisplayNameForDownload(long id) {
        final Uri mediaStoreUri = ContentUris.withAppendedId(
                MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL), id);
        final long token = Binder.clearCallingIdentity();
        try (Cursor cursor = queryForSingleItem(mediaStoreUri,
                new String[] { DownloadColumns.RELATIVE_PATH, DownloadColumns.DISPLAY_NAME },
                null, null, null)) {
            final String relativePath = cursor.getString(0);
            final String displayName = cursor.getString(1);
            if (relativePath == null || displayName == null) {
                throw new IllegalStateException(
                        "relative_path and _display_name should not be null for " + mediaStoreUri);
            }
            return Pair.create(relativePath, displayName);
        } catch (FileNotFoundException e) {
            throw new IllegalStateException(e);
        } finally {
            Binder.restoreCallingIdentity(token);
        }
    }

    /**
     * Copied from MediaProvider.java
     *
     * Query the given {@link Uri}, expecting only a single item to be found.
     *
     * @throws FileNotFoundException if no items were found, or multiple items
     *             were found, or there was trouble reading the data.
     */
    private Cursor queryForSingleItem(Uri uri, String[] projection,
            String selection, String[] selectionArgs, CancellationSignal signal)
            throws FileNotFoundException {
        final Cursor c = getContext().getContentResolver().query(uri, projection,
                ContentResolver.createSqlQueryBundle(selection, selectionArgs, null), signal);
        if (c == null) {
            throw new FileNotFoundException("Missing cursor for " + uri);
        } else if (c.getCount() < 1) {
            IoUtils.closeQuietly(c);
            throw new FileNotFoundException("No item at " + uri);
        } else if (c.getCount() > 1) {
            IoUtils.closeQuietly(c);
            throw new FileNotFoundException("Multiple items at " + uri);
        }

        if (c.moveToFirst()) {
            return c;
        } else {
            IoUtils.closeQuietly(c);
            throw new FileNotFoundException("Failed to read row from " + uri);
        }
    }

    private void includeDownloadsFromMediaStore(@NonNull MatrixCursor result,
            @Nullable Bundle queryArgs,
            @Nullable Set<String> filePaths, @NonNull ArrayList<Uri> notificationUris,
            @Nullable String parentId, int limit, boolean includePending) {
        if (limit == 0) {
            return;
        }

        final long token = Binder.clearCallingIdentity();

        final Uri uriInner = MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL);
        final Bundle queryArgsInner = new Bundle();

        final Pair<String, String[]> selectionPair = buildSearchSelection(
                queryArgs, filePaths, parentId);
        queryArgsInner.putString(ContentResolver.QUERY_ARG_SQL_SELECTION,
                selectionPair.first);
        queryArgsInner.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS,
                selectionPair.second);
        if (limit != NO_LIMIT) {
            queryArgsInner.putInt(ContentResolver.QUERY_ARG_LIMIT, limit);
        }
        if (includePending) {
            queryArgsInner.putInt(MediaStore.QUERY_ARG_MATCH_PENDING, MediaStore.MATCH_INCLUDE);
        }

        try (Cursor cursor = getContext().getContentResolver().query(uriInner,
                null, queryArgsInner, null)) {
            final boolean shouldExcludeMedia = queryArgs != null && queryArgs.getBoolean(
                    DocumentsContract.QUERY_ARG_EXCLUDE_MEDIA, false /* defaultValue */);
            while (cursor.moveToNext()) {
                includeDownloadFromMediaStore(result, cursor, filePaths, shouldExcludeMedia);
            }
            notificationUris.add(MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL));
            notificationUris.add(MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL));
        } finally {
            Binder.restoreCallingIdentity(token);
        }
    }

    private void includeDownloadFromMediaStore(@NonNull MatrixCursor result,
            @NonNull Cursor mediaCursor, @Nullable Set<String> filePaths,
            boolean shouldExcludeMedia) {
        final String mimeType = getMimeType(mediaCursor);

        // Image, Audio and Video are excluded from buildSearchSelection in querySearchDocuments
        // and queryRecentDocuments. Only exclude document type here for both cases.
        if (shouldExcludeMedia && MediaFile.isDocumentMimeType(mimeType)) {
            return;
        }

        final boolean isDir = Document.MIME_TYPE_DIR.equals(mimeType);
        final String docId = getDocIdForMediaStoreDownload(
                mediaCursor.getLong(mediaCursor.getColumnIndex(DownloadColumns._ID)), isDir);
        final String displayName = mediaCursor.getString(
                mediaCursor.getColumnIndex(DownloadColumns.DISPLAY_NAME));
        final long size = mediaCursor.getLong(
                mediaCursor.getColumnIndex(DownloadColumns.SIZE));
        final long lastModifiedMs = mediaCursor.getLong(
                mediaCursor.getColumnIndex(DownloadColumns.DATE_MODIFIED)) * 1000;
        final boolean isPending = mediaCursor.getInt(
                mediaCursor.getColumnIndex(DownloadColumns.IS_PENDING)) == 1;

        int extraFlags = isPending ? Document.FLAG_PARTIAL : 0;
        if (Document.MIME_TYPE_DIR.equals(mimeType)) {
            extraFlags |= Document.FLAG_DIR_SUPPORTS_CREATE;
        }
        if (!isPending) {
            extraFlags |= Document.FLAG_SUPPORTS_RENAME;
        }

        includeDownload(result, docId, displayName, null /* description */, size, mimeType,
                lastModifiedMs, extraFlags, isPending);
        if (filePaths != null) {
            filePaths.add(mediaCursor.getString(
                    mediaCursor.getColumnIndex(DownloadColumns.DATA)));
        }
    }

    private String getMimeType(@NonNull Cursor mediaCursor) {
        final String mimeType = mediaCursor.getString(
                mediaCursor.getColumnIndex(DownloadColumns.MIME_TYPE));
        if (mimeType == null) {
            return Document.MIME_TYPE_DIR;
        }
        return mimeType;
    }

    // Copied from MediaDocumentsProvider with some tweaks
    private Pair<String, String[]> buildSearchSelection(@Nullable Bundle queryArgs,
            @Nullable Set<String> filePaths, @Nullable String parentId) {
        final StringBuilder selection = new StringBuilder();
        final ArrayList<String> selectionArgs = new ArrayList<>();

        if (parentId == null && filePaths != null && filePaths.size() > 0) {
            if (selection.length() > 0) {
                selection.append(" AND ");
            }
            selection.append(DownloadColumns.DATA + " NOT IN (");
            selection.append(TextUtils.join(",", Collections.nCopies(filePaths.size(), "?")));
            selection.append(")");
            selectionArgs.addAll(filePaths);
        }

        if (parentId != null) {
            if (selection.length() > 0) {
                selection.append(" AND ");
            }
            selection.append(DownloadColumns.RELATIVE_PATH + "=?");
            final Pair<String, String> data = getRelativePathAndDisplayNameForDownload(
                    Long.parseLong(parentId));
            selectionArgs.add(data.first + data.second + "/");
        } else {
            if (selection.length() > 0) {
                selection.append(" AND ");
            }
            selection.append(DownloadColumns.RELATIVE_PATH + "=?");
            selectionArgs.add(Environment.DIRECTORY_DOWNLOADS + "/");
        }

        if (queryArgs != null) {
            final boolean shouldExcludeMedia = queryArgs.getBoolean(
                    DocumentsContract.QUERY_ARG_EXCLUDE_MEDIA, false /* defaultValue */);
            if (shouldExcludeMedia) {
                if (selection.length() > 0) {
                    selection.append(" AND ");
                }
                selection.append(DownloadColumns.MIME_TYPE + " NOT LIKE ?");
                selectionArgs.add("image/%");
                selection.append(" AND ");
                selection.append(DownloadColumns.MIME_TYPE + " NOT LIKE ?");
                selectionArgs.add("audio/%");
                selection.append(" AND ");
                selection.append(DownloadColumns.MIME_TYPE + " NOT LIKE ?");
                selectionArgs.add("video/%");
            }

            final String displayName = queryArgs.getString(
                    DocumentsContract.QUERY_ARG_DISPLAY_NAME);
            if (!TextUtils.isEmpty(displayName)) {
                if (selection.length() > 0) {
                    selection.append(" AND ");
                }
                selection.append(DownloadColumns.DISPLAY_NAME + " LIKE ?");
                selectionArgs.add("%" + displayName + "%");
            }

            final long lastModifiedAfter = queryArgs.getLong(
                    DocumentsContract.QUERY_ARG_LAST_MODIFIED_AFTER, -1 /* defaultValue */);
            if (lastModifiedAfter != -1) {
                if (selection.length() > 0) {
                    selection.append(" AND ");
                }
                selection.append(DownloadColumns.DATE_MODIFIED
                        + " > " + lastModifiedAfter / 1000);
            }

            final long fileSizeOver = queryArgs.getLong(
                    DocumentsContract.QUERY_ARG_FILE_SIZE_OVER, -1 /* defaultValue */);
            if (fileSizeOver != -1) {
                if (selection.length() > 0) {
                    selection.append(" AND ");
                }
                selection.append(DownloadColumns.SIZE + " > " + fileSizeOver);
            }

            final String[] mimeTypes = queryArgs.getStringArray(
                    DocumentsContract.QUERY_ARG_MIME_TYPES);
            if (mimeTypes != null && mimeTypes.length > 0) {
                if (selection.length() > 0) {
                    selection.append(" AND ");
                }

                selection.append("(");
                final List<String> tempSelectionArgs = new ArrayList<>();
                final StringBuilder tempSelection = new StringBuilder();
                List<String> wildcardMimeTypeList = new ArrayList<>();
                for (int i = 0; i < mimeTypes.length; ++i) {
                    final String mimeType = mimeTypes[i];
                    if (!TextUtils.isEmpty(mimeType) && mimeType.endsWith("/*")) {
                        wildcardMimeTypeList.add(mimeType);
                        continue;
                    }

                    if (tempSelectionArgs.size() > 0) {
                        tempSelection.append(",");
                    }
                    tempSelection.append("?");
                    tempSelectionArgs.add(mimeType);
                }

                for (int i = 0; i < wildcardMimeTypeList.size(); i++) {
                    selection.append(DownloadColumns.MIME_TYPE + " LIKE ?")
                            .append((i != wildcardMimeTypeList.size() - 1) ? " OR " : "");
                    final String mimeType = wildcardMimeTypeList.get(i);
                    selectionArgs.add(mimeType.substring(0, mimeType.length() - 1) + "%");
                }

                if (tempSelectionArgs.size() > 0) {
                    if (wildcardMimeTypeList.size() > 0) {
                        selection.append(" OR ");
                    }
                    selection.append(DownloadColumns.MIME_TYPE + " IN (")
                            .append(tempSelection.toString())
                            .append(")");
                    selectionArgs.addAll(tempSelectionArgs);
                }

                selection.append(")");
            }
        }

        return new Pair<>(selection.toString(), selectionArgs.toArray(new String[0]));
    }

    /**
     * A MatrixCursor that spins up a file observer when the first instance is
     * started ({@link #start()}, and stops the file observer when the last instance
     * closed ({@link #close()}. When file changes are observed, a content change
     * notification is sent on the Downloads content URI.
     *
     * <p>This is necessary as other processes, like ExternalStorageProvider,
     * can access and modify files directly (without sending operations
     * through DownloadStorageProvider).
     *
     * <p>Without this, contents accessible by one a Downloads cursor instance
     * (like the Downloads root in Files app) can become state.
     */
    private static final class DownloadsCursor extends MatrixCursor {

        private static final Object mLock = new Object();
        @GuardedBy("mLock")
        private static int mOpenCursorCount = 0;
        @GuardedBy("mLock")
        private static @Nullable ContentChangedRelay mFileWatcher;

        private final ContentResolver mResolver;

        DownloadsCursor(String[] projection, ContentResolver resolver) {
            super(resolveDocumentProjection(projection));
            mResolver = resolver;
        }

        void start() {
            synchronized (mLock) {
                if (mOpenCursorCount++ == 0) {
                    mFileWatcher = new ContentChangedRelay(mResolver,
                            Arrays.asList(getPublicDownloadsDirectory()));
                    mFileWatcher.startWatching();
                }
            }
        }

        @Override
        public void close() {
            super.close();
            synchronized (mLock) {
                if (--mOpenCursorCount == 0) {
                    mFileWatcher.stopWatching();
                    mFileWatcher = null;
                }
            }
        }
    }

    /**
     * A file observer that notifies on the Downloads content URI(s) when
     * files change on disk.
     */
    private static class ContentChangedRelay extends FileObserver {
        private static final int NOTIFY_EVENTS = ATTRIB | CLOSE_WRITE | MOVED_FROM | MOVED_TO
                | CREATE | DELETE | DELETE_SELF | MOVE_SELF;

        private File[] mDownloadDirs;
        private final ContentResolver mResolver;

        public ContentChangedRelay(ContentResolver resolver, List<File> downloadDirs) {
            super(downloadDirs, NOTIFY_EVENTS);
            mDownloadDirs = downloadDirs.toArray(new File[0]);
            mResolver = resolver;
        }

        @Override
        public void startWatching() {
            super.startWatching();
            if (DEBUG) Log.d(TAG, "Started watching for file changes in: "
                    + Arrays.toString(mDownloadDirs));
        }

        @Override
        public void stopWatching() {
            super.stopWatching();
            if (DEBUG) Log.d(TAG, "Stopped watching for file changes in: "
                    + Arrays.toString(mDownloadDirs));
        }

        @Override
        public void onEvent(int event, String path) {
            if ((event & NOTIFY_EVENTS) != 0) {
                if (DEBUG) Log.v(TAG, "Change detected at path: " + path);
                mResolver.notifyChange(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, null, false);
                mResolver.notifyChange(Downloads.Impl.CONTENT_URI, null, false);
            }
        }
    }
}
