/* * Copyright (C) 2015 Jorge Ruesga * * 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.ruesga.android.wallpapers.photophase; import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.AsyncTask; import android.os.AsyncTask.Status; import android.provider.MediaStore; import android.util.Log; import com.ruesga.android.wallpapers.photophase.preferences.PreferencesProvider.Preferences; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; /** * A class that load asynchronously the paths of all media stored in the device. * This class only seek at the specified paths */ public class MediaPictureDiscoverer { private static final String TAG = "MediaPictureDiscoverer"; private static final boolean DEBUG = false; private static final String[] PROJECTION = {MediaStore.MediaColumns.DATA}; /** * An interface that is called when new data is ready. */ public interface OnMediaPictureDiscoveredListener { /** * Called when the starting to fetch data * * @param userRequest If the user requested this media discovery */ void onStartMediaDiscovered(boolean userRequest); /** * Called when the all the data is ready * * @param images All the images paths found * @param userRequest If the user requested this media discovery */ void onEndMediaDiscovered(File[] images, boolean userRequest); /** * Called when the partial data is ready * * @param images All the images paths found * @param userRequest If the user requested this media discovery */ void onPartialMediaDiscovered(File[] images, boolean userRequest); } /** * The asynchronous task for query the MediaStore */ private class AsyncDiscoverTask extends AsyncTask<Void, File, List<File>> { private final ContentResolver mFinalContentResolver; private final OnMediaPictureDiscoveredListener mFinalCallback; private final Set<String> mFilter; private final Set<String> mLastAlbums; private final Set<String> mNewAlbums; private final boolean mIsAutoSelectNewAlbums; private final boolean mUserRequest; /** * Constructor of <code>AsyncDiscoverTask</code> * * @param cr The {@link ContentResolver} * @param cb The {@link OnMediaPictureDiscoveredListener} listener * @param userRequest If the request was generated by the user */ public AsyncDiscoverTask(ContentResolver cr, OnMediaPictureDiscoveredListener cb, boolean userRequest) { super(); mFinalContentResolver = cr; mFinalCallback = cb; mFilter = Preferences.Media.getSelectedMedia(mContext); mLastAlbums = Preferences.Media.getLastDiscorevedAlbums(mContext); mIsAutoSelectNewAlbums = Preferences.Media.isAutoSelectNewAlbums(mContext); mNewAlbums = new HashSet<>(); mUserRequest = userRequest; } /** * {@inheritDoc} */ @Override protected List<File> doInBackground(Void...params) { try { // Start progress publishProgress(); // Query external content List<File> paths = getPictures( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, PROJECTION, null, null); if (DEBUG) { int cc = paths.size(); Log.v(TAG, "Pictures found (" + cc + "):"); for (int i = 0; i < cc; i++) { Log.v(TAG, "\t" + paths.get(i)); } } return paths; } catch (Exception e) { Log.e(TAG, "AsyncDiscoverTask failed.", e); // Return and empty list return new ArrayList<>(); } finally { // Save the filter (could have new albums) Preferences.Media.setSelectedMedia(mContext, mFilter); Preferences.Media.setLastDiscorevedAlbums(mContext, mNewAlbums); } } /** * {@inheritDoc} */ @Override protected void onProgressUpdate(File... values) { if (mFinalCallback != null) { if (values == null || values.length == 0) { mFinalCallback.onStartMediaDiscovered(mUserRequest); } else { mFinalCallback.onPartialMediaDiscovered(values, mUserRequest); } } } /** * {@inheritDoc} */ @Override protected void onPostExecute(List<File> result) { if (mFinalCallback != null) { mFinalCallback.onEndMediaDiscovered(result.toArray(new File[result.size()]), mUserRequest); } } /** * {@inheritDoc} */ @Override protected void onCancelled(List<File> result) { // Nothing found if (mFinalCallback != null) { // Overwrite the user request setting. If the task is cancelled then // there is no notification to send to the user mFinalCallback.onEndMediaDiscovered(new File[]{}, false); } } /** * Method that return all the media store pictures for the content uri * * @param uri The content uri where to search * @param projection The field data to return * @param where A filter * @param args The filter arguments * @return List<File> The pictures found */ private List<File> getPictures( Uri uri, String[] projection, String where, String[] args) { long start = System.currentTimeMillis(); List<File> paths = new ArrayList<>(); List<File> partial = new ArrayList<>(); Cursor c = mFinalContentResolver.query(uri, projection, where, args, null); if (c != null) { try { int i = 0; while (c.moveToNext()) { // Only valid files (those i can read) String p = c.getString(0); if (p != null) { File f = new File(p); catalog(f); // Check if is a valid filter if (matchFilter(f)) { paths.add(f); partial.add(f); } } // Publish partial data if (i % 5 == 0 && partial.size() > 0) { publishProgress(partial.toArray(new File[partial.size()])); partial.clear(); } i++; } } finally { try { c.close(); } catch (Exception e) { // Ignore: handle exception } } } long end = System.currentTimeMillis(); if (DEBUG) Log.v(TAG, "Media reloaded in " + (end - start) + " milliseconds"); return paths; } /** * Method that checks if the picture match the preferences filter * * @param picture The picture to check * @return boolean whether the picture match the filter */ private boolean matchFilter(File picture) { return mFilter.contains(picture.getAbsolutePath()) || mFilter.contains(picture.getParentFile().getAbsolutePath()); } /** * Method that catalog the file (set its album and determine if is a new album) * * @param f The file to catalog */ private void catalog(File f) { File parent = f.getParentFile(); String albumPath = parent.getAbsolutePath(); // Add to new albums mNewAlbums.add(albumPath); // Is a new album? if (!mLastAlbums.contains(albumPath)) { // Is in the filter? if (mIsAutoSelectNewAlbums && !mFilter.contains(albumPath)) { // Add the album to the selected filter mFilter.add(parent.getAbsolutePath()); } } } } private final Context mContext; private AsyncDiscoverTask mTask; /** * Constructor of <code>MediaPictureDiscoverer</code>. * * @param ctx The current context */ public MediaPictureDiscoverer(Context ctx) { super(); mContext = ctx; } /** * Method that request an asynchronous reload of the media store picture data. * * @param userRequest If the request was generated by the user * @param cb Asynchronous callback */ public synchronized void discover(boolean userRequest, OnMediaPictureDiscoveredListener cb) { if (mTask != null && mTask.getStatus().compareTo(Status.FINISHED) != 0 && !mTask.isCancelled()) { mTask.cancel(true); mTask = null; } if (AndroidHelper.hasReadExternalStoragePermissionGranted(mContext)) { mTask = new AsyncDiscoverTask(mContext.getContentResolver(), cb, userRequest); mTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } else { // Notify that we don't have any files cb.onEndMediaDiscovered(new File[0], userRequest); } } /** * Method that request a synchronous reload of the media store picture data. * * @param album The parent album or null * @return all the images found */ public List<File> obtain(File album) { if (AndroidHelper.hasReadExternalStoragePermissionGranted(mContext)) { ContentResolver cr = mContext.getContentResolver(); long start = System.currentTimeMillis(); // Query external content Cursor c = cr.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, PROJECTION, null, null, null); List<File> images = new ArrayList<>(); if (c != null) { try { while (c.moveToNext()) { // Only valid files (those i can read) String p = c.getString(0); if (p != null) { File f = new File(p); if (f.getAbsolutePath().contains(album.getAbsolutePath())) { images.add(f); } } } } finally { try { c.close(); } catch (Exception e) { // Ignore: handle exception } } long end = System.currentTimeMillis(); if (DEBUG) Log.v(TAG, "Media loaded in " + (end - start) + " milliseconds"); } Collections.sort(images); return images; } return null; } /** * Method that destroy the references of this class */ public void recycle() { if (mTask != null && !mTask.isCancelled()) { mTask.cancel(true); } } }