/* * 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.textures; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Rect; import android.graphics.RectF; import android.media.effect.EffectContext; import android.opengl.GLES20; import android.os.Handler; import android.text.TextUtils; import android.util.Log; import android.widget.Toast; import com.ruesga.android.wallpapers.photophase.FixedQueue; import com.ruesga.android.wallpapers.photophase.FixedQueue.EmptyQueueException; import com.ruesga.android.wallpapers.photophase.GLESSurfaceDispatcher; import com.ruesga.android.wallpapers.photophase.MediaPictureDiscoverer; import com.ruesga.android.wallpapers.photophase.MediaPictureDiscoverer.OnMediaPictureDiscoveredListener; import com.ruesga.android.wallpapers.photophase.R; import com.ruesga.android.wallpapers.photophase.borders.Borders; import com.ruesga.android.wallpapers.photophase.effects.Effects; import com.ruesga.android.wallpapers.photophase.model.Disposition; import com.ruesga.android.wallpapers.photophase.preferences.PreferencesProvider; import com.ruesga.android.wallpapers.photophase.preferences.PreferencesProvider.Preferences; import com.ruesga.android.wallpapers.photophase.utils.BitmapUtils; import com.ruesga.android.wallpapers.photophase.utils.GLESUtil; import com.ruesga.android.wallpapers.photophase.utils.GLESUtil.GLESTextureInfo; import com.ruesga.android.wallpapers.photophase.utils.Utils; import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * A class that manages the acquisition of new textures. */ public class PhotoPhaseTextureManager extends TextureManager implements OnMediaPictureDiscoveredListener { private static final String TAG = "TextureManager"; private static final int QUEUE_SIZE = 1; private final Context mContext; private final Handler mHandler; private final Object mEffectsSync = new Object(); private Effects mEffects; private Borders mBorders; private final Object mSync; private final List<TextureRequestor> mPendingRequests; private final FixedQueue<GLESTextureInfo> mQueue = new FixedQueue<>(QUEUE_SIZE); private BackgroundPictureLoaderThread mBackgroundTask; private final MediaPictureDiscoverer mPictureDiscoverer; private Rect mScreenDimensions; private Rect mDimensions; private final GLESSurfaceDispatcher mDispatcher; // The status of the texture manager: // 0 - Loading // 1 - Loaded // 2 - Error private byte mStatus; private boolean mFirstLoad = true; /** * A private runnable that will run in the GLThread */ private class PictureDispatcher implements Runnable { File mImage; GLESTextureInfo ti = null; final Object mWait = new Object(); /** * {@inheritDoc} */ @Override public void run() { try { // Load the bitmap and create a fake gles information ti = GLESUtil.loadFadeTexture(mImage, mDimensions); boolean enqueue; synchronized (mSync) { enqueue = mPendingRequests.size() == 0; } synchronized (mSync) { // Notify the new images to all pending frames if (!enqueue) { // Invalid textures are also reported, so requestor can handle it TextureRequestor requestor = mPendingRequests.remove(0); applyToRequestor(requestor, ti); } else { // Add to the queue (only valid textures) if (ti.bitmap != null) { mQueue.insert(ti); } } } } catch (Throwable e) { Log.e(TAG, "Something was wrong loading the texture: " + mImage.getAbsolutePath(), e); } finally { // Notify that we have a new image synchronized (mWait) { mWait.notify(); } } } } /** * Constructor of <code>PhotoPhaseTextureManager</code> * * @param ctx The current context * @param effectCtx The current effect context * @param dispatcher The GLES dispatcher * @param requestors The number of requestors * @param screenDimensions The screen dimensions */ public PhotoPhaseTextureManager(final Context ctx, final Handler handler, final EffectContext effectCtx, GLESSurfaceDispatcher dispatcher, int requestors, Rect screenDimensions) { super(); mContext = ctx; mHandler = handler; mEffects = new Effects(ctx, effectCtx); mBorders = new Borders(ctx, effectCtx); mDispatcher = dispatcher; mScreenDimensions = screenDimensions; mDimensions = screenDimensions; // For now, use the screen dimensions as the preferred dimensions for bitmaps mSync = new Object(); mPendingRequests = new ArrayList<>(requestors); mPictureDiscoverer = new MediaPictureDiscoverer(mContext); // Run the media discovery thread mBackgroundTask = new BackgroundPictureLoaderThread(); mBackgroundTask.mTaskPaused = false; reloadMedia(false); } /** * Method that update the effect context if the EGL context change * * @param effectCtx The new effect context */ public void updateEffectContext(final EffectContext effectCtx) { synchronized (mEffectsSync) { if (mEffects != null) { mEffects.release(); mEffects = null; } mEffects = new Effects(mContext, effectCtx); if (mBorders != null) { mBorders.release(); mBorders = null; } mBorders = new Borders(mContext, effectCtx); } emptyTextureQueue(true); } /** * Method that allow to change the preferred dimensions of the bitmaps loaded * * @param dimensions The new dimensions */ public void setDimensions(Rect dimensions) { mDimensions = dimensions; } /** * Method that allow to change the screen dimensions * * @param dimensions The new dimensions */ public void setScreenDimesions(Rect dimensions) { mScreenDimensions = dimensions; } /** * Method that returns if the texture manager is paused * * @return boolean whether the texture manager is paused */ @SuppressWarnings("unused") public boolean isPaused() { return mBackgroundTask != null && mBackgroundTask.mTaskPaused; } /** * Method that pauses the internal threads * * @param pause If the thread is paused (true) or resumed (false) */ public synchronized void setPause(boolean pause) { synchronized (mBackgroundTask.mLoadSync) { mBackgroundTask.mTaskPaused = pause; if (!mBackgroundTask.mTaskPaused) { mBackgroundTask.mLoadSync.notify(); } } } /** * Method that reload the references of media pictures * * @param userRequest If the request was generated by the user */ public void reloadMedia(final boolean userRequest) { Log.d(TAG, "Reload media picture data"); // Discovery new media // GLThread doesn't run in the UI thread and AsyncThread can't create a // valid handler in ICS (it's fixed in JB+) so we force to run the async // thread in a valid UI thread mHandler.post(new Runnable() { @Override public void run() { mPictureDiscoverer.discover(userRequest, PhotoPhaseTextureManager.this); } }); } @Override public void request(TextureRequestor requestor) { synchronized (mSync) { try { GLESTextureInfo ti = mQueue.remove(); applyToRequestor(requestor, ti); } catch (EmptyQueueException eqex) { // Add to queue of pending request to be notified when // we have a new bitmap in the queue mPendingRequests.add(requestor); } } synchronized (mBackgroundTask.mLoadSync) { mBackgroundTask.mLoadSync.notify(); } } /** * Method that removes all the textures from the queue * * @param reload Forces a reload of the queue */ public void emptyTextureQueue(boolean reload) { synchronized (mSync) { // Recycle the textures try { List<GLESTextureInfo> all = mQueue.removeAll(); for (GLESTextureInfo info : all) { if (GLES20.glIsTexture(info.handle)) { int[] textures = new int[] {info.handle}; if (GLESUtil.DEBUG_GL_MEMOBJS) { Log.d(GLESUtil.DEBUG_GL_MEMOBJS_DEL_TAG, "glDeleteTextures: [" + info.handle + "]"); } GLES20.glDeleteTextures(1, textures, 0); GLESUtil.glesCheckError("glDeleteTextures"); } // Return the bitmap info.bitmap.recycle(); info.bitmap = null; } } catch (EmptyQueueException eqex) { // Ignore } // Remove all pictures in the queue try { mQueue.removeAll(); } catch (EmptyQueueException ex) { // Ignore } // Reload the queue if (reload) { synchronized (mBackgroundTask.mLoadSync) { mBackgroundTask.resetAvailableImages(); mBackgroundTask.mLoadSync.notify(); } } } } /** * Method that cancels a request did it previously. * * @param requestor The requestor of the texture */ @SuppressWarnings("unused") public void cancelRequest(TextureRequestor requestor) { synchronized (mSync) { if (mPendingRequests.contains(requestor)) { mPendingRequests.remove(requestor); } } } /** * {@inheritDoc} */ @Override public void onStartMediaDiscovered(boolean userRequest) { // No images but thread should start here to received partial data this.mStatus = 0; // Loading if (mBackgroundTask != null) { mBackgroundTask.setAvailableImages(new File[]{}); if (!mBackgroundTask.mRun) { mBackgroundTask.start(); } else { synchronized (mBackgroundTask.mLoadSync) { mBackgroundTask.mLoadSync.notify(); } } } } /** * {@inheritDoc} */ @Override public void onPartialMediaDiscovered(File[] images, boolean userRequest) { if (mBackgroundTask != null) { // In order to continue with the last media shown, we need all the // images to process them if (!Preferences.Media.isRememberLastMediaShown(mContext)) { mBackgroundTask.setPartialAvailableImages(images); } } } /** * {@inheritDoc} */ @Override @SuppressWarnings("boxing") public void onEndMediaDiscovered(File[] images, boolean userRequest) { // Now we have the paths of the images to use. Notify to the thread to // load pictures in background if (mBackgroundTask != null) { mBackgroundTask.setAvailableImages(images); if (images != null && images.length > 0) { mFirstLoad = false; } synchronized (mBackgroundTask.mLoadSync) { mBackgroundTask.mLoadSync.notify(); } this.mStatus = 1; // Loaded // Audit int found = images == null ? 0 : images.length; Log.d(TAG, "Media picture data reloaded: " + found + " images found."); if (userRequest) { CharSequence msg = String.format(mContext.getResources().getQuantityText( R.plurals.msg_media_reload_complete, found).toString(), found); Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show(); } } else { this.mStatus = 2; // Error } } /** * Method that destroy the references of this class */ public void recycle() { // Destroy the media discovery task mPictureDiscoverer.recycle(); synchronized (mEffectsSync) { if (mEffects != null) { mEffects.release(); } if (mBorders != null) { mBorders.release(); } } // Destroy the background task if (mBackgroundTask != null) { mBackgroundTask.mRun = false; try { synchronized (mBackgroundTask.mLoadSync) { mBackgroundTask.interrupt(); } } catch (Exception e) { // Ignore } } mBackgroundTask = null; } /** * Returns the status of the texture manager * * @return byte The status */ public byte getStatus() { return mStatus; } /** * Returns if the texture manager is empty * * @return boolean If the texture manager is empty */ public boolean isEmpty() { return mBackgroundTask != null && mBackgroundTask.mEmpty; } /** * Method that load the gles texture and apply to the requestor frame (which includes * fix the aspect ratio and/or effects and borders) * * @param requestor The requestor target * @param ti The original texture information (the one with the bitmap one) */ private void applyToRequestor(TextureRequestor requestor, GLESTextureInfo ti) { // Transform requestor dimensions to screen dimensions RectF dimens = requestor.getRequestorDimensions(); Rect pixels = new Rect( 0, 0, (int)(mScreenDimensions.width() * dimens.width() / 2), (int)(mScreenDimensions.height() * dimens.height() / 2)); final Disposition disposition = requestor.getDisposition(); synchronized (mEffectsSync) { if (disposition.hasFlag(Disposition.EFFECT_FLAG)) { ti.effect = mEffects.getNextEffect(); } if (disposition.hasFlag(Disposition.BORDER_FLAG)) { ti.border = mBorders.getNextBorder(); } } // Check if we have to apply any correction to the image GLESTextureInfo dst; if (ti.bitmap != null && Preferences.General.isFixAspectRatio(mContext)) { // Create a texture of power of two here to avoid scaling the bitmap twice int w = pixels.width(); int h = pixels.height(); if (!BitmapUtils.isPowerOfTwo(w, h) && PreferencesProvider.Preferences.General.isPowerOfTwo(mContext)) { w = h = BitmapUtils.calculateUpperPowerOfTwo(Math.min(w, h)); } // Create a thumbnail of the image Bitmap thumb = BitmapUtils.createScaledBitmap( ti.bitmap, w, h, BitmapUtils.ScalingLogic.CROP); if (!thumb.equals(ti.bitmap)) { ti.bitmap.recycle(); } dst = GLESUtil.loadTexture(mContext, thumb, ti.effect, ti.border, pixels); } else { // Load the texture without any correction dst = GLESUtil.loadTexture( mContext, ti.bitmap, ti.effect, ti.border, pixels); } // Swap references ti.bitmap = dst.bitmap; ti.handle = dst.handle; ti.effect = null; ti.border = null; dst.handle = 0; dst.bitmap = null; // And notify to the requestor requestor.setTextureHandle(ti); // Clean up memory if (ti.bitmap != null) { ti.bitmap.recycle(); ti.bitmap = null; } } /** * An internal thread to load pictures in background */ private class BackgroundPictureLoaderThread extends Thread { final Object mLoadSync = new Object(); boolean mRun; boolean mTaskPaused; boolean mEmpty; private final List<File> mNewImages; private final List<File> mUsedImages; /** * Constructor of <code>BackgroundPictureLoaderThread</code>. */ public BackgroundPictureLoaderThread() { super(); mNewImages = new ArrayList<>(); mUsedImages = new ArrayList<>(); } /** * Method that sets the current available images. * * @param images The current images */ public void setAvailableImages(File[] images) { synchronized (mLoadSync) { List<File> filtered = new ArrayList<>(Arrays.asList(images)); mUsedImages.retainAll(filtered); filtered.removeAll(mUsedImages); mNewImages.clear(); mNewImages.addAll(filtered); if (mFirstLoad) { reuseLastShownMedia(); } // Retain used images int count = mUsedImages.size() - 1; for (int i = count; i >= 0; i--) { File image = mUsedImages.get(i); if (!mNewImages.contains(image)) { mUsedImages.remove(image); } else { mNewImages.remove(image); } } mEmpty = images.length == 0; } } /** * Method that adds some available images. * * @param images The current images */ public void setPartialAvailableImages(File[] images) { synchronized (mLoadSync) { mNewImages.addAll(Arrays.asList(images)); mEmpty = images.length == 0; } } /** * Method that reset the current available images queue. */ public void resetAvailableImages() { synchronized (mLoadSync) { mNewImages.addAll(mUsedImages); mUsedImages.clear(); } } private void reuseLastShownMedia() { if (!Preferences.Media.isRandomSequence(mContext) && Preferences.Media.isRememberLastMediaShown(mContext)) { String lastMedia = Preferences.Media.getLastMediaShown(mContext); if (!TextUtils.isEmpty(lastMedia)) { File lastMediaFile = new File(lastMedia); int pos = mNewImages.indexOf(lastMediaFile); // Only if not exists or is not the next in the list if (pos > 0) { // Remove all items to used for (int i = pos - 1; i >= 0; i--) { mUsedImages.add(mNewImages.remove(i)); } } } } } /** * {@inheritDoc} */ @Override public void run() { mRun = true; while (mRun) { // Check if we need to load more images while (!mTaskPaused && PhotoPhaseTextureManager.this.mQueue.items() < PhotoPhaseTextureManager.this.mQueue.size()) { File image; synchronized (mLoadSync) { // Swap arrays if needed if (mNewImages.size() == 0) { mNewImages.addAll(mUsedImages); mUsedImages.clear(); } if (mNewImages.size() == 0) { if (!mEmpty) { reloadMedia(false); } break; } // Extract a random or sequential image int low = 0; int high = mNewImages.size() - 1; if (Preferences.Media.isRandomSequence(mContext)) { image = mNewImages.remove(Utils.getNextRandom(low, high)); } else { image = mNewImages.remove(0); } Preferences.Media.setLastMediaShown(mContext, image.getPath()); // Add to used images mUsedImages.add(image); } // Run commands in the GLThread if (!mRun) break; PictureDispatcher pd = new PictureDispatcher(); pd.mImage = image; mDispatcher.dispatch(pd); // Wait until the texture is loaded try { synchronized (pd.mWait) { pd.mWait.wait(); } } catch (Exception e) { // Ignore } } // Wait for new request synchronized (mLoadSync) { try { mLoadSync.wait(); } catch (Exception e) { // Ignore } } } } } }