/* SD Scanner - A manual implementation of the SD rescan process, compatible * with Android 4.4. * * This file contains the fragment that actually performs all scan activity * and retains state across configuration changes. * * Copyright (C) 2013-2014 Jeremy Erickson * * 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 2 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. */ package com.gmail.jerickson314.sdscanner; import android.app.Activity; import android.app.Fragment; import android.content.ContentUris; import android.content.Context; import android.content.SharedPreferences; import android.database.Cursor; import android.database.DatabaseUtils; import android.media.MediaScannerConnection; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.SystemClock; import android.provider.MediaStore; import android.util.Log; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Date; import java.util.HashSet; import java.util.Iterator; import java.util.TreeSet; public class ScanFragment extends Fragment { private static final String[] MEDIA_PROJECTION = {MediaStore.MediaColumns.DATA, MediaStore.MediaColumns.DATE_MODIFIED}; private static final String[] STAR = {"*"}; private static final int DB_RETRIES = 3; Context mApplicationContext; ArrayList<String> mPathNames; TreeSet<File> mFilesToProcess; int mLastGoodProcessedIndex; private Handler mHandler = new Handler(); int mProgressNum; UIStringGenerator mProgressText = new UIStringGenerator(R.string.progress_unstarted_label); UIStringGenerator mDebugMessages = new UIStringGenerator(); boolean mStartButtonEnabled; boolean mHasStarted = false; /** * Callback interface used by the fragment to update the Activity. */ static interface ScanProgressCallbacks { void updateProgressNum(int progressNum); void updateProgressText(UIStringGenerator progressText); void updateDebugMessages(UIStringGenerator debugMessages); void updatePath(String path); void updateStartButtonEnabled(boolean startButtonEnabled); void signalFinished(); } private ScanProgressCallbacks mCallbacks; private void updateProgressNum(int progressNum) { mProgressNum = progressNum; if (mCallbacks != null) { mCallbacks.updateProgressNum(mProgressNum); } } private void updateProgressText(int resId) { updateProgressText(new UIStringGenerator(resId)); } private void updateProgressText(int resId, String string) { updateProgressText(new UIStringGenerator(resId, string)); } private void updateProgressText(UIStringGenerator progressText) { mProgressText = progressText; if (mCallbacks != null) { mCallbacks.updateProgressText(mProgressText); } } private void addDebugMessage(int resId, String string) { mDebugMessages.addSubGenerator(resId); mDebugMessages.addSubGenerator(string + "\n"); if (mCallbacks != null) { mCallbacks.updateDebugMessages(mDebugMessages); } } private void addDebugMessage(String debugMessage) { mDebugMessages.addSubGenerator(debugMessage + "\n"); if (mCallbacks != null) { mCallbacks.updateDebugMessages(mDebugMessages); } } private void resetDebugMessages() { mDebugMessages = new UIStringGenerator(); if (mCallbacks != null) { mCallbacks.updateDebugMessages(mDebugMessages); } } private void updateStartButtonEnabled(boolean startButtonEnabled) { mStartButtonEnabled = startButtonEnabled; if (mCallbacks != null) { mCallbacks.updateStartButtonEnabled(mStartButtonEnabled); } } private void signalFinished() { if (mCallbacks != null) { mCallbacks.signalFinished(); } } public int getProgressNum() { return mProgressNum; } public UIStringGenerator getProgressText() { return mProgressText; } public UIStringGenerator getDebugMessages() { return mDebugMessages; } public boolean getStartButtonEnabled() { return mStartButtonEnabled; } public boolean getHasStarted() { return mHasStarted; } @Override public void onAttach(Activity activity) { super.onAttach(activity); mCallbacks = (ScanProgressCallbacks) activity; mApplicationContext = activity.getApplicationContext(); } public ScanFragment() { super(); // Set correct initial values. mProgressNum = 0; mStartButtonEnabled = true; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Retain this fragment across configuration changes. setRetainInstance(true); } // Purely for debugging and not normally used, so does not translate // strings. public void listPathNamesOnDebug() { StringBuffer listString = new StringBuffer(); listString.append("\n\nScanning paths:\n"); Iterator<String> iterator = mPathNames.iterator(); while (iterator.hasNext()) { listString.append(iterator.next() + "\n"); } addDebugMessage(listString.toString()); } public void scannerEnded() { updateProgressNum(0); updateProgressText(R.string.progress_completed_label); updateStartButtonEnabled(true); signalFinished(); } public void startMediaScanner(){ //listPathNamesOnDebug(); if (mPathNames.size() == 0) { scannerEnded(); } else { MediaScannerConnection.scanFile( mApplicationContext, mPathNames.toArray(new String[mPathNames.size()]), null, new MediaScannerConnection.OnScanCompletedListener() { public void onScanCompleted(String path, Uri uri) { mHandler.post(new Updater(path)); } }); } } public void startScan(File path, boolean restrictDbUpdate) { mHasStarted = true; updateStartButtonEnabled(false); updateProgressText(R.string.progress_filelist_label); mFilesToProcess = new TreeSet<File>(); resetDebugMessages(); if (path.exists()) { this.new PreprocessTask().execute(new ScanParameters(path, restrictDbUpdate)); } else { updateProgressText(R.string.progress_error_bad_path_label); updateStartButtonEnabled(true); signalFinished(); } } static class ProgressUpdate { public enum Type { DATABASE, STATE, DEBUG } Type mType; public Type getType() { return mType; } int mResId; public int getResId() { return mResId; } String mString; public String getString() { return mString; } int mProgress; public int getProgress() { return mProgress; } public ProgressUpdate(Type type, int resId, String string, int progress) { mType = type; mResId = resId; mString = string; mProgress = progress; } } static ProgressUpdate debugUpdate(int resId, String string) { return new ProgressUpdate(ProgressUpdate.Type.DEBUG, resId, string, 0); } static ProgressUpdate debugUpdate(int resId) { return debugUpdate(resId, ""); } static ProgressUpdate databaseUpdate(String file, int progress) { return new ProgressUpdate(ProgressUpdate.Type.DATABASE, 0, file, progress); } static ProgressUpdate stateUpdate(int resId) { return new ProgressUpdate(ProgressUpdate.Type.STATE, resId, "", 0); } static class ScanParameters { File mPath; boolean mRestrictDbUpdate; public ScanParameters(File path, boolean restrictDbUpdate) { mPath = path; mRestrictDbUpdate = restrictDbUpdate; } public File getPath() { return mPath; } public boolean shouldScan(File file, boolean fromDb) throws IOException { // Empty directory check. if (file.isDirectory()) { File[] files = file.listFiles(); if (files == null || files.length == 0) { Log.w("SDScanner", "Scan of empty directory " + file.getCanonicalPath() + " skipped to avoid bug."); return false; } } if (!mRestrictDbUpdate && fromDb) { return true; } while (file != null) { if (file.equals(mPath)) { return true; } file = file.getParentFile(); } // If we fell through here, got up to root without encountering the // path to scan. if (!fromDb) { Log.w("SDScanner", "File " + file.getCanonicalPath() + " outside of scan directory skipped."); } return false; } } class PreprocessTask extends AsyncTask<ScanParameters, ProgressUpdate, Void> { private void recursiveAddFiles(File file, ScanParameters scanParameters) throws IOException { if (!scanParameters.shouldScan(file, false)) { // If we got here, there file was either outside the scan // directory, or was an empty directory. return; } if (!mFilesToProcess.add(file)) { // Avoid infinite recursion caused by symlinks. // If mFilesToProcess already contains this file, add() will // return false. return; } if (file.isDirectory()) { boolean nomedia = new File(file, ".nomedia").exists(); // Only recurse downward if not blocked by nomedia. if (!nomedia) { File[] files = file.listFiles(); if (files != null) { for (File nextFile : files) { recursiveAddFiles(nextFile.getCanonicalFile(), scanParameters); } } else { publishProgress(debugUpdate( R.string.skipping_folder_label, " " + file.getPath())); } } } } protected void dbOneTry(ScanParameters parameters) { Cursor cursor = mApplicationContext.getContentResolver().query( MediaStore.Files.getContentUri("external"), MEDIA_PROJECTION, //STAR, null, null, null); int data_column = cursor.getColumnIndex(MediaStore.MediaColumns.DATA); int modified_column = cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED); int totalSize = cursor.getCount(); int currentItem = 0; int reportFreq = 0; // Used to calibrate reporting frequency long startTime = SystemClock.currentThreadTimeMillis(); while (cursor.moveToNext()) { currentItem++; try { File file = new File(cursor.getString(data_column)).getCanonicalFile(); if ((!file.exists() || file.lastModified() / 1000L > cursor.getLong(modified_column)) && parameters.shouldScan(file, true)) { // Media scanner handles these cases. // Is a set, so OK if already present. mFilesToProcess.add(file); } else { // Don't want to waste time scanning an up-to-date // file. mFilesToProcess.remove(file); } if (reportFreq == 0) { // Calibration phase if (SystemClock.currentThreadTimeMillis() - startTime > 25) { reportFreq = currentItem + 1; } } else if (currentItem % reportFreq == 0) { publishProgress(databaseUpdate(file.getPath(), (100 * currentItem) / totalSize)); } } catch (IOException ex) { // Just ignore it for now. } } // Don't need the cursor any more. cursor.close(); } @Override protected Void doInBackground(ScanParameters... parameters) { try { recursiveAddFiles(parameters[0].getPath(), parameters[0]); } catch (IOException Ex) { // Do nothing. } // Parse database publishProgress(stateUpdate(R.string.progress_database_label)); boolean dbSuccess = false; int numRetries = 0; while (!dbSuccess && numRetries < DB_RETRIES) { dbSuccess = true; try { dbOneTry(parameters[0]); } catch (Exception Ex) { // For any of these errors, try again. numRetries++; dbSuccess = false; if (numRetries < DB_RETRIES) { publishProgress(stateUpdate( R.string.db_error_retrying)); SystemClock.sleep(1000); } } } if (numRetries > 0) { if (dbSuccess) { publishProgress(debugUpdate(R.string.db_error_recovered)); } else { publishProgress(debugUpdate(R.string.db_error_failure)); } } // Prepare final path list for processing. mPathNames = new ArrayList<String>(mFilesToProcess.size()); Iterator<File> iterator = mFilesToProcess.iterator(); while (iterator.hasNext()) { mPathNames.add(iterator.next().getPath()); } mLastGoodProcessedIndex = -1; return null; } @Override protected void onProgressUpdate(ProgressUpdate... progress) { switch (progress[0].getType()) { case DATABASE: updateProgressText(R.string.database_proc, " " + progress[0].getString()); updateProgressNum(progress[0].getProgress()); break; case STATE: updateProgressText(progress[0].getResId()); updateProgressNum(0); break; case DEBUG: addDebugMessage(progress[0].getResId(), progress[0].getString()); } } @Override protected void onPostExecute(Void result) { startMediaScanner(); } } class Updater implements Runnable { String mPathScanned; public Updater(String path) { mPathScanned = path; } public void run() { if (mLastGoodProcessedIndex + 1 < mPathNames.size() && mPathNames.get(mLastGoodProcessedIndex + 1).equals(mPathScanned)) { mLastGoodProcessedIndex++; } else { int newIndex = mPathNames.indexOf(mPathScanned); if (newIndex > -1) { mLastGoodProcessedIndex = newIndex; } } int progress = (100 * (mLastGoodProcessedIndex + 1)) / mPathNames.size(); if (progress == 100) { scannerEnded(); } else { updateProgressNum(progress); updateProgressText(R.string.final_proc, " " + mPathScanned); } } } }