com.bt.download.android.gui.UniversalScanner.java Source code

Java tutorial

Introduction

Here is the source code for com.bt.download.android.gui.UniversalScanner.java

Source

/*
 * Created by Angel Leon (@gubatron), Alden Torres (aldenml)
 * Copyright (c) 2011, 2012, FrostWire(TM). All rights reserved.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.bt.download.android.gui;

import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;

import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.media.MediaScannerConnection;
import android.media.MediaScannerConnection.MediaScannerConnectionClient;
import android.net.Uri;
import android.os.SystemClock;

import com.bt.download.android.core.ConfigurationManager;
import com.bt.download.android.core.Constants;
import com.bt.download.android.core.FileDescriptor;
import com.bt.download.android.core.MediaType;
import com.bt.download.android.core.providers.UniversalStore;
import com.bt.download.android.core.providers.UniversalStore.Documents;
import com.bt.download.android.core.providers.UniversalStore.Documents.DocumentsColumns;
import com.bt.download.android.gui.util.UIUtils;
import com.frostwire.logging.Logger;

/**
 * 
 * @author gubatron
 * @author aldenml
 * 
 */
public final class UniversalScanner {

    private static final Logger LOG = Logger.getLogger(UniversalScanner.class);

    private final Context context;

    public UniversalScanner(Context context) {
        this.context = context;
    }

    public void scan(final String filePath) {
        scan(Arrays.asList(new File(filePath)));
    }

    public void scan(final Collection<File> filesToScan) {
        new MultiFileAndroidScanner(filesToScan).scan();
    }

    private static void shareFinishedDownload(FileDescriptor fd) {
        if (fd != null) {
            if (ConfigurationManager.instance().getBoolean(Constants.PREF_KEY_TRANSFER_SHARE_FINISHED_DOWNLOADS)) {
                fd.shared = true;
                Librarian.instance().updateSharedStates(fd.fileType, Arrays.asList(fd));
            }
            Librarian.instance().invalidateCountCache(fd.fileType);
        }
    }

    private void scanDocument(String filePath) {
        File file = new File(filePath);

        if (documentExists(filePath, file.length())) {
            return;
        }

        String displayName = FilenameUtils.getBaseName(file.getName());

        ContentResolver cr = context.getContentResolver();

        ContentValues values = new ContentValues();

        values.put(DocumentsColumns.DATA, filePath);
        values.put(DocumentsColumns.SIZE, file.length());
        values.put(DocumentsColumns.DISPLAY_NAME, displayName);
        values.put(DocumentsColumns.TITLE, displayName);
        values.put(DocumentsColumns.DATE_ADDED, System.currentTimeMillis());
        values.put(DocumentsColumns.DATE_MODIFIED, file.lastModified());
        values.put(DocumentsColumns.MIME_TYPE, UIUtils.getMimeType(filePath));

        Uri uri = cr.insert(Documents.Media.CONTENT_URI, values);

        FileDescriptor fd = new FileDescriptor();
        fd.fileType = Constants.FILE_TYPE_DOCUMENTS;
        fd.id = Integer.valueOf(uri.getLastPathSegment());

        shareFinishedDownload(fd);
    }

    private boolean documentExists(String filePath, long size) {
        boolean result = false;

        Cursor c = null;

        try {
            ContentResolver cr = context.getContentResolver();
            c = cr.query(UniversalStore.Documents.Media.CONTENT_URI, new String[] { DocumentsColumns._ID },
                    DocumentsColumns.DATA + "=?" + " AND " + DocumentsColumns.SIZE + "=?",
                    new String[] { filePath, String.valueOf(size) }, null);
            result = c != null && c.getCount() != 0;
        } catch (Throwable e) {
            LOG.warn("Error detecting if file exists: " + filePath, e);
        } finally {
            if (c != null) {
                c.close();
            }
        }

        return result;
    }

    private final class MultiFileAndroidScanner implements MediaScannerConnectionClient {

        private MediaScannerConnection connection;
        private final Collection<File> files;
        private int numCompletedScans;

        public MultiFileAndroidScanner(Collection<File> filesToScan) {
            this.files = filesToScan;
            numCompletedScans = 0;
        }

        public void scan() {
            try {
                connection = new MediaScannerConnection(context, this);
                connection.connect();
            } catch (Throwable e) {
                LOG.warn("Error scanning file with android internal scanner, one retry", e);
                SystemClock.sleep(1000);
                connection = new MediaScannerConnection(context, this);
                connection.connect();
            }
        }

        public void onMediaScannerConnected() {
            try {
                /** should only arrive here on connected state, but let's double check since it's possible */
                if (connection.isConnected() && files != null && !files.isEmpty()) {
                    for (File f : files) {
                        connection.scanFile(f.getAbsolutePath(), null);
                    }
                }
            } catch (IllegalStateException e) {
                LOG.warn("Scanner service wasn't really connected or service was null", e);
                //should we try to connect again? don't want to end up in endless loop
                //maybe destroy connection?
            }
        }

        public void onScanCompleted(String path, Uri uri) {
            /** This will work if onScanCompleted is invoked after scanFile finishes. */
            numCompletedScans++;
            if (numCompletedScans == files.size()) {
                connection.disconnect();
            }

            MediaType mt = MediaType.getMediaTypeForExtension(FilenameUtils.getExtension(path));

            if (uri != null && !path.contains("/Android/data/" + context.getPackageName())) {
                if (mt != null && mt.getId() == Constants.FILE_TYPE_DOCUMENTS) {
                    scanDocument(path);
                } else {
                    //LOG.debug("Scanned new file: " + uri);
                    FileDescriptor fd = Librarian.instance().getFileDescriptor(uri);
                    if (fd != null) {
                        shareFinishedDownload(fd);
                    }
                }
            } else {
                if (path.endsWith(".apk")) {
                    //LOG.debug("Can't scan apk for security concerns: " + path);
                } else if (mt != null) {
                    if (mt.getId() == Constants.FILE_TYPE_AUDIO || mt.getId() == Constants.FILE_TYPE_VIDEOS
                            || mt.getId() == Constants.FILE_TYPE_PICTURES) {
                        scanPrivateFile(uri, path, mt);
                    }
                } else {
                    scanDocument(path);
                    //LOG.debug("Scanned new file as document: " + path);
                }
            }
        }
    }

    /**
     * Android geniuses put a .nomedia file on the .../Android/data/ folder
     * inside the secondary external storage path, therefore, all attempts
     * to use MediaScannerConnection to scan a media file fail. Therefore we
     * have this method to insert the file's metadata manually on the content provider.
     * @param path
     */
    private void scanPrivateFile(Uri oldUri, String filePath, MediaType mt) {
        try {
            int n = context.getContentResolver().delete(oldUri, null, null);
            if (n > 0) {
                LOG.debug("Deleted from Files provider: " + oldUri);
            }
            Uri uri = nativeScanFile(context, filePath);

            if (uri != null) {
                FileDescriptor fd = new FileDescriptor();
                fd.fileType = (byte) mt.getId();
                fd.id = Integer.valueOf(uri.getLastPathSegment());

                shareFinishedDownload(fd);
            }
        } catch (Throwable e) {
            // eat
            e.printStackTrace();
        }
    }

    private static Uri nativeScanFile(Context context, String path) {
        try {
            File f = new File(path);

            Class<?> clazz = Class.forName("android.media.MediaScanner");

            Constructor<?> mediaScannerC = clazz.getDeclaredConstructor(Context.class);
            Object scanner = mediaScannerC.newInstance(context);

            Field mClientF = clazz.getDeclaredField("mClient");
            mClientF.setAccessible(true);
            Object mClient = mClientF.get(scanner);

            Method scanSingleFileM = clazz.getDeclaredMethod("scanSingleFile", String.class, String.class,
                    String.class);
            Uri fileUri = (Uri) scanSingleFileM.invoke(scanner, f.getAbsolutePath(), "external", "data/raw");
            int n = context.getContentResolver().delete(fileUri, null, null);
            if (n > 0) {
                LOG.debug("Deleted from Files provider: " + fileUri);
            }

            Field mNoMediaF = mClient.getClass().getDeclaredField("mNoMedia");
            mNoMediaF.setAccessible(true);
            mNoMediaF.setBoolean(mClient, false);

            // This is only for HTC (tested only on HTC One M8)
            try {
                Field mFileCacheF = clazz.getDeclaredField("mFileCache");
                mFileCacheF.setAccessible(true);
                mFileCacheF.set(scanner, new HashMap<String, Object>());
            } catch (Throwable e) {
                // no an HTC, I need some time to refactor this hack
            }

            Method doScanFileM = mClient.getClass().getDeclaredMethod("doScanFile", String.class, String.class,
                    long.class, long.class, boolean.class, boolean.class, boolean.class);
            Uri mediaUri = (Uri) doScanFileM.invoke(mClient, f.getAbsolutePath(), null, f.lastModified(),
                    f.length(), false, true, false);

            Method releaseM = clazz.getDeclaredMethod("release");
            releaseM.invoke(scanner);

            return mediaUri;

        } catch (Throwable e) {
            e.printStackTrace();
            return null;
        }
    }

    public void scanDir(File privateDir) {
        scan(FileUtils.listFiles(privateDir, null, true));
    }
}