/*
* 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));
}
}