blob: 273c29532d28035ccce7081296282f8aaf27aec2 [file] [log] [blame]
/*
* Copyright (C) 2010 The Android Open Source Project
*
* 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.android.gallery3d.photoeditor;
import android.graphics.Bitmap;
import android.os.Bundle;
import com.android.gallery3d.photoeditor.filters.Filter;
import java.util.ArrayList;
import java.util.Stack;
/**
* A stack of filters to be applied onto a photo.
*/
public class FilterStack {
/**
* Listener of stack changes.
*/
public interface StackListener {
void onStackChanged(boolean canUndo, boolean canRedo);
}
private static final String APPLIED_STACK_KEY = "applied_stack";
private static final String REDO_STACK_KEY = "redo_stack";
private final Stack<Filter> appliedStack = new Stack<Filter>();
private final Stack<Filter> redoStack = new Stack<Filter>();
// Use two photo buffers as in and out in turns to apply filters in the stack.
private final Photo[] buffers = new Photo[2];
private final PhotoView photoView;
private final StackListener stackListener;
private Photo source;
private Runnable queuedTopFilterChange;
private boolean outputTopFilter;
private volatile boolean paused;
public FilterStack(PhotoView photoView, StackListener stackListener, Bundle savedState) {
this.photoView = photoView;
this.stackListener = stackListener;
if (savedState != null) {
appliedStack.addAll(getFilters(savedState, APPLIED_STACK_KEY));
redoStack.addAll(getFilters(savedState, REDO_STACK_KEY));
outputTopFilter = true;
stackListener.onStackChanged(!appliedStack.empty(), !redoStack.empty());
}
}
private ArrayList<Filter> getFilters(Bundle savedState, String key) {
// Infer Filter array-list from the Parcelable array-list by the specified returned type.
return savedState.getParcelableArrayList(key);
}
public void saveStacks(Bundle outState) {
outState.putParcelableArrayList(APPLIED_STACK_KEY, new ArrayList<Filter>(appliedStack));
outState.putParcelableArrayList(REDO_STACK_KEY, new ArrayList<Filter>(redoStack));
}
private void reallocateBuffer(int target) {
int other = target ^ 1;
buffers[target] = Photo.create(buffers[other].width(), buffers[other].height());
}
private void invalidate() {
// In/out buffers need redrawn by re-applying filters on source photo.
for (int i = 0; i < buffers.length; i++) {
if (buffers[i] != null) {
buffers[i].clear();
buffers[i] = null;
}
}
if (source != null) {
buffers[0] = Photo.create(source.width(), source.height());
reallocateBuffer(1);
// Source photo will be displayed if there is no filter stacked.
Photo photo = source;
int size = outputTopFilter ? appliedStack.size() : appliedStack.size() - 1;
for (int i = 0; i < size && !paused; i++) {
photo = runFilter(i);
}
// Clear photo-view transformation when the top filter will be outputted.
photoView.setPhoto(photo, outputTopFilter);
}
}
private void invalidateTopFilter() {
if (!appliedStack.empty()) {
outputTopFilter = true;
photoView.setPhoto(runFilter(appliedStack.size() - 1), true);
}
}
private Photo runFilter(int filterIndex) {
int out = getOutBufferIndex(filterIndex);
Photo input = (filterIndex > 0) ? buffers[out ^ 1] : source;
if ((input != null) && (buffers[out] != null)) {
if (!buffers[out].matchDimension(input)) {
buffers[out].clear();
reallocateBuffer(out);
}
appliedStack.get(filterIndex).process(input, buffers[out]);
nativeEglSetFenceAndWait();
return buffers[out];
}
return null;
}
private int getOutBufferIndex(int filterIndex) {
// buffers[0] and buffers[1] are swapped in turns as the in/out buffers for
// processing stacked filters. For example, the first filter reads buffer[0] and
// writes buffer[1]; the second filter then reads buffer[1] and writes buffer[0].
// The returned index should only be used when the applied filter stack isn't empty.
return (filterIndex + 1) % 2;
}
private void callbackDone(final OnDoneCallback callback) {
// GL thread calls back to report UI thread the task is done.
photoView.post(new Runnable() {
@Override
public void run() {
callback.onDone();
}
});
}
private void stackChanged() {
// GL thread calls back to report UI thread the stack is changed.
final boolean canUndo = !appliedStack.empty();
final boolean canRedo = !redoStack.empty();
photoView.post(new Runnable() {
@Override
public void run() {
stackListener.onStackChanged(canUndo, canRedo);
}
});
}
public void getOutputBitmap(final OnDoneBitmapCallback callback) {
photoView.queue(new Runnable() {
@Override
public void run() {
int filterIndex = appliedStack.size() - (outputTopFilter ? 1 : 2);
Photo photo = (filterIndex < 0) ? source : buffers[getOutBufferIndex(filterIndex)];
final Bitmap bitmap = (photo != null) ? photo.save() : null;
photoView.post(new Runnable() {
@Override
public void run() {
callback.onDone(bitmap);
}
});
}
});
}
public void setPhotoSource(final Bitmap bitmap, final OnDoneCallback callback) {
photoView.queue(new Runnable() {
@Override
public void run() {
source = Photo.create(bitmap);
invalidate();
callbackDone(callback);
}
});
}
private void pushFilterInternal(Filter filter) {
appliedStack.push(filter);
outputTopFilter = false;
stackChanged();
}
public void pushFilter(final Filter filter) {
photoView.queue(new Runnable() {
@Override
public void run() {
while (!redoStack.empty()) {
redoStack.pop().release();
}
pushFilterInternal(filter);
}
});
}
public void undo(final OnDoneCallback callback) {
photoView.queue(new Runnable() {
@Override
public void run() {
if (!appliedStack.empty()) {
redoStack.push(appliedStack.pop());
stackChanged();
invalidate();
}
callbackDone(callback);
}
});
}
public void redo(final OnDoneCallback callback) {
photoView.queue(new Runnable() {
@Override
public void run() {
if (!redoStack.empty()) {
pushFilterInternal(redoStack.pop());
invalidateTopFilter();
}
callbackDone(callback);
}
});
}
public void topFilterChanged(final OnDoneCallback callback) {
// Remove the outdated top-filter change before queuing a new one.
if (queuedTopFilterChange != null) {
photoView.remove(queuedTopFilterChange);
}
queuedTopFilterChange = new Runnable() {
@Override
public void run() {
invalidateTopFilter();
callbackDone(callback);
}
};
photoView.queue(queuedTopFilterChange);
}
public void onPause() {
// Flush pending queued operations and release effect-context before GL context is lost.
// Use the flag to break from lengthy invalidate() in GL thread for not blocking onPause().
paused = true;
photoView.flush();
photoView.queueEvent(new Runnable() {
@Override
public void run() {
Filter.releaseContext();
// Textures will be automatically deleted when GL context is lost.
photoView.setPhoto(null, false);
source = null;
for (int i = 0; i < buffers.length; i++) {
buffers[i] = null;
}
}
});
photoView.onPause();
}
public void onResume() {
photoView.onResume();
paused = false;
}
static {
System.loadLibrary("jni_eglfence");
}
private native void nativeEglSetFenceAndWait();
}