diff options
Diffstat (limited to 'src/com/android/gallery3d')
412 files changed, 74774 insertions, 0 deletions
diff --git a/src/com/android/gallery3d/anim/AlphaAnimation.java b/src/com/android/gallery3d/anim/AlphaAnimation.java new file mode 100644 index 000000000..f9f4cbd2c --- /dev/null +++ b/src/com/android/gallery3d/anim/AlphaAnimation.java @@ -0,0 +1,48 @@ +/* + * 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.anim; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.glrenderer.GLCanvas; + +public class AlphaAnimation extends CanvasAnimation { + private final float mStartAlpha; + private final float mEndAlpha; + private float mCurrentAlpha; + + public AlphaAnimation(float from, float to) { + mStartAlpha = from; + mEndAlpha = to; + mCurrentAlpha = from; + } + + @Override + public void apply(GLCanvas canvas) { + canvas.multiplyAlpha(mCurrentAlpha); + } + + @Override + public int getCanvasSaveFlags() { + return GLCanvas.SAVE_FLAG_ALPHA; + } + + @Override + protected void onCalculate(float progress) { + mCurrentAlpha = Utils.clamp(mStartAlpha + + (mEndAlpha - mStartAlpha) * progress, 0f, 1f); + } +} diff --git a/src/com/android/gallery3d/anim/Animation.java b/src/com/android/gallery3d/anim/Animation.java new file mode 100644 index 000000000..cc117bbce --- /dev/null +++ b/src/com/android/gallery3d/anim/Animation.java @@ -0,0 +1,92 @@ +/* + * 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.anim; + +import android.view.animation.Interpolator; + +import com.android.gallery3d.common.Utils; + +// Animation calculates a value according to the current input time. +// +// 1. First we need to use setDuration(int) to set the duration of the +// animation. The duration is in milliseconds. +// 2. Then we should call start(). The actual start time is the first value +// passed to calculate(long). +// 3. Each time we want to get an animation value, we call +// calculate(long currentTimeMillis) to ask the Animation to calculate it. +// The parameter passed to calculate(long) should be nonnegative. +// 4. Use get() to get that value. +// +// In step 3, onCalculate(float progress) is called so subclasses can calculate +// the value according to progress (progress is a value in [0,1]). +// +// Before onCalculate(float) is called, There is an optional interpolator which +// can change the progress value. The interpolator can be set by +// setInterpolator(Interpolator). If the interpolator is used, the value passed +// to onCalculate may be (for example, the overshoot effect). +// +// The isActive() method returns true after the animation start() is called and +// before calculate is passed a value which reaches the duration of the +// animation. +// +// The start() method can be called again to restart the Animation. +// +abstract public class Animation { + private static final long ANIMATION_START = -1; + private static final long NO_ANIMATION = -2; + + private long mStartTime = NO_ANIMATION; + private int mDuration; + private Interpolator mInterpolator; + + public void setInterpolator(Interpolator interpolator) { + mInterpolator = interpolator; + } + + public void setDuration(int duration) { + mDuration = duration; + } + + public void start() { + mStartTime = ANIMATION_START; + } + + public void setStartTime(long time) { + mStartTime = time; + } + + public boolean isActive() { + return mStartTime != NO_ANIMATION; + } + + public void forceStop() { + mStartTime = NO_ANIMATION; + } + + public boolean calculate(long currentTimeMillis) { + if (mStartTime == NO_ANIMATION) return false; + if (mStartTime == ANIMATION_START) mStartTime = currentTimeMillis; + int elapse = (int) (currentTimeMillis - mStartTime); + float x = Utils.clamp((float) elapse / mDuration, 0f, 1f); + Interpolator i = mInterpolator; + onCalculate(i != null ? i.getInterpolation(x) : x); + if (elapse >= mDuration) mStartTime = NO_ANIMATION; + return mStartTime != NO_ANIMATION; + } + + abstract protected void onCalculate(float progress); +} diff --git a/src/com/android/gallery3d/anim/CanvasAnimation.java b/src/com/android/gallery3d/anim/CanvasAnimation.java new file mode 100644 index 000000000..cdc66c6ba --- /dev/null +++ b/src/com/android/gallery3d/anim/CanvasAnimation.java @@ -0,0 +1,25 @@ +/* + * 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.anim; + +import com.android.gallery3d.glrenderer.GLCanvas; + +public abstract class CanvasAnimation extends Animation { + + public abstract int getCanvasSaveFlags(); + public abstract void apply(GLCanvas canvas); +} diff --git a/src/com/android/gallery3d/anim/FloatAnimation.java b/src/com/android/gallery3d/anim/FloatAnimation.java new file mode 100644 index 000000000..1294ec2f4 --- /dev/null +++ b/src/com/android/gallery3d/anim/FloatAnimation.java @@ -0,0 +1,40 @@ +/* + * 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.anim; + +public class FloatAnimation extends Animation { + + private final float mFrom; + private final float mTo; + private float mCurrent; + + public FloatAnimation(float from, float to, int duration) { + mFrom = from; + mTo = to; + mCurrent = from; + setDuration(duration); + } + + @Override + protected void onCalculate(float progress) { + mCurrent = mFrom + (mTo - mFrom) * progress; + } + + public float get() { + return mCurrent; + } +} diff --git a/src/com/android/gallery3d/anim/StateTransitionAnimation.java b/src/com/android/gallery3d/anim/StateTransitionAnimation.java new file mode 100644 index 000000000..bf8a54405 --- /dev/null +++ b/src/com/android/gallery3d/anim/StateTransitionAnimation.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2012 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.anim; + +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; + +import com.android.gallery3d.glrenderer.GLCanvas; +import com.android.gallery3d.glrenderer.RawTexture; +import com.android.gallery3d.ui.GLView; +import com.android.gallery3d.ui.TiledScreenNail; + +public class StateTransitionAnimation extends Animation { + + public static class Spec { + public static final Spec OUTGOING; + public static final Spec INCOMING; + public static final Spec PHOTO_INCOMING; + + private static final Interpolator DEFAULT_INTERPOLATOR = + new DecelerateInterpolator(); + + public int duration = 330; + public float backgroundAlphaFrom = 0; + public float backgroundAlphaTo = 0; + public float backgroundScaleFrom = 0; + public float backgroundScaleTo = 0; + public float contentAlphaFrom = 1; + public float contentAlphaTo = 1; + public float contentScaleFrom = 1; + public float contentScaleTo = 1; + public float overlayAlphaFrom = 0; + public float overlayAlphaTo = 0; + public float overlayScaleFrom = 0; + public float overlayScaleTo = 0; + public Interpolator interpolator = DEFAULT_INTERPOLATOR; + + static { + OUTGOING = new Spec(); + OUTGOING.backgroundAlphaFrom = 0.5f; + OUTGOING.backgroundAlphaTo = 0f; + OUTGOING.backgroundScaleFrom = 1f; + OUTGOING.backgroundScaleTo = 0f; + OUTGOING.contentAlphaFrom = 0.5f; + OUTGOING.contentAlphaTo = 1f; + OUTGOING.contentScaleFrom = 3f; + OUTGOING.contentScaleTo = 1f; + + INCOMING = new Spec(); + INCOMING.overlayAlphaFrom = 1f; + INCOMING.overlayAlphaTo = 0f; + INCOMING.overlayScaleFrom = 1f; + INCOMING.overlayScaleTo = 3f; + INCOMING.contentAlphaFrom = 0f; + INCOMING.contentAlphaTo = 1f; + INCOMING.contentScaleFrom = 0.25f; + INCOMING.contentScaleTo = 1f; + + PHOTO_INCOMING = INCOMING; + } + + private static Spec specForTransition(Transition t) { + switch (t) { + case Outgoing: + return Spec.OUTGOING; + case Incoming: + return Spec.INCOMING; + case PhotoIncoming: + return Spec.PHOTO_INCOMING; + case None: + default: + return null; + } + } + } + + public static enum Transition { None, Outgoing, Incoming, PhotoIncoming } + + private final Spec mTransitionSpec; + private float mCurrentContentScale; + private float mCurrentContentAlpha; + private float mCurrentBackgroundScale; + private float mCurrentBackgroundAlpha; + private float mCurrentOverlayScale; + private float mCurrentOverlayAlpha; + private RawTexture mOldScreenTexture; + + public StateTransitionAnimation(Transition t, RawTexture oldScreen) { + this(Spec.specForTransition(t), oldScreen); + } + + public StateTransitionAnimation(Spec spec, RawTexture oldScreen) { + mTransitionSpec = spec != null ? spec : Spec.OUTGOING; + setDuration(mTransitionSpec.duration); + setInterpolator(mTransitionSpec.interpolator); + mOldScreenTexture = oldScreen; + TiledScreenNail.disableDrawPlaceholder(); + } + + @Override + public boolean calculate(long currentTimeMillis) { + boolean retval = super.calculate(currentTimeMillis); + if (!isActive()) { + if (mOldScreenTexture != null) { + mOldScreenTexture.recycle(); + mOldScreenTexture = null; + } + TiledScreenNail.enableDrawPlaceholder(); + } + return retval; + } + + @Override + protected void onCalculate(float progress) { + mCurrentContentScale = mTransitionSpec.contentScaleFrom + + (mTransitionSpec.contentScaleTo - mTransitionSpec.contentScaleFrom) * progress; + mCurrentContentAlpha = mTransitionSpec.contentAlphaFrom + + (mTransitionSpec.contentAlphaTo - mTransitionSpec.contentAlphaFrom) * progress; + mCurrentBackgroundAlpha = mTransitionSpec.backgroundAlphaFrom + + (mTransitionSpec.backgroundAlphaTo - mTransitionSpec.backgroundAlphaFrom) + * progress; + mCurrentBackgroundScale = mTransitionSpec.backgroundScaleFrom + + (mTransitionSpec.backgroundScaleTo - mTransitionSpec.backgroundScaleFrom) + * progress; + mCurrentOverlayScale = mTransitionSpec.overlayScaleFrom + + (mTransitionSpec.overlayScaleTo - mTransitionSpec.overlayScaleFrom) * progress; + mCurrentOverlayAlpha = mTransitionSpec.overlayAlphaFrom + + (mTransitionSpec.overlayAlphaTo - mTransitionSpec.overlayAlphaFrom) * progress; + } + + private void applyOldTexture(GLView view, GLCanvas canvas, float alpha, float scale, boolean clear) { + if (mOldScreenTexture == null) + return; + if (clear) canvas.clearBuffer(view.getBackgroundColor()); + canvas.save(); + canvas.setAlpha(alpha); + int xOffset = view.getWidth() / 2; + int yOffset = view.getHeight() / 2; + canvas.translate(xOffset, yOffset); + canvas.scale(scale, scale, 1); + mOldScreenTexture.draw(canvas, -xOffset, -yOffset); + canvas.restore(); + } + + public void applyBackground(GLView view, GLCanvas canvas) { + if (mCurrentBackgroundAlpha > 0f) { + applyOldTexture(view, canvas, mCurrentBackgroundAlpha, mCurrentBackgroundScale, true); + } + } + + public void applyContentTransform(GLView view, GLCanvas canvas) { + int xOffset = view.getWidth() / 2; + int yOffset = view.getHeight() / 2; + canvas.translate(xOffset, yOffset); + canvas.scale(mCurrentContentScale, mCurrentContentScale, 1); + canvas.translate(-xOffset, -yOffset); + canvas.setAlpha(mCurrentContentAlpha); + } + + public void applyOverlay(GLView view, GLCanvas canvas) { + if (mCurrentOverlayAlpha > 0f) { + applyOldTexture(view, canvas, mCurrentOverlayAlpha, mCurrentOverlayScale, false); + } + } +} diff --git a/src/com/android/gallery3d/app/AbstractGalleryActivity.java b/src/com/android/gallery3d/app/AbstractGalleryActivity.java new file mode 100644 index 000000000..ac39aa560 --- /dev/null +++ b/src/com/android/gallery3d/app/AbstractGalleryActivity.java @@ -0,0 +1,343 @@ +/* + * 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.app; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnCancelListener; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.ServiceConnection; +import android.content.res.Configuration; +import android.os.Bundle; +import android.os.IBinder; +import android.view.Menu; +import android.view.MenuItem; +import android.view.Window; +import android.view.WindowManager; + +import com.android.gallery3d.R; +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.ui.GLRoot; +import com.android.gallery3d.ui.GLRootView; +import com.android.gallery3d.util.LightCycleHelper.PanoramaViewHelper; +import com.android.gallery3d.util.ThreadPool; +import com.android.photos.data.GalleryBitmapPool; + +public class AbstractGalleryActivity extends Activity implements GalleryContext { + @SuppressWarnings("unused") + private static final String TAG = "AbstractGalleryActivity"; + private GLRootView mGLRootView; + private StateManager mStateManager; + private GalleryActionBar mActionBar; + private OrientationManager mOrientationManager; + private TransitionStore mTransitionStore = new TransitionStore(); + private boolean mDisableToggleStatusBar; + private PanoramaViewHelper mPanoramaViewHelper; + + private AlertDialog mAlertDialog = null; + private BroadcastReceiver mMountReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (getExternalCacheDir() != null) onStorageReady(); + } + }; + private IntentFilter mMountFilter = new IntentFilter(Intent.ACTION_MEDIA_MOUNTED); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mOrientationManager = new OrientationManager(this); + toggleStatusBarByOrientation(); + getWindow().setBackgroundDrawable(null); + mPanoramaViewHelper = new PanoramaViewHelper(this); + mPanoramaViewHelper.onCreate(); + doBindBatchService(); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + mGLRootView.lockRenderThread(); + try { + super.onSaveInstanceState(outState); + getStateManager().saveState(outState); + } finally { + mGLRootView.unlockRenderThread(); + } + } + + @Override + public void onConfigurationChanged(Configuration config) { + super.onConfigurationChanged(config); + mStateManager.onConfigurationChange(config); + getGalleryActionBar().onConfigurationChanged(); + invalidateOptionsMenu(); + toggleStatusBarByOrientation(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + return getStateManager().createOptionsMenu(menu); + } + + @Override + public Context getAndroidContext() { + return this; + } + + @Override + public DataManager getDataManager() { + return ((GalleryApp) getApplication()).getDataManager(); + } + + @Override + public ThreadPool getThreadPool() { + return ((GalleryApp) getApplication()).getThreadPool(); + } + + public synchronized StateManager getStateManager() { + if (mStateManager == null) { + mStateManager = new StateManager(this); + } + return mStateManager; + } + + public GLRoot getGLRoot() { + return mGLRootView; + } + + public OrientationManager getOrientationManager() { + return mOrientationManager; + } + + @Override + public void setContentView(int resId) { + super.setContentView(resId); + mGLRootView = (GLRootView) findViewById(R.id.gl_root_view); + } + + protected void onStorageReady() { + if (mAlertDialog != null) { + mAlertDialog.dismiss(); + mAlertDialog = null; + unregisterReceiver(mMountReceiver); + } + } + + @Override + protected void onStart() { + super.onStart(); + if (getExternalCacheDir() == null) { + OnCancelListener onCancel = new OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + finish(); + } + }; + OnClickListener onClick = new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }; + AlertDialog.Builder builder = new AlertDialog.Builder(this) + .setTitle(R.string.no_external_storage_title) + .setMessage(R.string.no_external_storage) + .setNegativeButton(android.R.string.cancel, onClick) + .setOnCancelListener(onCancel); + if (ApiHelper.HAS_SET_ICON_ATTRIBUTE) { + setAlertDialogIconAttribute(builder); + } else { + builder.setIcon(android.R.drawable.ic_dialog_alert); + } + mAlertDialog = builder.show(); + registerReceiver(mMountReceiver, mMountFilter); + } + mPanoramaViewHelper.onStart(); + } + + @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) + private static void setAlertDialogIconAttribute( + AlertDialog.Builder builder) { + builder.setIconAttribute(android.R.attr.alertDialogIcon); + } + + @Override + protected void onStop() { + super.onStop(); + if (mAlertDialog != null) { + unregisterReceiver(mMountReceiver); + mAlertDialog.dismiss(); + mAlertDialog = null; + } + mPanoramaViewHelper.onStop(); + } + + @Override + protected void onResume() { + super.onResume(); + mGLRootView.lockRenderThread(); + try { + getStateManager().resume(); + getDataManager().resume(); + } finally { + mGLRootView.unlockRenderThread(); + } + mGLRootView.onResume(); + mOrientationManager.resume(); + } + + @Override + protected void onPause() { + super.onPause(); + mOrientationManager.pause(); + mGLRootView.onPause(); + mGLRootView.lockRenderThread(); + try { + getStateManager().pause(); + getDataManager().pause(); + } finally { + mGLRootView.unlockRenderThread(); + } + GalleryBitmapPool.getInstance().clear(); + MediaItem.getBytesBufferPool().clear(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + mGLRootView.lockRenderThread(); + try { + getStateManager().destroy(); + } finally { + mGLRootView.unlockRenderThread(); + } + doUnbindBatchService(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + mGLRootView.lockRenderThread(); + try { + getStateManager().notifyActivityResult( + requestCode, resultCode, data); + } finally { + mGLRootView.unlockRenderThread(); + } + } + + @Override + public void onBackPressed() { + // send the back event to the top sub-state + GLRoot root = getGLRoot(); + root.lockRenderThread(); + try { + getStateManager().onBackPressed(); + } finally { + root.unlockRenderThread(); + } + } + + public GalleryActionBar getGalleryActionBar() { + if (mActionBar == null) { + mActionBar = new GalleryActionBar(this); + } + return mActionBar; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + GLRoot root = getGLRoot(); + root.lockRenderThread(); + try { + return getStateManager().itemSelected(item); + } finally { + root.unlockRenderThread(); + } + } + + protected void disableToggleStatusBar() { + mDisableToggleStatusBar = true; + } + + // Shows status bar in portrait view, hide in landscape view + private void toggleStatusBarByOrientation() { + if (mDisableToggleStatusBar) return; + + Window win = getWindow(); + if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) { + win.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + } else { + win.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + } + } + + public TransitionStore getTransitionStore() { + return mTransitionStore; + } + + public PanoramaViewHelper getPanoramaViewHelper() { + return mPanoramaViewHelper; + } + + protected boolean isFullscreen() { + return (getWindow().getAttributes().flags + & WindowManager.LayoutParams.FLAG_FULLSCREEN) != 0; + } + + private BatchService mBatchService; + private boolean mBatchServiceIsBound = false; + private ServiceConnection mBatchServiceConnection = new ServiceConnection() { + public void onServiceConnected(ComponentName className, IBinder service) { + mBatchService = ((BatchService.LocalBinder)service).getService(); + } + + public void onServiceDisconnected(ComponentName className) { + mBatchService = null; + } + }; + + private void doBindBatchService() { + bindService(new Intent(this, BatchService.class), mBatchServiceConnection, Context.BIND_AUTO_CREATE); + mBatchServiceIsBound = true; + } + + private void doUnbindBatchService() { + if (mBatchServiceIsBound) { + // Detach our existing connection. + unbindService(mBatchServiceConnection); + mBatchServiceIsBound = false; + } + } + + public ThreadPool getBatchServiceThreadPoolIfAvailable() { + if (mBatchServiceIsBound && mBatchService != null) { + return mBatchService.getThreadPool(); + } else { + throw new RuntimeException("Batch service unavailable"); + } + } +} diff --git a/src/com/android/gallery3d/app/ActivityState.java b/src/com/android/gallery3d/app/ActivityState.java new file mode 100644 index 000000000..2f1e0c9d9 --- /dev/null +++ b/src/com/android/gallery3d/app/ActivityState.java @@ -0,0 +1,276 @@ +/* + * 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.app; + +import android.app.ActionBar; +import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Configuration; +import android.os.BatteryManager; +import android.os.Bundle; +import android.view.HapticFeedbackConstants; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.Window; +import android.view.WindowManager; + +import com.android.gallery3d.R; +import com.android.gallery3d.anim.StateTransitionAnimation; +import com.android.gallery3d.glrenderer.RawTexture; +import com.android.gallery3d.ui.GLView; +import com.android.gallery3d.ui.PreparePageFadeoutTexture; +import com.android.gallery3d.util.GalleryUtils; + +abstract public class ActivityState { + protected static final int FLAG_HIDE_ACTION_BAR = 1; + protected static final int FLAG_HIDE_STATUS_BAR = 2; + protected static final int FLAG_SCREEN_ON_WHEN_PLUGGED = 4; + protected static final int FLAG_SCREEN_ON_ALWAYS = 8; + protected static final int FLAG_ALLOW_LOCK_WHILE_SCREEN_ON = 16; + protected static final int FLAG_SHOW_WHEN_LOCKED = 32; + + protected AbstractGalleryActivity mActivity; + protected Bundle mData; + protected int mFlags; + + protected ResultEntry mReceivedResults; + protected ResultEntry mResult; + + protected static class ResultEntry { + public int requestCode; + public int resultCode = Activity.RESULT_CANCELED; + public Intent resultData; + } + + private boolean mDestroyed = false; + private boolean mPlugged = false; + boolean mIsFinishing = false; + + private static final String KEY_TRANSITION_IN = "transition-in"; + + private StateTransitionAnimation.Transition mNextTransition = + StateTransitionAnimation.Transition.None; + private StateTransitionAnimation mIntroAnimation; + private GLView mContentPane; + + protected ActivityState() { + } + + protected void setContentPane(GLView content) { + mContentPane = content; + if (mIntroAnimation != null) { + mContentPane.setIntroAnimation(mIntroAnimation); + mIntroAnimation = null; + } + mContentPane.setBackgroundColor(getBackgroundColor()); + mActivity.getGLRoot().setContentPane(mContentPane); + } + + void initialize(AbstractGalleryActivity activity, Bundle data) { + mActivity = activity; + mData = data; + } + + public Bundle getData() { + return mData; + } + + protected void onBackPressed() { + mActivity.getStateManager().finishState(this); + } + + protected void setStateResult(int resultCode, Intent data) { + if (mResult == null) return; + mResult.resultCode = resultCode; + mResult.resultData = data; + } + + protected void onConfigurationChanged(Configuration config) { + } + + protected void onSaveState(Bundle outState) { + } + + protected void onStateResult(int requestCode, int resultCode, Intent data) { + } + + protected float[] mBackgroundColor; + + protected int getBackgroundColorId() { + return R.color.default_background; + } + + protected float[] getBackgroundColor() { + return mBackgroundColor; + } + + protected void onCreate(Bundle data, Bundle storedState) { + mBackgroundColor = GalleryUtils.intColorToFloatARGBArray( + mActivity.getResources().getColor(getBackgroundColorId())); + } + + protected void clearStateResult() { + } + + BroadcastReceiver mPowerIntentReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (Intent.ACTION_BATTERY_CHANGED.equals(action)) { + boolean plugged = (0 != intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0)); + + if (plugged != mPlugged) { + mPlugged = plugged; + setScreenFlags(); + } + } + } + }; + + private void setScreenFlags() { + final Window win = mActivity.getWindow(); + final WindowManager.LayoutParams params = win.getAttributes(); + if ((0 != (mFlags & FLAG_SCREEN_ON_ALWAYS)) || + (mPlugged && 0 != (mFlags & FLAG_SCREEN_ON_WHEN_PLUGGED))) { + params.flags |= WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; + } else { + params.flags &= ~WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; + } + if (0 != (mFlags & FLAG_ALLOW_LOCK_WHILE_SCREEN_ON)) { + params.flags |= WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON; + } else { + params.flags &= ~WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON; + } + if (0 != (mFlags & FLAG_SHOW_WHEN_LOCKED)) { + params.flags |= WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED; + } else { + params.flags &= ~WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED; + } + win.setAttributes(params); + } + + protected void transitionOnNextPause(Class<? extends ActivityState> outgoing, + Class<? extends ActivityState> incoming, StateTransitionAnimation.Transition hint) { + if (outgoing == SinglePhotoPage.class && incoming == AlbumPage.class) { + mNextTransition = StateTransitionAnimation.Transition.Outgoing; + } else if (outgoing == AlbumPage.class && incoming == SinglePhotoPage.class) { + mNextTransition = StateTransitionAnimation.Transition.PhotoIncoming; + } else { + mNextTransition = hint; + } + } + + protected void performHapticFeedback(int feedbackConstant) { + mActivity.getWindow().getDecorView().performHapticFeedback(feedbackConstant, + HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); + } + + protected void onPause() { + if (0 != (mFlags & FLAG_SCREEN_ON_WHEN_PLUGGED)) { + ((Activity) mActivity).unregisterReceiver(mPowerIntentReceiver); + } + if (mNextTransition != StateTransitionAnimation.Transition.None) { + mActivity.getTransitionStore().put(KEY_TRANSITION_IN, mNextTransition); + PreparePageFadeoutTexture.prepareFadeOutTexture(mActivity, mContentPane); + mNextTransition = StateTransitionAnimation.Transition.None; + } + } + + // should only be called by StateManager + void resume() { + AbstractGalleryActivity activity = mActivity; + ActionBar actionBar = activity.getActionBar(); + if (actionBar != null) { + if ((mFlags & FLAG_HIDE_ACTION_BAR) != 0) { + actionBar.hide(); + } else { + actionBar.show(); + } + int stateCount = mActivity.getStateManager().getStateCount(); + mActivity.getGalleryActionBar().setDisplayOptions(stateCount > 1, true); + // Default behavior, this can be overridden in ActivityState's onResume. + actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD); + } + + activity.invalidateOptionsMenu(); + + setScreenFlags(); + + boolean lightsOut = ((mFlags & FLAG_HIDE_STATUS_BAR) != 0); + mActivity.getGLRoot().setLightsOutMode(lightsOut); + + ResultEntry entry = mReceivedResults; + if (entry != null) { + mReceivedResults = null; + onStateResult(entry.requestCode, entry.resultCode, entry.resultData); + } + + if (0 != (mFlags & FLAG_SCREEN_ON_WHEN_PLUGGED)) { + // we need to know whether the device is plugged in to do this correctly + final IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_BATTERY_CHANGED); + activity.registerReceiver(mPowerIntentReceiver, filter); + } + + onResume(); + + // the transition store should be cleared after resume; + mActivity.getTransitionStore().clear(); + } + + // a subclass of ActivityState should override the method to resume itself + protected void onResume() { + RawTexture fade = mActivity.getTransitionStore().get( + PreparePageFadeoutTexture.KEY_FADE_TEXTURE); + mNextTransition = mActivity.getTransitionStore().get( + KEY_TRANSITION_IN, StateTransitionAnimation.Transition.None); + if (mNextTransition != StateTransitionAnimation.Transition.None) { + mIntroAnimation = new StateTransitionAnimation(mNextTransition, fade); + mNextTransition = StateTransitionAnimation.Transition.None; + } + } + + protected boolean onCreateActionBar(Menu menu) { + // TODO: we should return false if there is no menu to show + // this is a workaround for a bug in system + return true; + } + + protected boolean onItemSelected(MenuItem item) { + return false; + } + + protected void onDestroy() { + mDestroyed = true; + } + + boolean isDestroyed() { + return mDestroyed; + } + + public boolean isFinishing() { + return mIsFinishing; + } + + protected MenuInflater getSupportMenuInflater() { + return mActivity.getMenuInflater(); + } +} diff --git a/src/com/android/gallery3d/app/AlbumDataLoader.java b/src/com/android/gallery3d/app/AlbumDataLoader.java new file mode 100644 index 000000000..28a822830 --- /dev/null +++ b/src/com/android/gallery3d/app/AlbumDataLoader.java @@ -0,0 +1,397 @@ +/* + * 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.app; + +import android.os.Handler; +import android.os.Message; +import android.os.Process; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.ContentListener; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.ui.SynchronizedHandler; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; + +public class AlbumDataLoader { + @SuppressWarnings("unused") + private static final String TAG = "AlbumDataAdapter"; + private static final int DATA_CACHE_SIZE = 1000; + + private static final int MSG_LOAD_START = 1; + private static final int MSG_LOAD_FINISH = 2; + private static final int MSG_RUN_OBJECT = 3; + + private static final int MIN_LOAD_COUNT = 32; + private static final int MAX_LOAD_COUNT = 64; + + private final MediaItem[] mData; + private final long[] mItemVersion; + private final long[] mSetVersion; + + public static interface DataListener { + public void onContentChanged(int index); + public void onSizeChanged(int size); + } + + private int mActiveStart = 0; + private int mActiveEnd = 0; + + private int mContentStart = 0; + private int mContentEnd = 0; + + private final MediaSet mSource; + private long mSourceVersion = MediaObject.INVALID_DATA_VERSION; + + private final Handler mMainHandler; + private int mSize = 0; + + private DataListener mDataListener; + private MySourceListener mSourceListener = new MySourceListener(); + private LoadingListener mLoadingListener; + + private ReloadTask mReloadTask; + // the data version on which last loading failed + private long mFailedVersion = MediaObject.INVALID_DATA_VERSION; + + public AlbumDataLoader(AbstractGalleryActivity context, MediaSet mediaSet) { + mSource = mediaSet; + + mData = new MediaItem[DATA_CACHE_SIZE]; + mItemVersion = new long[DATA_CACHE_SIZE]; + mSetVersion = new long[DATA_CACHE_SIZE]; + Arrays.fill(mItemVersion, MediaObject.INVALID_DATA_VERSION); + Arrays.fill(mSetVersion, MediaObject.INVALID_DATA_VERSION); + + mMainHandler = new SynchronizedHandler(context.getGLRoot()) { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_RUN_OBJECT: + ((Runnable) message.obj).run(); + return; + case MSG_LOAD_START: + if (mLoadingListener != null) mLoadingListener.onLoadingStarted(); + return; + case MSG_LOAD_FINISH: + if (mLoadingListener != null) { + boolean loadingFailed = + (mFailedVersion != MediaObject.INVALID_DATA_VERSION); + mLoadingListener.onLoadingFinished(loadingFailed); + } + return; + } + } + }; + } + + public void resume() { + mSource.addContentListener(mSourceListener); + mReloadTask = new ReloadTask(); + mReloadTask.start(); + } + + public void pause() { + mReloadTask.terminate(); + mReloadTask = null; + mSource.removeContentListener(mSourceListener); + } + + public MediaItem get(int index) { + if (!isActive(index)) { + return mSource.getMediaItem(index, 1).get(0); + } + return mData[index % mData.length]; + } + + public int getActiveStart() { + return mActiveStart; + } + + public boolean isActive(int index) { + return index >= mActiveStart && index < mActiveEnd; + } + + public int size() { + return mSize; + } + + // Returns the index of the MediaItem with the given path or + // -1 if the path is not cached + public int findItem(Path id) { + for (int i = mContentStart; i < mContentEnd; i++) { + MediaItem item = mData[i % DATA_CACHE_SIZE]; + if (item != null && id == item.getPath()) { + return i; + } + } + return -1; + } + + private void clearSlot(int slotIndex) { + mData[slotIndex] = null; + mItemVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION; + mSetVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION; + } + + private void setContentWindow(int contentStart, int contentEnd) { + if (contentStart == mContentStart && contentEnd == mContentEnd) return; + int end = mContentEnd; + int start = mContentStart; + + // We need change the content window before calling reloadData(...) + synchronized (this) { + mContentStart = contentStart; + mContentEnd = contentEnd; + } + long[] itemVersion = mItemVersion; + long[] setVersion = mSetVersion; + if (contentStart >= end || start >= contentEnd) { + for (int i = start, n = end; i < n; ++i) { + clearSlot(i % DATA_CACHE_SIZE); + } + } else { + for (int i = start; i < contentStart; ++i) { + clearSlot(i % DATA_CACHE_SIZE); + } + for (int i = contentEnd, n = end; i < n; ++i) { + clearSlot(i % DATA_CACHE_SIZE); + } + } + if (mReloadTask != null) mReloadTask.notifyDirty(); + } + + public void setActiveWindow(int start, int end) { + if (start == mActiveStart && end == mActiveEnd) return; + + Utils.assertTrue(start <= end + && end - start <= mData.length && end <= mSize); + + int length = mData.length; + mActiveStart = start; + mActiveEnd = end; + + // If no data is visible, keep the cache content + if (start == end) return; + + int contentStart = Utils.clamp((start + end) / 2 - length / 2, + 0, Math.max(0, mSize - length)); + int contentEnd = Math.min(contentStart + length, mSize); + if (mContentStart > start || mContentEnd < end + || Math.abs(contentStart - mContentStart) > MIN_LOAD_COUNT) { + setContentWindow(contentStart, contentEnd); + } + } + + private class MySourceListener implements ContentListener { + @Override + public void onContentDirty() { + if (mReloadTask != null) mReloadTask.notifyDirty(); + } + } + + public void setDataListener(DataListener listener) { + mDataListener = listener; + } + + public void setLoadingListener(LoadingListener listener) { + mLoadingListener = listener; + } + + private <T> T executeAndWait(Callable<T> callable) { + FutureTask<T> task = new FutureTask<T>(callable); + mMainHandler.sendMessage( + mMainHandler.obtainMessage(MSG_RUN_OBJECT, task)); + try { + return task.get(); + } catch (InterruptedException e) { + return null; + } catch (ExecutionException e) { + throw new RuntimeException(e); + } + } + + private static class UpdateInfo { + public long version; + public int reloadStart; + public int reloadCount; + + public int size; + public ArrayList<MediaItem> items; + } + + private class GetUpdateInfo implements Callable<UpdateInfo> { + private final long mVersion; + + public GetUpdateInfo(long version) { + mVersion = version; + } + + @Override + public UpdateInfo call() throws Exception { + if (mFailedVersion == mVersion) { + // previous loading failed, return null to pause loading + return null; + } + UpdateInfo info = new UpdateInfo(); + long version = mVersion; + info.version = mSourceVersion; + info.size = mSize; + long setVersion[] = mSetVersion; + for (int i = mContentStart, n = mContentEnd; i < n; ++i) { + int index = i % DATA_CACHE_SIZE; + if (setVersion[index] != version) { + info.reloadStart = i; + info.reloadCount = Math.min(MAX_LOAD_COUNT, n - i); + return info; + } + } + return mSourceVersion == mVersion ? null : info; + } + } + + private class UpdateContent implements Callable<Void> { + + private UpdateInfo mUpdateInfo; + + public UpdateContent(UpdateInfo info) { + mUpdateInfo = info; + } + + @Override + public Void call() throws Exception { + UpdateInfo info = mUpdateInfo; + mSourceVersion = info.version; + if (mSize != info.size) { + mSize = info.size; + if (mDataListener != null) mDataListener.onSizeChanged(mSize); + if (mContentEnd > mSize) mContentEnd = mSize; + if (mActiveEnd > mSize) mActiveEnd = mSize; + } + + ArrayList<MediaItem> items = info.items; + + mFailedVersion = MediaObject.INVALID_DATA_VERSION; + if ((items == null) || items.isEmpty()) { + if (info.reloadCount > 0) { + mFailedVersion = info.version; + Log.d(TAG, "loading failed: " + mFailedVersion); + } + return null; + } + int start = Math.max(info.reloadStart, mContentStart); + int end = Math.min(info.reloadStart + items.size(), mContentEnd); + + for (int i = start; i < end; ++i) { + int index = i % DATA_CACHE_SIZE; + mSetVersion[index] = info.version; + MediaItem updateItem = items.get(i - info.reloadStart); + long itemVersion = updateItem.getDataVersion(); + if (mItemVersion[index] != itemVersion) { + mItemVersion[index] = itemVersion; + mData[index] = updateItem; + if (mDataListener != null && i >= mActiveStart && i < mActiveEnd) { + mDataListener.onContentChanged(i); + } + } + } + return null; + } + } + + /* + * The thread model of ReloadTask + * * + * [Reload Task] [Main Thread] + * | | + * getUpdateInfo() --> | (synchronous call) + * (wait) <---- getUpdateInfo() + * | | + * Load Data | + * | | + * updateContent() --> | (synchronous call) + * (wait) updateContent() + * | | + * | | + */ + private class ReloadTask extends Thread { + + private volatile boolean mActive = true; + private volatile boolean mDirty = true; + private boolean mIsLoading = false; + + private void updateLoading(boolean loading) { + if (mIsLoading == loading) return; + mIsLoading = loading; + mMainHandler.sendEmptyMessage(loading ? MSG_LOAD_START : MSG_LOAD_FINISH); + } + + @Override + public void run() { + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + + boolean updateComplete = false; + while (mActive) { + synchronized (this) { + if (mActive && !mDirty && updateComplete) { + updateLoading(false); + if (mFailedVersion != MediaObject.INVALID_DATA_VERSION) { + Log.d(TAG, "reload pause"); + } + Utils.waitWithoutInterrupt(this); + if (mActive && (mFailedVersion != MediaObject.INVALID_DATA_VERSION)) { + Log.d(TAG, "reload resume"); + } + continue; + } + mDirty = false; + } + updateLoading(true); + long version = mSource.reload(); + UpdateInfo info = executeAndWait(new GetUpdateInfo(version)); + updateComplete = info == null; + if (updateComplete) continue; + if (info.version != version) { + info.size = mSource.getMediaItemCount(); + info.version = version; + } + if (info.reloadCount > 0) { + info.items = mSource.getMediaItem(info.reloadStart, info.reloadCount); + } + executeAndWait(new UpdateContent(info)); + } + updateLoading(false); + } + + public synchronized void notifyDirty() { + mDirty = true; + notifyAll(); + } + + public synchronized void terminate() { + mActive = false; + notifyAll(); + } + } +} diff --git a/src/com/android/gallery3d/app/AlbumPage.java b/src/com/android/gallery3d/app/AlbumPage.java new file mode 100644 index 000000000..658abbbd4 --- /dev/null +++ b/src/com/android/gallery3d/app/AlbumPage.java @@ -0,0 +1,786 @@ +/* + * 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.app; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.graphics.Rect; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.provider.MediaStore; +import android.view.HapticFeedbackConstants; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.widget.Toast; + +import com.android.gallery3d.R; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaDetails; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.filtershow.crop.CropActivity; +import com.android.gallery3d.filtershow.crop.CropExtras; +import com.android.gallery3d.glrenderer.FadeTexture; +import com.android.gallery3d.glrenderer.GLCanvas; +import com.android.gallery3d.ui.ActionModeHandler; +import com.android.gallery3d.ui.ActionModeHandler.ActionModeListener; +import com.android.gallery3d.ui.AlbumSlotRenderer; +import com.android.gallery3d.ui.DetailsHelper; +import com.android.gallery3d.ui.DetailsHelper.CloseListener; +import com.android.gallery3d.ui.GLRoot; +import com.android.gallery3d.ui.GLView; +import com.android.gallery3d.ui.PhotoFallbackEffect; +import com.android.gallery3d.ui.RelativePosition; +import com.android.gallery3d.ui.SelectionManager; +import com.android.gallery3d.ui.SlotView; +import com.android.gallery3d.ui.SynchronizedHandler; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.GalleryUtils; +import com.android.gallery3d.util.MediaSetUtils; + + +public class AlbumPage extends ActivityState implements GalleryActionBar.ClusterRunner, + SelectionManager.SelectionListener, MediaSet.SyncListener, GalleryActionBar.OnAlbumModeSelectedListener { + @SuppressWarnings("unused") + private static final String TAG = "AlbumPage"; + + public static final String KEY_MEDIA_PATH = "media-path"; + public static final String KEY_PARENT_MEDIA_PATH = "parent-media-path"; + public static final String KEY_SET_CENTER = "set-center"; + public static final String KEY_AUTO_SELECT_ALL = "auto-select-all"; + public static final String KEY_SHOW_CLUSTER_MENU = "cluster-menu"; + public static final String KEY_EMPTY_ALBUM = "empty-album"; + public static final String KEY_RESUME_ANIMATION = "resume_animation"; + + private static final int REQUEST_SLIDESHOW = 1; + public static final int REQUEST_PHOTO = 2; + private static final int REQUEST_DO_ANIMATION = 3; + + private static final int BIT_LOADING_RELOAD = 1; + private static final int BIT_LOADING_SYNC = 2; + + private static final float USER_DISTANCE_METER = 0.3f; + + private boolean mIsActive = false; + private AlbumSlotRenderer mAlbumView; + private Path mMediaSetPath; + private String mParentMediaSetString; + private SlotView mSlotView; + + private AlbumDataLoader mAlbumDataAdapter; + + protected SelectionManager mSelectionManager; + + private boolean mGetContent; + private boolean mShowClusterMenu; + + private ActionModeHandler mActionModeHandler; + private int mFocusIndex = 0; + private DetailsHelper mDetailsHelper; + private MyDetailsSource mDetailsSource; + private MediaSet mMediaSet; + private boolean mShowDetails; + private float mUserDistance; // in pixel + private Future<Integer> mSyncTask = null; + private boolean mLaunchedFromPhotoPage; + private boolean mInCameraApp; + private boolean mInCameraAndWantQuitOnPause; + + private int mLoadingBits = 0; + private boolean mInitialSynced = false; + private int mSyncResult; + private boolean mLoadingFailed; + private RelativePosition mOpenCenter = new RelativePosition(); + + private Handler mHandler; + private static final int MSG_PICK_PHOTO = 0; + + private PhotoFallbackEffect mResumeEffect; + private PhotoFallbackEffect.PositionProvider mPositionProvider = + new PhotoFallbackEffect.PositionProvider() { + @Override + public Rect getPosition(int index) { + Rect rect = mSlotView.getSlotRect(index); + Rect bounds = mSlotView.bounds(); + rect.offset(bounds.left - mSlotView.getScrollX(), + bounds.top - mSlotView.getScrollY()); + return rect; + } + + @Override + public int getItemIndex(Path path) { + int start = mSlotView.getVisibleStart(); + int end = mSlotView.getVisibleEnd(); + for (int i = start; i < end; ++i) { + MediaItem item = mAlbumDataAdapter.get(i); + if (item != null && item.getPath() == path) return i; + } + return -1; + } + }; + + @Override + protected int getBackgroundColorId() { + return R.color.album_background; + } + + private final GLView mRootPane = new GLView() { + private final float mMatrix[] = new float[16]; + + @Override + protected void onLayout( + boolean changed, int left, int top, int right, int bottom) { + + int slotViewTop = mActivity.getGalleryActionBar().getHeight(); + int slotViewBottom = bottom - top; + int slotViewRight = right - left; + + if (mShowDetails) { + mDetailsHelper.layout(left, slotViewTop, right, bottom); + } else { + mAlbumView.setHighlightItemPath(null); + } + + // Set the mSlotView as a reference point to the open animation + mOpenCenter.setReferencePosition(0, slotViewTop); + mSlotView.layout(0, slotViewTop, slotViewRight, slotViewBottom); + GalleryUtils.setViewPointMatrix(mMatrix, + (right - left) / 2, (bottom - top) / 2, -mUserDistance); + } + + @Override + protected void render(GLCanvas canvas) { + canvas.save(GLCanvas.SAVE_FLAG_MATRIX); + canvas.multiplyMatrix(mMatrix, 0); + super.render(canvas); + + if (mResumeEffect != null) { + boolean more = mResumeEffect.draw(canvas); + if (!more) { + mResumeEffect = null; + mAlbumView.setSlotFilter(null); + } + // We want to render one more time even when no more effect + // required. So that the animated thumbnails could be draw + // with declarations in super.render(). + invalidate(); + } + canvas.restore(); + } + }; + + // This are the transitions we want: + // + // +--------+ +------------+ +-------+ +----------+ + // | Camera |---------->| Fullscreen |--->| Album |--->| AlbumSet | + // | View | thumbnail | Photo | up | Page | up | Page | + // +--------+ +------------+ +-------+ +----------+ + // ^ | | ^ | + // | | | | | close + // +----------back--------+ +----back----+ +--back-> app + // + @Override + protected void onBackPressed() { + if (mShowDetails) { + hideDetails(); + } else if (mSelectionManager.inSelectionMode()) { + mSelectionManager.leaveSelectionMode(); + } else { + if(mLaunchedFromPhotoPage) { + mActivity.getTransitionStore().putIfNotPresent( + PhotoPage.KEY_ALBUMPAGE_TRANSITION, + PhotoPage.MSG_ALBUMPAGE_RESUMED); + } + // TODO: fix this regression + // mAlbumView.savePositions(PositionRepository.getInstance(mActivity)); + if (mInCameraApp) { + super.onBackPressed(); + } else { + onUpPressed(); + } + } + } + + private void onUpPressed() { + if (mInCameraApp) { + GalleryUtils.startGalleryActivity(mActivity); + } else if (mActivity.getStateManager().getStateCount() > 1) { + super.onBackPressed(); + } else if (mParentMediaSetString != null) { + Bundle data = new Bundle(getData()); + data.putString(AlbumSetPage.KEY_MEDIA_PATH, mParentMediaSetString); + mActivity.getStateManager().switchState( + this, AlbumSetPage.class, data); + } + } + + private void onDown(int index) { + mAlbumView.setPressedIndex(index); + } + + private void onUp(boolean followedByLongPress) { + if (followedByLongPress) { + // Avoid showing press-up animations for long-press. + mAlbumView.setPressedIndex(-1); + } else { + mAlbumView.setPressedUp(); + } + } + + private void onSingleTapUp(int slotIndex) { + if (!mIsActive) return; + + if (mSelectionManager.inSelectionMode()) { + MediaItem item = mAlbumDataAdapter.get(slotIndex); + if (item == null) return; // Item not ready yet, ignore the click + mSelectionManager.toggle(item.getPath()); + mSlotView.invalidate(); + } else { + // Render transition in pressed state + mAlbumView.setPressedIndex(slotIndex); + mAlbumView.setPressedUp(); + mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_PICK_PHOTO, slotIndex, 0), + FadeTexture.DURATION); + } + } + + private void pickPhoto(int slotIndex) { + pickPhoto(slotIndex, false); + } + + private void pickPhoto(int slotIndex, boolean startInFilmstrip) { + if (!mIsActive) return; + + if (!startInFilmstrip) { + // Launch photos in lights out mode + mActivity.getGLRoot().setLightsOutMode(true); + } + + MediaItem item = mAlbumDataAdapter.get(slotIndex); + if (item == null) return; // Item not ready yet, ignore the click + if (mGetContent) { + onGetContent(item); + } else if (mLaunchedFromPhotoPage) { + TransitionStore transitions = mActivity.getTransitionStore(); + transitions.put( + PhotoPage.KEY_ALBUMPAGE_TRANSITION, + PhotoPage.MSG_ALBUMPAGE_PICKED); + transitions.put(PhotoPage.KEY_INDEX_HINT, slotIndex); + onBackPressed(); + } else { + // Get into the PhotoPage. + // mAlbumView.savePositions(PositionRepository.getInstance(mActivity)); + Bundle data = new Bundle(); + data.putInt(PhotoPage.KEY_INDEX_HINT, slotIndex); + data.putParcelable(PhotoPage.KEY_OPEN_ANIMATION_RECT, + mSlotView.getSlotRect(slotIndex, mRootPane)); + data.putString(PhotoPage.KEY_MEDIA_SET_PATH, + mMediaSetPath.toString()); + data.putString(PhotoPage.KEY_MEDIA_ITEM_PATH, + item.getPath().toString()); + data.putInt(PhotoPage.KEY_ALBUMPAGE_TRANSITION, + PhotoPage.MSG_ALBUMPAGE_STARTED); + data.putBoolean(PhotoPage.KEY_START_IN_FILMSTRIP, + startInFilmstrip); + data.putBoolean(PhotoPage.KEY_IN_CAMERA_ROLL, mMediaSet.isCameraRoll()); + if (startInFilmstrip) { + mActivity.getStateManager().switchState(this, FilmstripPage.class, data); + } else { + mActivity.getStateManager().startStateForResult( + SinglePhotoPage.class, REQUEST_PHOTO, data); + } + } + } + + private void onGetContent(final MediaItem item) { + DataManager dm = mActivity.getDataManager(); + Activity activity = mActivity; + if (mData.getString(Gallery.EXTRA_CROP) != null) { + Uri uri = dm.getContentUri(item.getPath()); + Intent intent = new Intent(CropActivity.CROP_ACTION, uri) + .addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT) + .putExtras(getData()); + if (mData.getParcelable(MediaStore.EXTRA_OUTPUT) == null) { + intent.putExtra(CropExtras.KEY_RETURN_DATA, true); + } + activity.startActivity(intent); + activity.finish(); + } else { + Intent intent = new Intent(null, item.getContentUri()) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + activity.setResult(Activity.RESULT_OK, intent); + activity.finish(); + } + } + + public void onLongTap(int slotIndex) { + if (mGetContent) return; + MediaItem item = mAlbumDataAdapter.get(slotIndex); + if (item == null) return; + mSelectionManager.setAutoLeaveSelectionMode(true); + mSelectionManager.toggle(item.getPath()); + mSlotView.invalidate(); + } + + @Override + public void doCluster(int clusterType) { + String basePath = mMediaSet.getPath().toString(); + String newPath = FilterUtils.newClusterPath(basePath, clusterType); + Bundle data = new Bundle(getData()); + data.putString(AlbumSetPage.KEY_MEDIA_PATH, newPath); + if (mShowClusterMenu) { + Context context = mActivity.getAndroidContext(); + data.putString(AlbumSetPage.KEY_SET_TITLE, mMediaSet.getName()); + data.putString(AlbumSetPage.KEY_SET_SUBTITLE, + GalleryActionBar.getClusterByTypeString(context, clusterType)); + } + + // mAlbumView.savePositions(PositionRepository.getInstance(mActivity)); + mActivity.getStateManager().startStateForResult( + AlbumSetPage.class, REQUEST_DO_ANIMATION, data); + } + + @Override + protected void onCreate(Bundle data, Bundle restoreState) { + super.onCreate(data, restoreState); + mUserDistance = GalleryUtils.meterToPixel(USER_DISTANCE_METER); + initializeViews(); + initializeData(data); + mGetContent = data.getBoolean(Gallery.KEY_GET_CONTENT, false); + mShowClusterMenu = data.getBoolean(KEY_SHOW_CLUSTER_MENU, false); + mDetailsSource = new MyDetailsSource(); + Context context = mActivity.getAndroidContext(); + + if (data.getBoolean(KEY_AUTO_SELECT_ALL)) { + mSelectionManager.selectAll(); + } + + mLaunchedFromPhotoPage = + mActivity.getStateManager().hasStateClass(FilmstripPage.class); + mInCameraApp = data.getBoolean(PhotoPage.KEY_APP_BRIDGE, false); + + mHandler = new SynchronizedHandler(mActivity.getGLRoot()) { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_PICK_PHOTO: { + pickPhoto(message.arg1); + break; + } + default: + throw new AssertionError(message.what); + } + } + }; + } + + @Override + protected void onResume() { + super.onResume(); + mIsActive = true; + + mResumeEffect = mActivity.getTransitionStore().get(KEY_RESUME_ANIMATION); + if (mResumeEffect != null) { + mAlbumView.setSlotFilter(mResumeEffect); + mResumeEffect.setPositionProvider(mPositionProvider); + mResumeEffect.start(); + } + + setContentPane(mRootPane); + + boolean enableHomeButton = (mActivity.getStateManager().getStateCount() > 1) | + mParentMediaSetString != null; + GalleryActionBar actionBar = mActivity.getGalleryActionBar(); + actionBar.setDisplayOptions(enableHomeButton, false); + if (!mGetContent) { + actionBar.enableAlbumModeMenu(GalleryActionBar.ALBUM_GRID_MODE_SELECTED, this); + } + + // Set the reload bit here to prevent it exit this page in clearLoadingBit(). + setLoadingBit(BIT_LOADING_RELOAD); + mLoadingFailed = false; + mAlbumDataAdapter.resume(); + + mAlbumView.resume(); + mAlbumView.setPressedIndex(-1); + mActionModeHandler.resume(); + if (!mInitialSynced) { + setLoadingBit(BIT_LOADING_SYNC); + mSyncTask = mMediaSet.requestSync(this); + } + mInCameraAndWantQuitOnPause = mInCameraApp; + } + + @Override + protected void onPause() { + super.onPause(); + mIsActive = false; + + if (mSelectionManager.inSelectionMode()) { + mSelectionManager.leaveSelectionMode(); + } + mAlbumView.setSlotFilter(null); + mActionModeHandler.pause(); + mAlbumDataAdapter.pause(); + mAlbumView.pause(); + DetailsHelper.pause(); + if (!mGetContent) { + mActivity.getGalleryActionBar().disableAlbumModeMenu(true); + } + + if (mSyncTask != null) { + mSyncTask.cancel(); + mSyncTask = null; + clearLoadingBit(BIT_LOADING_SYNC); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (mAlbumDataAdapter != null) { + mAlbumDataAdapter.setLoadingListener(null); + } + mActionModeHandler.destroy(); + } + + private void initializeViews() { + mSelectionManager = new SelectionManager(mActivity, false); + mSelectionManager.setSelectionListener(this); + Config.AlbumPage config = Config.AlbumPage.get(mActivity); + mSlotView = new SlotView(mActivity, config.slotViewSpec); + mAlbumView = new AlbumSlotRenderer(mActivity, mSlotView, + mSelectionManager, config.placeholderColor); + mSlotView.setSlotRenderer(mAlbumView); + mRootPane.addComponent(mSlotView); + mSlotView.setListener(new SlotView.SimpleListener() { + @Override + public void onDown(int index) { + AlbumPage.this.onDown(index); + } + + @Override + public void onUp(boolean followedByLongPress) { + AlbumPage.this.onUp(followedByLongPress); + } + + @Override + public void onSingleTapUp(int slotIndex) { + AlbumPage.this.onSingleTapUp(slotIndex); + } + + @Override + public void onLongTap(int slotIndex) { + AlbumPage.this.onLongTap(slotIndex); + } + }); + mActionModeHandler = new ActionModeHandler(mActivity, mSelectionManager); + mActionModeHandler.setActionModeListener(new ActionModeListener() { + @Override + public boolean onActionItemClicked(MenuItem item) { + return onItemSelected(item); + } + }); + } + + private void initializeData(Bundle data) { + mMediaSetPath = Path.fromString(data.getString(KEY_MEDIA_PATH)); + mParentMediaSetString = data.getString(KEY_PARENT_MEDIA_PATH); + mMediaSet = mActivity.getDataManager().getMediaSet(mMediaSetPath); + if (mMediaSet == null) { + Utils.fail("MediaSet is null. Path = %s", mMediaSetPath); + } + mSelectionManager.setSourceMediaSet(mMediaSet); + mAlbumDataAdapter = new AlbumDataLoader(mActivity, mMediaSet); + mAlbumDataAdapter.setLoadingListener(new MyLoadingListener()); + mAlbumView.setModel(mAlbumDataAdapter); + } + + private void showDetails() { + mShowDetails = true; + if (mDetailsHelper == null) { + mDetailsHelper = new DetailsHelper(mActivity, mRootPane, mDetailsSource); + mDetailsHelper.setCloseListener(new CloseListener() { + @Override + public void onClose() { + hideDetails(); + } + }); + } + mDetailsHelper.show(); + } + + private void hideDetails() { + mShowDetails = false; + mDetailsHelper.hide(); + mAlbumView.setHighlightItemPath(null); + mSlotView.invalidate(); + } + + @Override + protected boolean onCreateActionBar(Menu menu) { + GalleryActionBar actionBar = mActivity.getGalleryActionBar(); + MenuInflater inflator = getSupportMenuInflater(); + if (mGetContent) { + inflator.inflate(R.menu.pickup, menu); + int typeBits = mData.getInt(Gallery.KEY_TYPE_BITS, + DataManager.INCLUDE_IMAGE); + actionBar.setTitle(GalleryUtils.getSelectionModePrompt(typeBits)); + } else { + inflator.inflate(R.menu.album, menu); + actionBar.setTitle(mMediaSet.getName()); + + FilterUtils.setupMenuItems(actionBar, mMediaSetPath, true); + + menu.findItem(R.id.action_group_by).setVisible(mShowClusterMenu); + menu.findItem(R.id.action_camera).setVisible( + MediaSetUtils.isCameraSource(mMediaSetPath) + && GalleryUtils.isCameraAvailable(mActivity)); + + } + actionBar.setSubtitle(null); + return true; + } + + private void prepareAnimationBackToFilmstrip(int slotIndex) { + if (mAlbumDataAdapter == null || !mAlbumDataAdapter.isActive(slotIndex)) return; + MediaItem item = mAlbumDataAdapter.get(slotIndex); + if (item == null) return; + TransitionStore transitions = mActivity.getTransitionStore(); + transitions.put(PhotoPage.KEY_INDEX_HINT, slotIndex); + transitions.put(PhotoPage.KEY_OPEN_ANIMATION_RECT, + mSlotView.getSlotRect(slotIndex, mRootPane)); + } + + private void switchToFilmstrip() { + if (mAlbumDataAdapter.size() < 1) return; + int targetPhoto = mSlotView.getVisibleStart(); + prepareAnimationBackToFilmstrip(targetPhoto); + if(mLaunchedFromPhotoPage) { + onBackPressed(); + } else { + pickPhoto(targetPhoto, true); + } + } + + @Override + protected boolean onItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: { + onUpPressed(); + return true; + } + case R.id.action_cancel: + mActivity.getStateManager().finishState(this); + return true; + case R.id.action_select: + mSelectionManager.setAutoLeaveSelectionMode(false); + mSelectionManager.enterSelectionMode(); + return true; + case R.id.action_group_by: { + mActivity.getGalleryActionBar().showClusterDialog(this); + return true; + } + case R.id.action_slideshow: { + mInCameraAndWantQuitOnPause = false; + Bundle data = new Bundle(); + data.putString(SlideshowPage.KEY_SET_PATH, + mMediaSetPath.toString()); + data.putBoolean(SlideshowPage.KEY_REPEAT, true); + mActivity.getStateManager().startStateForResult( + SlideshowPage.class, REQUEST_SLIDESHOW, data); + return true; + } + case R.id.action_details: { + if (mShowDetails) { + hideDetails(); + } else { + showDetails(); + } + return true; + } + case R.id.action_camera: { + GalleryUtils.startCameraActivity(mActivity); + return true; + } + default: + return false; + } + } + + @Override + protected void onStateResult(int request, int result, Intent data) { + switch (request) { + case REQUEST_SLIDESHOW: { + // data could be null, if there is no images in the album + if (data == null) return; + mFocusIndex = data.getIntExtra(SlideshowPage.KEY_PHOTO_INDEX, 0); + mSlotView.setCenterIndex(mFocusIndex); + break; + } + case REQUEST_PHOTO: { + if (data == null) return; + mFocusIndex = data.getIntExtra(PhotoPage.KEY_RETURN_INDEX_HINT, 0); + mSlotView.makeSlotVisible(mFocusIndex); + break; + } + case REQUEST_DO_ANIMATION: { + mSlotView.startRisingAnimation(); + break; + } + } + } + + @Override + public void onSelectionModeChange(int mode) { + switch (mode) { + case SelectionManager.ENTER_SELECTION_MODE: { + mActionModeHandler.startActionMode(); + performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + break; + } + case SelectionManager.LEAVE_SELECTION_MODE: { + mActionModeHandler.finishActionMode(); + mRootPane.invalidate(); + break; + } + case SelectionManager.SELECT_ALL_MODE: { + mActionModeHandler.updateSupportedOperation(); + mRootPane.invalidate(); + break; + } + } + } + + @Override + public void onSelectionChange(Path path, boolean selected) { + int count = mSelectionManager.getSelectedCount(); + String format = mActivity.getResources().getQuantityString( + R.plurals.number_of_items_selected, count); + mActionModeHandler.setTitle(String.format(format, count)); + mActionModeHandler.updateSupportedOperation(path, selected); + } + + @Override + public void onSyncDone(final MediaSet mediaSet, final int resultCode) { + Log.d(TAG, "onSyncDone: " + Utils.maskDebugInfo(mediaSet.getName()) + " result=" + + resultCode); + ((Activity) mActivity).runOnUiThread(new Runnable() { + @Override + public void run() { + GLRoot root = mActivity.getGLRoot(); + root.lockRenderThread(); + mSyncResult = resultCode; + try { + if (resultCode == MediaSet.SYNC_RESULT_SUCCESS) { + mInitialSynced = true; + } + clearLoadingBit(BIT_LOADING_SYNC); + showSyncErrorIfNecessary(mLoadingFailed); + } finally { + root.unlockRenderThread(); + } + } + }); + } + + // Show sync error toast when all the following conditions are met: + // (1) both loading and sync are done, + // (2) sync result is error, + // (3) the page is still active, and + // (4) no photo is shown or loading fails. + private void showSyncErrorIfNecessary(boolean loadingFailed) { + if ((mLoadingBits == 0) && (mSyncResult == MediaSet.SYNC_RESULT_ERROR) && mIsActive + && (loadingFailed || (mAlbumDataAdapter.size() == 0))) { + Toast.makeText(mActivity, R.string.sync_album_error, + Toast.LENGTH_LONG).show(); + } + } + + private void setLoadingBit(int loadTaskBit) { + mLoadingBits |= loadTaskBit; + } + + private void clearLoadingBit(int loadTaskBit) { + mLoadingBits &= ~loadTaskBit; + if (mLoadingBits == 0 && mIsActive) { + if (mAlbumDataAdapter.size() == 0) { + Intent result = new Intent(); + result.putExtra(KEY_EMPTY_ALBUM, true); + setStateResult(Activity.RESULT_OK, result); + mActivity.getStateManager().finishState(this); + } + } + } + + private class MyLoadingListener implements LoadingListener { + @Override + public void onLoadingStarted() { + setLoadingBit(BIT_LOADING_RELOAD); + mLoadingFailed = false; + } + + @Override + public void onLoadingFinished(boolean loadingFailed) { + clearLoadingBit(BIT_LOADING_RELOAD); + mLoadingFailed = loadingFailed; + showSyncErrorIfNecessary(loadingFailed); + } + } + + private class MyDetailsSource implements DetailsHelper.DetailsSource { + private int mIndex; + + @Override + public int size() { + return mAlbumDataAdapter.size(); + } + + @Override + public int setIndex() { + Path id = mSelectionManager.getSelected(false).get(0); + mIndex = mAlbumDataAdapter.findItem(id); + return mIndex; + } + + @Override + public MediaDetails getDetails() { + // this relies on setIndex() being called beforehand + MediaObject item = mAlbumDataAdapter.get(mIndex); + if (item != null) { + mAlbumView.setHighlightItemPath(item.getPath()); + return item.getDetails(); + } else { + return null; + } + } + } + + @Override + public void onAlbumModeSelected(int mode) { + if (mode == GalleryActionBar.ALBUM_FILMSTRIP_MODE_SELECTED) { + switchToFilmstrip(); + } + } +} diff --git a/src/com/android/gallery3d/app/AlbumPicker.java b/src/com/android/gallery3d/app/AlbumPicker.java new file mode 100644 index 000000000..65eb77291 --- /dev/null +++ b/src/com/android/gallery3d/app/AlbumPicker.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2011 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.app; + +import android.content.Intent; +import android.os.Bundle; + +import com.android.gallery3d.R; +import com.android.gallery3d.data.DataManager; + +public class AlbumPicker extends PickerActivity { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setTitle(R.string.select_album); + Intent intent = getIntent(); + Bundle extras = intent.getExtras(); + Bundle data = extras == null ? new Bundle() : new Bundle(extras); + + data.putBoolean(Gallery.KEY_GET_ALBUM, true); + data.putString(AlbumSetPage.KEY_MEDIA_PATH, + getDataManager().getTopSetPath(DataManager.INCLUDE_IMAGE)); + getStateManager().startState(AlbumSetPage.class, data); + } +} diff --git a/src/com/android/gallery3d/app/AlbumSetDataLoader.java b/src/com/android/gallery3d/app/AlbumSetDataLoader.java new file mode 100644 index 000000000..cf380f812 --- /dev/null +++ b/src/com/android/gallery3d/app/AlbumSetDataLoader.java @@ -0,0 +1,393 @@ +/* + * 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.app; + +import android.os.Handler; +import android.os.Message; +import android.os.Process; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.ContentListener; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.ui.SynchronizedHandler; + +import java.util.Arrays; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; + +public class AlbumSetDataLoader { + @SuppressWarnings("unused") + private static final String TAG = "AlbumSetDataAdapter"; + + private static final int INDEX_NONE = -1; + + private static final int MIN_LOAD_COUNT = 4; + + private static final int MSG_LOAD_START = 1; + private static final int MSG_LOAD_FINISH = 2; + private static final int MSG_RUN_OBJECT = 3; + + public static interface DataListener { + public void onContentChanged(int index); + public void onSizeChanged(int size); + } + + private final MediaSet[] mData; + private final MediaItem[] mCoverItem; + private final int[] mTotalCount; + private final long[] mItemVersion; + private final long[] mSetVersion; + + private int mActiveStart = 0; + private int mActiveEnd = 0; + + private int mContentStart = 0; + private int mContentEnd = 0; + + private final MediaSet mSource; + private long mSourceVersion = MediaObject.INVALID_DATA_VERSION; + private int mSize; + + private DataListener mDataListener; + private LoadingListener mLoadingListener; + private ReloadTask mReloadTask; + + private final Handler mMainHandler; + + private final MySourceListener mSourceListener = new MySourceListener(); + + public AlbumSetDataLoader(AbstractGalleryActivity activity, MediaSet albumSet, int cacheSize) { + mSource = Utils.checkNotNull(albumSet); + mCoverItem = new MediaItem[cacheSize]; + mData = new MediaSet[cacheSize]; + mTotalCount = new int[cacheSize]; + mItemVersion = new long[cacheSize]; + mSetVersion = new long[cacheSize]; + Arrays.fill(mItemVersion, MediaObject.INVALID_DATA_VERSION); + Arrays.fill(mSetVersion, MediaObject.INVALID_DATA_VERSION); + + mMainHandler = new SynchronizedHandler(activity.getGLRoot()) { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_RUN_OBJECT: + ((Runnable) message.obj).run(); + return; + case MSG_LOAD_START: + if (mLoadingListener != null) mLoadingListener.onLoadingStarted(); + return; + case MSG_LOAD_FINISH: + if (mLoadingListener != null) mLoadingListener.onLoadingFinished(false); + return; + } + } + }; + } + + public void pause() { + mReloadTask.terminate(); + mReloadTask = null; + mSource.removeContentListener(mSourceListener); + } + + public void resume() { + mSource.addContentListener(mSourceListener); + mReloadTask = new ReloadTask(); + mReloadTask.start(); + } + + private void assertIsActive(int index) { + if (index < mActiveStart && index >= mActiveEnd) { + throw new IllegalArgumentException(String.format( + "%s not in (%s, %s)", index, mActiveStart, mActiveEnd)); + } + } + + public MediaSet getMediaSet(int index) { + assertIsActive(index); + return mData[index % mData.length]; + } + + public MediaItem getCoverItem(int index) { + assertIsActive(index); + return mCoverItem[index % mCoverItem.length]; + } + + public int getTotalCount(int index) { + assertIsActive(index); + return mTotalCount[index % mTotalCount.length]; + } + + public int getActiveStart() { + return mActiveStart; + } + + public boolean isActive(int index) { + return index >= mActiveStart && index < mActiveEnd; + } + + public int size() { + return mSize; + } + + // Returns the index of the MediaSet with the given path or + // -1 if the path is not cached + public int findSet(Path id) { + int length = mData.length; + for (int i = mContentStart; i < mContentEnd; i++) { + MediaSet set = mData[i % length]; + if (set != null && id == set.getPath()) { + return i; + } + } + return -1; + } + + private void clearSlot(int slotIndex) { + mData[slotIndex] = null; + mCoverItem[slotIndex] = null; + mTotalCount[slotIndex] = 0; + mItemVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION; + mSetVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION; + } + + private void setContentWindow(int contentStart, int contentEnd) { + if (contentStart == mContentStart && contentEnd == mContentEnd) return; + int length = mCoverItem.length; + + int start = this.mContentStart; + int end = this.mContentEnd; + + mContentStart = contentStart; + mContentEnd = contentEnd; + + if (contentStart >= end || start >= contentEnd) { + for (int i = start, n = end; i < n; ++i) { + clearSlot(i % length); + } + } else { + for (int i = start; i < contentStart; ++i) { + clearSlot(i % length); + } + for (int i = contentEnd, n = end; i < n; ++i) { + clearSlot(i % length); + } + } + mReloadTask.notifyDirty(); + } + + public void setActiveWindow(int start, int end) { + if (start == mActiveStart && end == mActiveEnd) return; + + Utils.assertTrue(start <= end + && end - start <= mCoverItem.length && end <= mSize); + + mActiveStart = start; + mActiveEnd = end; + + int length = mCoverItem.length; + // If no data is visible, keep the cache content + if (start == end) return; + + int contentStart = Utils.clamp((start + end) / 2 - length / 2, + 0, Math.max(0, mSize - length)); + int contentEnd = Math.min(contentStart + length, mSize); + if (mContentStart > start || mContentEnd < end + || Math.abs(contentStart - mContentStart) > MIN_LOAD_COUNT) { + setContentWindow(contentStart, contentEnd); + } + } + + private class MySourceListener implements ContentListener { + @Override + public void onContentDirty() { + mReloadTask.notifyDirty(); + } + } + + public void setModelListener(DataListener listener) { + mDataListener = listener; + } + + public void setLoadingListener(LoadingListener listener) { + mLoadingListener = listener; + } + + private static class UpdateInfo { + public long version; + public int index; + + public int size; + public MediaSet item; + public MediaItem cover; + public int totalCount; + } + + private class GetUpdateInfo implements Callable<UpdateInfo> { + + private final long mVersion; + + public GetUpdateInfo(long version) { + mVersion = version; + } + + private int getInvalidIndex(long version) { + long setVersion[] = mSetVersion; + int length = setVersion.length; + for (int i = mContentStart, n = mContentEnd; i < n; ++i) { + int index = i % length; + if (setVersion[i % length] != version) return i; + } + return INDEX_NONE; + } + + @Override + public UpdateInfo call() throws Exception { + int index = getInvalidIndex(mVersion); + if (index == INDEX_NONE && mSourceVersion == mVersion) return null; + UpdateInfo info = new UpdateInfo(); + info.version = mSourceVersion; + info.index = index; + info.size = mSize; + return info; + } + } + + private class UpdateContent implements Callable<Void> { + private final UpdateInfo mUpdateInfo; + + public UpdateContent(UpdateInfo info) { + mUpdateInfo = info; + } + + @Override + public Void call() { + // Avoid notifying listeners of status change after pause + // Otherwise gallery will be in inconsistent state after resume. + if (mReloadTask == null) return null; + UpdateInfo info = mUpdateInfo; + mSourceVersion = info.version; + if (mSize != info.size) { + mSize = info.size; + if (mDataListener != null) mDataListener.onSizeChanged(mSize); + if (mContentEnd > mSize) mContentEnd = mSize; + if (mActiveEnd > mSize) mActiveEnd = mSize; + } + // Note: info.index could be INDEX_NONE, i.e., -1 + if (info.index >= mContentStart && info.index < mContentEnd) { + int pos = info.index % mCoverItem.length; + mSetVersion[pos] = info.version; + long itemVersion = info.item.getDataVersion(); + if (mItemVersion[pos] == itemVersion) return null; + mItemVersion[pos] = itemVersion; + mData[pos] = info.item; + mCoverItem[pos] = info.cover; + mTotalCount[pos] = info.totalCount; + if (mDataListener != null + && info.index >= mActiveStart && info.index < mActiveEnd) { + mDataListener.onContentChanged(info.index); + } + } + return null; + } + } + + private <T> T executeAndWait(Callable<T> callable) { + FutureTask<T> task = new FutureTask<T>(callable); + mMainHandler.sendMessage( + mMainHandler.obtainMessage(MSG_RUN_OBJECT, task)); + try { + return task.get(); + } catch (InterruptedException e) { + return null; + } catch (ExecutionException e) { + throw new RuntimeException(e); + } + } + + // TODO: load active range first + private class ReloadTask extends Thread { + private volatile boolean mActive = true; + private volatile boolean mDirty = true; + private volatile boolean mIsLoading = false; + + private void updateLoading(boolean loading) { + if (mIsLoading == loading) return; + mIsLoading = loading; + mMainHandler.sendEmptyMessage(loading ? MSG_LOAD_START : MSG_LOAD_FINISH); + } + + @Override + public void run() { + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + + boolean updateComplete = false; + while (mActive) { + synchronized (this) { + if (mActive && !mDirty && updateComplete) { + if (!mSource.isLoading()) updateLoading(false); + Utils.waitWithoutInterrupt(this); + continue; + } + } + mDirty = false; + updateLoading(true); + + long version = mSource.reload(); + UpdateInfo info = executeAndWait(new GetUpdateInfo(version)); + updateComplete = info == null; + if (updateComplete) continue; + if (info.version != version) { + info.version = version; + info.size = mSource.getSubMediaSetCount(); + + // If the size becomes smaller after reload(), we may + // receive from GetUpdateInfo an index which is too + // big. Because the main thread is not aware of the size + // change until we call UpdateContent. + if (info.index >= info.size) { + info.index = INDEX_NONE; + } + } + if (info.index != INDEX_NONE) { + info.item = mSource.getSubMediaSet(info.index); + if (info.item == null) continue; + info.cover = info.item.getCoverMediaItem(); + info.totalCount = info.item.getTotalMediaItemCount(); + } + executeAndWait(new UpdateContent(info)); + } + updateLoading(false); + } + + public synchronized void notifyDirty() { + mDirty = true; + notifyAll(); + } + + public synchronized void terminate() { + mActive = false; + notifyAll(); + } + } +} + + diff --git a/src/com/android/gallery3d/app/AlbumSetPage.java b/src/com/android/gallery3d/app/AlbumSetPage.java new file mode 100644 index 000000000..dd9d8ec41 --- /dev/null +++ b/src/com/android/gallery3d/app/AlbumSetPage.java @@ -0,0 +1,764 @@ +/* + * 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.app; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.view.HapticFeedbackConstants; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.RelativeLayout; +import android.widget.Toast; + +import com.android.gallery3d.R; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaDetails; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.glrenderer.FadeTexture; +import com.android.gallery3d.glrenderer.GLCanvas; +import com.android.gallery3d.picasasource.PicasaSource; +import com.android.gallery3d.settings.GallerySettings; +import com.android.gallery3d.ui.ActionModeHandler; +import com.android.gallery3d.ui.ActionModeHandler.ActionModeListener; +import com.android.gallery3d.ui.AlbumSetSlotRenderer; +import com.android.gallery3d.ui.DetailsHelper; +import com.android.gallery3d.ui.DetailsHelper.CloseListener; +import com.android.gallery3d.ui.GLRoot; +import com.android.gallery3d.ui.GLView; +import com.android.gallery3d.ui.SelectionManager; +import com.android.gallery3d.ui.SlotView; +import com.android.gallery3d.ui.SynchronizedHandler; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.GalleryUtils; +import com.android.gallery3d.util.HelpUtils; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; + +public class AlbumSetPage extends ActivityState implements + SelectionManager.SelectionListener, GalleryActionBar.ClusterRunner, + EyePosition.EyePositionListener, MediaSet.SyncListener { + @SuppressWarnings("unused") + private static final String TAG = "AlbumSetPage"; + + private static final int MSG_PICK_ALBUM = 1; + + public static final String KEY_MEDIA_PATH = "media-path"; + public static final String KEY_SET_TITLE = "set-title"; + public static final String KEY_SET_SUBTITLE = "set-subtitle"; + public static final String KEY_SELECTED_CLUSTER_TYPE = "selected-cluster"; + + private static final int DATA_CACHE_SIZE = 256; + private static final int REQUEST_DO_ANIMATION = 1; + + private static final int BIT_LOADING_RELOAD = 1; + private static final int BIT_LOADING_SYNC = 2; + + private boolean mIsActive = false; + private SlotView mSlotView; + private AlbumSetSlotRenderer mAlbumSetView; + private Config.AlbumSetPage mConfig; + + private MediaSet mMediaSet; + private String mTitle; + private String mSubtitle; + private boolean mShowClusterMenu; + private GalleryActionBar mActionBar; + private int mSelectedAction; + + protected SelectionManager mSelectionManager; + private AlbumSetDataLoader mAlbumSetDataAdapter; + + private boolean mGetContent; + private boolean mGetAlbum; + private ActionModeHandler mActionModeHandler; + private DetailsHelper mDetailsHelper; + private MyDetailsSource mDetailsSource; + private boolean mShowDetails; + private EyePosition mEyePosition; + private Handler mHandler; + + // The eyes' position of the user, the origin is at the center of the + // device and the unit is in pixels. + private float mX; + private float mY; + private float mZ; + + private Future<Integer> mSyncTask = null; + + private int mLoadingBits = 0; + private boolean mInitialSynced = false; + + private Button mCameraButton; + private boolean mShowedEmptyToastForSelf = false; + + @Override + protected int getBackgroundColorId() { + return R.color.albumset_background; + } + + private final GLView mRootPane = new GLView() { + private final float mMatrix[] = new float[16]; + + @Override + protected void onLayout( + boolean changed, int left, int top, int right, int bottom) { + mEyePosition.resetPosition(); + + int slotViewTop = mActionBar.getHeight() + mConfig.paddingTop; + int slotViewBottom = bottom - top - mConfig.paddingBottom; + int slotViewRight = right - left; + + if (mShowDetails) { + mDetailsHelper.layout(left, slotViewTop, right, bottom); + } else { + mAlbumSetView.setHighlightItemPath(null); + } + + mSlotView.layout(0, slotViewTop, slotViewRight, slotViewBottom); + } + + @Override + protected void render(GLCanvas canvas) { + canvas.save(GLCanvas.SAVE_FLAG_MATRIX); + GalleryUtils.setViewPointMatrix(mMatrix, + getWidth() / 2 + mX, getHeight() / 2 + mY, mZ); + canvas.multiplyMatrix(mMatrix, 0); + super.render(canvas); + canvas.restore(); + } + }; + + @Override + public void onEyePositionChanged(float x, float y, float z) { + mRootPane.lockRendering(); + mX = x; + mY = y; + mZ = z; + mRootPane.unlockRendering(); + mRootPane.invalidate(); + } + + @Override + public void onBackPressed() { + if (mShowDetails) { + hideDetails(); + } else if (mSelectionManager.inSelectionMode()) { + mSelectionManager.leaveSelectionMode(); + } else { + super.onBackPressed(); + } + } + + private void getSlotCenter(int slotIndex, int center[]) { + Rect offset = new Rect(); + mRootPane.getBoundsOf(mSlotView, offset); + Rect r = mSlotView.getSlotRect(slotIndex); + int scrollX = mSlotView.getScrollX(); + int scrollY = mSlotView.getScrollY(); + center[0] = offset.left + (r.left + r.right) / 2 - scrollX; + center[1] = offset.top + (r.top + r.bottom) / 2 - scrollY; + } + + public void onSingleTapUp(int slotIndex) { + if (!mIsActive) return; + + if (mSelectionManager.inSelectionMode()) { + MediaSet targetSet = mAlbumSetDataAdapter.getMediaSet(slotIndex); + if (targetSet == null) return; // Content is dirty, we shall reload soon + mSelectionManager.toggle(targetSet.getPath()); + mSlotView.invalidate(); + } else { + // Show pressed-up animation for the single-tap. + mAlbumSetView.setPressedIndex(slotIndex); + mAlbumSetView.setPressedUp(); + mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_PICK_ALBUM, slotIndex, 0), + FadeTexture.DURATION); + } + } + + private static boolean albumShouldOpenInFilmstrip(MediaSet album) { + int itemCount = album.getMediaItemCount(); + ArrayList<MediaItem> list = (itemCount == 1) ? album.getMediaItem(0, 1) : null; + // open in film strip only if there's one item in the album and the item exists + return (list != null && !list.isEmpty()); + } + + WeakReference<Toast> mEmptyAlbumToast = null; + + private void showEmptyAlbumToast(int toastLength) { + Toast toast; + if (mEmptyAlbumToast != null) { + toast = mEmptyAlbumToast.get(); + if (toast != null) { + toast.show(); + return; + } + } + toast = Toast.makeText(mActivity, R.string.empty_album, toastLength); + mEmptyAlbumToast = new WeakReference<Toast>(toast); + toast.show(); + } + + private void hideEmptyAlbumToast() { + if (mEmptyAlbumToast != null) { + Toast toast = mEmptyAlbumToast.get(); + if (toast != null) toast.cancel(); + } + } + + private void pickAlbum(int slotIndex) { + if (!mIsActive) return; + + MediaSet targetSet = mAlbumSetDataAdapter.getMediaSet(slotIndex); + if (targetSet == null) return; // Content is dirty, we shall reload soon + if (targetSet.getTotalMediaItemCount() == 0) { + showEmptyAlbumToast(Toast.LENGTH_SHORT); + return; + } + hideEmptyAlbumToast(); + + String mediaPath = targetSet.getPath().toString(); + + Bundle data = new Bundle(getData()); + int[] center = new int[2]; + getSlotCenter(slotIndex, center); + data.putIntArray(AlbumPage.KEY_SET_CENTER, center); + if (mGetAlbum && targetSet.isLeafAlbum()) { + Activity activity = mActivity; + Intent result = new Intent() + .putExtra(AlbumPicker.KEY_ALBUM_PATH, targetSet.getPath().toString()); + activity.setResult(Activity.RESULT_OK, result); + activity.finish(); + } else if (targetSet.getSubMediaSetCount() > 0) { + data.putString(AlbumSetPage.KEY_MEDIA_PATH, mediaPath); + mActivity.getStateManager().startStateForResult( + AlbumSetPage.class, REQUEST_DO_ANIMATION, data); + } else { + if (!mGetContent && albumShouldOpenInFilmstrip(targetSet)) { + data.putParcelable(PhotoPage.KEY_OPEN_ANIMATION_RECT, + mSlotView.getSlotRect(slotIndex, mRootPane)); + data.putInt(PhotoPage.KEY_INDEX_HINT, 0); + data.putString(PhotoPage.KEY_MEDIA_SET_PATH, + mediaPath); + data.putBoolean(PhotoPage.KEY_START_IN_FILMSTRIP, true); + data.putBoolean(PhotoPage.KEY_IN_CAMERA_ROLL, targetSet.isCameraRoll()); + mActivity.getStateManager().startStateForResult( + FilmstripPage.class, AlbumPage.REQUEST_PHOTO, data); + return; + } + data.putString(AlbumPage.KEY_MEDIA_PATH, mediaPath); + + // We only show cluster menu in the first AlbumPage in stack + boolean inAlbum = mActivity.getStateManager().hasStateClass(AlbumPage.class); + data.putBoolean(AlbumPage.KEY_SHOW_CLUSTER_MENU, !inAlbum); + mActivity.getStateManager().startStateForResult( + AlbumPage.class, REQUEST_DO_ANIMATION, data); + } + } + + private void onDown(int index) { + mAlbumSetView.setPressedIndex(index); + } + + private void onUp(boolean followedByLongPress) { + if (followedByLongPress) { + // Avoid showing press-up animations for long-press. + mAlbumSetView.setPressedIndex(-1); + } else { + mAlbumSetView.setPressedUp(); + } + } + + public void onLongTap(int slotIndex) { + if (mGetContent || mGetAlbum) return; + MediaSet set = mAlbumSetDataAdapter.getMediaSet(slotIndex); + if (set == null) return; + mSelectionManager.setAutoLeaveSelectionMode(true); + mSelectionManager.toggle(set.getPath()); + mSlotView.invalidate(); + } + + @Override + public void doCluster(int clusterType) { + String basePath = mMediaSet.getPath().toString(); + String newPath = FilterUtils.switchClusterPath(basePath, clusterType); + Bundle data = new Bundle(getData()); + data.putString(AlbumSetPage.KEY_MEDIA_PATH, newPath); + data.putInt(KEY_SELECTED_CLUSTER_TYPE, clusterType); + mActivity.getStateManager().switchState(this, AlbumSetPage.class, data); + } + + @Override + public void onCreate(Bundle data, Bundle restoreState) { + super.onCreate(data, restoreState); + initializeViews(); + initializeData(data); + Context context = mActivity.getAndroidContext(); + mGetContent = data.getBoolean(Gallery.KEY_GET_CONTENT, false); + mGetAlbum = data.getBoolean(Gallery.KEY_GET_ALBUM, false); + mTitle = data.getString(AlbumSetPage.KEY_SET_TITLE); + mSubtitle = data.getString(AlbumSetPage.KEY_SET_SUBTITLE); + mEyePosition = new EyePosition(context, this); + mDetailsSource = new MyDetailsSource(); + mActionBar = mActivity.getGalleryActionBar(); + mSelectedAction = data.getInt(AlbumSetPage.KEY_SELECTED_CLUSTER_TYPE, + FilterUtils.CLUSTER_BY_ALBUM); + + mHandler = new SynchronizedHandler(mActivity.getGLRoot()) { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_PICK_ALBUM: { + pickAlbum(message.arg1); + break; + } + default: throw new AssertionError(message.what); + } + } + }; + } + + @Override + public void onDestroy() { + super.onDestroy(); + cleanupCameraButton(); + mActionModeHandler.destroy(); + } + + private boolean setupCameraButton() { + if (!GalleryUtils.isCameraAvailable(mActivity)) return false; + RelativeLayout galleryRoot = (RelativeLayout) ((Activity) mActivity) + .findViewById(R.id.gallery_root); + if (galleryRoot == null) return false; + + mCameraButton = new Button(mActivity); + mCameraButton.setText(R.string.camera_label); + mCameraButton.setCompoundDrawablesWithIntrinsicBounds(0, R.drawable.frame_overlay_gallery_camera, 0, 0); + mCameraButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View arg0) { + GalleryUtils.startCameraActivity(mActivity); + } + }); + RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams( + RelativeLayout.LayoutParams.WRAP_CONTENT, + RelativeLayout.LayoutParams.WRAP_CONTENT); + lp.addRule(RelativeLayout.CENTER_IN_PARENT); + galleryRoot.addView(mCameraButton, lp); + return true; + } + + private void cleanupCameraButton() { + if (mCameraButton == null) return; + RelativeLayout galleryRoot = (RelativeLayout) ((Activity) mActivity) + .findViewById(R.id.gallery_root); + if (galleryRoot == null) return; + galleryRoot.removeView(mCameraButton); + mCameraButton = null; + } + + private void showCameraButton() { + if (mCameraButton == null && !setupCameraButton()) return; + mCameraButton.setVisibility(View.VISIBLE); + } + + private void hideCameraButton() { + if (mCameraButton == null) return; + mCameraButton.setVisibility(View.GONE); + } + + private void clearLoadingBit(int loadingBit) { + mLoadingBits &= ~loadingBit; + if (mLoadingBits == 0 && mIsActive) { + if (mAlbumSetDataAdapter.size() == 0) { + // If this is not the top of the gallery folder hierarchy, + // tell the parent AlbumSetPage instance to handle displaying + // the empty album toast, otherwise show it within this + // instance + if (mActivity.getStateManager().getStateCount() > 1) { + Intent result = new Intent(); + result.putExtra(AlbumPage.KEY_EMPTY_ALBUM, true); + setStateResult(Activity.RESULT_OK, result); + mActivity.getStateManager().finishState(this); + } else { + mShowedEmptyToastForSelf = true; + showEmptyAlbumToast(Toast.LENGTH_LONG); + mSlotView.invalidate(); + showCameraButton(); + } + return; + } + } + // Hide the empty album toast if we are in the root instance of + // AlbumSetPage and the album is no longer empty (for instance, + // after a sync is completed and web albums have been synced) + if (mShowedEmptyToastForSelf) { + mShowedEmptyToastForSelf = false; + hideEmptyAlbumToast(); + hideCameraButton(); + } + } + + private void setLoadingBit(int loadingBit) { + mLoadingBits |= loadingBit; + } + + @Override + public void onPause() { + super.onPause(); + mIsActive = false; + mAlbumSetDataAdapter.pause(); + mAlbumSetView.pause(); + mActionModeHandler.pause(); + mEyePosition.pause(); + DetailsHelper.pause(); + // Call disableClusterMenu to avoid receiving callback after paused. + // Don't hide menu here otherwise the list menu will disappear earlier than + // the action bar, which is janky and unwanted behavior. + mActionBar.disableClusterMenu(false); + if (mSyncTask != null) { + mSyncTask.cancel(); + mSyncTask = null; + clearLoadingBit(BIT_LOADING_SYNC); + } + } + + @Override + public void onResume() { + super.onResume(); + mIsActive = true; + setContentPane(mRootPane); + + // Set the reload bit here to prevent it exit this page in clearLoadingBit(). + setLoadingBit(BIT_LOADING_RELOAD); + mAlbumSetDataAdapter.resume(); + + mAlbumSetView.resume(); + mEyePosition.resume(); + mActionModeHandler.resume(); + if (mShowClusterMenu) { + mActionBar.enableClusterMenu(mSelectedAction, this); + } + if (!mInitialSynced) { + setLoadingBit(BIT_LOADING_SYNC); + mSyncTask = mMediaSet.requestSync(AlbumSetPage.this); + } + } + + private void initializeData(Bundle data) { + String mediaPath = data.getString(AlbumSetPage.KEY_MEDIA_PATH); + mMediaSet = mActivity.getDataManager().getMediaSet(mediaPath); + mSelectionManager.setSourceMediaSet(mMediaSet); + mAlbumSetDataAdapter = new AlbumSetDataLoader( + mActivity, mMediaSet, DATA_CACHE_SIZE); + mAlbumSetDataAdapter.setLoadingListener(new MyLoadingListener()); + mAlbumSetView.setModel(mAlbumSetDataAdapter); + } + + private void initializeViews() { + mSelectionManager = new SelectionManager(mActivity, true); + mSelectionManager.setSelectionListener(this); + + mConfig = Config.AlbumSetPage.get(mActivity); + mSlotView = new SlotView(mActivity, mConfig.slotViewSpec); + mAlbumSetView = new AlbumSetSlotRenderer( + mActivity, mSelectionManager, mSlotView, mConfig.labelSpec, + mConfig.placeholderColor); + mSlotView.setSlotRenderer(mAlbumSetView); + mSlotView.setListener(new SlotView.SimpleListener() { + @Override + public void onDown(int index) { + AlbumSetPage.this.onDown(index); + } + + @Override + public void onUp(boolean followedByLongPress) { + AlbumSetPage.this.onUp(followedByLongPress); + } + + @Override + public void onSingleTapUp(int slotIndex) { + AlbumSetPage.this.onSingleTapUp(slotIndex); + } + + @Override + public void onLongTap(int slotIndex) { + AlbumSetPage.this.onLongTap(slotIndex); + } + }); + + mActionModeHandler = new ActionModeHandler(mActivity, mSelectionManager); + mActionModeHandler.setActionModeListener(new ActionModeListener() { + @Override + public boolean onActionItemClicked(MenuItem item) { + return onItemSelected(item); + } + }); + mRootPane.addComponent(mSlotView); + } + + @Override + protected boolean onCreateActionBar(Menu menu) { + Activity activity = mActivity; + final boolean inAlbum = mActivity.getStateManager().hasStateClass(AlbumPage.class); + MenuInflater inflater = getSupportMenuInflater(); + + if (mGetContent) { + inflater.inflate(R.menu.pickup, menu); + int typeBits = mData.getInt( + Gallery.KEY_TYPE_BITS, DataManager.INCLUDE_IMAGE); + mActionBar.setTitle(GalleryUtils.getSelectionModePrompt(typeBits)); + } else if (mGetAlbum) { + inflater.inflate(R.menu.pickup, menu); + mActionBar.setTitle(R.string.select_album); + } else { + inflater.inflate(R.menu.albumset, menu); + boolean wasShowingClusterMenu = mShowClusterMenu; + mShowClusterMenu = !inAlbum; + boolean selectAlbums = !inAlbum && + mActionBar.getClusterTypeAction() == FilterUtils.CLUSTER_BY_ALBUM; + MenuItem selectItem = menu.findItem(R.id.action_select); + selectItem.setTitle(activity.getString( + selectAlbums ? R.string.select_album : R.string.select_group)); + + MenuItem cameraItem = menu.findItem(R.id.action_camera); + cameraItem.setVisible(GalleryUtils.isCameraAvailable(activity)); + + FilterUtils.setupMenuItems(mActionBar, mMediaSet.getPath(), false); + + Intent helpIntent = HelpUtils.getHelpIntent(activity); + + MenuItem helpItem = menu.findItem(R.id.action_general_help); + helpItem.setVisible(helpIntent != null); + if (helpIntent != null) helpItem.setIntent(helpIntent); + + mActionBar.setTitle(mTitle); + mActionBar.setSubtitle(mSubtitle); + if (mShowClusterMenu != wasShowingClusterMenu) { + if (mShowClusterMenu) { + mActionBar.enableClusterMenu(mSelectedAction, this); + } else { + mActionBar.disableClusterMenu(true); + } + } + } + return true; + } + + @Override + protected boolean onItemSelected(MenuItem item) { + Activity activity = mActivity; + switch (item.getItemId()) { + case R.id.action_cancel: + activity.setResult(Activity.RESULT_CANCELED); + activity.finish(); + return true; + case R.id.action_select: + mSelectionManager.setAutoLeaveSelectionMode(false); + mSelectionManager.enterSelectionMode(); + return true; + case R.id.action_details: + if (mAlbumSetDataAdapter.size() != 0) { + if (mShowDetails) { + hideDetails(); + } else { + showDetails(); + } + } else { + Toast.makeText(activity, + activity.getText(R.string.no_albums_alert), + Toast.LENGTH_SHORT).show(); + } + return true; + case R.id.action_camera: { + GalleryUtils.startCameraActivity(activity); + return true; + } + case R.id.action_manage_offline: { + Bundle data = new Bundle(); + String mediaPath = mActivity.getDataManager().getTopSetPath( + DataManager.INCLUDE_ALL); + data.putString(AlbumSetPage.KEY_MEDIA_PATH, mediaPath); + mActivity.getStateManager().startState(ManageCachePage.class, data); + return true; + } + case R.id.action_sync_picasa_albums: { + PicasaSource.requestSync(activity); + return true; + } + case R.id.action_settings: { + activity.startActivity(new Intent(activity, GallerySettings.class)); + return true; + } + default: + return false; + } + } + + @Override + protected void onStateResult(int requestCode, int resultCode, Intent data) { + if (data != null && data.getBooleanExtra(AlbumPage.KEY_EMPTY_ALBUM, false)) { + showEmptyAlbumToast(Toast.LENGTH_SHORT); + } + switch (requestCode) { + case REQUEST_DO_ANIMATION: { + mSlotView.startRisingAnimation(); + } + } + } + + private String getSelectedString() { + int count = mSelectionManager.getSelectedCount(); + int action = mActionBar.getClusterTypeAction(); + int string = action == FilterUtils.CLUSTER_BY_ALBUM + ? R.plurals.number_of_albums_selected + : R.plurals.number_of_groups_selected; + String format = mActivity.getResources().getQuantityString(string, count); + return String.format(format, count); + } + + @Override + public void onSelectionModeChange(int mode) { + switch (mode) { + case SelectionManager.ENTER_SELECTION_MODE: { + mActionBar.disableClusterMenu(true); + mActionModeHandler.startActionMode(); + performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + break; + } + case SelectionManager.LEAVE_SELECTION_MODE: { + mActionModeHandler.finishActionMode(); + if (mShowClusterMenu) { + mActionBar.enableClusterMenu(mSelectedAction, this); + } + mRootPane.invalidate(); + break; + } + case SelectionManager.SELECT_ALL_MODE: { + mActionModeHandler.updateSupportedOperation(); + mRootPane.invalidate(); + break; + } + } + } + + @Override + public void onSelectionChange(Path path, boolean selected) { + mActionModeHandler.setTitle(getSelectedString()); + mActionModeHandler.updateSupportedOperation(path, selected); + } + + private void hideDetails() { + mShowDetails = false; + mDetailsHelper.hide(); + mAlbumSetView.setHighlightItemPath(null); + mSlotView.invalidate(); + } + + private void showDetails() { + mShowDetails = true; + if (mDetailsHelper == null) { + mDetailsHelper = new DetailsHelper(mActivity, mRootPane, mDetailsSource); + mDetailsHelper.setCloseListener(new CloseListener() { + @Override + public void onClose() { + hideDetails(); + } + }); + } + mDetailsHelper.show(); + } + + @Override + public void onSyncDone(final MediaSet mediaSet, final int resultCode) { + if (resultCode == MediaSet.SYNC_RESULT_ERROR) { + Log.d(TAG, "onSyncDone: " + Utils.maskDebugInfo(mediaSet.getName()) + " result=" + + resultCode); + } + ((Activity) mActivity).runOnUiThread(new Runnable() { + @Override + public void run() { + GLRoot root = mActivity.getGLRoot(); + root.lockRenderThread(); + try { + if (resultCode == MediaSet.SYNC_RESULT_SUCCESS) { + mInitialSynced = true; + } + clearLoadingBit(BIT_LOADING_SYNC); + if (resultCode == MediaSet.SYNC_RESULT_ERROR && mIsActive) { + Log.w(TAG, "failed to load album set"); + } + } finally { + root.unlockRenderThread(); + } + } + }); + } + + private class MyLoadingListener implements LoadingListener { + @Override + public void onLoadingStarted() { + setLoadingBit(BIT_LOADING_RELOAD); + } + + @Override + public void onLoadingFinished(boolean loadingFailed) { + clearLoadingBit(BIT_LOADING_RELOAD); + } + } + + private class MyDetailsSource implements DetailsHelper.DetailsSource { + private int mIndex; + + @Override + public int size() { + return mAlbumSetDataAdapter.size(); + } + + @Override + public int setIndex() { + Path id = mSelectionManager.getSelected(false).get(0); + mIndex = mAlbumSetDataAdapter.findSet(id); + return mIndex; + } + + @Override + public MediaDetails getDetails() { + MediaObject item = mAlbumSetDataAdapter.getMediaSet(mIndex); + if (item != null) { + mAlbumSetView.setHighlightItemPath(item.getPath()); + return item.getDetails(); + } else { + return null; + } + } + } +} diff --git a/src/com/android/gallery3d/app/AppBridge.java b/src/com/android/gallery3d/app/AppBridge.java new file mode 100644 index 000000000..ee55fa6db --- /dev/null +++ b/src/com/android/gallery3d/app/AppBridge.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2012 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.app; + +import android.graphics.Rect; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.gallery3d.ui.ScreenNail; + +// This is the bridge to connect a PhotoPage to the external environment. +public abstract class AppBridge implements Parcelable { + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + } + + ////////////////////////////////////////////////////////////////////////// + // These are requests sent from PhotoPage to the app + ////////////////////////////////////////////////////////////////////////// + + public abstract boolean isPanorama(); + public abstract boolean isStaticCamera(); + public abstract ScreenNail attachScreenNail(); + public abstract void detachScreenNail(); + + // Return true if the tap is consumed. + public abstract boolean onSingleTapUp(int x, int y); + + // This is used to notify that the screen nail will be drawn in full screen + // or not in next draw() call. + public abstract void onFullScreenChanged(boolean full); + + ////////////////////////////////////////////////////////////////////////// + // These are requests send from app to PhotoPage + ////////////////////////////////////////////////////////////////////////// + + public interface Server { + // Set the camera frame relative to GLRootView. + public void setCameraRelativeFrame(Rect frame); + // Switch to the previous or next picture using the capture animation. + // The offset is -1 to switch to the previous picture, 1 to switch to + // the next picture. + public boolean switchWithCaptureAnimation(int offset); + // Enable or disable the swiping gestures (the default is enabled). + public void setSwipingEnabled(boolean enabled); + // Notify that the ScreenNail is changed. + public void notifyScreenNailChanged(); + // Add a new media item to the secure album. + public void addSecureAlbumItem(boolean isVideo, int id); + } + + // If server is null, the services are not available. + public abstract void setServer(Server server); +} diff --git a/src/com/android/gallery3d/app/BatchService.java b/src/com/android/gallery3d/app/BatchService.java new file mode 100644 index 000000000..564001d5b --- /dev/null +++ b/src/com/android/gallery3d/app/BatchService.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2012 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.app; + +import android.app.Service; +import android.content.Intent; +import android.os.Binder; +import android.os.IBinder; + +import com.android.gallery3d.util.ThreadPool; + +public class BatchService extends Service { + + public class LocalBinder extends Binder { + BatchService getService() { + return BatchService.this; + } + } + + private final IBinder mBinder = new LocalBinder(); + private ThreadPool mThreadPool = new ThreadPool(1, 1); + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + // The threadpool returned by getThreadPool must have only 1 thread + // running at a time, as MenuExecutor (atrociously) depends on this + // guarantee for synchronization. + public ThreadPool getThreadPool() { + return mThreadPool; + } +} diff --git a/src/com/android/gallery3d/app/CommonControllerOverlay.java b/src/com/android/gallery3d/app/CommonControllerOverlay.java new file mode 100644 index 000000000..9adb4e7a8 --- /dev/null +++ b/src/com/android/gallery3d/app/CommonControllerOverlay.java @@ -0,0 +1,346 @@ +/* + * Copyright (C) 2012 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.app; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Rect; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.ImageView.ScaleType; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import com.android.gallery3d.R; + +/** + * The common playback controller for the Movie Player or Video Trimming. + */ +public abstract class CommonControllerOverlay extends FrameLayout implements + ControllerOverlay, + OnClickListener, + TimeBar.Listener { + + protected enum State { + PLAYING, + PAUSED, + ENDED, + ERROR, + LOADING + } + + private static final float ERROR_MESSAGE_RELATIVE_PADDING = 1.0f / 6; + + protected Listener mListener; + + protected final View mBackground; + protected TimeBar mTimeBar; + + protected View mMainView; + protected final LinearLayout mLoadingView; + protected final TextView mErrorView; + protected final ImageView mPlayPauseReplayView; + + protected State mState; + + protected boolean mCanReplay = true; + + public void setSeekable(boolean canSeek) { + mTimeBar.setSeekable(canSeek); + } + + public CommonControllerOverlay(Context context) { + super(context); + + mState = State.LOADING; + // TODO: Move the following layout code into xml file. + LayoutParams wrapContent = + new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + LayoutParams matchParent = + new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + + mBackground = new View(context); + mBackground.setBackgroundColor(context.getResources().getColor(R.color.darker_transparent)); + addView(mBackground, matchParent); + + // Depending on the usage, the timeBar can show a single scrubber, or + // multiple ones for trimming. + createTimeBar(context); + addView(mTimeBar, wrapContent); + mTimeBar.setContentDescription( + context.getResources().getString(R.string.accessibility_time_bar)); + mLoadingView = new LinearLayout(context); + mLoadingView.setOrientation(LinearLayout.VERTICAL); + mLoadingView.setGravity(Gravity.CENTER_HORIZONTAL); + ProgressBar spinner = new ProgressBar(context); + spinner.setIndeterminate(true); + mLoadingView.addView(spinner, wrapContent); + TextView loadingText = createOverlayTextView(context); + loadingText.setText(R.string.loading_video); + mLoadingView.addView(loadingText, wrapContent); + addView(mLoadingView, wrapContent); + + mPlayPauseReplayView = new ImageView(context); + mPlayPauseReplayView.setImageResource(R.drawable.ic_vidcontrol_play); + mPlayPauseReplayView.setContentDescription( + context.getResources().getString(R.string.accessibility_play_video)); + mPlayPauseReplayView.setBackgroundResource(R.drawable.bg_vidcontrol); + mPlayPauseReplayView.setScaleType(ScaleType.CENTER); + mPlayPauseReplayView.setFocusable(true); + mPlayPauseReplayView.setClickable(true); + mPlayPauseReplayView.setOnClickListener(this); + addView(mPlayPauseReplayView, wrapContent); + + mErrorView = createOverlayTextView(context); + addView(mErrorView, matchParent); + + RelativeLayout.LayoutParams params = + new RelativeLayout.LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + setLayoutParams(params); + hide(); + } + + abstract protected void createTimeBar(Context context); + + private TextView createOverlayTextView(Context context) { + TextView view = new TextView(context); + view.setGravity(Gravity.CENTER); + view.setTextColor(0xFFFFFFFF); + view.setPadding(0, 15, 0, 15); + return view; + } + + @Override + public void setListener(Listener listener) { + this.mListener = listener; + } + + @Override + public void setCanReplay(boolean canReplay) { + this.mCanReplay = canReplay; + } + + @Override + public View getView() { + return this; + } + + @Override + public void showPlaying() { + mState = State.PLAYING; + showMainView(mPlayPauseReplayView); + } + + @Override + public void showPaused() { + mState = State.PAUSED; + showMainView(mPlayPauseReplayView); + } + + @Override + public void showEnded() { + mState = State.ENDED; + if (mCanReplay) showMainView(mPlayPauseReplayView); + } + + @Override + public void showLoading() { + mState = State.LOADING; + showMainView(mLoadingView); + } + + @Override + public void showErrorMessage(String message) { + mState = State.ERROR; + int padding = (int) (getMeasuredWidth() * ERROR_MESSAGE_RELATIVE_PADDING); + mErrorView.setPadding( + padding, mErrorView.getPaddingTop(), padding, mErrorView.getPaddingBottom()); + mErrorView.setText(message); + showMainView(mErrorView); + } + + @Override + public void setTimes(int currentTime, int totalTime, + int trimStartTime, int trimEndTime) { + mTimeBar.setTime(currentTime, totalTime, trimStartTime, trimEndTime); + } + + public void hide() { + mPlayPauseReplayView.setVisibility(View.INVISIBLE); + mLoadingView.setVisibility(View.INVISIBLE); + mBackground.setVisibility(View.INVISIBLE); + mTimeBar.setVisibility(View.INVISIBLE); + setVisibility(View.INVISIBLE); + setFocusable(true); + requestFocus(); + } + + private void showMainView(View view) { + mMainView = view; + mErrorView.setVisibility(mMainView == mErrorView ? View.VISIBLE : View.INVISIBLE); + mLoadingView.setVisibility(mMainView == mLoadingView ? View.VISIBLE : View.INVISIBLE); + mPlayPauseReplayView.setVisibility( + mMainView == mPlayPauseReplayView ? View.VISIBLE : View.INVISIBLE); + show(); + } + + @Override + public void show() { + updateViews(); + setVisibility(View.VISIBLE); + setFocusable(false); + } + + @Override + public void onClick(View view) { + if (mListener != null) { + if (view == mPlayPauseReplayView) { + if (mState == State.ENDED) { + if (mCanReplay) { + mListener.onReplay(); + } + } else if (mState == State.PAUSED || mState == State.PLAYING) { + mListener.onPlayPause(); + } + } + } + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + return super.onKeyDown(keyCode, event); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (super.onTouchEvent(event)) { + return true; + } + return false; + } + + // The paddings of 4 sides which covered by system components. E.g. + // +-----------------+\ + // | Action Bar | insets.top + // +-----------------+/ + // | | + // | Content Area | insets.right = insets.left = 0 + // | | + // +-----------------+\ + // | Navigation Bar | insets.bottom + // +-----------------+/ + // Please see View.fitSystemWindows() for more details. + private final Rect mWindowInsets = new Rect(); + + @Override + protected boolean fitSystemWindows(Rect insets) { + // We don't set the paddings of this View, otherwise, + // the content will get cropped outside window + mWindowInsets.set(insets); + return true; + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + Rect insets = mWindowInsets; + int pl = insets.left; // the left paddings + int pr = insets.right; + int pt = insets.top; + int pb = insets.bottom; + + int h = bottom - top; + int w = right - left; + boolean error = mErrorView.getVisibility() == View.VISIBLE; + + int y = h - pb; + // Put both TimeBar and Background just above the bottom system + // component. + // But extend the background to the width of the screen, since we don't + // care if it will be covered by a system component and it looks better. + mBackground.layout(0, y - mTimeBar.getBarHeight(), w, y); + mTimeBar.layout(pl, y - mTimeBar.getPreferredHeight(), w - pr, y); + + // Put the play/pause/next/ previous button in the center of the screen + layoutCenteredView(mPlayPauseReplayView, 0, 0, w, h); + + if (mMainView != null) { + layoutCenteredView(mMainView, 0, 0, w, h); + } + } + + private void layoutCenteredView(View view, int l, int t, int r, int b) { + int cw = view.getMeasuredWidth(); + int ch = view.getMeasuredHeight(); + int cl = (r - l - cw) / 2; + int ct = (b - t - ch) / 2; + view.layout(cl, ct, cl + cw, ct + ch); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + measureChildren(widthMeasureSpec, heightMeasureSpec); + } + + protected void updateViews() { + mBackground.setVisibility(View.VISIBLE); + mTimeBar.setVisibility(View.VISIBLE); + Resources resources = getContext().getResources(); + int imageResource = R.drawable.ic_vidcontrol_reload; + String contentDescription = resources.getString(R.string.accessibility_reload_video); + if (mState == State.PAUSED) { + imageResource = R.drawable.ic_vidcontrol_play; + contentDescription = resources.getString(R.string.accessibility_play_video); + } else if (mState == State.PLAYING) { + imageResource = R.drawable.ic_vidcontrol_pause; + contentDescription = resources.getString(R.string.accessibility_pause_video); + } + + mPlayPauseReplayView.setImageResource(imageResource); + mPlayPauseReplayView.setContentDescription(contentDescription); + mPlayPauseReplayView.setVisibility( + (mState != State.LOADING && mState != State.ERROR && + !(mState == State.ENDED && !mCanReplay)) + ? View.VISIBLE : View.GONE); + requestLayout(); + } + + // TimeBar listener + + @Override + public void onScrubbingStart() { + mListener.onSeekStart(); + } + + @Override + public void onScrubbingMove(int time) { + mListener.onSeekMove(time); + } + + @Override + public void onScrubbingEnd(int time, int trimStartTime, int trimEndTime) { + mListener.onSeekEnd(time, trimStartTime, trimEndTime); + } +} diff --git a/src/com/android/gallery3d/app/Config.java b/src/com/android/gallery3d/app/Config.java new file mode 100644 index 000000000..7183acc33 --- /dev/null +++ b/src/com/android/gallery3d/app/Config.java @@ -0,0 +1,127 @@ +/* + * 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.app; + +import android.content.Context; +import android.content.res.Resources; + +import com.android.gallery3d.R; +import com.android.gallery3d.ui.AlbumSetSlotRenderer; +import com.android.gallery3d.ui.SlotView; + +final class Config { + public static class AlbumSetPage { + private static AlbumSetPage sInstance; + + public SlotView.Spec slotViewSpec; + public AlbumSetSlotRenderer.LabelSpec labelSpec; + public int paddingTop; + public int paddingBottom; + public int placeholderColor; + + public static synchronized AlbumSetPage get(Context context) { + if (sInstance == null) { + sInstance = new AlbumSetPage(context); + } + return sInstance; + } + + private AlbumSetPage(Context context) { + Resources r = context.getResources(); + + placeholderColor = r.getColor(R.color.albumset_placeholder); + + slotViewSpec = new SlotView.Spec(); + slotViewSpec.rowsLand = r.getInteger(R.integer.albumset_rows_land); + slotViewSpec.rowsPort = r.getInteger(R.integer.albumset_rows_port); + slotViewSpec.slotGap = r.getDimensionPixelSize(R.dimen.albumset_slot_gap); + slotViewSpec.slotHeightAdditional = 0; + + paddingTop = r.getDimensionPixelSize(R.dimen.albumset_padding_top); + paddingBottom = r.getDimensionPixelSize(R.dimen.albumset_padding_bottom); + + labelSpec = new AlbumSetSlotRenderer.LabelSpec(); + labelSpec.labelBackgroundHeight = r.getDimensionPixelSize( + R.dimen.albumset_label_background_height); + labelSpec.titleOffset = r.getDimensionPixelSize( + R.dimen.albumset_title_offset); + labelSpec.countOffset = r.getDimensionPixelSize( + R.dimen.albumset_count_offset); + labelSpec.titleFontSize = r.getDimensionPixelSize( + R.dimen.albumset_title_font_size); + labelSpec.countFontSize = r.getDimensionPixelSize( + R.dimen.albumset_count_font_size); + labelSpec.leftMargin = r.getDimensionPixelSize( + R.dimen.albumset_left_margin); + labelSpec.titleRightMargin = r.getDimensionPixelSize( + R.dimen.albumset_title_right_margin); + labelSpec.iconSize = r.getDimensionPixelSize( + R.dimen.albumset_icon_size); + labelSpec.backgroundColor = r.getColor( + R.color.albumset_label_background); + labelSpec.titleColor = r.getColor(R.color.albumset_label_title); + labelSpec.countColor = r.getColor(R.color.albumset_label_count); + } + } + + public static class AlbumPage { + private static AlbumPage sInstance; + + public SlotView.Spec slotViewSpec; + public int placeholderColor; + + public static synchronized AlbumPage get(Context context) { + if (sInstance == null) { + sInstance = new AlbumPage(context); + } + return sInstance; + } + + private AlbumPage(Context context) { + Resources r = context.getResources(); + + placeholderColor = r.getColor(R.color.album_placeholder); + + slotViewSpec = new SlotView.Spec(); + slotViewSpec.rowsLand = r.getInteger(R.integer.album_rows_land); + slotViewSpec.rowsPort = r.getInteger(R.integer.album_rows_port); + slotViewSpec.slotGap = r.getDimensionPixelSize(R.dimen.album_slot_gap); + } + } + + public static class ManageCachePage extends AlbumSetPage { + private static ManageCachePage sInstance; + + public final int cachePinSize; + public final int cachePinMargin; + + public static synchronized ManageCachePage get(Context context) { + if (sInstance == null) { + sInstance = new ManageCachePage(context); + } + return sInstance; + } + + public ManageCachePage(Context context) { + super(context); + Resources r = context.getResources(); + cachePinSize = r.getDimensionPixelSize(R.dimen.cache_pin_size); + cachePinMargin = r.getDimensionPixelSize(R.dimen.cache_pin_margin); + } + } +} + diff --git a/src/com/android/gallery3d/app/ControllerOverlay.java b/src/com/android/gallery3d/app/ControllerOverlay.java new file mode 100644 index 000000000..078f59e28 --- /dev/null +++ b/src/com/android/gallery3d/app/ControllerOverlay.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2011 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.app; + +import android.view.View; + +public interface ControllerOverlay { + + interface Listener { + void onPlayPause(); + void onSeekStart(); + void onSeekMove(int time); + void onSeekEnd(int time, int trimStartTime, int trimEndTime); + void onShown(); + void onHidden(); + void onReplay(); + } + + void setListener(Listener listener); + + void setCanReplay(boolean canReplay); + + /** + * @return The overlay view that should be added to the player. + */ + View getView(); + + void show(); + + void showPlaying(); + + void showPaused(); + + void showEnded(); + + void showLoading(); + + void showErrorMessage(String message); + + void setTimes(int currentTime, int totalTime, + int trimStartTime, int trimEndTime); +} diff --git a/src/com/android/gallery3d/app/DialogPicker.java b/src/com/android/gallery3d/app/DialogPicker.java new file mode 100644 index 000000000..7ca86e5b4 --- /dev/null +++ b/src/com/android/gallery3d/app/DialogPicker.java @@ -0,0 +1,41 @@ +/* + * 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.app; + +import android.content.Intent; +import android.os.Bundle; + +import com.android.gallery3d.util.GalleryUtils; + +public class DialogPicker extends PickerActivity { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + int typeBits = GalleryUtils.determineTypeBits(this, getIntent()); + setTitle(GalleryUtils.getSelectionModePrompt(typeBits)); + Intent intent = getIntent(); + Bundle extras = intent.getExtras(); + Bundle data = extras == null ? new Bundle() : new Bundle(extras); + + data.putBoolean(Gallery.KEY_GET_CONTENT, true); + data.putString(AlbumSetPage.KEY_MEDIA_PATH, + getDataManager().getTopSetPath(typeBits)); + getStateManager().startState(AlbumSetPage.class, data); + } +} diff --git a/src/com/android/gallery3d/app/EyePosition.java b/src/com/android/gallery3d/app/EyePosition.java new file mode 100644 index 000000000..d99d97b0e --- /dev/null +++ b/src/com/android/gallery3d/app/EyePosition.java @@ -0,0 +1,226 @@ +/* + * 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.app; + +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.os.SystemClock; +import android.util.FloatMath; +import android.view.Display; +import android.view.Surface; +import android.view.WindowManager; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.util.GalleryUtils; + +public class EyePosition { + @SuppressWarnings("unused") + private static final String TAG = "EyePosition"; + + public interface EyePositionListener { + public void onEyePositionChanged(float x, float y, float z); + } + + private static final float GYROSCOPE_THRESHOLD = 0.15f; + private static final float GYROSCOPE_LIMIT = 10f; + private static final int GYROSCOPE_SETTLE_DOWN = 15; + private static final float GYROSCOPE_RESTORE_FACTOR = 0.995f; + + private static final float USER_ANGEL = (float) Math.toRadians(10); + private static final float USER_ANGEL_COS = FloatMath.cos(USER_ANGEL); + private static final float USER_ANGEL_SIN = FloatMath.sin(USER_ANGEL); + private static final float MAX_VIEW_RANGE = (float) 0.5; + private static final int NOT_STARTED = -1; + + private static final float USER_DISTANCE_METER = 0.3f; + + private Context mContext; + private EyePositionListener mListener; + private Display mDisplay; + // The eyes' position of the user, the origin is at the center of the + // device and the unit is in pixels. + private float mX; + private float mY; + private float mZ; + + private final float mUserDistance; // in pixel + private final float mLimit; + private long mStartTime = NOT_STARTED; + private Sensor mSensor; + private PositionListener mPositionListener = new PositionListener(); + + private int mGyroscopeCountdown = 0; + + public EyePosition(Context context, EyePositionListener listener) { + mContext = context; + mListener = listener; + mUserDistance = GalleryUtils.meterToPixel(USER_DISTANCE_METER); + mLimit = mUserDistance * MAX_VIEW_RANGE; + + WindowManager wManager = (WindowManager) mContext + .getSystemService(Context.WINDOW_SERVICE); + mDisplay = wManager.getDefaultDisplay(); + + // The 3D effect where the photo albums fan out in 3D based on angle + // of device tilt is currently disabled. +/* + SensorManager sManager = (SensorManager) mContext + .getSystemService(Context.SENSOR_SERVICE); + mSensor = sManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE); + if (mSensor == null) { + Log.w(TAG, "no gyroscope, use accelerometer instead"); + mSensor = sManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + } + if (mSensor == null) { + Log.w(TAG, "no sensor available"); + } +*/ + } + + public void resetPosition() { + mStartTime = NOT_STARTED; + mX = mY = 0; + mZ = -mUserDistance; + mListener.onEyePositionChanged(mX, mY, mZ); + } + + /* + * We assume the user is at the following position + * + * /|\ user's eye + * | / + * -G(gravity) | / + * |_/ + * / |/_____\ -Y (-y direction of device) + * user angel + */ + private void onAccelerometerChanged(float gx, float gy, float gz) { + + float x = gx, y = gy, z = gz; + + switch (mDisplay.getRotation()) { + case Surface.ROTATION_90: x = -gy; y= gx; break; + case Surface.ROTATION_180: x = -gx; y = -gy; break; + case Surface.ROTATION_270: x = gy; y = -gx; break; + } + + float temp = x * x + y * y + z * z; + float t = -y /temp; + + float tx = t * x; + float ty = -1 + t * y; + float tz = t * z; + + float length = FloatMath.sqrt(tx * tx + ty * ty + tz * tz); + float glength = FloatMath.sqrt(temp); + + mX = Utils.clamp((x * USER_ANGEL_COS / glength + + tx * USER_ANGEL_SIN / length) * mUserDistance, + -mLimit, mLimit); + mY = -Utils.clamp((y * USER_ANGEL_COS / glength + + ty * USER_ANGEL_SIN / length) * mUserDistance, + -mLimit, mLimit); + mZ = -FloatMath.sqrt( + mUserDistance * mUserDistance - mX * mX - mY * mY); + mListener.onEyePositionChanged(mX, mY, mZ); + } + + private void onGyroscopeChanged(float gx, float gy, float gz) { + long now = SystemClock.elapsedRealtime(); + float distance = (gx > 0 ? gx : -gx) + (gy > 0 ? gy : - gy); + if (distance < GYROSCOPE_THRESHOLD + || distance > GYROSCOPE_LIMIT || mGyroscopeCountdown > 0) { + --mGyroscopeCountdown; + mStartTime = now; + float limit = mUserDistance / 20f; + if (mX > limit || mX < -limit || mY > limit || mY < -limit) { + mX *= GYROSCOPE_RESTORE_FACTOR; + mY *= GYROSCOPE_RESTORE_FACTOR; + mZ = (float) -Math.sqrt( + mUserDistance * mUserDistance - mX * mX - mY * mY); + mListener.onEyePositionChanged(mX, mY, mZ); + } + return; + } + + float t = (now - mStartTime) / 1000f * mUserDistance * (-mZ); + mStartTime = now; + + float x = -gy, y = -gx; + switch (mDisplay.getRotation()) { + case Surface.ROTATION_90: x = -gx; y= gy; break; + case Surface.ROTATION_180: x = gy; y = gx; break; + case Surface.ROTATION_270: x = gx; y = -gy; break; + } + + mX = Utils.clamp((float) (mX + x * t / Math.hypot(mZ, mX)), + -mLimit, mLimit) * GYROSCOPE_RESTORE_FACTOR; + mY = Utils.clamp((float) (mY + y * t / Math.hypot(mZ, mY)), + -mLimit, mLimit) * GYROSCOPE_RESTORE_FACTOR; + + mZ = -FloatMath.sqrt( + mUserDistance * mUserDistance - mX * mX - mY * mY); + mListener.onEyePositionChanged(mX, mY, mZ); + } + + private class PositionListener implements SensorEventListener { + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + } + + @Override + public void onSensorChanged(SensorEvent event) { + switch (event.sensor.getType()) { + case Sensor.TYPE_GYROSCOPE: { + onGyroscopeChanged( + event.values[0], event.values[1], event.values[2]); + break; + } + case Sensor.TYPE_ACCELEROMETER: { + onAccelerometerChanged( + event.values[0], event.values[1], event.values[2]); + } + } + } + } + + public void pause() { + if (mSensor != null) { + SensorManager sManager = (SensorManager) mContext + .getSystemService(Context.SENSOR_SERVICE); + sManager.unregisterListener(mPositionListener); + } + } + + public void resume() { + if (mSensor != null) { + SensorManager sManager = (SensorManager) mContext + .getSystemService(Context.SENSOR_SERVICE); + sManager.registerListener(mPositionListener, + mSensor, SensorManager.SENSOR_DELAY_GAME); + } + + mStartTime = NOT_STARTED; + mGyroscopeCountdown = GYROSCOPE_SETTLE_DOWN; + mX = mY = 0; + mZ = -mUserDistance; + mListener.onEyePositionChanged(mX, mY, mZ); + } +} diff --git a/src/com/android/gallery3d/app/FilmstripPage.java b/src/com/android/gallery3d/app/FilmstripPage.java new file mode 100644 index 000000000..a9726cdc9 --- /dev/null +++ b/src/com/android/gallery3d/app/FilmstripPage.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2012 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.app; + +public class FilmstripPage extends PhotoPage { + +} diff --git a/src/com/android/gallery3d/app/FilterUtils.java b/src/com/android/gallery3d/app/FilterUtils.java new file mode 100644 index 000000000..bc28a9cc1 --- /dev/null +++ b/src/com/android/gallery3d/app/FilterUtils.java @@ -0,0 +1,257 @@ +/* + * 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.app; + +import com.android.gallery3d.R; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.Path; + +// This class handles filtering and clustering. +// +// We allow at most only one filter operation at a time (Currently it +// doesn't make sense to use more than one). Also each clustering operation +// can be applied at most once. In addition, there is one more constraint +// ("fixed set constraint") described below. +// +// A clustered album (not including album set) and its base sets are fixed. +// For example, +// +// /cluster/{base_set}/time/7 +// +// This set and all sets inside base_set (recursively) are fixed because +// 1. We can not change this set to use another clustering condition (like +// changing "time" to "location"). +// 2. Neither can we change any set in the base_set. +// The reason is in both cases the 7th set may not exist in the new clustering. +// --------------------- +// newPath operation: create a new path based on a source path and put an extra +// condition on top of it: +// +// T = newFilterPath(S, filterType); +// T = newClusterPath(S, clusterType); +// +// Similar functions can be used to replace the current condition (if there is one). +// +// T = switchFilterPath(S, filterType); +// T = switchClusterPath(S, clusterType); +// +// For all fixed set in the path defined above, if some clusterType and +// filterType are already used, they cannot not be used as parameter for these +// functions. setupMenuItems() makes sure those types cannot be selected. +// +public class FilterUtils { + @SuppressWarnings("unused") + private static final String TAG = "FilterUtils"; + + public static final int CLUSTER_BY_ALBUM = 1; + public static final int CLUSTER_BY_TIME = 2; + public static final int CLUSTER_BY_LOCATION = 4; + public static final int CLUSTER_BY_TAG = 8; + public static final int CLUSTER_BY_SIZE = 16; + public static final int CLUSTER_BY_FACE = 32; + + public static final int FILTER_IMAGE_ONLY = 1; + public static final int FILTER_VIDEO_ONLY = 2; + public static final int FILTER_ALL = 4; + + // These are indices of the return values of getAppliedFilters(). + // The _F suffix means "fixed". + private static final int CLUSTER_TYPE = 0; + private static final int FILTER_TYPE = 1; + private static final int CLUSTER_TYPE_F = 2; + private static final int FILTER_TYPE_F = 3; + private static final int CLUSTER_CURRENT_TYPE = 4; + private static final int FILTER_CURRENT_TYPE = 5; + + public static void setupMenuItems(GalleryActionBar actionBar, Path path, boolean inAlbum) { + int[] result = new int[6]; + getAppliedFilters(path, result); + int ctype = result[CLUSTER_TYPE]; + int ftype = result[FILTER_TYPE]; + int ftypef = result[FILTER_TYPE_F]; + int ccurrent = result[CLUSTER_CURRENT_TYPE]; + int fcurrent = result[FILTER_CURRENT_TYPE]; + + setMenuItemApplied(actionBar, CLUSTER_BY_TIME, + (ctype & CLUSTER_BY_TIME) != 0, (ccurrent & CLUSTER_BY_TIME) != 0); + setMenuItemApplied(actionBar, CLUSTER_BY_LOCATION, + (ctype & CLUSTER_BY_LOCATION) != 0, (ccurrent & CLUSTER_BY_LOCATION) != 0); + setMenuItemApplied(actionBar, CLUSTER_BY_TAG, + (ctype & CLUSTER_BY_TAG) != 0, (ccurrent & CLUSTER_BY_TAG) != 0); + setMenuItemApplied(actionBar, CLUSTER_BY_FACE, + (ctype & CLUSTER_BY_FACE) != 0, (ccurrent & CLUSTER_BY_FACE) != 0); + + actionBar.setClusterItemVisibility(CLUSTER_BY_ALBUM, !inAlbum || ctype == 0); + + setMenuItemApplied(actionBar, R.id.action_cluster_album, ctype == 0, + ccurrent == 0); + + // A filtering is available if it's not applied, and the old filtering + // (if any) is not fixed. + setMenuItemAppliedEnabled(actionBar, R.string.show_images_only, + (ftype & FILTER_IMAGE_ONLY) != 0, + (ftype & FILTER_IMAGE_ONLY) == 0 && ftypef == 0, + (fcurrent & FILTER_IMAGE_ONLY) != 0); + setMenuItemAppliedEnabled(actionBar, R.string.show_videos_only, + (ftype & FILTER_VIDEO_ONLY) != 0, + (ftype & FILTER_VIDEO_ONLY) == 0 && ftypef == 0, + (fcurrent & FILTER_VIDEO_ONLY) != 0); + setMenuItemAppliedEnabled(actionBar, R.string.show_all, + ftype == 0, ftype != 0 && ftypef == 0, fcurrent == 0); + } + + // Gets the filters applied in the path. + private static void getAppliedFilters(Path path, int[] result) { + getAppliedFilters(path, result, false); + } + + private static void getAppliedFilters(Path path, int[] result, boolean underCluster) { + String[] segments = path.split(); + // Recurse into sub media sets. + for (int i = 0; i < segments.length; i++) { + if (segments[i].startsWith("{")) { + String[] sets = Path.splitSequence(segments[i]); + for (int j = 0; j < sets.length; j++) { + Path sub = Path.fromString(sets[j]); + getAppliedFilters(sub, result, underCluster); + } + } + } + + // update current selection + if (segments[0].equals("cluster")) { + // if this is a clustered album, set underCluster to true. + if (segments.length == 4) { + underCluster = true; + } + + int ctype = toClusterType(segments[2]); + result[CLUSTER_TYPE] |= ctype; + result[CLUSTER_CURRENT_TYPE] = ctype; + if (underCluster) { + result[CLUSTER_TYPE_F] |= ctype; + } + } + } + + private static int toClusterType(String s) { + if (s.equals("time")) { + return CLUSTER_BY_TIME; + } else if (s.equals("location")) { + return CLUSTER_BY_LOCATION; + } else if (s.equals("tag")) { + return CLUSTER_BY_TAG; + } else if (s.equals("size")) { + return CLUSTER_BY_SIZE; + } else if (s.equals("face")) { + return CLUSTER_BY_FACE; + } + return 0; + } + + private static void setMenuItemApplied( + GalleryActionBar model, int id, boolean applied, boolean updateTitle) { + model.setClusterItemEnabled(id, !applied); + } + + private static void setMenuItemAppliedEnabled(GalleryActionBar model, int id, boolean applied, boolean enabled, boolean updateTitle) { + model.setClusterItemEnabled(id, enabled); + } + + // Add a specified filter to the path. + public static String newFilterPath(String base, int filterType) { + int mediaType; + switch (filterType) { + case FILTER_IMAGE_ONLY: + mediaType = MediaObject.MEDIA_TYPE_IMAGE; + break; + case FILTER_VIDEO_ONLY: + mediaType = MediaObject.MEDIA_TYPE_VIDEO; + break; + default: /* FILTER_ALL */ + return base; + } + + return "/filter/mediatype/" + mediaType + "/{" + base + "}"; + } + + // Add a specified clustering to the path. + public static String newClusterPath(String base, int clusterType) { + String kind; + switch (clusterType) { + case CLUSTER_BY_TIME: + kind = "time"; + break; + case CLUSTER_BY_LOCATION: + kind = "location"; + break; + case CLUSTER_BY_TAG: + kind = "tag"; + break; + case CLUSTER_BY_SIZE: + kind = "size"; + break; + case CLUSTER_BY_FACE: + kind = "face"; + break; + default: /* CLUSTER_BY_ALBUM */ + return base; + } + + return "/cluster/{" + base + "}/" + kind; + } + + // Change the topmost clustering to the specified type. + public static String switchClusterPath(String base, int clusterType) { + return newClusterPath(removeOneClusterFromPath(base), clusterType); + } + + // Remove the topmost clustering (if any) from the path. + private static String removeOneClusterFromPath(String base) { + boolean[] done = new boolean[1]; + return removeOneClusterFromPath(base, done); + } + + private static String removeOneClusterFromPath(String base, boolean[] done) { + if (done[0]) return base; + + String[] segments = Path.split(base); + if (segments[0].equals("cluster")) { + done[0] = true; + return Path.splitSequence(segments[1])[0]; + } + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < segments.length; i++) { + sb.append("/"); + if (segments[i].startsWith("{")) { + sb.append("{"); + String[] sets = Path.splitSequence(segments[i]); + for (int j = 0; j < sets.length; j++) { + if (j > 0) { + sb.append(","); + } + sb.append(removeOneClusterFromPath(sets[j], done)); + } + sb.append("}"); + } else { + sb.append(segments[i]); + } + } + return sb.toString(); + } +} diff --git a/src/com/android/gallery3d/app/Gallery.java b/src/com/android/gallery3d/app/Gallery.java new file mode 100644 index 000000000..baef56b44 --- /dev/null +++ b/src/com/android/gallery3d/app/Gallery.java @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2009 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.app; + +import android.app.Dialog; +import android.content.ContentResolver; +import android.content.DialogInterface; +import android.content.DialogInterface.OnCancelListener; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.view.InputDevice; +import android.view.MotionEvent; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; +import android.widget.Toast; + +import com.android.gallery3d.R; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.picasasource.PicasaSource; +import com.android.gallery3d.util.GalleryUtils; + +public final class Gallery extends AbstractGalleryActivity implements OnCancelListener { + public static final String EXTRA_SLIDESHOW = "slideshow"; + public static final String EXTRA_DREAM = "dream"; + public static final String EXTRA_CROP = "crop"; + + public static final String ACTION_REVIEW = "com.android.camera.action.REVIEW"; + public static final String KEY_GET_CONTENT = "get-content"; + public static final String KEY_GET_ALBUM = "get-album"; + public static final String KEY_TYPE_BITS = "type-bits"; + public static final String KEY_MEDIA_TYPES = "mediaTypes"; + public static final String KEY_DISMISS_KEYGUARD = "dismiss-keyguard"; + + private static final String TAG = "Gallery"; + private Dialog mVersionCheckDialog; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_ACTION_BAR); + requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY); + + if (getIntent().getBooleanExtra(KEY_DISMISS_KEYGUARD, false)) { + getWindow().addFlags( + WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD); + } + + setContentView(R.layout.main); + + if (savedInstanceState != null) { + getStateManager().restoreFromState(savedInstanceState); + } else { + initializeByIntent(); + } + } + + private void initializeByIntent() { + Intent intent = getIntent(); + String action = intent.getAction(); + + if (Intent.ACTION_GET_CONTENT.equalsIgnoreCase(action)) { + startGetContent(intent); + } else if (Intent.ACTION_PICK.equalsIgnoreCase(action)) { + // We do NOT really support the PICK intent. Handle it as + // the GET_CONTENT. However, we need to translate the type + // in the intent here. + Log.w(TAG, "action PICK is not supported"); + String type = Utils.ensureNotNull(intent.getType()); + if (type.startsWith("vnd.android.cursor.dir/")) { + if (type.endsWith("/image")) intent.setType("image/*"); + if (type.endsWith("/video")) intent.setType("video/*"); + } + startGetContent(intent); + } else if (Intent.ACTION_VIEW.equalsIgnoreCase(action) + || ACTION_REVIEW.equalsIgnoreCase(action)){ + startViewAction(intent); + } else { + startDefaultPage(); + } + } + + public void startDefaultPage() { + PicasaSource.showSignInReminder(this); + Bundle data = new Bundle(); + data.putString(AlbumSetPage.KEY_MEDIA_PATH, + getDataManager().getTopSetPath(DataManager.INCLUDE_ALL)); + getStateManager().startState(AlbumSetPage.class, data); + mVersionCheckDialog = PicasaSource.getVersionCheckDialog(this); + if (mVersionCheckDialog != null) { + mVersionCheckDialog.setOnCancelListener(this); + } + } + + private void startGetContent(Intent intent) { + Bundle data = intent.getExtras() != null + ? new Bundle(intent.getExtras()) + : new Bundle(); + data.putBoolean(KEY_GET_CONTENT, true); + int typeBits = GalleryUtils.determineTypeBits(this, intent); + data.putInt(KEY_TYPE_BITS, typeBits); + data.putString(AlbumSetPage.KEY_MEDIA_PATH, + getDataManager().getTopSetPath(typeBits)); + getStateManager().startState(AlbumSetPage.class, data); + } + + private String getContentType(Intent intent) { + String type = intent.getType(); + if (type != null) { + return GalleryUtils.MIME_TYPE_PANORAMA360.equals(type) + ? MediaItem.MIME_TYPE_JPEG : type; + } + + Uri uri = intent.getData(); + try { + return getContentResolver().getType(uri); + } catch (Throwable t) { + Log.w(TAG, "get type fail", t); + return null; + } + } + + private void startViewAction(Intent intent) { + Boolean slideshow = intent.getBooleanExtra(EXTRA_SLIDESHOW, false); + if (slideshow) { + getActionBar().hide(); + DataManager manager = getDataManager(); + Path path = manager.findPathByUri(intent.getData(), intent.getType()); + if (path == null || manager.getMediaObject(path) + instanceof MediaItem) { + path = Path.fromString( + manager.getTopSetPath(DataManager.INCLUDE_IMAGE)); + } + Bundle data = new Bundle(); + data.putString(SlideshowPage.KEY_SET_PATH, path.toString()); + data.putBoolean(SlideshowPage.KEY_RANDOM_ORDER, true); + data.putBoolean(SlideshowPage.KEY_REPEAT, true); + if (intent.getBooleanExtra(EXTRA_DREAM, false)) { + data.putBoolean(SlideshowPage.KEY_DREAM, true); + } + getStateManager().startState(SlideshowPage.class, data); + } else { + Bundle data = new Bundle(); + DataManager dm = getDataManager(); + Uri uri = intent.getData(); + String contentType = getContentType(intent); + if (contentType == null) { + Toast.makeText(this, + R.string.no_such_item, Toast.LENGTH_LONG).show(); + finish(); + return; + } + if (uri == null) { + int typeBits = GalleryUtils.determineTypeBits(this, intent); + data.putInt(KEY_TYPE_BITS, typeBits); + data.putString(AlbumSetPage.KEY_MEDIA_PATH, + getDataManager().getTopSetPath(typeBits)); + getStateManager().startState(AlbumSetPage.class, data); + } else if (contentType.startsWith( + ContentResolver.CURSOR_DIR_BASE_TYPE)) { + int mediaType = intent.getIntExtra(KEY_MEDIA_TYPES, 0); + if (mediaType != 0) { + uri = uri.buildUpon().appendQueryParameter( + KEY_MEDIA_TYPES, String.valueOf(mediaType)) + .build(); + } + Path setPath = dm.findPathByUri(uri, null); + MediaSet mediaSet = null; + if (setPath != null) { + mediaSet = (MediaSet) dm.getMediaObject(setPath); + } + if (mediaSet != null) { + if (mediaSet.isLeafAlbum()) { + data.putString(AlbumPage.KEY_MEDIA_PATH, setPath.toString()); + data.putString(AlbumPage.KEY_PARENT_MEDIA_PATH, + dm.getTopSetPath(DataManager.INCLUDE_ALL)); + getStateManager().startState(AlbumPage.class, data); + } else { + data.putString(AlbumSetPage.KEY_MEDIA_PATH, setPath.toString()); + getStateManager().startState(AlbumSetPage.class, data); + } + } else { + startDefaultPage(); + } + } else { + Path itemPath = dm.findPathByUri(uri, contentType); + Path albumPath = dm.getDefaultSetOf(itemPath); + + data.putString(PhotoPage.KEY_MEDIA_ITEM_PATH, itemPath.toString()); + + // TODO: Make the parameter "SingleItemOnly" public so other + // activities can reference it. + boolean singleItemOnly = (albumPath == null) + || intent.getBooleanExtra("SingleItemOnly", false); + if (!singleItemOnly) { + data.putString(PhotoPage.KEY_MEDIA_SET_PATH, albumPath.toString()); + // when FLAG_ACTIVITY_NEW_TASK is set, (e.g. when intent is fired + // from notification), back button should behave the same as up button + // rather than taking users back to the home screen + if (intent.getBooleanExtra(PhotoPage.KEY_TREAT_BACK_AS_UP, false) + || ((intent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) != 0)) { + data.putBoolean(PhotoPage.KEY_TREAT_BACK_AS_UP, true); + } + } + + getStateManager().startState(SinglePhotoPage.class, data); + } + } + } + + @Override + protected void onResume() { + Utils.assertTrue(getStateManager().getStateCount() > 0); + super.onResume(); + if (mVersionCheckDialog != null) { + mVersionCheckDialog.show(); + } + } + + @Override + protected void onPause() { + super.onPause(); + if (mVersionCheckDialog != null) { + mVersionCheckDialog.dismiss(); + } + } + + @Override + public void onCancel(DialogInterface dialog) { + if (dialog == mVersionCheckDialog) { + mVersionCheckDialog = null; + } + } + + @Override + public boolean onGenericMotionEvent(MotionEvent event) { + final boolean isTouchPad = (event.getSource() + & InputDevice.SOURCE_CLASS_POSITION) != 0; + if (isTouchPad) { + float maxX = event.getDevice().getMotionRange(MotionEvent.AXIS_X).getMax(); + float maxY = event.getDevice().getMotionRange(MotionEvent.AXIS_Y).getMax(); + View decor = getWindow().getDecorView(); + float scaleX = decor.getWidth() / maxX; + float scaleY = decor.getHeight() / maxY; + float x = event.getX() * scaleX; + //x = decor.getWidth() - x; // invert x + float y = event.getY() * scaleY; + //y = decor.getHeight() - y; // invert y + MotionEvent touchEvent = MotionEvent.obtain(event.getDownTime(), + event.getEventTime(), event.getAction(), x, y, event.getMetaState()); + return dispatchTouchEvent(touchEvent); + } + return super.onGenericMotionEvent(event); + } +} diff --git a/src/com/android/gallery3d/app/GalleryActionBar.java b/src/com/android/gallery3d/app/GalleryActionBar.java new file mode 100644 index 000000000..588f5842a --- /dev/null +++ b/src/com/android/gallery3d/app/GalleryActionBar.java @@ -0,0 +1,438 @@ +/* + * Copyright (C) 2011 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.app; + +import android.annotation.TargetApi; +import android.app.ActionBar; +import android.app.ActionBar.OnMenuVisibilityListener; +import android.app.ActionBar.OnNavigationListener; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.res.Resources; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ShareActionProvider; +import android.widget.TextView; +import android.widget.TwoLineListItem; + +import com.android.gallery3d.R; +import com.android.gallery3d.common.ApiHelper; + +import java.util.ArrayList; + +public class GalleryActionBar implements OnNavigationListener { + @SuppressWarnings("unused") + private static final String TAG = "GalleryActionBar"; + + private ClusterRunner mClusterRunner; + private CharSequence[] mTitles; + private ArrayList<Integer> mActions; + private Context mContext; + private LayoutInflater mInflater; + private AbstractGalleryActivity mActivity; + private ActionBar mActionBar; + private int mCurrentIndex; + private ClusterAdapter mAdapter = new ClusterAdapter(); + + private AlbumModeAdapter mAlbumModeAdapter; + private OnAlbumModeSelectedListener mAlbumModeListener; + private int mLastAlbumModeSelected; + private CharSequence [] mAlbumModes; + public static final int ALBUM_FILMSTRIP_MODE_SELECTED = 0; + public static final int ALBUM_GRID_MODE_SELECTED = 1; + + public interface ClusterRunner { + public void doCluster(int id); + } + + public interface OnAlbumModeSelectedListener { + public void onAlbumModeSelected(int mode); + } + + private static class ActionItem { + public int action; + public boolean enabled; + public boolean visible; + public int spinnerTitle; + public int dialogTitle; + public int clusterBy; + + public ActionItem(int action, boolean applied, boolean enabled, int title, + int clusterBy) { + this(action, applied, enabled, title, title, clusterBy); + } + + public ActionItem(int action, boolean applied, boolean enabled, int spinnerTitle, + int dialogTitle, int clusterBy) { + this.action = action; + this.enabled = enabled; + this.spinnerTitle = spinnerTitle; + this.dialogTitle = dialogTitle; + this.clusterBy = clusterBy; + this.visible = true; + } + } + + private static final ActionItem[] sClusterItems = new ActionItem[] { + new ActionItem(FilterUtils.CLUSTER_BY_ALBUM, true, false, R.string.albums, + R.string.group_by_album), + new ActionItem(FilterUtils.CLUSTER_BY_LOCATION, true, false, + R.string.locations, R.string.location, R.string.group_by_location), + new ActionItem(FilterUtils.CLUSTER_BY_TIME, true, false, R.string.times, + R.string.time, R.string.group_by_time), + new ActionItem(FilterUtils.CLUSTER_BY_FACE, true, false, R.string.people, + R.string.group_by_faces), + new ActionItem(FilterUtils.CLUSTER_BY_TAG, true, false, R.string.tags, + R.string.group_by_tags) + }; + + private class ClusterAdapter extends BaseAdapter { + + @Override + public int getCount() { + return sClusterItems.length; + } + + @Override + public Object getItem(int position) { + return sClusterItems[position]; + } + + @Override + public long getItemId(int position) { + return sClusterItems[position].action; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = mInflater.inflate(R.layout.action_bar_text, + parent, false); + } + TextView view = (TextView) convertView; + view.setText(sClusterItems[position].spinnerTitle); + return convertView; + } + } + + private class AlbumModeAdapter extends BaseAdapter { + @Override + public int getCount() { + return mAlbumModes.length; + } + + @Override + public Object getItem(int position) { + return mAlbumModes[position]; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = mInflater.inflate(R.layout.action_bar_two_line_text, + parent, false); + } + TwoLineListItem view = (TwoLineListItem) convertView; + view.getText1().setText(mActionBar.getTitle()); + view.getText2().setText((CharSequence) getItem(position)); + return convertView; + } + + @Override + public View getDropDownView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = mInflater.inflate(R.layout.action_bar_text, + parent, false); + } + TextView view = (TextView) convertView; + view.setText((CharSequence) getItem(position)); + return convertView; + } + } + + public static String getClusterByTypeString(Context context, int type) { + for (ActionItem item : sClusterItems) { + if (item.action == type) { + return context.getString(item.clusterBy); + } + } + return null; + } + + public GalleryActionBar(AbstractGalleryActivity activity) { + mActionBar = activity.getActionBar(); + mContext = activity.getAndroidContext(); + mActivity = activity; + mInflater = ((Activity) mActivity).getLayoutInflater(); + mCurrentIndex = 0; + } + + private void createDialogData() { + ArrayList<CharSequence> titles = new ArrayList<CharSequence>(); + mActions = new ArrayList<Integer>(); + for (ActionItem item : sClusterItems) { + if (item.enabled && item.visible) { + titles.add(mContext.getString(item.dialogTitle)); + mActions.add(item.action); + } + } + mTitles = new CharSequence[titles.size()]; + titles.toArray(mTitles); + } + + public int getHeight() { + return mActionBar != null ? mActionBar.getHeight() : 0; + } + + public void setClusterItemEnabled(int id, boolean enabled) { + for (ActionItem item : sClusterItems) { + if (item.action == id) { + item.enabled = enabled; + return; + } + } + } + + public void setClusterItemVisibility(int id, boolean visible) { + for (ActionItem item : sClusterItems) { + if (item.action == id) { + item.visible = visible; + return; + } + } + } + + public int getClusterTypeAction() { + return sClusterItems[mCurrentIndex].action; + } + + public void enableClusterMenu(int action, ClusterRunner runner) { + if (mActionBar != null) { + // Don't set cluster runner until action bar is ready. + mClusterRunner = null; + mActionBar.setListNavigationCallbacks(mAdapter, this); + mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST); + setSelectedAction(action); + mClusterRunner = runner; + } + } + + // The only use case not to hideMenu in this method is to ensure + // all elements disappear at the same time when exiting gallery. + // hideMenu should always be true in all other cases. + public void disableClusterMenu(boolean hideMenu) { + if (mActionBar != null) { + mClusterRunner = null; + if (hideMenu) { + mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD); + } + } + } + + public void onConfigurationChanged() { + if (mActionBar != null && mAlbumModeListener != null) { + OnAlbumModeSelectedListener listener = mAlbumModeListener; + enableAlbumModeMenu(mLastAlbumModeSelected, listener); + } + } + + public void enableAlbumModeMenu(int selected, OnAlbumModeSelectedListener listener) { + if (mActionBar != null) { + if (mAlbumModeAdapter == null) { + // Initialize the album mode options if they haven't been already + Resources res = mActivity.getResources(); + mAlbumModes = new CharSequence[] { + res.getString(R.string.switch_photo_filmstrip), + res.getString(R.string.switch_photo_grid)}; + mAlbumModeAdapter = new AlbumModeAdapter(); + } + mAlbumModeListener = null; + mLastAlbumModeSelected = selected; + mActionBar.setListNavigationCallbacks(mAlbumModeAdapter, this); + mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST); + mActionBar.setSelectedNavigationItem(selected); + mAlbumModeListener = listener; + } + } + + public void disableAlbumModeMenu(boolean hideMenu) { + if (mActionBar != null) { + mAlbumModeListener = null; + if (hideMenu) { + mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD); + } + } + } + + public void showClusterDialog(final ClusterRunner clusterRunner) { + createDialogData(); + final ArrayList<Integer> actions = mActions; + new AlertDialog.Builder(mContext).setTitle(R.string.group_by).setItems( + mTitles, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // Need to lock rendering when operations invoked by system UI (main thread) are + // modifying slot data used in GL thread for rendering. + mActivity.getGLRoot().lockRenderThread(); + try { + clusterRunner.doCluster(actions.get(which).intValue()); + } finally { + mActivity.getGLRoot().unlockRenderThread(); + } + } + }).create().show(); + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + private void setHomeButtonEnabled(boolean enabled) { + if (mActionBar != null) mActionBar.setHomeButtonEnabled(enabled); + } + + public void setDisplayOptions(boolean displayHomeAsUp, boolean showTitle) { + if (mActionBar == null) return; + int options = 0; + if (displayHomeAsUp) options |= ActionBar.DISPLAY_HOME_AS_UP; + if (showTitle) options |= ActionBar.DISPLAY_SHOW_TITLE; + + mActionBar.setDisplayOptions(options, + ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_TITLE); + mActionBar.setHomeButtonEnabled(displayHomeAsUp); + } + + public void setTitle(String title) { + if (mActionBar != null) mActionBar.setTitle(title); + } + + public void setTitle(int titleId) { + if (mActionBar != null) { + mActionBar.setTitle(mContext.getString(titleId)); + } + } + + public void setSubtitle(String title) { + if (mActionBar != null) mActionBar.setSubtitle(title); + } + + public void show() { + if (mActionBar != null) mActionBar.show(); + } + + public void hide() { + if (mActionBar != null) mActionBar.hide(); + } + + public void addOnMenuVisibilityListener(OnMenuVisibilityListener listener) { + if (mActionBar != null) mActionBar.addOnMenuVisibilityListener(listener); + } + + public void removeOnMenuVisibilityListener(OnMenuVisibilityListener listener) { + if (mActionBar != null) mActionBar.removeOnMenuVisibilityListener(listener); + } + + public boolean setSelectedAction(int type) { + if (mActionBar == null) return false; + + for (int i = 0, n = sClusterItems.length; i < n; i++) { + ActionItem item = sClusterItems[i]; + if (item.action == type) { + mActionBar.setSelectedNavigationItem(i); + mCurrentIndex = i; + return true; + } + } + return false; + } + + @Override + public boolean onNavigationItemSelected(int itemPosition, long itemId) { + if (itemPosition != mCurrentIndex && mClusterRunner != null + || mAlbumModeListener != null) { + // Need to lock rendering when operations invoked by system UI (main thread) are + // modifying slot data used in GL thread for rendering. + mActivity.getGLRoot().lockRenderThread(); + try { + if (mAlbumModeListener != null) { + mAlbumModeListener.onAlbumModeSelected(itemPosition); + } else { + mClusterRunner.doCluster(sClusterItems[itemPosition].action); + } + } finally { + mActivity.getGLRoot().unlockRenderThread(); + } + } + return false; + } + + private Menu mActionBarMenu; + private ShareActionProvider mSharePanoramaActionProvider; + private ShareActionProvider mShareActionProvider; + private Intent mSharePanoramaIntent; + private Intent mShareIntent; + + public void createActionBarMenu(int menuRes, Menu menu) { + mActivity.getMenuInflater().inflate(menuRes, menu); + mActionBarMenu = menu; + + MenuItem item = menu.findItem(R.id.action_share_panorama); + if (item != null) { + mSharePanoramaActionProvider = (ShareActionProvider) + item.getActionProvider(); + mSharePanoramaActionProvider + .setShareHistoryFileName("panorama_share_history.xml"); + mSharePanoramaActionProvider.setShareIntent(mSharePanoramaIntent); + } + + item = menu.findItem(R.id.action_share); + if (item != null) { + mShareActionProvider = (ShareActionProvider) + item.getActionProvider(); + mShareActionProvider + .setShareHistoryFileName("share_history.xml"); + mShareActionProvider.setShareIntent(mShareIntent); + } + } + + public Menu getMenu() { + return mActionBarMenu; + } + + public void setShareIntents(Intent sharePanoramaIntent, Intent shareIntent, + ShareActionProvider.OnShareTargetSelectedListener onShareListener) { + mSharePanoramaIntent = sharePanoramaIntent; + if (mSharePanoramaActionProvider != null) { + mSharePanoramaActionProvider.setShareIntent(sharePanoramaIntent); + } + mShareIntent = shareIntent; + if (mShareActionProvider != null) { + mShareActionProvider.setShareIntent(shareIntent); + mShareActionProvider.setOnShareTargetSelectedListener( + onShareListener); + } + } +} diff --git a/src/com/android/gallery3d/app/GalleryApp.java b/src/com/android/gallery3d/app/GalleryApp.java new file mode 100644 index 000000000..b56b8a82c --- /dev/null +++ b/src/com/android/gallery3d/app/GalleryApp.java @@ -0,0 +1,41 @@ +/* + * 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.app; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.Resources; +import android.os.Looper; + +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.DownloadCache; +import com.android.gallery3d.data.ImageCacheService; +import com.android.gallery3d.util.ThreadPool; + +public interface GalleryApp { + public DataManager getDataManager(); + + public StitchingProgressManager getStitchingProgressManager(); + public ImageCacheService getImageCacheService(); + public DownloadCache getDownloadCache(); + public ThreadPool getThreadPool(); + + public Context getAndroidContext(); + public Looper getMainLooper(); + public ContentResolver getContentResolver(); + public Resources getResources(); +} diff --git a/src/com/android/gallery3d/app/GalleryAppImpl.java b/src/com/android/gallery3d/app/GalleryAppImpl.java new file mode 100644 index 000000000..2abdaa0c1 --- /dev/null +++ b/src/com/android/gallery3d/app/GalleryAppImpl.java @@ -0,0 +1,127 @@ +/* + * 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.app; + +import android.app.Application; +import android.content.Context; +import android.os.AsyncTask; + +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.DownloadCache; +import com.android.gallery3d.data.ImageCacheService; +import com.android.gallery3d.gadget.WidgetUtils; +import com.android.gallery3d.picasasource.PicasaSource; +import com.android.gallery3d.util.GalleryUtils; +import com.android.gallery3d.util.LightCycleHelper; +import com.android.gallery3d.util.ThreadPool; +import com.android.gallery3d.util.UsageStatistics; +import com.android.photos.data.MediaCache; + +import java.io.File; + +public class GalleryAppImpl extends Application implements GalleryApp { + + private static final String DOWNLOAD_FOLDER = "download"; + private static final long DOWNLOAD_CAPACITY = 64 * 1024 * 1024; // 64M + + private ImageCacheService mImageCacheService; + private Object mLock = new Object(); + private DataManager mDataManager; + private ThreadPool mThreadPool; + private DownloadCache mDownloadCache; + private StitchingProgressManager mStitchingProgressManager; + + @Override + public void onCreate() { + super.onCreate(); + com.android.camera.Util.initialize(this); + initializeAsyncTask(); + GalleryUtils.initialize(this); + WidgetUtils.initialize(this); + PicasaSource.initialize(this); + UsageStatistics.initialize(this); + MediaCache.initialize(this); + + mStitchingProgressManager = LightCycleHelper.createStitchingManagerInstance(this); + if (mStitchingProgressManager != null) { + mStitchingProgressManager.addChangeListener(getDataManager()); + } + } + + @Override + public Context getAndroidContext() { + return this; + } + + @Override + public synchronized DataManager getDataManager() { + if (mDataManager == null) { + mDataManager = new DataManager(this); + mDataManager.initializeSourceMap(); + } + return mDataManager; + } + + @Override + public StitchingProgressManager getStitchingProgressManager() { + return mStitchingProgressManager; + } + + @Override + public ImageCacheService getImageCacheService() { + // This method may block on file I/O so a dedicated lock is needed here. + synchronized (mLock) { + if (mImageCacheService == null) { + mImageCacheService = new ImageCacheService(getAndroidContext()); + } + return mImageCacheService; + } + } + + @Override + public synchronized ThreadPool getThreadPool() { + if (mThreadPool == null) { + mThreadPool = new ThreadPool(); + } + return mThreadPool; + } + + @Override + public synchronized DownloadCache getDownloadCache() { + if (mDownloadCache == null) { + File cacheDir = new File(getExternalCacheDir(), DOWNLOAD_FOLDER); + + if (!cacheDir.isDirectory()) cacheDir.mkdirs(); + + if (!cacheDir.isDirectory()) { + throw new RuntimeException( + "fail to create: " + cacheDir.getAbsolutePath()); + } + mDownloadCache = new DownloadCache(this, cacheDir, DOWNLOAD_CAPACITY); + } + return mDownloadCache; + } + + private void initializeAsyncTask() { + // AsyncTask class needs to be loaded in UI thread. + // So we load it here to comply the rule. + try { + Class.forName(AsyncTask.class.getName()); + } catch (ClassNotFoundException e) { + } + } +} diff --git a/src/com/android/gallery3d/app/GalleryContext.java b/src/com/android/gallery3d/app/GalleryContext.java new file mode 100644 index 000000000..06f4fe4d1 --- /dev/null +++ b/src/com/android/gallery3d/app/GalleryContext.java @@ -0,0 +1,34 @@ +/* + * 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.app; + +import android.content.Context; +import android.content.res.Resources; +import android.os.Looper; + +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.util.ThreadPool; + +public interface GalleryContext { + public DataManager getDataManager(); + + public Context getAndroidContext(); + + public Looper getMainLooper(); + public Resources getResources(); + public ThreadPool getThreadPool(); +} diff --git a/src/com/android/gallery3d/app/LoadingListener.java b/src/com/android/gallery3d/app/LoadingListener.java new file mode 100644 index 000000000..e94df9307 --- /dev/null +++ b/src/com/android/gallery3d/app/LoadingListener.java @@ -0,0 +1,27 @@ +/* + * 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.app; + +public interface LoadingListener { + public void onLoadingStarted(); + /** + * Called when loading is complete or no further progress can be made. + * + * @param loadingFailed true if data source cannot provide requested data + */ + public void onLoadingFinished(boolean loadingFailed); +} diff --git a/src/com/android/gallery3d/app/Log.java b/src/com/android/gallery3d/app/Log.java new file mode 100644 index 000000000..07a8ea588 --- /dev/null +++ b/src/com/android/gallery3d/app/Log.java @@ -0,0 +1,53 @@ +/* + * 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.app; + +public class Log { + public static int v(String tag, String msg) { + return android.util.Log.v(tag, msg); + } + public static int v(String tag, String msg, Throwable tr) { + return android.util.Log.v(tag, msg, tr); + } + public static int d(String tag, String msg) { + return android.util.Log.d(tag, msg); + } + public static int d(String tag, String msg, Throwable tr) { + return android.util.Log.d(tag, msg, tr); + } + public static int i(String tag, String msg) { + return android.util.Log.i(tag, msg); + } + public static int i(String tag, String msg, Throwable tr) { + return android.util.Log.i(tag, msg, tr); + } + public static int w(String tag, String msg) { + return android.util.Log.w(tag, msg); + } + public static int w(String tag, String msg, Throwable tr) { + return android.util.Log.w(tag, msg, tr); + } + public static int w(String tag, Throwable tr) { + return android.util.Log.w(tag, tr); + } + public static int e(String tag, String msg) { + return android.util.Log.e(tag, msg); + } + public static int e(String tag, String msg, Throwable tr) { + return android.util.Log.e(tag, msg, tr); + } +} diff --git a/src/com/android/gallery3d/app/ManageCachePage.java b/src/com/android/gallery3d/app/ManageCachePage.java new file mode 100644 index 000000000..4f5c35819 --- /dev/null +++ b/src/com/android/gallery3d/app/ManageCachePage.java @@ -0,0 +1,419 @@ +/* + * 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.app; + +import android.app.Activity; +import android.content.res.Configuration; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.text.format.Formatter; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.FrameLayout; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; + +import com.android.gallery3d.R; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.glrenderer.GLCanvas; +import com.android.gallery3d.ui.CacheStorageUsageInfo; +import com.android.gallery3d.ui.GLRoot; +import com.android.gallery3d.ui.GLView; +import com.android.gallery3d.ui.ManageCacheDrawer; +import com.android.gallery3d.ui.MenuExecutor; +import com.android.gallery3d.ui.SelectionManager; +import com.android.gallery3d.ui.SlotView; +import com.android.gallery3d.ui.SynchronizedHandler; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.GalleryUtils; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import java.util.ArrayList; + +public class ManageCachePage extends ActivityState implements + SelectionManager.SelectionListener, MenuExecutor.ProgressListener, + EyePosition.EyePositionListener, OnClickListener { + public static final String KEY_MEDIA_PATH = "media-path"; + + @SuppressWarnings("unused") + private static final String TAG = "ManageCachePage"; + + private static final int DATA_CACHE_SIZE = 256; + private static final int MSG_REFRESH_STORAGE_INFO = 1; + private static final int MSG_REQUEST_LAYOUT = 2; + private static final int PROGRESS_BAR_MAX = 10000; + + private SlotView mSlotView; + private MediaSet mMediaSet; + + protected SelectionManager mSelectionManager; + protected ManageCacheDrawer mSelectionDrawer; + private AlbumSetDataLoader mAlbumSetDataAdapter; + + private EyePosition mEyePosition; + + // The eyes' position of the user, the origin is at the center of the + // device and the unit is in pixels. + private float mX; + private float mY; + private float mZ; + + private int mAlbumCountToMakeAvailableOffline; + private View mFooterContent; + private CacheStorageUsageInfo mCacheStorageInfo; + private Future<Void> mUpdateStorageInfo; + private Handler mHandler; + private boolean mLayoutReady = false; + + @Override + protected int getBackgroundColorId() { + return R.color.cache_background; + } + + private GLView mRootPane = new GLView() { + private float mMatrix[] = new float[16]; + + @Override + protected void renderBackground(GLCanvas view) { + view.clearBuffer(getBackgroundColor()); + } + + @Override + protected void onLayout( + boolean changed, int left, int top, int right, int bottom) { + // Hack: our layout depends on other components on the screen. + // We assume the other components will complete before we get a change + // to run a message in main thread. + if (!mLayoutReady) { + mHandler.sendEmptyMessage(MSG_REQUEST_LAYOUT); + return; + } + mLayoutReady = false; + + mEyePosition.resetPosition(); + int slotViewTop = mActivity.getGalleryActionBar().getHeight(); + int slotViewBottom = bottom - top; + + View footer = mActivity.findViewById(R.id.footer); + if (footer != null) { + int location[] = {0, 0}; + footer.getLocationOnScreen(location); + slotViewBottom = location[1]; + } + + mSlotView.layout(0, slotViewTop, right - left, slotViewBottom); + } + + @Override + protected void render(GLCanvas canvas) { + canvas.save(GLCanvas.SAVE_FLAG_MATRIX); + GalleryUtils.setViewPointMatrix(mMatrix, + getWidth() / 2 + mX, getHeight() / 2 + mY, mZ); + canvas.multiplyMatrix(mMatrix, 0); + super.render(canvas); + canvas.restore(); + } + }; + + @Override + public void onEyePositionChanged(float x, float y, float z) { + mRootPane.lockRendering(); + mX = x; + mY = y; + mZ = z; + mRootPane.unlockRendering(); + mRootPane.invalidate(); + } + + private void onDown(int index) { + mSelectionDrawer.setPressedIndex(index); + } + + private void onUp() { + mSelectionDrawer.setPressedIndex(-1); + } + + public void onSingleTapUp(int slotIndex) { + MediaSet targetSet = mAlbumSetDataAdapter.getMediaSet(slotIndex); + if (targetSet == null) return; // Content is dirty, we shall reload soon + + // ignore selection action if the target set does not support cache + // operation (like a local album). + if ((targetSet.getSupportedOperations() + & MediaSet.SUPPORT_CACHE) == 0) { + showToastForLocalAlbum(); + return; + } + + Path path = targetSet.getPath(); + boolean isFullyCached = + (targetSet.getCacheFlag() == MediaObject.CACHE_FLAG_FULL); + boolean isSelected = mSelectionManager.isItemSelected(path); + + if (!isFullyCached) { + // We only count the media sets that will be made available offline + // in this session. + if (isSelected) { + --mAlbumCountToMakeAvailableOffline; + } else { + ++mAlbumCountToMakeAvailableOffline; + } + } + + long sizeOfTarget = targetSet.getCacheSize(); + mCacheStorageInfo.increaseTargetCacheSize( + (isFullyCached ^ isSelected) ? -sizeOfTarget : sizeOfTarget); + refreshCacheStorageInfo(); + + mSelectionManager.toggle(path); + mSlotView.invalidate(); + } + + @Override + public void onCreate(Bundle data, Bundle restoreState) { + super.onCreate(data, restoreState); + mCacheStorageInfo = new CacheStorageUsageInfo(mActivity); + initializeViews(); + initializeData(data); + mEyePosition = new EyePosition(mActivity.getAndroidContext(), this); + mHandler = new SynchronizedHandler(mActivity.getGLRoot()) { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_REFRESH_STORAGE_INFO: + refreshCacheStorageInfo(); + break; + case MSG_REQUEST_LAYOUT: { + mLayoutReady = true; + removeMessages(MSG_REQUEST_LAYOUT); + mRootPane.requestLayout(); + break; + } + } + } + }; + } + + @Override + public void onConfigurationChanged(Configuration config) { + // We use different layout resources for different configs + initializeFooterViews(); + FrameLayout layout = (FrameLayout) ((Activity) mActivity).findViewById(R.id.footer); + if (layout.getVisibility() == View.VISIBLE) { + layout.removeAllViews(); + layout.addView(mFooterContent); + } + } + + @Override + public void onPause() { + super.onPause(); + mAlbumSetDataAdapter.pause(); + mSelectionDrawer.pause(); + mEyePosition.pause(); + + if (mUpdateStorageInfo != null) { + mUpdateStorageInfo.cancel(); + mUpdateStorageInfo = null; + } + mHandler.removeMessages(MSG_REFRESH_STORAGE_INFO); + + FrameLayout layout = (FrameLayout) ((Activity) mActivity).findViewById(R.id.footer); + layout.removeAllViews(); + layout.setVisibility(View.INVISIBLE); + } + + private Job<Void> mUpdateStorageInfoJob = new Job<Void>() { + @Override + public Void run(JobContext jc) { + mCacheStorageInfo.loadStorageInfo(jc); + if (!jc.isCancelled()) { + mHandler.sendEmptyMessage(MSG_REFRESH_STORAGE_INFO); + } + return null; + } + }; + + @Override + public void onResume() { + super.onResume(); + setContentPane(mRootPane); + mAlbumSetDataAdapter.resume(); + mSelectionDrawer.resume(); + mEyePosition.resume(); + mUpdateStorageInfo = mActivity.getThreadPool().submit(mUpdateStorageInfoJob); + FrameLayout layout = (FrameLayout) ((Activity) mActivity).findViewById(R.id.footer); + layout.addView(mFooterContent); + layout.setVisibility(View.VISIBLE); + } + + private void initializeData(Bundle data) { + String mediaPath = data.getString(ManageCachePage.KEY_MEDIA_PATH); + mMediaSet = mActivity.getDataManager().getMediaSet(mediaPath); + mSelectionManager.setSourceMediaSet(mMediaSet); + + // We will always be in selection mode in this page. + mSelectionManager.setAutoLeaveSelectionMode(false); + mSelectionManager.enterSelectionMode(); + + mAlbumSetDataAdapter = new AlbumSetDataLoader( + mActivity, mMediaSet, DATA_CACHE_SIZE); + mSelectionDrawer.setModel(mAlbumSetDataAdapter); + } + + private void initializeViews() { + Activity activity = mActivity; + + mSelectionManager = new SelectionManager(mActivity, true); + mSelectionManager.setSelectionListener(this); + + Config.ManageCachePage config = Config.ManageCachePage.get(activity); + mSlotView = new SlotView(mActivity, config.slotViewSpec); + mSelectionDrawer = new ManageCacheDrawer(mActivity, mSelectionManager, mSlotView, + config.labelSpec, config.cachePinSize, config.cachePinMargin); + mSlotView.setSlotRenderer(mSelectionDrawer); + mSlotView.setListener(new SlotView.SimpleListener() { + @Override + public void onDown(int index) { + ManageCachePage.this.onDown(index); + } + + @Override + public void onUp(boolean followedByLongPress) { + ManageCachePage.this.onUp(); + } + + @Override + public void onSingleTapUp(int slotIndex) { + ManageCachePage.this.onSingleTapUp(slotIndex); + } + }); + mRootPane.addComponent(mSlotView); + initializeFooterViews(); + } + + private void initializeFooterViews() { + Activity activity = mActivity; + + LayoutInflater inflater = activity.getLayoutInflater(); + mFooterContent = inflater.inflate(R.layout.manage_offline_bar, null); + + mFooterContent.findViewById(R.id.done).setOnClickListener(this); + refreshCacheStorageInfo(); + } + + @Override + public void onClick(View view) { + Utils.assertTrue(view.getId() == R.id.done); + GLRoot root = mActivity.getGLRoot(); + root.lockRenderThread(); + try { + ArrayList<Path> ids = mSelectionManager.getSelected(false); + if (ids.size() == 0) { + onBackPressed(); + return; + } + showToast(); + + MenuExecutor menuExecutor = new MenuExecutor(mActivity, mSelectionManager); + menuExecutor.startAction(R.id.action_toggle_full_caching, + R.string.process_caching_requests, this); + } finally { + root.unlockRenderThread(); + } + } + + private void showToast() { + if (mAlbumCountToMakeAvailableOffline > 0) { + Activity activity = mActivity; + Toast.makeText(activity, activity.getResources().getQuantityString( + R.plurals.make_albums_available_offline, + mAlbumCountToMakeAvailableOffline), + Toast.LENGTH_SHORT).show(); + } + } + + private void showToastForLocalAlbum() { + Activity activity = mActivity; + Toast.makeText(activity, activity.getResources().getString( + R.string.try_to_set_local_album_available_offline), + Toast.LENGTH_SHORT).show(); + } + + private void refreshCacheStorageInfo() { + ProgressBar progressBar = (ProgressBar) mFooterContent.findViewById(R.id.progress); + TextView status = (TextView) mFooterContent.findViewById(R.id.status); + progressBar.setMax(PROGRESS_BAR_MAX); + long totalBytes = mCacheStorageInfo.getTotalBytes(); + long usedBytes = mCacheStorageInfo.getUsedBytes(); + long expectedBytes = mCacheStorageInfo.getExpectedUsedBytes(); + long freeBytes = mCacheStorageInfo.getFreeBytes(); + + Activity activity = mActivity; + if (totalBytes == 0) { + progressBar.setProgress(0); + progressBar.setSecondaryProgress(0); + + // TODO: get the string translated + String label = activity.getString(R.string.free_space_format, "-"); + status.setText(label); + } else { + progressBar.setProgress((int) (usedBytes * PROGRESS_BAR_MAX / totalBytes)); + progressBar.setSecondaryProgress( + (int) (expectedBytes * PROGRESS_BAR_MAX / totalBytes)); + String label = activity.getString(R.string.free_space_format, + Formatter.formatFileSize(activity, freeBytes)); + status.setText(label); + } + } + + @Override + public void onProgressComplete(int result) { + onBackPressed(); + } + + @Override + public void onProgressUpdate(int index) { + } + + @Override + public void onSelectionModeChange(int mode) { + } + + @Override + public void onSelectionChange(Path path, boolean selected) { + } + + @Override + public void onConfirmDialogDismissed(boolean confirmed) { + } + + @Override + public void onConfirmDialogShown() { + } + + @Override + public void onProgressStart() { + } +} diff --git a/src/com/android/gallery3d/app/MovieActivity.java b/src/com/android/gallery3d/app/MovieActivity.java new file mode 100644 index 000000000..40edbbe4d --- /dev/null +++ b/src/com/android/gallery3d/app/MovieActivity.java @@ -0,0 +1,263 @@ +/* + * Copyright (C) 2007 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.app; + +import android.annotation.TargetApi; +import android.app.ActionBar; +import android.app.Activity; +import android.content.AsyncQueryHandler; +import android.content.ContentResolver; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.media.AudioManager; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.provider.MediaStore; +import android.provider.OpenableColumns; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; +import android.widget.ShareActionProvider; + +import com.android.gallery3d.R; +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.common.Utils; + +/** + * This activity plays a video from a specified URI. + * + * The client of this activity can pass a logo bitmap in the intent (KEY_LOGO_BITMAP) + * to set the action bar logo so the playback process looks more seamlessly integrated with + * the original activity. + */ +public class MovieActivity extends Activity { + @SuppressWarnings("unused") + private static final String TAG = "MovieActivity"; + public static final String KEY_LOGO_BITMAP = "logo-bitmap"; + public static final String KEY_TREAT_UP_AS_BACK = "treat-up-as-back"; + + private MoviePlayer mPlayer; + private boolean mFinishOnCompletion; + private Uri mUri; + private boolean mTreatUpAsBack; + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + private void setSystemUiVisibility(View rootView) { + if (ApiHelper.HAS_VIEW_SYSTEM_UI_FLAG_LAYOUT_STABLE) { + rootView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + requestWindowFeature(Window.FEATURE_ACTION_BAR); + requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY); + + setContentView(R.layout.movie_view); + View rootView = findViewById(R.id.movie_view_root); + + setSystemUiVisibility(rootView); + + Intent intent = getIntent(); + initializeActionBar(intent); + mFinishOnCompletion = intent.getBooleanExtra( + MediaStore.EXTRA_FINISH_ON_COMPLETION, true); + mTreatUpAsBack = intent.getBooleanExtra(KEY_TREAT_UP_AS_BACK, false); + mPlayer = new MoviePlayer(rootView, this, intent.getData(), savedInstanceState, + !mFinishOnCompletion) { + @Override + public void onCompletion() { + if (mFinishOnCompletion) { + finish(); + } + } + }; + if (intent.hasExtra(MediaStore.EXTRA_SCREEN_ORIENTATION)) { + int orientation = intent.getIntExtra( + MediaStore.EXTRA_SCREEN_ORIENTATION, + ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); + if (orientation != getRequestedOrientation()) { + setRequestedOrientation(orientation); + } + } + Window win = getWindow(); + WindowManager.LayoutParams winParams = win.getAttributes(); + winParams.buttonBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_OFF; + winParams.flags |= WindowManager.LayoutParams.FLAG_FULLSCREEN; + win.setAttributes(winParams); + + // We set the background in the theme to have the launching animation. + // But for the performance (and battery), we remove the background here. + win.setBackgroundDrawable(null); + } + + private void setActionBarLogoFromIntent(Intent intent) { + Bitmap logo = intent.getParcelableExtra(KEY_LOGO_BITMAP); + if (logo != null) { + getActionBar().setLogo( + new BitmapDrawable(getResources(), logo)); + } + } + + private void initializeActionBar(Intent intent) { + mUri = intent.getData(); + final ActionBar actionBar = getActionBar(); + if (actionBar == null) { + return; + } + setActionBarLogoFromIntent(intent); + actionBar.setDisplayOptions( + ActionBar.DISPLAY_HOME_AS_UP, + ActionBar.DISPLAY_HOME_AS_UP); + + String title = intent.getStringExtra(Intent.EXTRA_TITLE); + if (title != null) { + actionBar.setTitle(title); + } else { + // Displays the filename as title, reading the filename from the + // interface: {@link android.provider.OpenableColumns#DISPLAY_NAME}. + AsyncQueryHandler queryHandler = + new AsyncQueryHandler(getContentResolver()) { + @Override + protected void onQueryComplete(int token, Object cookie, + Cursor cursor) { + try { + if ((cursor != null) && cursor.moveToFirst()) { + String displayName = cursor.getString(0); + + // Just show empty title if other apps don't set + // DISPLAY_NAME + actionBar.setTitle((displayName == null) ? "" : + displayName); + } + } finally { + Utils.closeSilently(cursor); + } + } + }; + queryHandler.startQuery(0, null, mUri, + new String[] {OpenableColumns.DISPLAY_NAME}, null, null, + null); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + getMenuInflater().inflate(R.menu.movie, menu); + + // Document says EXTRA_STREAM should be a content: Uri + // So, we only share the video if it's "content:". + MenuItem shareItem = menu.findItem(R.id.action_share); + if (ContentResolver.SCHEME_CONTENT.equals(mUri.getScheme())) { + shareItem.setVisible(true); + ((ShareActionProvider) shareItem.getActionProvider()) + .setShareIntent(createShareIntent()); + } else { + shareItem.setVisible(false); + } + return true; + } + + private Intent createShareIntent() { + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType("video/*"); + intent.putExtra(Intent.EXTRA_STREAM, mUri); + return intent; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + if (id == android.R.id.home) { + if (mTreatUpAsBack) { + finish(); + } else { + startActivity(new Intent(this, Gallery.class)); + finish(); + } + return true; + } else if (id == R.id.action_share) { + startActivity(Intent.createChooser(createShareIntent(), + getString(R.string.share))); + return true; + } + return false; + } + + @Override + public void onStart() { + ((AudioManager) getSystemService(AUDIO_SERVICE)) + .requestAudioFocus(null, AudioManager.STREAM_MUSIC, + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); + super.onStart(); + } + + @Override + protected void onStop() { + ((AudioManager) getSystemService(AUDIO_SERVICE)) + .abandonAudioFocus(null); + super.onStop(); + } + + @Override + public void onPause() { + mPlayer.onPause(); + super.onPause(); + } + + @Override + public void onResume() { + mPlayer.onResume(); + super.onResume(); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + mPlayer.onSaveInstanceState(outState); + } + + @Override + public void onDestroy() { + mPlayer.onDestroy(); + super.onDestroy(); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + return mPlayer.onKeyDown(keyCode, event) + || super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + return mPlayer.onKeyUp(keyCode, event) + || super.onKeyUp(keyCode, event); + } +} diff --git a/src/com/android/gallery3d/app/MovieControllerOverlay.java b/src/com/android/gallery3d/app/MovieControllerOverlay.java new file mode 100644 index 000000000..f01e619c6 --- /dev/null +++ b/src/com/android/gallery3d/app/MovieControllerOverlay.java @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2011 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.app; + +import android.content.Context; +import android.os.Handler; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.animation.Animation; +import android.view.animation.Animation.AnimationListener; +import android.view.animation.AnimationUtils; +import com.android.gallery3d.R; + +/** + * The playback controller for the Movie Player. + */ +public class MovieControllerOverlay extends CommonControllerOverlay implements + AnimationListener { + + private boolean hidden; + + private final Handler handler; + private final Runnable startHidingRunnable; + private final Animation hideAnimation; + + public MovieControllerOverlay(Context context) { + super(context); + + handler = new Handler(); + startHidingRunnable = new Runnable() { + @Override + public void run() { + startHiding(); + } + }; + + hideAnimation = AnimationUtils.loadAnimation(context, R.anim.player_out); + hideAnimation.setAnimationListener(this); + + hide(); + } + + @Override + protected void createTimeBar(Context context) { + mTimeBar = new TimeBar(context, this); + } + + @Override + public void hide() { + boolean wasHidden = hidden; + hidden = true; + super.hide(); + if (mListener != null && wasHidden != hidden) { + mListener.onHidden(); + } + } + + + @Override + public void show() { + boolean wasHidden = hidden; + hidden = false; + super.show(); + if (mListener != null && wasHidden != hidden) { + mListener.onShown(); + } + maybeStartHiding(); + } + + private void maybeStartHiding() { + cancelHiding(); + if (mState == State.PLAYING) { + handler.postDelayed(startHidingRunnable, 2500); + } + } + + private void startHiding() { + startHideAnimation(mBackground); + startHideAnimation(mTimeBar); + startHideAnimation(mPlayPauseReplayView); + } + + private void startHideAnimation(View view) { + if (view.getVisibility() == View.VISIBLE) { + view.startAnimation(hideAnimation); + } + } + + private void cancelHiding() { + handler.removeCallbacks(startHidingRunnable); + mBackground.setAnimation(null); + mTimeBar.setAnimation(null); + mPlayPauseReplayView.setAnimation(null); + } + + @Override + public void onAnimationStart(Animation animation) { + // Do nothing. + } + + @Override + public void onAnimationRepeat(Animation animation) { + // Do nothing. + } + + @Override + public void onAnimationEnd(Animation animation) { + hide(); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (hidden) { + show(); + } + return super.onKeyDown(keyCode, event); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (super.onTouchEvent(event)) { + return true; + } + + if (hidden) { + show(); + return true; + } + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + cancelHiding(); + if (mState == State.PLAYING || mState == State.PAUSED) { + mListener.onPlayPause(); + } + break; + case MotionEvent.ACTION_UP: + maybeStartHiding(); + break; + } + return true; + } + + @Override + protected void updateViews() { + if (hidden) { + return; + } + super.updateViews(); + } + + // TimeBar listener + + @Override + public void onScrubbingStart() { + cancelHiding(); + super.onScrubbingStart(); + } + + @Override + public void onScrubbingMove(int time) { + cancelHiding(); + super.onScrubbingMove(time); + } + + @Override + public void onScrubbingEnd(int time, int trimStartTime, int trimEndTime) { + maybeStartHiding(); + super.onScrubbingEnd(time, trimStartTime, trimEndTime); + } +} diff --git a/src/com/android/gallery3d/app/MoviePlayer.java b/src/com/android/gallery3d/app/MoviePlayer.java new file mode 100644 index 000000000..ce9183483 --- /dev/null +++ b/src/com/android/gallery3d/app/MoviePlayer.java @@ -0,0 +1,525 @@ +/* + * Copyright (C) 2009 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.app; + +import android.annotation.TargetApi; +import android.app.AlertDialog; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnCancelListener; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.content.IntentFilter; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.VideoView; + +import com.android.gallery3d.R; +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.common.BlobCache; +import com.android.gallery3d.util.CacheManager; +import com.android.gallery3d.util.GalleryUtils; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; + +public class MoviePlayer implements + MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener, + ControllerOverlay.Listener { + @SuppressWarnings("unused") + private static final String TAG = "MoviePlayer"; + + private static final String KEY_VIDEO_POSITION = "video-position"; + private static final String KEY_RESUMEABLE_TIME = "resumeable-timeout"; + + // These are constants in KeyEvent, appearing on API level 11. + private static final int KEYCODE_MEDIA_PLAY = 126; + private static final int KEYCODE_MEDIA_PAUSE = 127; + + // Copied from MediaPlaybackService in the Music Player app. + private static final String SERVICECMD = "com.android.music.musicservicecommand"; + private static final String CMDNAME = "command"; + private static final String CMDPAUSE = "pause"; + + private static final long BLACK_TIMEOUT = 500; + + // If we resume the acitivty with in RESUMEABLE_TIMEOUT, we will keep playing. + // Otherwise, we pause the player. + private static final long RESUMEABLE_TIMEOUT = 3 * 60 * 1000; // 3 mins + + private Context mContext; + private final VideoView mVideoView; + private final View mRootView; + private final Bookmarker mBookmarker; + private final Uri mUri; + private final Handler mHandler = new Handler(); + private final AudioBecomingNoisyReceiver mAudioBecomingNoisyReceiver; + private final MovieControllerOverlay mController; + + private long mResumeableTime = Long.MAX_VALUE; + private int mVideoPosition = 0; + private boolean mHasPaused = false; + private int mLastSystemUiVis = 0; + + // If the time bar is being dragged. + private boolean mDragging; + + // If the time bar is visible. + private boolean mShowing; + + private final Runnable mPlayingChecker = new Runnable() { + @Override + public void run() { + if (mVideoView.isPlaying()) { + mController.showPlaying(); + } else { + mHandler.postDelayed(mPlayingChecker, 250); + } + } + }; + + private final Runnable mProgressChecker = new Runnable() { + @Override + public void run() { + int pos = setProgress(); + mHandler.postDelayed(mProgressChecker, 1000 - (pos % 1000)); + } + }; + + public MoviePlayer(View rootView, final MovieActivity movieActivity, + Uri videoUri, Bundle savedInstance, boolean canReplay) { + mContext = movieActivity.getApplicationContext(); + mRootView = rootView; + mVideoView = (VideoView) rootView.findViewById(R.id.surface_view); + mBookmarker = new Bookmarker(movieActivity); + mUri = videoUri; + + mController = new MovieControllerOverlay(mContext); + ((ViewGroup)rootView).addView(mController.getView()); + mController.setListener(this); + mController.setCanReplay(canReplay); + + mVideoView.setOnErrorListener(this); + mVideoView.setOnCompletionListener(this); + mVideoView.setVideoURI(mUri); + mVideoView.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + mController.show(); + return true; + } + }); + mVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { + @Override + public void onPrepared(MediaPlayer player) { + if (!mVideoView.canSeekForward() || !mVideoView.canSeekBackward()) { + mController.setSeekable(false); + } else { + mController.setSeekable(true); + } + setProgress(); + } + }); + + // The SurfaceView is transparent before drawing the first frame. + // This makes the UI flashing when open a video. (black -> old screen + // -> video) However, we have no way to know the timing of the first + // frame. So, we hide the VideoView for a while to make sure the + // video has been drawn on it. + mVideoView.postDelayed(new Runnable() { + @Override + public void run() { + mVideoView.setVisibility(View.VISIBLE); + } + }, BLACK_TIMEOUT); + + setOnSystemUiVisibilityChangeListener(); + // Hide system UI by default + showSystemUi(false); + + mAudioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver(); + mAudioBecomingNoisyReceiver.register(); + + Intent i = new Intent(SERVICECMD); + i.putExtra(CMDNAME, CMDPAUSE); + movieActivity.sendBroadcast(i); + + if (savedInstance != null) { // this is a resumed activity + mVideoPosition = savedInstance.getInt(KEY_VIDEO_POSITION, 0); + mResumeableTime = savedInstance.getLong(KEY_RESUMEABLE_TIME, Long.MAX_VALUE); + mVideoView.start(); + mVideoView.suspend(); + mHasPaused = true; + } else { + final Integer bookmark = mBookmarker.getBookmark(mUri); + if (bookmark != null) { + showResumeDialog(movieActivity, bookmark); + } else { + startVideo(); + } + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + private void setOnSystemUiVisibilityChangeListener() { + if (!ApiHelper.HAS_VIEW_SYSTEM_UI_FLAG_HIDE_NAVIGATION) return; + + // When the user touches the screen or uses some hard key, the framework + // will change system ui visibility from invisible to visible. We show + // the media control and enable system UI (e.g. ActionBar) to be visible at this point + mVideoView.setOnSystemUiVisibilityChangeListener( + new View.OnSystemUiVisibilityChangeListener() { + @Override + public void onSystemUiVisibilityChange(int visibility) { + int diff = mLastSystemUiVis ^ visibility; + mLastSystemUiVis = visibility; + if ((diff & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) != 0 + && (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0) { + mController.show(); + } + } + }); + } + + @SuppressWarnings("deprecation") + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + private void showSystemUi(boolean visible) { + if (!ApiHelper.HAS_VIEW_SYSTEM_UI_FLAG_LAYOUT_STABLE) return; + + int flag = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE; + if (!visible) { + // We used the deprecated "STATUS_BAR_HIDDEN" for unbundling + flag |= View.STATUS_BAR_HIDDEN | View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; + } + mVideoView.setSystemUiVisibility(flag); + } + + public void onSaveInstanceState(Bundle outState) { + outState.putInt(KEY_VIDEO_POSITION, mVideoPosition); + outState.putLong(KEY_RESUMEABLE_TIME, mResumeableTime); + } + + private void showResumeDialog(Context context, final int bookmark) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.resume_playing_title); + builder.setMessage(String.format( + context.getString(R.string.resume_playing_message), + GalleryUtils.formatDuration(context, bookmark / 1000))); + builder.setOnCancelListener(new OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + onCompletion(); + } + }); + builder.setPositiveButton( + R.string.resume_playing_resume, new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + mVideoView.seekTo(bookmark); + startVideo(); + } + }); + builder.setNegativeButton( + R.string.resume_playing_restart, new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + startVideo(); + } + }); + builder.show(); + } + + public void onPause() { + mHasPaused = true; + mHandler.removeCallbacksAndMessages(null); + mVideoPosition = mVideoView.getCurrentPosition(); + mBookmarker.setBookmark(mUri, mVideoPosition, mVideoView.getDuration()); + mVideoView.suspend(); + mResumeableTime = System.currentTimeMillis() + RESUMEABLE_TIMEOUT; + } + + public void onResume() { + if (mHasPaused) { + mVideoView.seekTo(mVideoPosition); + mVideoView.resume(); + + // If we have slept for too long, pause the play + if (System.currentTimeMillis() > mResumeableTime) { + pauseVideo(); + } + } + mHandler.post(mProgressChecker); + } + + public void onDestroy() { + mVideoView.stopPlayback(); + mAudioBecomingNoisyReceiver.unregister(); + } + + // This updates the time bar display (if necessary). It is called every + // second by mProgressChecker and also from places where the time bar needs + // to be updated immediately. + private int setProgress() { + if (mDragging || !mShowing) { + return 0; + } + int position = mVideoView.getCurrentPosition(); + int duration = mVideoView.getDuration(); + mController.setTimes(position, duration, 0, 0); + return position; + } + + private void startVideo() { + // For streams that we expect to be slow to start up, show a + // progress spinner until playback starts. + String scheme = mUri.getScheme(); + if ("http".equalsIgnoreCase(scheme) || "rtsp".equalsIgnoreCase(scheme)) { + mController.showLoading(); + mHandler.removeCallbacks(mPlayingChecker); + mHandler.postDelayed(mPlayingChecker, 250); + } else { + mController.showPlaying(); + mController.hide(); + } + + mVideoView.start(); + setProgress(); + } + + private void playVideo() { + mVideoView.start(); + mController.showPlaying(); + setProgress(); + } + + private void pauseVideo() { + mVideoView.pause(); + mController.showPaused(); + } + + // Below are notifications from VideoView + @Override + public boolean onError(MediaPlayer player, int arg1, int arg2) { + mHandler.removeCallbacksAndMessages(null); + // VideoView will show an error dialog if we return false, so no need + // to show more message. + mController.showErrorMessage(""); + return false; + } + + @Override + public void onCompletion(MediaPlayer mp) { + mController.showEnded(); + onCompletion(); + } + + public void onCompletion() { + } + + // Below are notifications from ControllerOverlay + @Override + public void onPlayPause() { + if (mVideoView.isPlaying()) { + pauseVideo(); + } else { + playVideo(); + } + } + + @Override + public void onSeekStart() { + mDragging = true; + } + + @Override + public void onSeekMove(int time) { + mVideoView.seekTo(time); + } + + @Override + public void onSeekEnd(int time, int start, int end) { + mDragging = false; + mVideoView.seekTo(time); + setProgress(); + } + + @Override + public void onShown() { + mShowing = true; + setProgress(); + showSystemUi(true); + } + + @Override + public void onHidden() { + mShowing = false; + showSystemUi(false); + } + + @Override + public void onReplay() { + startVideo(); + } + + // Below are key events passed from MovieActivity. + public boolean onKeyDown(int keyCode, KeyEvent event) { + + // Some headsets will fire off 7-10 events on a single click + if (event.getRepeatCount() > 0) { + return isMediaKey(keyCode); + } + + switch (keyCode) { + case KeyEvent.KEYCODE_HEADSETHOOK: + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + if (mVideoView.isPlaying()) { + pauseVideo(); + } else { + playVideo(); + } + return true; + case KEYCODE_MEDIA_PAUSE: + if (mVideoView.isPlaying()) { + pauseVideo(); + } + return true; + case KEYCODE_MEDIA_PLAY: + if (!mVideoView.isPlaying()) { + playVideo(); + } + return true; + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: + case KeyEvent.KEYCODE_MEDIA_NEXT: + // TODO: Handle next / previous accordingly, for now we're + // just consuming the events. + return true; + } + return false; + } + + public boolean onKeyUp(int keyCode, KeyEvent event) { + return isMediaKey(keyCode); + } + + private static boolean isMediaKey(int keyCode) { + return keyCode == KeyEvent.KEYCODE_HEADSETHOOK + || keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS + || keyCode == KeyEvent.KEYCODE_MEDIA_NEXT + || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE + || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY + || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE; + } + + // We want to pause when the headset is unplugged. + private class AudioBecomingNoisyReceiver extends BroadcastReceiver { + + public void register() { + mContext.registerReceiver(this, + new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)); + } + + public void unregister() { + mContext.unregisterReceiver(this); + } + + @Override + public void onReceive(Context context, Intent intent) { + if (mVideoView.isPlaying()) pauseVideo(); + } + } +} + +class Bookmarker { + private static final String TAG = "Bookmarker"; + + private static final String BOOKMARK_CACHE_FILE = "bookmark"; + private static final int BOOKMARK_CACHE_MAX_ENTRIES = 100; + private static final int BOOKMARK_CACHE_MAX_BYTES = 10 * 1024; + private static final int BOOKMARK_CACHE_VERSION = 1; + + private static final int HALF_MINUTE = 30 * 1000; + private static final int TWO_MINUTES = 4 * HALF_MINUTE; + + private final Context mContext; + + public Bookmarker(Context context) { + mContext = context; + } + + public void setBookmark(Uri uri, int bookmark, int duration) { + try { + BlobCache cache = CacheManager.getCache(mContext, + BOOKMARK_CACHE_FILE, BOOKMARK_CACHE_MAX_ENTRIES, + BOOKMARK_CACHE_MAX_BYTES, BOOKMARK_CACHE_VERSION); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeUTF(uri.toString()); + dos.writeInt(bookmark); + dos.writeInt(duration); + dos.flush(); + cache.insert(uri.hashCode(), bos.toByteArray()); + } catch (Throwable t) { + Log.w(TAG, "setBookmark failed", t); + } + } + + public Integer getBookmark(Uri uri) { + try { + BlobCache cache = CacheManager.getCache(mContext, + BOOKMARK_CACHE_FILE, BOOKMARK_CACHE_MAX_ENTRIES, + BOOKMARK_CACHE_MAX_BYTES, BOOKMARK_CACHE_VERSION); + + byte[] data = cache.lookup(uri.hashCode()); + if (data == null) return null; + + DataInputStream dis = new DataInputStream( + new ByteArrayInputStream(data)); + + String uriString = DataInputStream.readUTF(dis); + int bookmark = dis.readInt(); + int duration = dis.readInt(); + + if (!uriString.equals(uri.toString())) { + return null; + } + + if ((bookmark < HALF_MINUTE) || (duration < TWO_MINUTES) + || (bookmark > (duration - HALF_MINUTE))) { + return null; + } + return Integer.valueOf(bookmark); + } catch (Throwable t) { + Log.w(TAG, "getBookmark failed", t); + } + return null; + } +} diff --git a/src/com/android/gallery3d/app/MuteVideo.java b/src/com/android/gallery3d/app/MuteVideo.java new file mode 100644 index 000000000..d3f3aa594 --- /dev/null +++ b/src/com/android/gallery3d/app/MuteVideo.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2012 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.app; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.Intent; +import android.net.Uri; +import android.os.Handler; +import android.provider.MediaStore; +import android.widget.Toast; + +import com.android.gallery3d.R; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.util.SaveVideoFileInfo; +import com.android.gallery3d.util.SaveVideoFileUtils; + +import java.io.IOException; + +public class MuteVideo { + + private ProgressDialog mMuteProgress; + + private String mFilePath = null; + private Uri mUri = null; + private SaveVideoFileInfo mDstFileInfo = null; + private Activity mActivity = null; + private final Handler mHandler = new Handler(); + + final String TIME_STAMP_NAME = "'MUTE'_yyyyMMdd_HHmmss"; + + public MuteVideo(String filePath, Uri uri, Activity activity) { + mUri = uri; + mFilePath = filePath; + mActivity = activity; + } + + public void muteInBackground() { + mDstFileInfo = SaveVideoFileUtils.getDstMp4FileInfo(TIME_STAMP_NAME, + mActivity.getContentResolver(), mUri, + mActivity.getString(R.string.folder_download)); + + showProgressDialog(); + new Thread(new Runnable() { + @Override + public void run() { + try { + VideoUtils.startMute(mFilePath, mDstFileInfo); + SaveVideoFileUtils.insertContent( + mDstFileInfo, mActivity.getContentResolver(), mUri); + } catch (IOException e) { + Toast.makeText(mActivity, mActivity.getString(R.string.video_mute_err), + Toast.LENGTH_SHORT).show(); + } + // After muting is done, trigger the UI changed. + mHandler.post(new Runnable() { + @Override + public void run() { + Toast.makeText(mActivity.getApplicationContext(), + mActivity.getString(R.string.save_into, + mDstFileInfo.mFolderName), + Toast.LENGTH_SHORT) + .show(); + + if (mMuteProgress != null) { + mMuteProgress.dismiss(); + mMuteProgress = null; + + // Show the result only when the activity not + // stopped. + Intent intent = new Intent(android.content.Intent.ACTION_VIEW); + intent.setDataAndType(Uri.fromFile(mDstFileInfo.mFile), "video/*"); + intent.putExtra(MediaStore.EXTRA_FINISH_ON_COMPLETION, false); + mActivity.startActivity(intent); + } + } + }); + } + }).start(); + } + + private void showProgressDialog() { + mMuteProgress = new ProgressDialog(mActivity); + mMuteProgress.setTitle(mActivity.getString(R.string.muting)); + mMuteProgress.setMessage(mActivity.getString(R.string.please_wait)); + mMuteProgress.setCancelable(false); + mMuteProgress.setCanceledOnTouchOutside(false); + mMuteProgress.show(); + } +} diff --git a/src/com/android/gallery3d/app/NotificationIds.java b/src/com/android/gallery3d/app/NotificationIds.java new file mode 100644 index 000000000..d697d854b --- /dev/null +++ b/src/com/android/gallery3d/app/NotificationIds.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2013 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.app; + +public class NotificationIds { + public static final int INGEST_NOTIFICATION_SCANNING = 10; + public static final int INGEST_NOTIFICATION_IMPORTING = 11; +} diff --git a/src/com/android/gallery3d/app/OrientationManager.java b/src/com/android/gallery3d/app/OrientationManager.java new file mode 100644 index 000000000..f2f632c9f --- /dev/null +++ b/src/com/android/gallery3d/app/OrientationManager.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2012 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.app; + +import android.app.Activity; +import android.content.ContentResolver; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.res.Configuration; +import android.provider.Settings; +import android.view.OrientationEventListener; +import android.view.Surface; + +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.ui.OrientationSource; + +public class OrientationManager implements OrientationSource { + private static final String TAG = "OrientationManager"; + + // Orientation hysteresis amount used in rounding, in degrees + private static final int ORIENTATION_HYSTERESIS = 5; + + private Activity mActivity; + private MyOrientationEventListener mOrientationListener; + // If the framework orientation is locked. + private boolean mOrientationLocked = false; + + // This is true if "Settings -> Display -> Rotation Lock" is checked. We + // don't allow the orientation to be unlocked if the value is true. + private boolean mRotationLockedSetting = false; + + public OrientationManager(Activity activity) { + mActivity = activity; + mOrientationListener = new MyOrientationEventListener(activity); + } + + public void resume() { + ContentResolver resolver = mActivity.getContentResolver(); + mRotationLockedSetting = Settings.System.getInt( + resolver, Settings.System.ACCELEROMETER_ROTATION, 0) != 1; + mOrientationListener.enable(); + } + + public void pause() { + mOrientationListener.disable(); + } + + //////////////////////////////////////////////////////////////////////////// + // Orientation handling + // + // We can choose to lock the framework orientation or not. If we lock the + // framework orientation, we calculate a a compensation value according to + // current device orientation and send it to listeners. If we don't lock + // the framework orientation, we always set the compensation value to 0. + //////////////////////////////////////////////////////////////////////////// + + // Lock the framework orientation to the current device orientation + public void lockOrientation() { + if (mOrientationLocked) return; + mOrientationLocked = true; + if (ApiHelper.HAS_ORIENTATION_LOCK) { + mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LOCKED); + } else { + mActivity.setRequestedOrientation(calculateCurrentScreenOrientation()); + } + } + + // Unlock the framework orientation, so it can change when the device + // rotates. + public void unlockOrientation() { + if (!mOrientationLocked) return; + mOrientationLocked = false; + Log.d(TAG, "unlock orientation"); + mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR); + } + + private int calculateCurrentScreenOrientation() { + int displayRotation = getDisplayRotation(); + // Display rotation >= 180 means we need to use the REVERSE landscape/portrait + boolean standard = displayRotation < 180; + if (mActivity.getResources().getConfiguration().orientation + == Configuration.ORIENTATION_LANDSCAPE) { + return standard + ? ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + : ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE; + } else { + if (displayRotation == 90 || displayRotation == 270) { + // If displayRotation = 90 or 270 then we are on a landscape + // device. On landscape devices, portrait is a 90 degree + // clockwise rotation from landscape, so we need + // to flip which portrait we pick as display rotation is counter clockwise + standard = !standard; + } + return standard + ? ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + : ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT; + } + } + + // This listens to the device orientation, so we can update the compensation. + private class MyOrientationEventListener extends OrientationEventListener { + public MyOrientationEventListener(Context context) { + super(context); + } + + @Override + public void onOrientationChanged(int orientation) { + // We keep the last known orientation. So if the user first orient + // the camera then point the camera to floor or sky, we still have + // the correct orientation. + if (orientation == ORIENTATION_UNKNOWN) return; + orientation = roundOrientation(orientation, 0); + } + } + + @Override + public int getDisplayRotation() { + return getDisplayRotation(mActivity); + } + + @Override + public int getCompensation() { + return 0; + } + + private static int roundOrientation(int orientation, int orientationHistory) { + boolean changeOrientation = false; + if (orientationHistory == OrientationEventListener.ORIENTATION_UNKNOWN) { + changeOrientation = true; + } else { + int dist = Math.abs(orientation - orientationHistory); + dist = Math.min(dist, 360 - dist); + changeOrientation = (dist >= 45 + ORIENTATION_HYSTERESIS); + } + if (changeOrientation) { + return ((orientation + 45) / 90 * 90) % 360; + } + return orientationHistory; + } + + private static int getDisplayRotation(Activity activity) { + int rotation = activity.getWindowManager().getDefaultDisplay() + .getRotation(); + switch (rotation) { + case Surface.ROTATION_0: return 0; + case Surface.ROTATION_90: return 90; + case Surface.ROTATION_180: return 180; + case Surface.ROTATION_270: return 270; + } + return 0; + } +} diff --git a/src/com/android/gallery3d/app/PackagesMonitor.java b/src/com/android/gallery3d/app/PackagesMonitor.java new file mode 100644 index 000000000..9b2412f1b --- /dev/null +++ b/src/com/android/gallery3d/app/PackagesMonitor.java @@ -0,0 +1,71 @@ +/* + * 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.app; + +import android.app.IntentService; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +import com.android.gallery3d.picasasource.PicasaSource; +import com.android.gallery3d.util.LightCycleHelper; + +public class PackagesMonitor extends BroadcastReceiver { + public static final String KEY_PACKAGES_VERSION = "packages-version"; + + public synchronized static int getPackagesVersion(Context context) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + return prefs.getInt(KEY_PACKAGES_VERSION, 1); + } + + @Override + public void onReceive(final Context context, final Intent intent) { + intent.setClass(context, AsyncService.class); + context.startService(intent); + } + + public static class AsyncService extends IntentService { + public AsyncService() { + super("GalleryPackagesMonitorAsync"); + } + + @Override + protected void onHandleIntent(Intent intent) { + onReceiveAsync(this, intent); + } + } + + // Runs in a background thread. + private static void onReceiveAsync(Context context, Intent intent) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + + int version = prefs.getInt(KEY_PACKAGES_VERSION, 1); + prefs.edit().putInt(KEY_PACKAGES_VERSION, version + 1).commit(); + + String action = intent.getAction(); + String packageName = intent.getData().getSchemeSpecificPart(); + if (Intent.ACTION_PACKAGE_ADDED.equals(action)) { + PicasaSource.onPackageAdded(context, packageName); + } else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) { + PicasaSource.onPackageRemoved(context, packageName); + } else if (Intent.ACTION_PACKAGE_CHANGED.equals(action)) { + PicasaSource.onPackageChanged(context, packageName); + } + } +} diff --git a/src/com/android/gallery3d/app/PanoramaMetadataSupport.java b/src/com/android/gallery3d/app/PanoramaMetadataSupport.java new file mode 100644 index 000000000..ba0c9e71a --- /dev/null +++ b/src/com/android/gallery3d/app/PanoramaMetadataSupport.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2012 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.app; + +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.MediaObject.PanoramaSupportCallback; +import com.android.gallery3d.data.PanoramaMetadataJob; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.FutureListener; +import com.android.gallery3d.util.LightCycleHelper; +import com.android.gallery3d.util.LightCycleHelper.PanoramaMetadata; + +import java.util.ArrayList; + +/** + * This class breaks out the off-thread panorama support checks so that the + * complexity can be shared between UriImage and LocalImage, which need to + * support panoramas. + */ +public class PanoramaMetadataSupport implements FutureListener<PanoramaMetadata> { + private Object mLock = new Object(); + private Future<PanoramaMetadata> mGetPanoMetadataTask; + private PanoramaMetadata mPanoramaMetadata; + private ArrayList<PanoramaSupportCallback> mCallbacksWaiting; + private MediaObject mMediaObject; + + public PanoramaMetadataSupport(MediaObject mediaObject) { + mMediaObject = mediaObject; + } + + public void getPanoramaSupport(GalleryApp app, PanoramaSupportCallback callback) { + synchronized (mLock) { + if (mPanoramaMetadata != null) { + callback.panoramaInfoAvailable(mMediaObject, mPanoramaMetadata.mUsePanoramaViewer, + mPanoramaMetadata.mIsPanorama360); + } else { + if (mCallbacksWaiting == null) { + mCallbacksWaiting = new ArrayList<PanoramaSupportCallback>(); + mGetPanoMetadataTask = app.getThreadPool().submit( + new PanoramaMetadataJob(app.getAndroidContext(), + mMediaObject.getContentUri()), this); + + } + mCallbacksWaiting.add(callback); + } + } + } + + public void clearCachedValues() { + synchronized (mLock) { + if (mPanoramaMetadata != null) { + mPanoramaMetadata = null; + } else if (mGetPanoMetadataTask != null) { + mGetPanoMetadataTask.cancel(); + for (PanoramaSupportCallback cb : mCallbacksWaiting) { + cb.panoramaInfoAvailable(mMediaObject, false, false); + } + mGetPanoMetadataTask = null; + mCallbacksWaiting = null; + } + } + } + + @Override + public void onFutureDone(Future<PanoramaMetadata> future) { + synchronized (mLock) { + mPanoramaMetadata = future.get(); + if (mPanoramaMetadata == null) { + // Error getting panorama data from file. Treat as not panorama. + mPanoramaMetadata = LightCycleHelper.NOT_PANORAMA; + } + for (PanoramaSupportCallback cb : mCallbacksWaiting) { + cb.panoramaInfoAvailable(mMediaObject, mPanoramaMetadata.mUsePanoramaViewer, + mPanoramaMetadata.mIsPanorama360); + } + mGetPanoMetadataTask = null; + mCallbacksWaiting = null; + } + } +} diff --git a/src/com/android/gallery3d/app/PhotoDataAdapter.java b/src/com/android/gallery3d/app/PhotoDataAdapter.java new file mode 100644 index 000000000..fd3a7cf73 --- /dev/null +++ b/src/com/android/gallery3d/app/PhotoDataAdapter.java @@ -0,0 +1,1133 @@ +/* + * 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.app; + +import android.graphics.Bitmap; +import android.graphics.BitmapRegionDecoder; +import android.os.Handler; +import android.os.Message; + +import com.android.gallery3d.common.BitmapUtils; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.ContentListener; +import com.android.gallery3d.data.LocalMediaItem; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.glrenderer.TiledTexture; +import com.android.gallery3d.ui.PhotoView; +import com.android.gallery3d.ui.ScreenNail; +import com.android.gallery3d.ui.SynchronizedHandler; +import com.android.gallery3d.ui.TileImageViewAdapter; +import com.android.gallery3d.ui.TiledScreenNail; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.FutureListener; +import com.android.gallery3d.util.MediaSetUtils; +import com.android.gallery3d.util.ThreadPool; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; + +public class PhotoDataAdapter implements PhotoPage.Model { + @SuppressWarnings("unused") + private static final String TAG = "PhotoDataAdapter"; + + private static final int MSG_LOAD_START = 1; + private static final int MSG_LOAD_FINISH = 2; + private static final int MSG_RUN_OBJECT = 3; + private static final int MSG_UPDATE_IMAGE_REQUESTS = 4; + + private static final int MIN_LOAD_COUNT = 16; + private static final int DATA_CACHE_SIZE = 256; + private static final int SCREEN_NAIL_MAX = PhotoView.SCREEN_NAIL_MAX; + private static final int IMAGE_CACHE_SIZE = 2 * SCREEN_NAIL_MAX + 1; + + private static final int BIT_SCREEN_NAIL = 1; + private static final int BIT_FULL_IMAGE = 2; + + // sImageFetchSeq is the fetching sequence for images. + // We want to fetch the current screennail first (offset = 0), the next + // screennail (offset = +1), then the previous screennail (offset = -1) etc. + // After all the screennail are fetched, we fetch the full images (only some + // of them because of we don't want to use too much memory). + private static ImageFetch[] sImageFetchSeq; + + private static class ImageFetch { + int indexOffset; + int imageBit; + public ImageFetch(int offset, int bit) { + indexOffset = offset; + imageBit = bit; + } + } + + static { + int k = 0; + sImageFetchSeq = new ImageFetch[1 + (IMAGE_CACHE_SIZE - 1) * 2 + 3]; + sImageFetchSeq[k++] = new ImageFetch(0, BIT_SCREEN_NAIL); + + for (int i = 1; i < IMAGE_CACHE_SIZE; ++i) { + sImageFetchSeq[k++] = new ImageFetch(i, BIT_SCREEN_NAIL); + sImageFetchSeq[k++] = new ImageFetch(-i, BIT_SCREEN_NAIL); + } + + sImageFetchSeq[k++] = new ImageFetch(0, BIT_FULL_IMAGE); + sImageFetchSeq[k++] = new ImageFetch(1, BIT_FULL_IMAGE); + sImageFetchSeq[k++] = new ImageFetch(-1, BIT_FULL_IMAGE); + } + + private final TileImageViewAdapter mTileProvider = new TileImageViewAdapter(); + + // PhotoDataAdapter caches MediaItems (data) and ImageEntries (image). + // + // The MediaItems are stored in the mData array, which has DATA_CACHE_SIZE + // entries. The valid index range are [mContentStart, mContentEnd). We keep + // mContentEnd - mContentStart <= DATA_CACHE_SIZE, so we can use + // (i % DATA_CACHE_SIZE) as index to the array. + // + // The valid MediaItem window size (mContentEnd - mContentStart) may be + // smaller than DATA_CACHE_SIZE because we only update the window and reload + // the MediaItems when there are significant changes to the window position + // (>= MIN_LOAD_COUNT). + private final MediaItem mData[] = new MediaItem[DATA_CACHE_SIZE]; + private int mContentStart = 0; + private int mContentEnd = 0; + + // The ImageCache is a Path-to-ImageEntry map. It only holds the + // ImageEntries in the range of [mActiveStart, mActiveEnd). We also keep + // mActiveEnd - mActiveStart <= IMAGE_CACHE_SIZE. Besides, the + // [mActiveStart, mActiveEnd) range must be contained within + // the [mContentStart, mContentEnd) range. + private HashMap<Path, ImageEntry> mImageCache = + new HashMap<Path, ImageEntry>(); + private int mActiveStart = 0; + private int mActiveEnd = 0; + + // mCurrentIndex is the "center" image the user is viewing. The change of + // mCurrentIndex triggers the data loading and image loading. + private int mCurrentIndex; + + // mChanges keeps the version number (of MediaItem) about the images. If any + // of the version number changes, we notify the view. This is used after a + // database reload or mCurrentIndex changes. + private final long mChanges[] = new long[IMAGE_CACHE_SIZE]; + // mPaths keeps the corresponding Path (of MediaItem) for the images. This + // is used to determine the item movement. + private final Path mPaths[] = new Path[IMAGE_CACHE_SIZE]; + + private final Handler mMainHandler; + private final ThreadPool mThreadPool; + + private final PhotoView mPhotoView; + private final MediaSet mSource; + private ReloadTask mReloadTask; + + private long mSourceVersion = MediaObject.INVALID_DATA_VERSION; + private int mSize = 0; + private Path mItemPath; + private int mCameraIndex; + private boolean mIsPanorama; + private boolean mIsStaticCamera; + private boolean mIsActive; + private boolean mNeedFullImage; + private int mFocusHintDirection = FOCUS_HINT_NEXT; + private Path mFocusHintPath = null; + + public interface DataListener extends LoadingListener { + public void onPhotoChanged(int index, Path item); + } + + private DataListener mDataListener; + + private final SourceListener mSourceListener = new SourceListener(); + private final TiledTexture.Uploader mUploader; + + // The path of the current viewing item will be stored in mItemPath. + // If mItemPath is not null, mCurrentIndex is only a hint for where we + // can find the item. If mItemPath is null, then we use the mCurrentIndex to + // find the image being viewed. cameraIndex is the index of the camera + // preview. If cameraIndex < 0, there is no camera preview. + public PhotoDataAdapter(AbstractGalleryActivity activity, PhotoView view, + MediaSet mediaSet, Path itemPath, int indexHint, int cameraIndex, + boolean isPanorama, boolean isStaticCamera) { + mSource = Utils.checkNotNull(mediaSet); + mPhotoView = Utils.checkNotNull(view); + mItemPath = Utils.checkNotNull(itemPath); + mCurrentIndex = indexHint; + mCameraIndex = cameraIndex; + mIsPanorama = isPanorama; + mIsStaticCamera = isStaticCamera; + mThreadPool = activity.getThreadPool(); + mNeedFullImage = true; + + Arrays.fill(mChanges, MediaObject.INVALID_DATA_VERSION); + + mUploader = new TiledTexture.Uploader(activity.getGLRoot()); + + mMainHandler = new SynchronizedHandler(activity.getGLRoot()) { + @SuppressWarnings("unchecked") + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_RUN_OBJECT: + ((Runnable) message.obj).run(); + return; + case MSG_LOAD_START: { + if (mDataListener != null) { + mDataListener.onLoadingStarted(); + } + return; + } + case MSG_LOAD_FINISH: { + if (mDataListener != null) { + mDataListener.onLoadingFinished(false); + } + return; + } + case MSG_UPDATE_IMAGE_REQUESTS: { + updateImageRequests(); + return; + } + default: throw new AssertionError(); + } + } + }; + + updateSlidingWindow(); + } + + private MediaItem getItemInternal(int index) { + if (index < 0 || index >= mSize) return null; + if (index >= mContentStart && index < mContentEnd) { + return mData[index % DATA_CACHE_SIZE]; + } + return null; + } + + private long getVersion(int index) { + MediaItem item = getItemInternal(index); + if (item == null) return MediaObject.INVALID_DATA_VERSION; + return item.getDataVersion(); + } + + private Path getPath(int index) { + MediaItem item = getItemInternal(index); + if (item == null) return null; + return item.getPath(); + } + + private void fireDataChange() { + // First check if data actually changed. + boolean changed = false; + for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) { + long newVersion = getVersion(mCurrentIndex + i); + if (mChanges[i + SCREEN_NAIL_MAX] != newVersion) { + mChanges[i + SCREEN_NAIL_MAX] = newVersion; + changed = true; + } + } + + if (!changed) return; + + // Now calculate the fromIndex array. fromIndex represents the item + // movement. It records the index where the picture come from. The + // special value Integer.MAX_VALUE means it's a new picture. + final int N = IMAGE_CACHE_SIZE; + int fromIndex[] = new int[N]; + + // Remember the old path array. + Path oldPaths[] = new Path[N]; + System.arraycopy(mPaths, 0, oldPaths, 0, N); + + // Update the mPaths array. + for (int i = 0; i < N; ++i) { + mPaths[i] = getPath(mCurrentIndex + i - SCREEN_NAIL_MAX); + } + + // Calculate the fromIndex array. + for (int i = 0; i < N; i++) { + Path p = mPaths[i]; + if (p == null) { + fromIndex[i] = Integer.MAX_VALUE; + continue; + } + + // Try to find the same path in the old array + int j; + for (j = 0; j < N; j++) { + if (oldPaths[j] == p) { + break; + } + } + fromIndex[i] = (j < N) ? j - SCREEN_NAIL_MAX : Integer.MAX_VALUE; + } + + mPhotoView.notifyDataChange(fromIndex, -mCurrentIndex, + mSize - 1 - mCurrentIndex); + } + + public void setDataListener(DataListener listener) { + mDataListener = listener; + } + + private void updateScreenNail(Path path, Future<ScreenNail> future) { + ImageEntry entry = mImageCache.get(path); + ScreenNail screenNail = future.get(); + + if (entry == null || entry.screenNailTask != future) { + if (screenNail != null) screenNail.recycle(); + return; + } + + entry.screenNailTask = null; + + // Combine the ScreenNails if we already have a BitmapScreenNail + if (entry.screenNail instanceof TiledScreenNail) { + TiledScreenNail original = (TiledScreenNail) entry.screenNail; + screenNail = original.combine(screenNail); + } + + if (screenNail == null) { + entry.failToLoad = true; + } else { + entry.failToLoad = false; + entry.screenNail = screenNail; + } + + for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) { + if (path == getPath(mCurrentIndex + i)) { + if (i == 0) updateTileProvider(entry); + mPhotoView.notifyImageChange(i); + break; + } + } + updateImageRequests(); + updateScreenNailUploadQueue(); + } + + private void updateFullImage(Path path, Future<BitmapRegionDecoder> future) { + ImageEntry entry = mImageCache.get(path); + if (entry == null || entry.fullImageTask != future) { + BitmapRegionDecoder fullImage = future.get(); + if (fullImage != null) fullImage.recycle(); + return; + } + + entry.fullImageTask = null; + entry.fullImage = future.get(); + if (entry.fullImage != null) { + if (path == getPath(mCurrentIndex)) { + updateTileProvider(entry); + mPhotoView.notifyImageChange(0); + } + } + updateImageRequests(); + } + + @Override + public void resume() { + mIsActive = true; + TiledTexture.prepareResources(); + + mSource.addContentListener(mSourceListener); + updateImageCache(); + updateImageRequests(); + + mReloadTask = new ReloadTask(); + mReloadTask.start(); + + fireDataChange(); + } + + @Override + public void pause() { + mIsActive = false; + + mReloadTask.terminate(); + mReloadTask = null; + + mSource.removeContentListener(mSourceListener); + + for (ImageEntry entry : mImageCache.values()) { + if (entry.fullImageTask != null) entry.fullImageTask.cancel(); + if (entry.screenNailTask != null) entry.screenNailTask.cancel(); + if (entry.screenNail != null) entry.screenNail.recycle(); + } + mImageCache.clear(); + mTileProvider.clear(); + + mUploader.clear(); + TiledTexture.freeResources(); + } + + private MediaItem getItem(int index) { + if (index < 0 || index >= mSize || !mIsActive) return null; + Utils.assertTrue(index >= mActiveStart && index < mActiveEnd); + + if (index >= mContentStart && index < mContentEnd) { + return mData[index % DATA_CACHE_SIZE]; + } + return null; + } + + private void updateCurrentIndex(int index) { + if (mCurrentIndex == index) return; + mCurrentIndex = index; + updateSlidingWindow(); + + MediaItem item = mData[index % DATA_CACHE_SIZE]; + mItemPath = item == null ? null : item.getPath(); + + updateImageCache(); + updateImageRequests(); + updateTileProvider(); + + if (mDataListener != null) { + mDataListener.onPhotoChanged(index, mItemPath); + } + + fireDataChange(); + } + + private void uploadScreenNail(int offset) { + int index = mCurrentIndex + offset; + if (index < mActiveStart || index >= mActiveEnd) return; + + MediaItem item = getItem(index); + if (item == null) return; + + ImageEntry e = mImageCache.get(item.getPath()); + if (e == null) return; + + ScreenNail s = e.screenNail; + if (s instanceof TiledScreenNail) { + TiledTexture t = ((TiledScreenNail) s).getTexture(); + if (t != null && !t.isReady()) mUploader.addTexture(t); + } + } + + private void updateScreenNailUploadQueue() { + mUploader.clear(); + uploadScreenNail(0); + for (int i = 1; i < IMAGE_CACHE_SIZE; ++i) { + uploadScreenNail(i); + uploadScreenNail(-i); + } + } + + @Override + public void moveTo(int index) { + updateCurrentIndex(index); + } + + @Override + public ScreenNail getScreenNail(int offset) { + int index = mCurrentIndex + offset; + if (index < 0 || index >= mSize || !mIsActive) return null; + Utils.assertTrue(index >= mActiveStart && index < mActiveEnd); + + MediaItem item = getItem(index); + if (item == null) return null; + + ImageEntry entry = mImageCache.get(item.getPath()); + if (entry == null) return null; + + // Create a default ScreenNail if the real one is not available yet, + // except for camera that a black screen is better than a gray tile. + if (entry.screenNail == null && !isCamera(offset)) { + entry.screenNail = newPlaceholderScreenNail(item); + if (offset == 0) updateTileProvider(entry); + } + + return entry.screenNail; + } + + @Override + public void getImageSize(int offset, PhotoView.Size size) { + MediaItem item = getItem(mCurrentIndex + offset); + if (item == null) { + size.width = 0; + size.height = 0; + } else { + size.width = item.getWidth(); + size.height = item.getHeight(); + } + } + + @Override + public int getImageRotation(int offset) { + MediaItem item = getItem(mCurrentIndex + offset); + return (item == null) ? 0 : item.getFullImageRotation(); + } + + @Override + public void setNeedFullImage(boolean enabled) { + mNeedFullImage = enabled; + mMainHandler.sendEmptyMessage(MSG_UPDATE_IMAGE_REQUESTS); + } + + @Override + public boolean isCamera(int offset) { + return mCurrentIndex + offset == mCameraIndex; + } + + @Override + public boolean isPanorama(int offset) { + return isCamera(offset) && mIsPanorama; + } + + @Override + public boolean isStaticCamera(int offset) { + return isCamera(offset) && mIsStaticCamera; + } + + @Override + public boolean isVideo(int offset) { + MediaItem item = getItem(mCurrentIndex + offset); + return (item == null) + ? false + : item.getMediaType() == MediaItem.MEDIA_TYPE_VIDEO; + } + + @Override + public boolean isDeletable(int offset) { + MediaItem item = getItem(mCurrentIndex + offset); + return (item == null) + ? false + : (item.getSupportedOperations() & MediaItem.SUPPORT_DELETE) != 0; + } + + @Override + public int getLoadingState(int offset) { + ImageEntry entry = mImageCache.get(getPath(mCurrentIndex + offset)); + if (entry == null) return LOADING_INIT; + if (entry.failToLoad) return LOADING_FAIL; + if (entry.screenNail != null) return LOADING_COMPLETE; + return LOADING_INIT; + } + + @Override + public ScreenNail getScreenNail() { + return getScreenNail(0); + } + + @Override + public int getImageHeight() { + return mTileProvider.getImageHeight(); + } + + @Override + public int getImageWidth() { + return mTileProvider.getImageWidth(); + } + + @Override + public int getLevelCount() { + return mTileProvider.getLevelCount(); + } + + @Override + public Bitmap getTile(int level, int x, int y, int tileSize) { + return mTileProvider.getTile(level, x, y, tileSize); + } + + @Override + public boolean isEmpty() { + return mSize == 0; + } + + @Override + public int getCurrentIndex() { + return mCurrentIndex; + } + + @Override + public MediaItem getMediaItem(int offset) { + int index = mCurrentIndex + offset; + if (index >= mContentStart && index < mContentEnd) { + return mData[index % DATA_CACHE_SIZE]; + } + return null; + } + + @Override + public void setCurrentPhoto(Path path, int indexHint) { + if (mItemPath == path) return; + mItemPath = path; + mCurrentIndex = indexHint; + updateSlidingWindow(); + updateImageCache(); + fireDataChange(); + + // We need to reload content if the path doesn't match. + MediaItem item = getMediaItem(0); + if (item != null && item.getPath() != path) { + if (mReloadTask != null) mReloadTask.notifyDirty(); + } + } + + @Override + public void setFocusHintDirection(int direction) { + mFocusHintDirection = direction; + } + + @Override + public void setFocusHintPath(Path path) { + mFocusHintPath = path; + } + + private void updateTileProvider() { + ImageEntry entry = mImageCache.get(getPath(mCurrentIndex)); + if (entry == null) { // in loading + mTileProvider.clear(); + } else { + updateTileProvider(entry); + } + } + + private void updateTileProvider(ImageEntry entry) { + ScreenNail screenNail = entry.screenNail; + BitmapRegionDecoder fullImage = entry.fullImage; + if (screenNail != null) { + if (fullImage != null) { + mTileProvider.setScreenNail(screenNail, + fullImage.getWidth(), fullImage.getHeight()); + mTileProvider.setRegionDecoder(fullImage); + } else { + int width = screenNail.getWidth(); + int height = screenNail.getHeight(); + mTileProvider.setScreenNail(screenNail, width, height); + } + } else { + mTileProvider.clear(); + } + } + + private void updateSlidingWindow() { + // 1. Update the image window + int start = Utils.clamp(mCurrentIndex - IMAGE_CACHE_SIZE / 2, + 0, Math.max(0, mSize - IMAGE_CACHE_SIZE)); + int end = Math.min(mSize, start + IMAGE_CACHE_SIZE); + + if (mActiveStart == start && mActiveEnd == end) return; + + mActiveStart = start; + mActiveEnd = end; + + // 2. Update the data window + start = Utils.clamp(mCurrentIndex - DATA_CACHE_SIZE / 2, + 0, Math.max(0, mSize - DATA_CACHE_SIZE)); + end = Math.min(mSize, start + DATA_CACHE_SIZE); + if (mContentStart > mActiveStart || mContentEnd < mActiveEnd + || Math.abs(start - mContentStart) > MIN_LOAD_COUNT) { + for (int i = mContentStart; i < mContentEnd; ++i) { + if (i < start || i >= end) { + mData[i % DATA_CACHE_SIZE] = null; + } + } + mContentStart = start; + mContentEnd = end; + if (mReloadTask != null) mReloadTask.notifyDirty(); + } + } + + private void updateImageRequests() { + if (!mIsActive) return; + + int currentIndex = mCurrentIndex; + MediaItem item = mData[currentIndex % DATA_CACHE_SIZE]; + if (item == null || item.getPath() != mItemPath) { + // current item mismatch - don't request image + return; + } + + // 1. Find the most wanted request and start it (if not already started). + Future<?> task = null; + for (int i = 0; i < sImageFetchSeq.length; i++) { + int offset = sImageFetchSeq[i].indexOffset; + int bit = sImageFetchSeq[i].imageBit; + if (bit == BIT_FULL_IMAGE && !mNeedFullImage) continue; + task = startTaskIfNeeded(currentIndex + offset, bit); + if (task != null) break; + } + + // 2. Cancel everything else. + for (ImageEntry entry : mImageCache.values()) { + if (entry.screenNailTask != null && entry.screenNailTask != task) { + entry.screenNailTask.cancel(); + entry.screenNailTask = null; + entry.requestedScreenNail = MediaObject.INVALID_DATA_VERSION; + } + if (entry.fullImageTask != null && entry.fullImageTask != task) { + entry.fullImageTask.cancel(); + entry.fullImageTask = null; + entry.requestedFullImage = MediaObject.INVALID_DATA_VERSION; + } + } + } + + private class ScreenNailJob implements Job<ScreenNail> { + private MediaItem mItem; + + public ScreenNailJob(MediaItem item) { + mItem = item; + } + + @Override + public ScreenNail run(JobContext jc) { + // We try to get a ScreenNail first, if it fails, we fallback to get + // a Bitmap and then wrap it in a BitmapScreenNail instead. + ScreenNail s = mItem.getScreenNail(); + if (s != null) return s; + + // If this is a temporary item, don't try to get its bitmap because + // it won't be available. We will get its bitmap after a data reload. + if (isTemporaryItem(mItem)) { + return newPlaceholderScreenNail(mItem); + } + + Bitmap bitmap = mItem.requestImage(MediaItem.TYPE_THUMBNAIL).run(jc); + if (jc.isCancelled()) return null; + if (bitmap != null) { + bitmap = BitmapUtils.rotateBitmap(bitmap, + mItem.getRotation() - mItem.getFullImageRotation(), true); + } + return bitmap == null ? null : new TiledScreenNail(bitmap); + } + } + + private class FullImageJob implements Job<BitmapRegionDecoder> { + private MediaItem mItem; + + public FullImageJob(MediaItem item) { + mItem = item; + } + + @Override + public BitmapRegionDecoder run(JobContext jc) { + if (isTemporaryItem(mItem)) { + return null; + } + return mItem.requestLargeImage().run(jc); + } + } + + // Returns true if we think this is a temporary item created by Camera. A + // temporary item is an image or a video whose data is still being + // processed, but an incomplete entry is created first in MediaProvider, so + // we can display them (in grey tile) even if they are not saved to disk + // yet. When the image or video data is actually saved, we will get + // notification from MediaProvider, reload data, and show the actual image + // or video data. + private boolean isTemporaryItem(MediaItem mediaItem) { + // Must have camera to create a temporary item. + if (mCameraIndex < 0) return false; + // Must be an item in camera roll. + if (!(mediaItem instanceof LocalMediaItem)) return false; + LocalMediaItem item = (LocalMediaItem) mediaItem; + if (item.getBucketId() != MediaSetUtils.CAMERA_BUCKET_ID) return false; + // Must have no size, but must have width and height information + if (item.getSize() != 0) return false; + if (item.getWidth() == 0) return false; + if (item.getHeight() == 0) return false; + // Must be created in the last 10 seconds. + if (item.getDateInMs() - System.currentTimeMillis() > 10000) return false; + return true; + } + + // Create a default ScreenNail when a ScreenNail is needed, but we don't yet + // have one available (because the image data is still being saved, or the + // Bitmap is still being loaded. + private ScreenNail newPlaceholderScreenNail(MediaItem item) { + int width = item.getWidth(); + int height = item.getHeight(); + return new TiledScreenNail(width, height); + } + + // Returns the task if we started the task or the task is already started. + private Future<?> startTaskIfNeeded(int index, int which) { + if (index < mActiveStart || index >= mActiveEnd) return null; + + ImageEntry entry = mImageCache.get(getPath(index)); + if (entry == null) return null; + MediaItem item = mData[index % DATA_CACHE_SIZE]; + Utils.assertTrue(item != null); + long version = item.getDataVersion(); + + if (which == BIT_SCREEN_NAIL && entry.screenNailTask != null + && entry.requestedScreenNail == version) { + return entry.screenNailTask; + } else if (which == BIT_FULL_IMAGE && entry.fullImageTask != null + && entry.requestedFullImage == version) { + return entry.fullImageTask; + } + + if (which == BIT_SCREEN_NAIL && entry.requestedScreenNail != version) { + entry.requestedScreenNail = version; + entry.screenNailTask = mThreadPool.submit( + new ScreenNailJob(item), + new ScreenNailListener(item)); + // request screen nail + return entry.screenNailTask; + } + if (which == BIT_FULL_IMAGE && entry.requestedFullImage != version + && (item.getSupportedOperations() + & MediaItem.SUPPORT_FULL_IMAGE) != 0) { + entry.requestedFullImage = version; + entry.fullImageTask = mThreadPool.submit( + new FullImageJob(item), + new FullImageListener(item)); + // request full image + return entry.fullImageTask; + } + return null; + } + + private void updateImageCache() { + HashSet<Path> toBeRemoved = new HashSet<Path>(mImageCache.keySet()); + for (int i = mActiveStart; i < mActiveEnd; ++i) { + MediaItem item = mData[i % DATA_CACHE_SIZE]; + if (item == null) continue; + Path path = item.getPath(); + ImageEntry entry = mImageCache.get(path); + toBeRemoved.remove(path); + if (entry != null) { + if (Math.abs(i - mCurrentIndex) > 1) { + if (entry.fullImageTask != null) { + entry.fullImageTask.cancel(); + entry.fullImageTask = null; + } + entry.fullImage = null; + entry.requestedFullImage = MediaObject.INVALID_DATA_VERSION; + } + if (entry.requestedScreenNail != item.getDataVersion()) { + // This ScreenNail is outdated, we want to update it if it's + // still a placeholder. + if (entry.screenNail instanceof TiledScreenNail) { + TiledScreenNail s = (TiledScreenNail) entry.screenNail; + s.updatePlaceholderSize( + item.getWidth(), item.getHeight()); + } + } + } else { + entry = new ImageEntry(); + mImageCache.put(path, entry); + } + } + + // Clear the data and requests for ImageEntries outside the new window. + for (Path path : toBeRemoved) { + ImageEntry entry = mImageCache.remove(path); + if (entry.fullImageTask != null) entry.fullImageTask.cancel(); + if (entry.screenNailTask != null) entry.screenNailTask.cancel(); + if (entry.screenNail != null) entry.screenNail.recycle(); + } + + updateScreenNailUploadQueue(); + } + + private class FullImageListener + implements Runnable, FutureListener<BitmapRegionDecoder> { + private final Path mPath; + private Future<BitmapRegionDecoder> mFuture; + + public FullImageListener(MediaItem item) { + mPath = item.getPath(); + } + + @Override + public void onFutureDone(Future<BitmapRegionDecoder> future) { + mFuture = future; + mMainHandler.sendMessage( + mMainHandler.obtainMessage(MSG_RUN_OBJECT, this)); + } + + @Override + public void run() { + updateFullImage(mPath, mFuture); + } + } + + private class ScreenNailListener + implements Runnable, FutureListener<ScreenNail> { + private final Path mPath; + private Future<ScreenNail> mFuture; + + public ScreenNailListener(MediaItem item) { + mPath = item.getPath(); + } + + @Override + public void onFutureDone(Future<ScreenNail> future) { + mFuture = future; + mMainHandler.sendMessage( + mMainHandler.obtainMessage(MSG_RUN_OBJECT, this)); + } + + @Override + public void run() { + updateScreenNail(mPath, mFuture); + } + } + + private static class ImageEntry { + public BitmapRegionDecoder fullImage; + public ScreenNail screenNail; + public Future<ScreenNail> screenNailTask; + public Future<BitmapRegionDecoder> fullImageTask; + public long requestedScreenNail = MediaObject.INVALID_DATA_VERSION; + public long requestedFullImage = MediaObject.INVALID_DATA_VERSION; + public boolean failToLoad = false; + } + + private class SourceListener implements ContentListener { + @Override + public void onContentDirty() { + if (mReloadTask != null) mReloadTask.notifyDirty(); + } + } + + private <T> T executeAndWait(Callable<T> callable) { + FutureTask<T> task = new FutureTask<T>(callable); + mMainHandler.sendMessage( + mMainHandler.obtainMessage(MSG_RUN_OBJECT, task)); + try { + return task.get(); + } catch (InterruptedException e) { + return null; + } catch (ExecutionException e) { + throw new RuntimeException(e); + } + } + + private static class UpdateInfo { + public long version; + public boolean reloadContent; + public Path target; + public int indexHint; + public int contentStart; + public int contentEnd; + + public int size; + public ArrayList<MediaItem> items; + } + + private class GetUpdateInfo implements Callable<UpdateInfo> { + + private boolean needContentReload() { + for (int i = mContentStart, n = mContentEnd; i < n; ++i) { + if (mData[i % DATA_CACHE_SIZE] == null) return true; + } + MediaItem current = mData[mCurrentIndex % DATA_CACHE_SIZE]; + return current == null || current.getPath() != mItemPath; + } + + @Override + public UpdateInfo call() throws Exception { + // TODO: Try to load some data in first update + UpdateInfo info = new UpdateInfo(); + info.version = mSourceVersion; + info.reloadContent = needContentReload(); + info.target = mItemPath; + info.indexHint = mCurrentIndex; + info.contentStart = mContentStart; + info.contentEnd = mContentEnd; + info.size = mSize; + return info; + } + } + + private class UpdateContent implements Callable<Void> { + UpdateInfo mUpdateInfo; + + public UpdateContent(UpdateInfo updateInfo) { + mUpdateInfo = updateInfo; + } + + @Override + public Void call() throws Exception { + UpdateInfo info = mUpdateInfo; + mSourceVersion = info.version; + + if (info.size != mSize) { + mSize = info.size; + if (mContentEnd > mSize) mContentEnd = mSize; + if (mActiveEnd > mSize) mActiveEnd = mSize; + } + + mCurrentIndex = info.indexHint; + updateSlidingWindow(); + + if (info.items != null) { + int start = Math.max(info.contentStart, mContentStart); + int end = Math.min(info.contentStart + info.items.size(), mContentEnd); + int dataIndex = start % DATA_CACHE_SIZE; + for (int i = start; i < end; ++i) { + mData[dataIndex] = info.items.get(i - info.contentStart); + if (++dataIndex == DATA_CACHE_SIZE) dataIndex = 0; + } + } + + // update mItemPath + MediaItem current = mData[mCurrentIndex % DATA_CACHE_SIZE]; + mItemPath = current == null ? null : current.getPath(); + + updateImageCache(); + updateTileProvider(); + updateImageRequests(); + + if (mDataListener != null) { + mDataListener.onPhotoChanged(mCurrentIndex, mItemPath); + } + + fireDataChange(); + return null; + } + } + + private class ReloadTask extends Thread { + private volatile boolean mActive = true; + private volatile boolean mDirty = true; + + private boolean mIsLoading = false; + + private void updateLoading(boolean loading) { + if (mIsLoading == loading) return; + mIsLoading = loading; + mMainHandler.sendEmptyMessage(loading ? MSG_LOAD_START : MSG_LOAD_FINISH); + } + + @Override + public void run() { + while (mActive) { + synchronized (this) { + if (!mDirty && mActive) { + updateLoading(false); + Utils.waitWithoutInterrupt(this); + continue; + } + } + mDirty = false; + UpdateInfo info = executeAndWait(new GetUpdateInfo()); + updateLoading(true); + long version = mSource.reload(); + if (info.version != version) { + info.reloadContent = true; + info.size = mSource.getMediaItemCount(); + } + if (!info.reloadContent) continue; + info.items = mSource.getMediaItem( + info.contentStart, info.contentEnd); + + int index = MediaSet.INDEX_NOT_FOUND; + + // First try to focus on the given hint path if there is one. + if (mFocusHintPath != null) { + index = findIndexOfPathInCache(info, mFocusHintPath); + mFocusHintPath = null; + } + + // Otherwise try to see if the currently focused item can be found. + if (index == MediaSet.INDEX_NOT_FOUND) { + MediaItem item = findCurrentMediaItem(info); + if (item != null && item.getPath() == info.target) { + index = info.indexHint; + } else { + index = findIndexOfTarget(info); + } + } + + // The image has been deleted. Focus on the next image (keep + // mCurrentIndex unchanged) or the previous image (decrease + // mCurrentIndex by 1). In page mode we want to see the next + // image, so we focus on the next one. In film mode we want the + // later images to shift left to fill the empty space, so we + // focus on the previous image (so it will not move). In any + // case the index needs to be limited to [0, mSize). + if (index == MediaSet.INDEX_NOT_FOUND) { + index = info.indexHint; + int focusHintDirection = mFocusHintDirection; + if (index == (mCameraIndex + 1)) { + focusHintDirection = FOCUS_HINT_NEXT; + } + if (focusHintDirection == FOCUS_HINT_PREVIOUS + && index > 0) { + index--; + } + } + + // Don't change index if mSize == 0 + if (mSize > 0) { + if (index >= mSize) index = mSize - 1; + } + + info.indexHint = index; + + executeAndWait(new UpdateContent(info)); + } + } + + public synchronized void notifyDirty() { + mDirty = true; + notifyAll(); + } + + public synchronized void terminate() { + mActive = false; + notifyAll(); + } + + private MediaItem findCurrentMediaItem(UpdateInfo info) { + ArrayList<MediaItem> items = info.items; + int index = info.indexHint - info.contentStart; + return index < 0 || index >= items.size() ? null : items.get(index); + } + + private int findIndexOfTarget(UpdateInfo info) { + if (info.target == null) return info.indexHint; + ArrayList<MediaItem> items = info.items; + + // First, try to find the item in the data just loaded + if (items != null) { + int i = findIndexOfPathInCache(info, info.target); + if (i != MediaSet.INDEX_NOT_FOUND) return i; + } + + // Not found, find it in mSource. + return mSource.getIndexOfItem(info.target, info.indexHint); + } + + private int findIndexOfPathInCache(UpdateInfo info, Path path) { + ArrayList<MediaItem> items = info.items; + for (int i = 0, n = items.size(); i < n; ++i) { + MediaItem item = items.get(i); + if (item != null && item.getPath() == path) { + return i + info.contentStart; + } + } + return MediaSet.INDEX_NOT_FOUND; + } + } +} diff --git a/src/com/android/gallery3d/app/PhotoPage.java b/src/com/android/gallery3d/app/PhotoPage.java new file mode 100644 index 000000000..7a71e9109 --- /dev/null +++ b/src/com/android/gallery3d/app/PhotoPage.java @@ -0,0 +1,1571 @@ +/* + * 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.app; + +import android.annotation.TargetApi; +import android.app.ActionBar.OnMenuVisibilityListener; +import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.net.Uri; +import android.nfc.NfcAdapter; +import android.nfc.NfcAdapter.CreateBeamUrisCallback; +import android.nfc.NfcEvent; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.SystemClock; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.RelativeLayout; +import android.widget.ShareActionProvider; +import android.widget.Toast; + +import com.android.camera.CameraActivity; +import com.android.camera.ProxyLauncher; +import com.android.gallery3d.R; +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.data.ComboAlbum; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.FilterDeleteSet; +import com.android.gallery3d.data.FilterSource; +import com.android.gallery3d.data.LocalImage; +import com.android.gallery3d.data.MediaDetails; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.MediaObject.PanoramaSupportCallback; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.data.SecureAlbum; +import com.android.gallery3d.data.SecureSource; +import com.android.gallery3d.data.SnailAlbum; +import com.android.gallery3d.data.SnailItem; +import com.android.gallery3d.data.SnailSource; +import com.android.gallery3d.filtershow.FilterShowActivity; +import com.android.gallery3d.filtershow.crop.CropActivity; +import com.android.gallery3d.picasasource.PicasaSource; +import com.android.gallery3d.ui.DetailsHelper; +import com.android.gallery3d.ui.DetailsHelper.CloseListener; +import com.android.gallery3d.ui.DetailsHelper.DetailsSource; +import com.android.gallery3d.ui.GLView; +import com.android.gallery3d.ui.MenuExecutor; +import com.android.gallery3d.ui.PhotoView; +import com.android.gallery3d.ui.SelectionManager; +import com.android.gallery3d.ui.SynchronizedHandler; +import com.android.gallery3d.util.GalleryUtils; +import com.android.gallery3d.util.UsageStatistics; + +public abstract class PhotoPage extends ActivityState implements + PhotoView.Listener, AppBridge.Server, ShareActionProvider.OnShareTargetSelectedListener, + PhotoPageBottomControls.Delegate, GalleryActionBar.OnAlbumModeSelectedListener { + private static final String TAG = "PhotoPage"; + + private static final int MSG_HIDE_BARS = 1; + private static final int MSG_ON_FULL_SCREEN_CHANGED = 4; + private static final int MSG_UPDATE_ACTION_BAR = 5; + private static final int MSG_UNFREEZE_GLROOT = 6; + private static final int MSG_WANT_BARS = 7; + private static final int MSG_REFRESH_BOTTOM_CONTROLS = 8; + private static final int MSG_ON_CAMERA_CENTER = 9; + private static final int MSG_ON_PICTURE_CENTER = 10; + private static final int MSG_REFRESH_IMAGE = 11; + private static final int MSG_UPDATE_PHOTO_UI = 12; + private static final int MSG_UPDATE_PROGRESS = 13; + private static final int MSG_UPDATE_DEFERRED = 14; + private static final int MSG_UPDATE_SHARE_URI = 15; + private static final int MSG_UPDATE_PANORAMA_UI = 16; + + private static final int HIDE_BARS_TIMEOUT = 3500; + private static final int UNFREEZE_GLROOT_TIMEOUT = 250; + + private static final int REQUEST_SLIDESHOW = 1; + private static final int REQUEST_CROP = 2; + private static final int REQUEST_CROP_PICASA = 3; + private static final int REQUEST_EDIT = 4; + private static final int REQUEST_PLAY_VIDEO = 5; + private static final int REQUEST_TRIM = 6; + + public static final String KEY_MEDIA_SET_PATH = "media-set-path"; + public static final String KEY_MEDIA_ITEM_PATH = "media-item-path"; + public static final String KEY_INDEX_HINT = "index-hint"; + public static final String KEY_OPEN_ANIMATION_RECT = "open-animation-rect"; + public static final String KEY_APP_BRIDGE = "app-bridge"; + public static final String KEY_TREAT_BACK_AS_UP = "treat-back-as-up"; + public static final String KEY_START_IN_FILMSTRIP = "start-in-filmstrip"; + public static final String KEY_RETURN_INDEX_HINT = "return-index-hint"; + public static final String KEY_SHOW_WHEN_LOCKED = "show_when_locked"; + public static final String KEY_IN_CAMERA_ROLL = "in_camera_roll"; + + public static final String KEY_ALBUMPAGE_TRANSITION = "albumpage-transition"; + public static final int MSG_ALBUMPAGE_NONE = 0; + public static final int MSG_ALBUMPAGE_STARTED = 1; + public static final int MSG_ALBUMPAGE_RESUMED = 2; + public static final int MSG_ALBUMPAGE_PICKED = 4; + + public static final String ACTION_NEXTGEN_EDIT = "action_nextgen_edit"; + public static final String ACTION_SIMPLE_EDIT = "action_simple_edit"; + + private GalleryApp mApplication; + private SelectionManager mSelectionManager; + + private PhotoView mPhotoView; + private PhotoPage.Model mModel; + private DetailsHelper mDetailsHelper; + private boolean mShowDetails; + + // mMediaSet could be null if there is no KEY_MEDIA_SET_PATH supplied. + // E.g., viewing a photo in gmail attachment + private FilterDeleteSet mMediaSet; + + // The mediaset used by camera launched from secure lock screen. + private SecureAlbum mSecureAlbum; + + private int mCurrentIndex = 0; + private Handler mHandler; + private boolean mShowBars = true; + private volatile boolean mActionBarAllowed = true; + private GalleryActionBar mActionBar; + private boolean mIsMenuVisible; + private boolean mHaveImageEditor; + private PhotoPageBottomControls mBottomControls; + private PhotoPageProgressBar mProgressBar; + private MediaItem mCurrentPhoto = null; + private MenuExecutor mMenuExecutor; + private boolean mIsActive; + private boolean mShowSpinner; + private String mSetPathString; + // This is the original mSetPathString before adding the camera preview item. + private String mOriginalSetPathString; + private AppBridge mAppBridge; + private SnailItem mScreenNailItem; + private SnailAlbum mScreenNailSet; + private OrientationManager mOrientationManager; + private boolean mTreatBackAsUp; + private boolean mStartInFilmstrip; + private boolean mHasCameraScreennailOrPlaceholder = false; + private boolean mRecenterCameraOnResume = true; + + // These are only valid after the panorama callback + private boolean mIsPanorama; + private boolean mIsPanorama360; + + private long mCameraSwitchCutoff = 0; + private boolean mSkipUpdateCurrentPhoto = false; + private static final long CAMERA_SWITCH_CUTOFF_THRESHOLD_MS = 300; + + private static final long DEFERRED_UPDATE_MS = 250; + private boolean mDeferredUpdateWaiting = false; + private long mDeferUpdateUntil = Long.MAX_VALUE; + + // The item that is deleted (but it can still be undeleted before commiting) + private Path mDeletePath; + private boolean mDeleteIsFocus; // whether the deleted item was in focus + + private Uri[] mNfcPushUris = new Uri[1]; + + private final MyMenuVisibilityListener mMenuVisibilityListener = + new MyMenuVisibilityListener(); + private UpdateProgressListener mProgressListener; + + private final PanoramaSupportCallback mUpdatePanoramaMenuItemsCallback = new PanoramaSupportCallback() { + @Override + public void panoramaInfoAvailable(MediaObject mediaObject, boolean isPanorama, + boolean isPanorama360) { + if (mediaObject == mCurrentPhoto) { + mHandler.obtainMessage(MSG_UPDATE_PANORAMA_UI, isPanorama360 ? 1 : 0, 0, + mediaObject).sendToTarget(); + } + } + }; + + private final PanoramaSupportCallback mRefreshBottomControlsCallback = new PanoramaSupportCallback() { + @Override + public void panoramaInfoAvailable(MediaObject mediaObject, boolean isPanorama, + boolean isPanorama360) { + if (mediaObject == mCurrentPhoto) { + mHandler.obtainMessage(MSG_REFRESH_BOTTOM_CONTROLS, isPanorama ? 1 : 0, isPanorama360 ? 1 : 0, + mediaObject).sendToTarget(); + } + } + }; + + private final PanoramaSupportCallback mUpdateShareURICallback = new PanoramaSupportCallback() { + @Override + public void panoramaInfoAvailable(MediaObject mediaObject, boolean isPanorama, + boolean isPanorama360) { + if (mediaObject == mCurrentPhoto) { + mHandler.obtainMessage(MSG_UPDATE_SHARE_URI, isPanorama360 ? 1 : 0, 0, mediaObject) + .sendToTarget(); + } + } + }; + + public static interface Model extends PhotoView.Model { + public void resume(); + public void pause(); + public boolean isEmpty(); + public void setCurrentPhoto(Path path, int indexHint); + } + + private class MyMenuVisibilityListener implements OnMenuVisibilityListener { + @Override + public void onMenuVisibilityChanged(boolean isVisible) { + mIsMenuVisible = isVisible; + refreshHidingMessage(); + } + } + + private class UpdateProgressListener implements StitchingChangeListener { + + @Override + public void onStitchingResult(Uri uri) { + sendUpdate(uri, MSG_REFRESH_IMAGE); + } + + @Override + public void onStitchingQueued(Uri uri) { + sendUpdate(uri, MSG_UPDATE_PROGRESS); + } + + @Override + public void onStitchingProgress(Uri uri, final int progress) { + sendUpdate(uri, MSG_UPDATE_PROGRESS); + } + + private void sendUpdate(Uri uri, int message) { + MediaObject currentPhoto = mCurrentPhoto; + boolean isCurrentPhoto = currentPhoto instanceof LocalImage + && currentPhoto.getContentUri().equals(uri); + if (isCurrentPhoto) { + mHandler.sendEmptyMessage(message); + } + } + }; + + @Override + protected int getBackgroundColorId() { + return R.color.photo_background; + } + + private final GLView mRootPane = new GLView() { + @Override + protected void onLayout( + boolean changed, int left, int top, int right, int bottom) { + mPhotoView.layout(0, 0, right - left, bottom - top); + if (mShowDetails) { + mDetailsHelper.layout(left, mActionBar.getHeight(), right, bottom); + } + } + }; + + @Override + public void onCreate(Bundle data, Bundle restoreState) { + super.onCreate(data, restoreState); + mActionBar = mActivity.getGalleryActionBar(); + mSelectionManager = new SelectionManager(mActivity, false); + mMenuExecutor = new MenuExecutor(mActivity, mSelectionManager); + + mPhotoView = new PhotoView(mActivity); + mPhotoView.setListener(this); + mRootPane.addComponent(mPhotoView); + mApplication = (GalleryApp) ((Activity) mActivity).getApplication(); + mOrientationManager = mActivity.getOrientationManager(); + mActivity.getGLRoot().setOrientationSource(mOrientationManager); + + mHandler = new SynchronizedHandler(mActivity.getGLRoot()) { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_HIDE_BARS: { + hideBars(); + break; + } + case MSG_REFRESH_BOTTOM_CONTROLS: { + if (mCurrentPhoto == message.obj && mBottomControls != null) { + mIsPanorama = message.arg1 == 1; + mIsPanorama360 = message.arg2 == 1; + mBottomControls.refresh(); + } + break; + } + case MSG_ON_FULL_SCREEN_CHANGED: { + if (mAppBridge != null) { + mAppBridge.onFullScreenChanged(message.arg1 == 1); + } + break; + } + case MSG_UPDATE_ACTION_BAR: { + updateBars(); + break; + } + case MSG_WANT_BARS: { + wantBars(); + break; + } + case MSG_UNFREEZE_GLROOT: { + mActivity.getGLRoot().unfreeze(); + break; + } + case MSG_UPDATE_DEFERRED: { + long nextUpdate = mDeferUpdateUntil - SystemClock.uptimeMillis(); + if (nextUpdate <= 0) { + mDeferredUpdateWaiting = false; + updateUIForCurrentPhoto(); + } else { + mHandler.sendEmptyMessageDelayed(MSG_UPDATE_DEFERRED, nextUpdate); + } + break; + } + case MSG_ON_CAMERA_CENTER: { + mSkipUpdateCurrentPhoto = false; + boolean stayedOnCamera = false; + if (!mPhotoView.getFilmMode()) { + stayedOnCamera = true; + } else if (SystemClock.uptimeMillis() < mCameraSwitchCutoff && + mMediaSet.getMediaItemCount() > 1) { + mPhotoView.switchToImage(1); + } else { + if (mAppBridge != null) mPhotoView.setFilmMode(false); + stayedOnCamera = true; + } + + if (stayedOnCamera) { + if (mAppBridge == null && mMediaSet.getTotalMediaItemCount() > 1) { + launchCamera(); + /* We got here by swiping from photo 1 to the + placeholder, so make it be the thing that + is in focus when the user presses back from + the camera app */ + mPhotoView.switchToImage(1); + } else { + updateBars(); + updateCurrentPhoto(mModel.getMediaItem(0)); + } + } + break; + } + case MSG_ON_PICTURE_CENTER: { + if (!mPhotoView.getFilmMode() && mCurrentPhoto != null + && (mCurrentPhoto.getSupportedOperations() & MediaObject.SUPPORT_ACTION) != 0) { + mPhotoView.setFilmMode(true); + } + break; + } + case MSG_REFRESH_IMAGE: { + final MediaItem photo = mCurrentPhoto; + mCurrentPhoto = null; + updateCurrentPhoto(photo); + break; + } + case MSG_UPDATE_PHOTO_UI: { + updateUIForCurrentPhoto(); + break; + } + case MSG_UPDATE_PROGRESS: { + updateProgressBar(); + break; + } + case MSG_UPDATE_SHARE_URI: { + if (mCurrentPhoto == message.obj) { + boolean isPanorama360 = message.arg1 != 0; + Uri contentUri = mCurrentPhoto.getContentUri(); + Intent panoramaIntent = null; + if (isPanorama360) { + panoramaIntent = createSharePanoramaIntent(contentUri); + } + Intent shareIntent = createShareIntent(mCurrentPhoto); + + mActionBar.setShareIntents(panoramaIntent, shareIntent, PhotoPage.this); + setNfcBeamPushUri(contentUri); + } + break; + } + case MSG_UPDATE_PANORAMA_UI: { + if (mCurrentPhoto == message.obj) { + boolean isPanorama360 = message.arg1 != 0; + updatePanoramaUI(isPanorama360); + } + break; + } + default: throw new AssertionError(message.what); + } + } + }; + + mSetPathString = data.getString(KEY_MEDIA_SET_PATH); + mOriginalSetPathString = mSetPathString; + setupNfcBeamPush(); + String itemPathString = data.getString(KEY_MEDIA_ITEM_PATH); + Path itemPath = itemPathString != null ? + Path.fromString(data.getString(KEY_MEDIA_ITEM_PATH)) : + null; + mTreatBackAsUp = data.getBoolean(KEY_TREAT_BACK_AS_UP, false); + mStartInFilmstrip = data.getBoolean(KEY_START_IN_FILMSTRIP, false); + boolean inCameraRoll = data.getBoolean(KEY_IN_CAMERA_ROLL, false); + mCurrentIndex = data.getInt(KEY_INDEX_HINT, 0); + if (mSetPathString != null) { + mShowSpinner = true; + mAppBridge = (AppBridge) data.getParcelable(KEY_APP_BRIDGE); + if (mAppBridge != null) { + mShowBars = false; + mHasCameraScreennailOrPlaceholder = true; + mAppBridge.setServer(this); + + // Get the ScreenNail from AppBridge and register it. + int id = SnailSource.newId(); + Path screenNailSetPath = SnailSource.getSetPath(id); + Path screenNailItemPath = SnailSource.getItemPath(id); + mScreenNailSet = (SnailAlbum) mActivity.getDataManager() + .getMediaObject(screenNailSetPath); + mScreenNailItem = (SnailItem) mActivity.getDataManager() + .getMediaObject(screenNailItemPath); + mScreenNailItem.setScreenNail(mAppBridge.attachScreenNail()); + + if (data.getBoolean(KEY_SHOW_WHEN_LOCKED, false)) { + // Set the flag to be on top of the lock screen. + mFlags |= FLAG_SHOW_WHEN_LOCKED; + } + + // Don't display "empty album" action item for capture intents. + if (!mSetPathString.equals("/local/all/0")) { + // Check if the path is a secure album. + if (SecureSource.isSecurePath(mSetPathString)) { + mSecureAlbum = (SecureAlbum) mActivity.getDataManager() + .getMediaSet(mSetPathString); + mShowSpinner = false; + } + mSetPathString = "/filter/empty/{"+mSetPathString+"}"; + } + + // Combine the original MediaSet with the one for ScreenNail + // from AppBridge. + mSetPathString = "/combo/item/{" + screenNailSetPath + + "," + mSetPathString + "}"; + + // Start from the screen nail. + itemPath = screenNailItemPath; + } else if (inCameraRoll && GalleryUtils.isCameraAvailable(mActivity)) { + mSetPathString = "/combo/item/{" + FilterSource.FILTER_CAMERA_SHORTCUT + + "," + mSetPathString + "}"; + mCurrentIndex++; + mHasCameraScreennailOrPlaceholder = true; + } + + MediaSet originalSet = mActivity.getDataManager() + .getMediaSet(mSetPathString); + if (mHasCameraScreennailOrPlaceholder && originalSet instanceof ComboAlbum) { + // Use the name of the camera album rather than the default + // ComboAlbum behavior + ((ComboAlbum) originalSet).useNameOfChild(1); + } + mSelectionManager.setSourceMediaSet(originalSet); + mSetPathString = "/filter/delete/{" + mSetPathString + "}"; + mMediaSet = (FilterDeleteSet) mActivity.getDataManager() + .getMediaSet(mSetPathString); + if (mMediaSet == null) { + Log.w(TAG, "failed to restore " + mSetPathString); + } + if (itemPath == null) { + int mediaItemCount = mMediaSet.getMediaItemCount(); + if (mediaItemCount > 0) { + if (mCurrentIndex >= mediaItemCount) mCurrentIndex = 0; + itemPath = mMediaSet.getMediaItem(mCurrentIndex, 1) + .get(0).getPath(); + } else { + // Bail out, PhotoPage can't load on an empty album + return; + } + } + PhotoDataAdapter pda = new PhotoDataAdapter( + mActivity, mPhotoView, mMediaSet, itemPath, mCurrentIndex, + mAppBridge == null ? -1 : 0, + mAppBridge == null ? false : mAppBridge.isPanorama(), + mAppBridge == null ? false : mAppBridge.isStaticCamera()); + mModel = pda; + mPhotoView.setModel(mModel); + + pda.setDataListener(new PhotoDataAdapter.DataListener() { + + @Override + public void onPhotoChanged(int index, Path item) { + int oldIndex = mCurrentIndex; + mCurrentIndex = index; + + if (mHasCameraScreennailOrPlaceholder) { + if (mCurrentIndex > 0) { + mSkipUpdateCurrentPhoto = false; + } + + if (oldIndex == 0 && mCurrentIndex > 0 + && !mPhotoView.getFilmMode()) { + mPhotoView.setFilmMode(true); + if (mAppBridge != null) { + UsageStatistics.onEvent("CameraToFilmstrip", + UsageStatistics.TRANSITION_SWIPE, null); + } + } else if (oldIndex == 2 && mCurrentIndex == 1) { + mCameraSwitchCutoff = SystemClock.uptimeMillis() + + CAMERA_SWITCH_CUTOFF_THRESHOLD_MS; + mPhotoView.stopScrolling(); + } else if (oldIndex >= 1 && mCurrentIndex == 0) { + mPhotoView.setWantPictureCenterCallbacks(true); + mSkipUpdateCurrentPhoto = true; + } + } + if (!mSkipUpdateCurrentPhoto) { + if (item != null) { + MediaItem photo = mModel.getMediaItem(0); + if (photo != null) updateCurrentPhoto(photo); + } + updateBars(); + } + // Reset the timeout for the bars after a swipe + refreshHidingMessage(); + } + + @Override + public void onLoadingFinished(boolean loadingFailed) { + if (!mModel.isEmpty()) { + MediaItem photo = mModel.getMediaItem(0); + if (photo != null) updateCurrentPhoto(photo); + } else if (mIsActive) { + // We only want to finish the PhotoPage if there is no + // deletion that the user can undo. + if (mMediaSet.getNumberOfDeletions() == 0) { + mActivity.getStateManager().finishState( + PhotoPage.this); + } + } + } + + @Override + public void onLoadingStarted() { + } + }); + } else { + // Get default media set by the URI + MediaItem mediaItem = (MediaItem) + mActivity.getDataManager().getMediaObject(itemPath); + mModel = new SinglePhotoDataAdapter(mActivity, mPhotoView, mediaItem); + mPhotoView.setModel(mModel); + updateCurrentPhoto(mediaItem); + mShowSpinner = false; + } + + mPhotoView.setFilmMode(mStartInFilmstrip && mMediaSet.getMediaItemCount() > 1); + RelativeLayout galleryRoot = (RelativeLayout) ((Activity) mActivity) + .findViewById(mAppBridge != null ? R.id.content : R.id.gallery_root); + if (galleryRoot != null) { + if (mSecureAlbum == null) { + mBottomControls = new PhotoPageBottomControls(this, mActivity, galleryRoot); + } + StitchingProgressManager progressManager = mApplication.getStitchingProgressManager(); + if (progressManager != null) { + mProgressBar = new PhotoPageProgressBar(mActivity, galleryRoot); + mProgressListener = new UpdateProgressListener(); + progressManager.addChangeListener(mProgressListener); + if (mSecureAlbum != null) { + progressManager.addChangeListener(mSecureAlbum); + } + } + } + } + + @Override + public void onPictureCenter(boolean isCamera) { + isCamera = isCamera || (mHasCameraScreennailOrPlaceholder && mAppBridge == null); + mPhotoView.setWantPictureCenterCallbacks(false); + mHandler.removeMessages(MSG_ON_CAMERA_CENTER); + mHandler.removeMessages(MSG_ON_PICTURE_CENTER); + mHandler.sendEmptyMessage(isCamera ? MSG_ON_CAMERA_CENTER : MSG_ON_PICTURE_CENTER); + } + + @Override + public boolean canDisplayBottomControls() { + return mIsActive && !mPhotoView.canUndo(); + } + + @Override + public boolean canDisplayBottomControl(int control) { + if (mCurrentPhoto == null) { + return false; + } + switch(control) { + case R.id.photopage_bottom_control_edit: + return mHaveImageEditor && mShowBars + && !mPhotoView.getFilmMode() + && (mCurrentPhoto.getSupportedOperations() & MediaItem.SUPPORT_EDIT) != 0 + && mCurrentPhoto.getMediaType() == MediaObject.MEDIA_TYPE_IMAGE; + case R.id.photopage_bottom_control_panorama: + return mIsPanorama; + case R.id.photopage_bottom_control_tiny_planet: + return mHaveImageEditor && mShowBars + && mIsPanorama360 && !mPhotoView.getFilmMode(); + default: + return false; + } + } + + @Override + public void onBottomControlClicked(int control) { + switch(control) { + case R.id.photopage_bottom_control_edit: + launchPhotoEditor(); + return; + case R.id.photopage_bottom_control_panorama: + mActivity.getPanoramaViewHelper() + .showPanorama(mCurrentPhoto.getContentUri()); + return; + case R.id.photopage_bottom_control_tiny_planet: + launchTinyPlanet(); + return; + default: + return; + } + } + + @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN) + private void setupNfcBeamPush() { + if (!ApiHelper.HAS_SET_BEAM_PUSH_URIS) return; + + NfcAdapter adapter = NfcAdapter.getDefaultAdapter(mActivity); + if (adapter != null) { + adapter.setBeamPushUris(null, mActivity); + adapter.setBeamPushUrisCallback(new CreateBeamUrisCallback() { + @Override + public Uri[] createBeamUris(NfcEvent event) { + return mNfcPushUris; + } + }, mActivity); + } + } + + private void setNfcBeamPushUri(Uri uri) { + mNfcPushUris[0] = uri; + } + + private static Intent createShareIntent(MediaObject mediaObject) { + int type = mediaObject.getMediaType(); + return new Intent(Intent.ACTION_SEND) + .setType(MenuExecutor.getMimeType(type)) + .putExtra(Intent.EXTRA_STREAM, mediaObject.getContentUri()) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + + private static Intent createSharePanoramaIntent(Uri contentUri) { + return new Intent(Intent.ACTION_SEND) + .setType(GalleryUtils.MIME_TYPE_PANORAMA360) + .putExtra(Intent.EXTRA_STREAM, contentUri) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + + private void overrideTransitionToEditor() { + ((Activity) mActivity).overridePendingTransition(android.R.anim.fade_in, + android.R.anim.fade_out); + } + + private void launchTinyPlanet() { + // Deep link into tiny planet + MediaItem current = mModel.getMediaItem(0); + Intent intent = new Intent(FilterShowActivity.TINY_PLANET_ACTION); + intent.setClass(mActivity, FilterShowActivity.class); + intent.setDataAndType(current.getContentUri(), current.getMimeType()) + .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.putExtra(FilterShowActivity.LAUNCH_FULLSCREEN, + mActivity.isFullscreen()); + mActivity.startActivityForResult(intent, REQUEST_EDIT); + overrideTransitionToEditor(); + } + + private void launchCamera() { + Intent intent = new Intent(mActivity, CameraActivity.class) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mRecenterCameraOnResume = false; + mActivity.startActivity(intent); + } + + private void launchPhotoEditor() { + MediaItem current = mModel.getMediaItem(0); + if (current == null || (current.getSupportedOperations() + & MediaObject.SUPPORT_EDIT) == 0) { + return; + } + + Intent intent = new Intent(ACTION_NEXTGEN_EDIT); + + intent.setDataAndType(current.getContentUri(), current.getMimeType()) + .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + if (mActivity.getPackageManager() + .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY).size() == 0) { + intent.setAction(Intent.ACTION_EDIT); + } + intent.putExtra(FilterShowActivity.LAUNCH_FULLSCREEN, + mActivity.isFullscreen()); + ((Activity) mActivity).startActivityForResult(Intent.createChooser(intent, null), + REQUEST_EDIT); + overrideTransitionToEditor(); + } + + private void launchSimpleEditor() { + MediaItem current = mModel.getMediaItem(0); + if (current == null || (current.getSupportedOperations() + & MediaObject.SUPPORT_EDIT) == 0) { + return; + } + + Intent intent = new Intent(ACTION_SIMPLE_EDIT); + + intent.setDataAndType(current.getContentUri(), current.getMimeType()) + .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + if (mActivity.getPackageManager() + .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY).size() == 0) { + intent.setAction(Intent.ACTION_EDIT); + } + intent.putExtra(FilterShowActivity.LAUNCH_FULLSCREEN, + mActivity.isFullscreen()); + ((Activity) mActivity).startActivityForResult(Intent.createChooser(intent, null), + REQUEST_EDIT); + overrideTransitionToEditor(); + } + + private void requestDeferredUpdate() { + mDeferUpdateUntil = SystemClock.uptimeMillis() + DEFERRED_UPDATE_MS; + if (!mDeferredUpdateWaiting) { + mDeferredUpdateWaiting = true; + mHandler.sendEmptyMessageDelayed(MSG_UPDATE_DEFERRED, DEFERRED_UPDATE_MS); + } + } + + private void updateUIForCurrentPhoto() { + if (mCurrentPhoto == null) return; + + // If by swiping or deletion the user ends up on an action item + // and zoomed in, zoom out so that the context of the action is + // more clear + if ((mCurrentPhoto.getSupportedOperations() & MediaObject.SUPPORT_ACTION) != 0 + && !mPhotoView.getFilmMode()) { + mPhotoView.setWantPictureCenterCallbacks(true); + } + + updateMenuOperations(); + refreshBottomControlsWhenReady(); + if (mShowDetails) { + mDetailsHelper.reloadDetails(); + } + if ((mSecureAlbum == null) + && (mCurrentPhoto.getSupportedOperations() & MediaItem.SUPPORT_SHARE) != 0) { + mCurrentPhoto.getPanoramaSupport(mUpdateShareURICallback); + } + updateProgressBar(); + } + + private void updateCurrentPhoto(MediaItem photo) { + if (mCurrentPhoto == photo) return; + mCurrentPhoto = photo; + if (mPhotoView.getFilmMode()) { + requestDeferredUpdate(); + } else { + updateUIForCurrentPhoto(); + } + } + + private void updateProgressBar() { + if (mProgressBar != null) { + mProgressBar.hideProgress(); + StitchingProgressManager progressManager = mApplication.getStitchingProgressManager(); + if (progressManager != null && mCurrentPhoto instanceof LocalImage) { + Integer progress = progressManager.getProgress(mCurrentPhoto.getContentUri()); + if (progress != null) { + mProgressBar.setProgress(progress); + } + } + } + } + + private void updateMenuOperations() { + Menu menu = mActionBar.getMenu(); + + // it could be null if onCreateActionBar has not been called yet + if (menu == null) return; + + MenuItem item = menu.findItem(R.id.action_slideshow); + if (item != null) { + item.setVisible((mSecureAlbum == null) && canDoSlideShow()); + } + if (mCurrentPhoto == null) return; + + int supportedOperations = mCurrentPhoto.getSupportedOperations(); + if (mSecureAlbum != null) { + supportedOperations &= MediaObject.SUPPORT_DELETE; + } else { + mCurrentPhoto.getPanoramaSupport(mUpdatePanoramaMenuItemsCallback); + if (!mHaveImageEditor) { + supportedOperations &= ~MediaObject.SUPPORT_EDIT; + } + } + MenuExecutor.updateMenuOperation(menu, supportedOperations); + } + + private boolean canDoSlideShow() { + if (mMediaSet == null || mCurrentPhoto == null) { + return false; + } + if (mCurrentPhoto.getMediaType() != MediaObject.MEDIA_TYPE_IMAGE) { + return false; + } + return true; + } + + ////////////////////////////////////////////////////////////////////////// + // Action Bar show/hide management + ////////////////////////////////////////////////////////////////////////// + + private void showBars() { + if (mShowBars) return; + mShowBars = true; + mOrientationManager.unlockOrientation(); + mActionBar.show(); + mActivity.getGLRoot().setLightsOutMode(false); + refreshHidingMessage(); + refreshBottomControlsWhenReady(); + } + + private void hideBars() { + if (!mShowBars) return; + mShowBars = false; + mActionBar.hide(); + mActivity.getGLRoot().setLightsOutMode(true); + mHandler.removeMessages(MSG_HIDE_BARS); + refreshBottomControlsWhenReady(); + } + + private void refreshHidingMessage() { + mHandler.removeMessages(MSG_HIDE_BARS); + if (!mIsMenuVisible && !mPhotoView.getFilmMode()) { + mHandler.sendEmptyMessageDelayed(MSG_HIDE_BARS, HIDE_BARS_TIMEOUT); + } + } + + private boolean canShowBars() { + // No bars if we are showing camera preview. + if (mAppBridge != null && mCurrentIndex == 0 + && !mPhotoView.getFilmMode()) return false; + + // No bars if it's not allowed. + if (!mActionBarAllowed) return false; + + Configuration config = mActivity.getResources().getConfiguration(); + if (config.touchscreen == Configuration.TOUCHSCREEN_NOTOUCH) { + return false; + } + + return true; + } + + private void wantBars() { + if (canShowBars()) showBars(); + } + + private void toggleBars() { + if (mShowBars) { + hideBars(); + } else { + if (canShowBars()) showBars(); + } + } + + private void updateBars() { + if (!canShowBars()) { + hideBars(); + } + } + + @Override + protected void onBackPressed() { + if (mShowDetails) { + hideDetails(); + } else if (mAppBridge == null || !switchWithCaptureAnimation(-1)) { + // We are leaving this page. Set the result now. + setResult(); + if (mStartInFilmstrip && !mPhotoView.getFilmMode()) { + mPhotoView.setFilmMode(true); + } else if (mTreatBackAsUp) { + onUpPressed(); + } else { + super.onBackPressed(); + } + } + } + + private void onUpPressed() { + if ((mStartInFilmstrip || mAppBridge != null) + && !mPhotoView.getFilmMode()) { + mPhotoView.setFilmMode(true); + return; + } + + if (mActivity.getStateManager().getStateCount() > 1) { + setResult(); + super.onBackPressed(); + return; + } + + if (mOriginalSetPathString == null) return; + + if (mAppBridge == null) { + // We're in view mode so set up the stacks on our own. + Bundle data = new Bundle(getData()); + data.putString(AlbumPage.KEY_MEDIA_PATH, mOriginalSetPathString); + data.putString(AlbumPage.KEY_PARENT_MEDIA_PATH, + mActivity.getDataManager().getTopSetPath( + DataManager.INCLUDE_ALL)); + mActivity.getStateManager().switchState(this, AlbumPage.class, data); + } else { + GalleryUtils.startGalleryActivity(mActivity); + } + } + + private void setResult() { + Intent result = null; + result = new Intent(); + result.putExtra(KEY_RETURN_INDEX_HINT, mCurrentIndex); + setStateResult(Activity.RESULT_OK, result); + } + + ////////////////////////////////////////////////////////////////////////// + // AppBridge.Server interface + ////////////////////////////////////////////////////////////////////////// + + @Override + public void setCameraRelativeFrame(Rect frame) { + mPhotoView.setCameraRelativeFrame(frame); + } + + @Override + public boolean switchWithCaptureAnimation(int offset) { + return mPhotoView.switchWithCaptureAnimation(offset); + } + + @Override + public void setSwipingEnabled(boolean enabled) { + mPhotoView.setSwipingEnabled(enabled); + } + + @Override + public void notifyScreenNailChanged() { + mScreenNailItem.setScreenNail(mAppBridge.attachScreenNail()); + mScreenNailSet.notifyChange(); + } + + @Override + public void addSecureAlbumItem(boolean isVideo, int id) { + mSecureAlbum.addMediaItem(isVideo, id); + } + + @Override + protected boolean onCreateActionBar(Menu menu) { + mActionBar.createActionBarMenu(R.menu.photo, menu); + mHaveImageEditor = GalleryUtils.isEditorAvailable(mActivity, "image/*"); + updateMenuOperations(); + mActionBar.setTitle(mMediaSet != null ? mMediaSet.getName() : ""); + return true; + } + + private MenuExecutor.ProgressListener mConfirmDialogListener = + new MenuExecutor.ProgressListener() { + @Override + public void onProgressUpdate(int index) {} + + @Override + public void onProgressComplete(int result) {} + + @Override + public void onConfirmDialogShown() { + mHandler.removeMessages(MSG_HIDE_BARS); + } + + @Override + public void onConfirmDialogDismissed(boolean confirmed) { + refreshHidingMessage(); + } + + @Override + public void onProgressStart() {} + }; + + private void switchToGrid() { + if (mActivity.getStateManager().hasStateClass(AlbumPage.class)) { + onUpPressed(); + } else { + if (mOriginalSetPathString == null) return; + if (mProgressBar != null) { + updateCurrentPhoto(null); + mProgressBar.hideProgress(); + } + Bundle data = new Bundle(getData()); + data.putString(AlbumPage.KEY_MEDIA_PATH, mOriginalSetPathString); + data.putString(AlbumPage.KEY_PARENT_MEDIA_PATH, + mActivity.getDataManager().getTopSetPath( + DataManager.INCLUDE_ALL)); + + // We only show cluster menu in the first AlbumPage in stack + // TODO: Enable this when running from the camera app + boolean inAlbum = mActivity.getStateManager().hasStateClass(AlbumPage.class); + data.putBoolean(AlbumPage.KEY_SHOW_CLUSTER_MENU, !inAlbum + && mAppBridge == null); + + data.putBoolean(PhotoPage.KEY_APP_BRIDGE, mAppBridge != null); + + // Account for live preview being first item + mActivity.getTransitionStore().put(KEY_RETURN_INDEX_HINT, + mAppBridge != null ? mCurrentIndex - 1 : mCurrentIndex); + + if (mHasCameraScreennailOrPlaceholder && mAppBridge != null) { + mActivity.getStateManager().startState(AlbumPage.class, data); + } else { + mActivity.getStateManager().switchState(this, AlbumPage.class, data); + } + } + } + + @Override + protected boolean onItemSelected(MenuItem item) { + if (mModel == null) return true; + refreshHidingMessage(); + MediaItem current = mModel.getMediaItem(0); + + // This is a shield for monkey when it clicks the action bar + // menu when transitioning from filmstrip to camera + if (current instanceof SnailItem) return true; + // TODO: We should check the current photo against the MediaItem + // that the menu was initially created for. We need to fix this + // after PhotoPage being refactored. + if (current == null) { + // item is not ready, ignore + return true; + } + int currentIndex = mModel.getCurrentIndex(); + Path path = current.getPath(); + + DataManager manager = mActivity.getDataManager(); + int action = item.getItemId(); + String confirmMsg = null; + switch (action) { + case android.R.id.home: { + onUpPressed(); + return true; + } + case R.id.action_slideshow: { + Bundle data = new Bundle(); + data.putString(SlideshowPage.KEY_SET_PATH, mMediaSet.getPath().toString()); + data.putString(SlideshowPage.KEY_ITEM_PATH, path.toString()); + data.putInt(SlideshowPage.KEY_PHOTO_INDEX, currentIndex); + data.putBoolean(SlideshowPage.KEY_REPEAT, true); + mActivity.getStateManager().startStateForResult( + SlideshowPage.class, REQUEST_SLIDESHOW, data); + return true; + } + case R.id.action_crop: { + Activity activity = mActivity; + Intent intent = new Intent(CropActivity.CROP_ACTION); + intent.setClass(activity, CropActivity.class); + intent.setDataAndType(manager.getContentUri(path), current.getMimeType()) + .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + activity.startActivityForResult(intent, PicasaSource.isPicasaImage(current) + ? REQUEST_CROP_PICASA + : REQUEST_CROP); + return true; + } + case R.id.action_trim: { + Intent intent = new Intent(mActivity, TrimVideo.class); + intent.setData(manager.getContentUri(path)); + // We need the file path to wrap this into a RandomAccessFile. + intent.putExtra(KEY_MEDIA_ITEM_PATH, current.getFilePath()); + mActivity.startActivityForResult(intent, REQUEST_TRIM); + return true; + } + case R.id.action_mute: { + MuteVideo muteVideo = new MuteVideo(current.getFilePath(), + manager.getContentUri(path), mActivity); + muteVideo.muteInBackground(); + return true; + } + case R.id.action_edit: { + launchPhotoEditor(); + return true; + } + case R.id.action_simple_edit: { + launchSimpleEditor(); + return true; + } + case R.id.action_details: { + if (mShowDetails) { + hideDetails(); + } else { + showDetails(); + } + return true; + } + case R.id.action_delete: + confirmMsg = mActivity.getResources().getQuantityString( + R.plurals.delete_selection, 1); + case R.id.action_setas: + case R.id.action_rotate_ccw: + case R.id.action_rotate_cw: + case R.id.action_show_on_map: + mSelectionManager.deSelectAll(); + mSelectionManager.toggle(path); + mMenuExecutor.onMenuClicked(item, confirmMsg, mConfirmDialogListener); + return true; + default : + return false; + } + } + + private void hideDetails() { + mShowDetails = false; + mDetailsHelper.hide(); + } + + private void showDetails() { + mShowDetails = true; + if (mDetailsHelper == null) { + mDetailsHelper = new DetailsHelper(mActivity, mRootPane, new MyDetailsSource()); + mDetailsHelper.setCloseListener(new CloseListener() { + @Override + public void onClose() { + hideDetails(); + } + }); + } + mDetailsHelper.show(); + } + + //////////////////////////////////////////////////////////////////////////// + // Callbacks from PhotoView + //////////////////////////////////////////////////////////////////////////// + @Override + public void onSingleTapUp(int x, int y) { + if (mAppBridge != null) { + if (mAppBridge.onSingleTapUp(x, y)) return; + } + + MediaItem item = mModel.getMediaItem(0); + if (item == null || item == mScreenNailItem) { + // item is not ready or it is camera preview, ignore + return; + } + + int supported = item.getSupportedOperations(); + boolean playVideo = ((supported & MediaItem.SUPPORT_PLAY) != 0); + boolean unlock = ((supported & MediaItem.SUPPORT_UNLOCK) != 0); + boolean goBack = ((supported & MediaItem.SUPPORT_BACK) != 0); + boolean launchCamera = ((supported & MediaItem.SUPPORT_CAMERA_SHORTCUT) != 0); + + if (playVideo) { + // determine if the point is at center (1/6) of the photo view. + // (The position of the "play" icon is at center (1/6) of the photo) + int w = mPhotoView.getWidth(); + int h = mPhotoView.getHeight(); + playVideo = (Math.abs(x - w / 2) * 12 <= w) + && (Math.abs(y - h / 2) * 12 <= h); + } + + if (playVideo) { + if (mSecureAlbum == null) { + playVideo(mActivity, item.getPlayUri(), item.getName()); + } else { + mActivity.getStateManager().finishState(this); + } + } else if (goBack) { + onBackPressed(); + } else if (unlock) { + Intent intent = new Intent(mActivity, Gallery.class); + intent.putExtra(Gallery.KEY_DISMISS_KEYGUARD, true); + mActivity.startActivity(intent); + } else if (launchCamera) { + launchCamera(); + } else { + toggleBars(); + } + } + + @Override + public void onActionBarAllowed(boolean allowed) { + mActionBarAllowed = allowed; + mHandler.sendEmptyMessage(MSG_UPDATE_ACTION_BAR); + } + + @Override + public void onActionBarWanted() { + mHandler.sendEmptyMessage(MSG_WANT_BARS); + } + + @Override + public void onFullScreenChanged(boolean full) { + Message m = mHandler.obtainMessage( + MSG_ON_FULL_SCREEN_CHANGED, full ? 1 : 0, 0); + m.sendToTarget(); + } + + // How we do delete/undo: + // + // When the user choose to delete a media item, we just tell the + // FilterDeleteSet to hide that item. If the user choose to undo it, we + // again tell FilterDeleteSet not to hide it. If the user choose to commit + // the deletion, we then actually delete the media item. + @Override + public void onDeleteImage(Path path, int offset) { + onCommitDeleteImage(); // commit the previous deletion + mDeletePath = path; + mDeleteIsFocus = (offset == 0); + mMediaSet.addDeletion(path, mCurrentIndex + offset); + } + + @Override + public void onUndoDeleteImage() { + if (mDeletePath == null) return; + // If the deletion was done on the focused item, we want the model to + // focus on it when it is undeleted. + if (mDeleteIsFocus) mModel.setFocusHintPath(mDeletePath); + mMediaSet.removeDeletion(mDeletePath); + mDeletePath = null; + } + + @Override + public void onCommitDeleteImage() { + if (mDeletePath == null) return; + mMenuExecutor.startSingleItemAction(R.id.action_delete, mDeletePath); + mDeletePath = null; + } + + public void playVideo(Activity activity, Uri uri, String title) { + try { + Intent intent = new Intent(Intent.ACTION_VIEW) + .setDataAndType(uri, "video/*") + .putExtra(Intent.EXTRA_TITLE, title) + .putExtra(MovieActivity.KEY_TREAT_UP_AS_BACK, true); + activity.startActivityForResult(intent, REQUEST_PLAY_VIDEO); + } catch (ActivityNotFoundException e) { + Toast.makeText(activity, activity.getString(R.string.video_err), + Toast.LENGTH_SHORT).show(); + } + } + + private void setCurrentPhotoByIntent(Intent intent) { + if (intent == null) return; + Path path = mApplication.getDataManager() + .findPathByUri(intent.getData(), intent.getType()); + if (path != null) { + Path albumPath = mApplication.getDataManager().getDefaultSetOf(path); + if (!albumPath.equalsIgnoreCase(mOriginalSetPathString)) { + // If the edited image is stored in a different album, we need + // to start a new activity state to show the new image + Bundle data = new Bundle(getData()); + data.putString(KEY_MEDIA_SET_PATH, albumPath.toString()); + data.putString(PhotoPage.KEY_MEDIA_ITEM_PATH, path.toString()); + mActivity.getStateManager().startState(SinglePhotoPage.class, data); + return; + } + mModel.setCurrentPhoto(path, mCurrentIndex); + } + } + + @Override + protected void onStateResult(int requestCode, int resultCode, Intent data) { + if (resultCode == Activity.RESULT_CANCELED) { + // This is a reset, not a canceled + return; + } + if (resultCode == ProxyLauncher.RESULT_USER_CANCELED) { + // Unmap reset vs. canceled + resultCode = Activity.RESULT_CANCELED; + } + mRecenterCameraOnResume = false; + switch (requestCode) { + case REQUEST_EDIT: + setCurrentPhotoByIntent(data); + break; + case REQUEST_CROP: + if (resultCode == Activity.RESULT_OK) { + setCurrentPhotoByIntent(data); + } + break; + case REQUEST_CROP_PICASA: { + if (resultCode == Activity.RESULT_OK) { + Context context = mActivity.getAndroidContext(); + String message = context.getString(R.string.crop_saved, + context.getString(R.string.folder_edited_online_photos)); + Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); + } + break; + } + case REQUEST_SLIDESHOW: { + if (data == null) break; + String path = data.getStringExtra(SlideshowPage.KEY_ITEM_PATH); + int index = data.getIntExtra(SlideshowPage.KEY_PHOTO_INDEX, 0); + if (path != null) { + mModel.setCurrentPhoto(Path.fromString(path), index); + } + } + } + } + + @Override + public void onPause() { + super.onPause(); + mIsActive = false; + + mActivity.getGLRoot().unfreeze(); + mHandler.removeMessages(MSG_UNFREEZE_GLROOT); + + DetailsHelper.pause(); + // Hide the detail dialog on exit + if (mShowDetails) hideDetails(); + if (mModel != null) { + mModel.pause(); + } + mPhotoView.pause(); + mHandler.removeMessages(MSG_HIDE_BARS); + mHandler.removeMessages(MSG_REFRESH_BOTTOM_CONTROLS); + refreshBottomControlsWhenReady(); + mActionBar.removeOnMenuVisibilityListener(mMenuVisibilityListener); + if (mShowSpinner) { + mActionBar.disableAlbumModeMenu(true); + } + onCommitDeleteImage(); + mMenuExecutor.pause(); + if (mMediaSet != null) mMediaSet.clearDeletion(); + } + + @Override + public void onCurrentImageUpdated() { + mActivity.getGLRoot().unfreeze(); + } + + @Override + public void onFilmModeChanged(boolean enabled) { + refreshBottomControlsWhenReady(); + if (mShowSpinner) { + if (enabled) { + mActionBar.enableAlbumModeMenu( + GalleryActionBar.ALBUM_FILMSTRIP_MODE_SELECTED, this); + } else { + mActionBar.disableAlbumModeMenu(true); + } + } + if (enabled) { + mHandler.removeMessages(MSG_HIDE_BARS); + UsageStatistics.onContentViewChanged( + UsageStatistics.COMPONENT_GALLERY, "FilmstripPage"); + } else { + refreshHidingMessage(); + if (mAppBridge == null || mCurrentIndex > 0) { + UsageStatistics.onContentViewChanged( + UsageStatistics.COMPONENT_GALLERY, "SinglePhotoPage"); + } else { + UsageStatistics.onContentViewChanged( + UsageStatistics.COMPONENT_CAMERA, "Unknown"); // TODO + } + } + } + + private void transitionFromAlbumPageIfNeeded() { + TransitionStore transitions = mActivity.getTransitionStore(); + + int albumPageTransition = transitions.get( + KEY_ALBUMPAGE_TRANSITION, MSG_ALBUMPAGE_NONE); + + if (albumPageTransition == MSG_ALBUMPAGE_NONE && mAppBridge != null + && mRecenterCameraOnResume) { + // Generally, resuming the PhotoPage when in Camera should + // reset to the capture mode to allow quick photo taking + mCurrentIndex = 0; + mPhotoView.resetToFirstPicture(); + } else { + int resumeIndex = transitions.get(KEY_INDEX_HINT, -1); + if (resumeIndex >= 0) { + if (mHasCameraScreennailOrPlaceholder) { + // Account for preview/placeholder being the first item + resumeIndex++; + } + if (resumeIndex < mMediaSet.getMediaItemCount()) { + mCurrentIndex = resumeIndex; + mModel.moveTo(mCurrentIndex); + } + } + } + + if (albumPageTransition == MSG_ALBUMPAGE_RESUMED) { + mPhotoView.setFilmMode(mStartInFilmstrip || mAppBridge != null); + } else if (albumPageTransition == MSG_ALBUMPAGE_PICKED) { + mPhotoView.setFilmMode(false); + } + } + + @Override + protected void onResume() { + super.onResume(); + + if (mModel == null) { + mActivity.getStateManager().finishState(this); + return; + } + transitionFromAlbumPageIfNeeded(); + + mActivity.getGLRoot().freeze(); + mIsActive = true; + setContentPane(mRootPane); + + mModel.resume(); + mPhotoView.resume(); + mActionBar.setDisplayOptions( + ((mSecureAlbum == null) && (mSetPathString != null)), false); + mActionBar.addOnMenuVisibilityListener(mMenuVisibilityListener); + refreshBottomControlsWhenReady(); + if (mShowSpinner && mPhotoView.getFilmMode()) { + mActionBar.enableAlbumModeMenu( + GalleryActionBar.ALBUM_FILMSTRIP_MODE_SELECTED, this); + } + if (!mShowBars) { + mActionBar.hide(); + mActivity.getGLRoot().setLightsOutMode(true); + } + boolean haveImageEditor = GalleryUtils.isEditorAvailable(mActivity, "image/*"); + if (haveImageEditor != mHaveImageEditor) { + mHaveImageEditor = haveImageEditor; + updateMenuOperations(); + } + + mRecenterCameraOnResume = true; + mHandler.sendEmptyMessageDelayed(MSG_UNFREEZE_GLROOT, UNFREEZE_GLROOT_TIMEOUT); + } + + @Override + protected void onDestroy() { + if (mAppBridge != null) { + mAppBridge.setServer(null); + mScreenNailItem.setScreenNail(null); + mAppBridge.detachScreenNail(); + mAppBridge = null; + mScreenNailSet = null; + mScreenNailItem = null; + } + mActivity.getGLRoot().setOrientationSource(null); + if (mBottomControls != null) mBottomControls.cleanup(); + + // Remove all pending messages. + mHandler.removeCallbacksAndMessages(null); + super.onDestroy(); + } + + private class MyDetailsSource implements DetailsSource { + + @Override + public MediaDetails getDetails() { + return mModel.getMediaItem(0).getDetails(); + } + + @Override + public int size() { + return mMediaSet != null ? mMediaSet.getMediaItemCount() : 1; + } + + @Override + public int setIndex() { + return mModel.getCurrentIndex(); + } + } + + @Override + public void onAlbumModeSelected(int mode) { + if (mode == GalleryActionBar.ALBUM_GRID_MODE_SELECTED) { + switchToGrid(); + } + } + + @Override + public void refreshBottomControlsWhenReady() { + if (mBottomControls == null) { + return; + } + MediaObject currentPhoto = mCurrentPhoto; + if (currentPhoto == null) { + mHandler.obtainMessage(MSG_REFRESH_BOTTOM_CONTROLS, 0, 0, currentPhoto).sendToTarget(); + } else { + currentPhoto.getPanoramaSupport(mRefreshBottomControlsCallback); + } + } + + private void updatePanoramaUI(boolean isPanorama360) { + Menu menu = mActionBar.getMenu(); + + // it could be null if onCreateActionBar has not been called yet + if (menu == null) { + return; + } + + MenuExecutor.updateMenuForPanorama(menu, isPanorama360, isPanorama360); + + if (isPanorama360) { + MenuItem item = menu.findItem(R.id.action_share); + if (item != null) { + item.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); + item.setTitle(mActivity.getResources().getString(R.string.share_as_photo)); + } + } else if ((mCurrentPhoto.getSupportedOperations() & MediaObject.SUPPORT_SHARE) != 0) { + MenuItem item = menu.findItem(R.id.action_share); + if (item != null) { + item.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); + item.setTitle(mActivity.getResources().getString(R.string.share)); + } + } + } + + @Override + public void onUndoBarVisibilityChanged(boolean visible) { + refreshBottomControlsWhenReady(); + } + + @Override + public boolean onShareTargetSelected(ShareActionProvider source, Intent intent) { + final long timestampMillis = mCurrentPhoto.getDateInMs(); + final String mediaType = getMediaTypeString(mCurrentPhoto); + UsageStatistics.onEvent(UsageStatistics.COMPONENT_GALLERY, + UsageStatistics.ACTION_SHARE, + mediaType, + timestampMillis > 0 + ? System.currentTimeMillis() - timestampMillis + : -1); + return false; + } + + private static String getMediaTypeString(MediaItem item) { + if (item.getMediaType() == MediaObject.MEDIA_TYPE_VIDEO) { + return "Video"; + } else if (item.getMediaType() == MediaObject.MEDIA_TYPE_IMAGE) { + return "Photo"; + } else { + return "Unknown:" + item.getMediaType(); + } + } + +} diff --git a/src/com/android/gallery3d/app/PhotoPageBottomControls.java b/src/com/android/gallery3d/app/PhotoPageBottomControls.java new file mode 100644 index 000000000..24b8ceb7e --- /dev/null +++ b/src/com/android/gallery3d/app/PhotoPageBottomControls.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2012 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.app; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; +import android.widget.RelativeLayout; + +import com.android.gallery3d.R; + +import java.util.HashMap; +import java.util.Map; + +public class PhotoPageBottomControls implements OnClickListener { + public interface Delegate { + public boolean canDisplayBottomControls(); + public boolean canDisplayBottomControl(int control); + public void onBottomControlClicked(int control); + public void refreshBottomControlsWhenReady(); + } + + private Delegate mDelegate; + private ViewGroup mParentLayout; + private ViewGroup mContainer; + + private boolean mContainerVisible = false; + private Map<View, Boolean> mControlsVisible = new HashMap<View, Boolean>(); + + private Animation mContainerAnimIn = new AlphaAnimation(0f, 1f); + private Animation mContainerAnimOut = new AlphaAnimation(1f, 0f); + private static final int CONTAINER_ANIM_DURATION_MS = 200; + + private static final int CONTROL_ANIM_DURATION_MS = 150; + private static Animation getControlAnimForVisibility(boolean visible) { + Animation anim = visible ? new AlphaAnimation(0f, 1f) + : new AlphaAnimation(1f, 0f); + anim.setDuration(CONTROL_ANIM_DURATION_MS); + return anim; + } + + public PhotoPageBottomControls(Delegate delegate, Context context, RelativeLayout layout) { + mDelegate = delegate; + mParentLayout = layout; + + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mContainer = (ViewGroup) inflater + .inflate(R.layout.photopage_bottom_controls, mParentLayout, false); + mParentLayout.addView(mContainer); + + for (int i = mContainer.getChildCount() - 1; i >= 0; i--) { + View child = mContainer.getChildAt(i); + child.setOnClickListener(this); + mControlsVisible.put(child, false); + } + + mContainerAnimIn.setDuration(CONTAINER_ANIM_DURATION_MS); + mContainerAnimOut.setDuration(CONTAINER_ANIM_DURATION_MS); + + mDelegate.refreshBottomControlsWhenReady(); + } + + private void hide() { + mContainer.clearAnimation(); + mContainerAnimOut.reset(); + mContainer.startAnimation(mContainerAnimOut); + mContainer.setVisibility(View.INVISIBLE); + } + + private void show() { + mContainer.clearAnimation(); + mContainerAnimIn.reset(); + mContainer.startAnimation(mContainerAnimIn); + mContainer.setVisibility(View.VISIBLE); + } + + public void refresh() { + boolean visible = mDelegate.canDisplayBottomControls(); + boolean containerVisibilityChanged = (visible != mContainerVisible); + if (containerVisibilityChanged) { + if (visible) { + show(); + } else { + hide(); + } + mContainerVisible = visible; + } + if (!mContainerVisible) { + return; + } + for (View control : mControlsVisible.keySet()) { + Boolean prevVisibility = mControlsVisible.get(control); + boolean curVisibility = mDelegate.canDisplayBottomControl(control.getId()); + if (prevVisibility.booleanValue() != curVisibility) { + if (!containerVisibilityChanged) { + control.clearAnimation(); + control.startAnimation(getControlAnimForVisibility(curVisibility)); + } + control.setVisibility(curVisibility ? View.VISIBLE : View.INVISIBLE); + mControlsVisible.put(control, curVisibility); + } + } + // Force a layout change + mContainer.requestLayout(); // Kick framework to draw the control. + } + + public void cleanup() { + mParentLayout.removeView(mContainer); + mControlsVisible.clear(); + } + + @Override + public void onClick(View view) { + if (mContainerVisible && mControlsVisible.get(view).booleanValue()) { + mDelegate.onBottomControlClicked(view.getId()); + } + } +} diff --git a/src/com/android/gallery3d/app/PhotoPageProgressBar.java b/src/com/android/gallery3d/app/PhotoPageProgressBar.java new file mode 100644 index 000000000..141fea698 --- /dev/null +++ b/src/com/android/gallery3d/app/PhotoPageProgressBar.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2012 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.app; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import android.widget.RelativeLayout; + +import com.android.gallery3d.R; + +public class PhotoPageProgressBar { + private ViewGroup mContainer; + private View mProgress; + + public PhotoPageProgressBar(Context context, RelativeLayout parentLayout) { + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mContainer = (ViewGroup) inflater.inflate(R.layout.photopage_progress_bar, parentLayout, + false); + parentLayout.addView(mContainer); + mProgress = mContainer.findViewById(R.id.photopage_progress_foreground); + } + + public void setProgress(int progressPercent) { + mContainer.setVisibility(View.VISIBLE); + LayoutParams layoutParams = mProgress.getLayoutParams(); + layoutParams.width = mContainer.getWidth() * progressPercent / 100; + mProgress.setLayoutParams(layoutParams); + } + + public void hideProgress() { + mContainer.setVisibility(View.INVISIBLE); + } +} diff --git a/src/com/android/gallery3d/app/PickerActivity.java b/src/com/android/gallery3d/app/PickerActivity.java new file mode 100644 index 000000000..d5bb218ea --- /dev/null +++ b/src/com/android/gallery3d/app/PickerActivity.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2011 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.app; + +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.Window; + +import com.android.gallery3d.R; +import com.android.gallery3d.ui.GLRootView; + +public class PickerActivity extends AbstractGalleryActivity + implements OnClickListener { + + public static final String KEY_ALBUM_PATH = "album-path"; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // We show the picker in two ways. One smaller screen we use a full + // screen window with an action bar. On larger screen we use a dialog. + boolean isDialog = getResources().getBoolean(R.bool.picker_is_dialog); + + if (!isDialog) { + requestWindowFeature(Window.FEATURE_ACTION_BAR); + requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY); + } + + setContentView(R.layout.dialog_picker); + + if (isDialog) { + // In dialog mode, we don't have the action bar to show the + // "cancel" action, so we show an additional "cancel" button. + View view = findViewById(R.id.cancel); + view.setOnClickListener(this); + view.setVisibility(View.VISIBLE); + + // We need this, otherwise the view will be dimmed because it + // is "behind" the dialog. + ((GLRootView) findViewById(R.id.gl_root_view)).setZOrderOnTop(true); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.pickup, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.action_cancel) { + finish(); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public void onClick(View v) { + if (v.getId() == R.id.cancel) finish(); + } +} diff --git a/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java b/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java new file mode 100644 index 000000000..00f2fe78f --- /dev/null +++ b/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java @@ -0,0 +1,263 @@ +/* + * 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.app; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.BitmapRegionDecoder; +import android.graphics.Rect; +import android.os.Handler; +import android.os.Message; + +import com.android.gallery3d.common.BitmapUtils; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.ui.BitmapScreenNail; +import com.android.gallery3d.ui.PhotoView; +import com.android.gallery3d.ui.ScreenNail; +import com.android.gallery3d.ui.SynchronizedHandler; +import com.android.gallery3d.ui.TileImageViewAdapter; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.FutureListener; +import com.android.gallery3d.util.ThreadPool; + +public class SinglePhotoDataAdapter extends TileImageViewAdapter + implements PhotoPage.Model { + + private static final String TAG = "SinglePhotoDataAdapter"; + private static final int SIZE_BACKUP = 1024; + private static final int MSG_UPDATE_IMAGE = 1; + + private MediaItem mItem; + private boolean mHasFullImage; + private Future<?> mTask; + private Handler mHandler; + + private PhotoView mPhotoView; + private ThreadPool mThreadPool; + private int mLoadingState = LOADING_INIT; + private BitmapScreenNail mBitmapScreenNail; + + public SinglePhotoDataAdapter( + AbstractGalleryActivity activity, PhotoView view, MediaItem item) { + mItem = Utils.checkNotNull(item); + mHasFullImage = (item.getSupportedOperations() & + MediaItem.SUPPORT_FULL_IMAGE) != 0; + mPhotoView = Utils.checkNotNull(view); + mHandler = new SynchronizedHandler(activity.getGLRoot()) { + @Override + @SuppressWarnings("unchecked") + public void handleMessage(Message message) { + Utils.assertTrue(message.what == MSG_UPDATE_IMAGE); + if (mHasFullImage) { + onDecodeLargeComplete((ImageBundle) message.obj); + } else { + onDecodeThumbComplete((Future<Bitmap>) message.obj); + } + } + }; + mThreadPool = activity.getThreadPool(); + } + + private static class ImageBundle { + public final BitmapRegionDecoder decoder; + public final Bitmap backupImage; + + public ImageBundle(BitmapRegionDecoder decoder, Bitmap backupImage) { + this.decoder = decoder; + this.backupImage = backupImage; + } + } + + private FutureListener<BitmapRegionDecoder> mLargeListener = + new FutureListener<BitmapRegionDecoder>() { + @Override + public void onFutureDone(Future<BitmapRegionDecoder> future) { + BitmapRegionDecoder decoder = future.get(); + if (decoder == null) return; + int width = decoder.getWidth(); + int height = decoder.getHeight(); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = BitmapUtils.computeSampleSize( + (float) SIZE_BACKUP / Math.max(width, height)); + Bitmap bitmap = decoder.decodeRegion(new Rect(0, 0, width, height), options); + mHandler.sendMessage(mHandler.obtainMessage( + MSG_UPDATE_IMAGE, new ImageBundle(decoder, bitmap))); + } + }; + + private FutureListener<Bitmap> mThumbListener = + new FutureListener<Bitmap>() { + @Override + public void onFutureDone(Future<Bitmap> future) { + mHandler.sendMessage( + mHandler.obtainMessage(MSG_UPDATE_IMAGE, future)); + } + }; + + @Override + public boolean isEmpty() { + return false; + } + + private void setScreenNail(Bitmap bitmap, int width, int height) { + mBitmapScreenNail = new BitmapScreenNail(bitmap); + setScreenNail(mBitmapScreenNail, width, height); + } + + private void onDecodeLargeComplete(ImageBundle bundle) { + try { + setScreenNail(bundle.backupImage, + bundle.decoder.getWidth(), bundle.decoder.getHeight()); + setRegionDecoder(bundle.decoder); + mPhotoView.notifyImageChange(0); + } catch (Throwable t) { + Log.w(TAG, "fail to decode large", t); + } + } + + private void onDecodeThumbComplete(Future<Bitmap> future) { + try { + Bitmap backup = future.get(); + if (backup == null) { + mLoadingState = LOADING_FAIL; + return; + } else { + mLoadingState = LOADING_COMPLETE; + } + setScreenNail(backup, backup.getWidth(), backup.getHeight()); + mPhotoView.notifyImageChange(0); + } catch (Throwable t) { + Log.w(TAG, "fail to decode thumb", t); + } + } + + @Override + public void resume() { + if (mTask == null) { + if (mHasFullImage) { + mTask = mThreadPool.submit( + mItem.requestLargeImage(), mLargeListener); + } else { + mTask = mThreadPool.submit( + mItem.requestImage(MediaItem.TYPE_THUMBNAIL), + mThumbListener); + } + } + } + + @Override + public void pause() { + Future<?> task = mTask; + task.cancel(); + task.waitDone(); + if (task.get() == null) { + mTask = null; + } + if (mBitmapScreenNail != null) { + mBitmapScreenNail.recycle(); + mBitmapScreenNail = null; + } + } + + @Override + public void moveTo(int index) { + throw new UnsupportedOperationException(); + } + + @Override + public void getImageSize(int offset, PhotoView.Size size) { + if (offset == 0) { + size.width = mItem.getWidth(); + size.height = mItem.getHeight(); + } else { + size.width = 0; + size.height = 0; + } + } + + @Override + public int getImageRotation(int offset) { + return (offset == 0) ? mItem.getFullImageRotation() : 0; + } + + @Override + public ScreenNail getScreenNail(int offset) { + return (offset == 0) ? getScreenNail() : null; + } + + @Override + public void setNeedFullImage(boolean enabled) { + // currently not necessary. + } + + @Override + public boolean isCamera(int offset) { + return false; + } + + @Override + public boolean isPanorama(int offset) { + return false; + } + + @Override + public boolean isStaticCamera(int offset) { + return false; + } + + @Override + public boolean isVideo(int offset) { + return mItem.getMediaType() == MediaItem.MEDIA_TYPE_VIDEO; + } + + @Override + public boolean isDeletable(int offset) { + return (mItem.getSupportedOperations() & MediaItem.SUPPORT_DELETE) != 0; + } + + @Override + public MediaItem getMediaItem(int offset) { + return offset == 0 ? mItem : null; + } + + @Override + public int getCurrentIndex() { + return 0; + } + + @Override + public void setCurrentPhoto(Path path, int indexHint) { + // ignore + } + + @Override + public void setFocusHintDirection(int direction) { + // ignore + } + + @Override + public void setFocusHintPath(Path path) { + // ignore + } + + @Override + public int getLoadingState(int offset) { + return mLoadingState; + } +} diff --git a/src/com/android/gallery3d/app/SinglePhotoPage.java b/src/com/android/gallery3d/app/SinglePhotoPage.java new file mode 100644 index 000000000..beb87d358 --- /dev/null +++ b/src/com/android/gallery3d/app/SinglePhotoPage.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2012 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.app; + +public class SinglePhotoPage extends PhotoPage { + +} diff --git a/src/com/android/gallery3d/app/SlideshowDataAdapter.java b/src/com/android/gallery3d/app/SlideshowDataAdapter.java new file mode 100644 index 000000000..7a0fba5fb --- /dev/null +++ b/src/com/android/gallery3d/app/SlideshowDataAdapter.java @@ -0,0 +1,204 @@ +/* + * 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.app; + +import android.graphics.Bitmap; + +import com.android.gallery3d.app.SlideshowPage.Slide; +import com.android.gallery3d.data.ContentListener; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.FutureListener; +import com.android.gallery3d.util.ThreadPool; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import java.util.LinkedList; +import java.util.concurrent.atomic.AtomicBoolean; + +public class SlideshowDataAdapter implements SlideshowPage.Model { + @SuppressWarnings("unused") + private static final String TAG = "SlideshowDataAdapter"; + + private static final int IMAGE_QUEUE_CAPACITY = 3; + + public interface SlideshowSource { + public void addContentListener(ContentListener listener); + public void removeContentListener(ContentListener listener); + public long reload(); + public MediaItem getMediaItem(int index); + public int findItemIndex(Path path, int hint); + } + + private final SlideshowSource mSource; + + private int mLoadIndex = 0; + private int mNextOutput = 0; + private boolean mIsActive = false; + private boolean mNeedReset; + private boolean mDataReady; + private Path mInitialPath; + + private final LinkedList<Slide> mImageQueue = new LinkedList<Slide>(); + + private Future<Void> mReloadTask; + private final ThreadPool mThreadPool; + + private long mDataVersion = MediaObject.INVALID_DATA_VERSION; + private final AtomicBoolean mNeedReload = new AtomicBoolean(false); + private final SourceListener mSourceListener = new SourceListener(); + + // The index is just a hint if initialPath is set + public SlideshowDataAdapter(GalleryContext context, SlideshowSource source, int index, + Path initialPath) { + mSource = source; + mInitialPath = initialPath; + mLoadIndex = index; + mNextOutput = index; + mThreadPool = context.getThreadPool(); + } + + private MediaItem loadItem() { + if (mNeedReload.compareAndSet(true, false)) { + long v = mSource.reload(); + if (v != mDataVersion) { + mDataVersion = v; + mNeedReset = true; + return null; + } + } + int index = mLoadIndex; + if (mInitialPath != null) { + index = mSource.findItemIndex(mInitialPath, index); + mInitialPath = null; + } + return mSource.getMediaItem(index); + } + + private class ReloadTask implements Job<Void> { + @Override + public Void run(JobContext jc) { + while (true) { + synchronized (SlideshowDataAdapter.this) { + while (mIsActive && (!mDataReady + || mImageQueue.size() >= IMAGE_QUEUE_CAPACITY)) { + try { + SlideshowDataAdapter.this.wait(); + } catch (InterruptedException ex) { + // ignored. + } + continue; + } + } + if (!mIsActive) return null; + mNeedReset = false; + + MediaItem item = loadItem(); + + if (mNeedReset) { + synchronized (SlideshowDataAdapter.this) { + mImageQueue.clear(); + mLoadIndex = mNextOutput; + } + continue; + } + + if (item == null) { + synchronized (SlideshowDataAdapter.this) { + if (!mNeedReload.get()) mDataReady = false; + SlideshowDataAdapter.this.notifyAll(); + } + continue; + } + + Bitmap bitmap = item + .requestImage(MediaItem.TYPE_THUMBNAIL) + .run(jc); + + if (bitmap != null) { + synchronized (SlideshowDataAdapter.this) { + mImageQueue.addLast( + new Slide(item, mLoadIndex, bitmap)); + if (mImageQueue.size() == 1) { + SlideshowDataAdapter.this.notifyAll(); + } + } + } + ++mLoadIndex; + } + } + } + + private class SourceListener implements ContentListener { + @Override + public void onContentDirty() { + synchronized (SlideshowDataAdapter.this) { + mNeedReload.set(true); + mDataReady = true; + SlideshowDataAdapter.this.notifyAll(); + } + } + } + + private synchronized Slide innerNextBitmap() { + while (mIsActive && mDataReady && mImageQueue.isEmpty()) { + try { + wait(); + } catch (InterruptedException t) { + throw new AssertionError(); + } + } + if (mImageQueue.isEmpty()) return null; + mNextOutput++; + this.notifyAll(); + return mImageQueue.removeFirst(); + } + + @Override + public Future<Slide> nextSlide(FutureListener<Slide> listener) { + return mThreadPool.submit(new Job<Slide>() { + @Override + public Slide run(JobContext jc) { + jc.setMode(ThreadPool.MODE_NONE); + return innerNextBitmap(); + } + }, listener); + } + + @Override + public void pause() { + synchronized (this) { + mIsActive = false; + notifyAll(); + } + mSource.removeContentListener(mSourceListener); + mReloadTask.cancel(); + mReloadTask.waitDone(); + mReloadTask = null; + } + + @Override + public synchronized void resume() { + mIsActive = true; + mSource.addContentListener(mSourceListener); + mNeedReload.set(true); + mDataReady = true; + mReloadTask = mThreadPool.submit(new ReloadTask()); + } +} diff --git a/src/com/android/gallery3d/app/SlideshowPage.java b/src/com/android/gallery3d/app/SlideshowPage.java new file mode 100644 index 000000000..174058dc8 --- /dev/null +++ b/src/com/android/gallery3d/app/SlideshowPage.java @@ -0,0 +1,366 @@ +/* + * 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.app; + +import android.app.Activity; +import android.content.Intent; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.view.MotionEvent; + +import com.android.gallery3d.R; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.ContentListener; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.glrenderer.GLCanvas; +import com.android.gallery3d.ui.GLView; +import com.android.gallery3d.ui.SlideshowView; +import com.android.gallery3d.ui.SynchronizedHandler; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.FutureListener; + +import java.util.ArrayList; +import java.util.Random; + +public class SlideshowPage extends ActivityState { + private static final String TAG = "SlideshowPage"; + + public static final String KEY_SET_PATH = "media-set-path"; + public static final String KEY_ITEM_PATH = "media-item-path"; + public static final String KEY_PHOTO_INDEX = "photo-index"; + public static final String KEY_RANDOM_ORDER = "random-order"; + public static final String KEY_REPEAT = "repeat"; + public static final String KEY_DREAM = "dream"; + + private static final long SLIDESHOW_DELAY = 3000; // 3 seconds + + private static final int MSG_LOAD_NEXT_BITMAP = 1; + private static final int MSG_SHOW_PENDING_BITMAP = 2; + + public static interface Model { + public void pause(); + + public void resume(); + + public Future<Slide> nextSlide(FutureListener<Slide> listener); + } + + public static class Slide { + public Bitmap bitmap; + public MediaItem item; + public int index; + + public Slide(MediaItem item, int index, Bitmap bitmap) { + this.bitmap = bitmap; + this.item = item; + this.index = index; + } + } + + private Handler mHandler; + private Model mModel; + private SlideshowView mSlideshowView; + + private Slide mPendingSlide = null; + private boolean mIsActive = false; + private final Intent mResultIntent = new Intent(); + + @Override + protected int getBackgroundColorId() { + return R.color.slideshow_background; + } + + private final GLView mRootPane = new GLView() { + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + mSlideshowView.layout(0, 0, right - left, bottom - top); + } + + @Override + protected boolean onTouch(MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_UP) { + onBackPressed(); + } + return true; + } + + @Override + protected void renderBackground(GLCanvas canvas) { + canvas.clearBuffer(getBackgroundColor()); + } + }; + + @Override + public void onCreate(Bundle data, Bundle restoreState) { + super.onCreate(data, restoreState); + mFlags |= (FLAG_HIDE_ACTION_BAR | FLAG_HIDE_STATUS_BAR); + if (data.getBoolean(KEY_DREAM)) { + // Dream screensaver only keeps screen on for plugged devices. + mFlags |= FLAG_SCREEN_ON_WHEN_PLUGGED | FLAG_SHOW_WHEN_LOCKED; + } else { + // User-initiated slideshow would always keep screen on. + mFlags |= FLAG_SCREEN_ON_ALWAYS; + } + + mHandler = new SynchronizedHandler(mActivity.getGLRoot()) { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_SHOW_PENDING_BITMAP: + showPendingBitmap(); + break; + case MSG_LOAD_NEXT_BITMAP: + loadNextBitmap(); + break; + default: throw new AssertionError(); + } + } + }; + initializeViews(); + initializeData(data); + } + + private void loadNextBitmap() { + mModel.nextSlide(new FutureListener<Slide>() { + @Override + public void onFutureDone(Future<Slide> future) { + mPendingSlide = future.get(); + mHandler.sendEmptyMessage(MSG_SHOW_PENDING_BITMAP); + } + }); + } + + private void showPendingBitmap() { + // mPendingBitmap could be null, if + // 1.) there is no more items + // 2.) mModel is paused + Slide slide = mPendingSlide; + if (slide == null) { + if (mIsActive) { + mActivity.getStateManager().finishState(SlideshowPage.this); + } + return; + } + + mSlideshowView.next(slide.bitmap, slide.item.getRotation()); + + setStateResult(Activity.RESULT_OK, mResultIntent + .putExtra(KEY_ITEM_PATH, slide.item.getPath().toString()) + .putExtra(KEY_PHOTO_INDEX, slide.index)); + mHandler.sendEmptyMessageDelayed(MSG_LOAD_NEXT_BITMAP, SLIDESHOW_DELAY); + } + + @Override + public void onPause() { + super.onPause(); + mIsActive = false; + mModel.pause(); + mSlideshowView.release(); + + mHandler.removeMessages(MSG_LOAD_NEXT_BITMAP); + mHandler.removeMessages(MSG_SHOW_PENDING_BITMAP); + } + + @Override + public void onResume() { + super.onResume(); + mIsActive = true; + mModel.resume(); + + if (mPendingSlide != null) { + showPendingBitmap(); + } else { + loadNextBitmap(); + } + } + + private void initializeData(Bundle data) { + boolean random = data.getBoolean(KEY_RANDOM_ORDER, false); + + // We only want to show slideshow for images only, not videos. + String mediaPath = data.getString(KEY_SET_PATH); + mediaPath = FilterUtils.newFilterPath(mediaPath, FilterUtils.FILTER_IMAGE_ONLY); + MediaSet mediaSet = mActivity.getDataManager().getMediaSet(mediaPath); + + if (random) { + boolean repeat = data.getBoolean(KEY_REPEAT); + mModel = new SlideshowDataAdapter(mActivity, + new ShuffleSource(mediaSet, repeat), 0, null); + setStateResult(Activity.RESULT_OK, mResultIntent.putExtra(KEY_PHOTO_INDEX, 0)); + } else { + int index = data.getInt(KEY_PHOTO_INDEX); + String itemPath = data.getString(KEY_ITEM_PATH); + Path path = itemPath != null ? Path.fromString(itemPath) : null; + boolean repeat = data.getBoolean(KEY_REPEAT); + mModel = new SlideshowDataAdapter(mActivity, new SequentialSource(mediaSet, repeat), + index, path); + setStateResult(Activity.RESULT_OK, mResultIntent.putExtra(KEY_PHOTO_INDEX, index)); + } + } + + private void initializeViews() { + mSlideshowView = new SlideshowView(); + mRootPane.addComponent(mSlideshowView); + setContentPane(mRootPane); + } + + private static MediaItem findMediaItem(MediaSet mediaSet, int index) { + for (int i = 0, n = mediaSet.getSubMediaSetCount(); i < n; ++i) { + MediaSet subset = mediaSet.getSubMediaSet(i); + int count = subset.getTotalMediaItemCount(); + if (index < count) { + return findMediaItem(subset, index); + } + index -= count; + } + ArrayList<MediaItem> list = mediaSet.getMediaItem(index, 1); + return list.isEmpty() ? null : list.get(0); + } + + private static class ShuffleSource implements SlideshowDataAdapter.SlideshowSource { + private static final int RETRY_COUNT = 5; + private final MediaSet mMediaSet; + private final Random mRandom = new Random(); + private int mOrder[] = new int[0]; + private final boolean mRepeat; + private long mSourceVersion = MediaSet.INVALID_DATA_VERSION; + private int mLastIndex = -1; + + public ShuffleSource(MediaSet mediaSet, boolean repeat) { + mMediaSet = Utils.checkNotNull(mediaSet); + mRepeat = repeat; + } + + @Override + public int findItemIndex(Path path, int hint) { + return hint; + } + + @Override + public MediaItem getMediaItem(int index) { + if (!mRepeat && index >= mOrder.length) return null; + if (mOrder.length == 0) return null; + mLastIndex = mOrder[index % mOrder.length]; + MediaItem item = findMediaItem(mMediaSet, mLastIndex); + for (int i = 0; i < RETRY_COUNT && item == null; ++i) { + Log.w(TAG, "fail to find image: " + mLastIndex); + mLastIndex = mRandom.nextInt(mOrder.length); + item = findMediaItem(mMediaSet, mLastIndex); + } + return item; + } + + @Override + public long reload() { + long version = mMediaSet.reload(); + if (version != mSourceVersion) { + mSourceVersion = version; + int count = mMediaSet.getTotalMediaItemCount(); + if (count != mOrder.length) generateOrderArray(count); + } + return version; + } + + private void generateOrderArray(int totalCount) { + if (mOrder.length != totalCount) { + mOrder = new int[totalCount]; + for (int i = 0; i < totalCount; ++i) { + mOrder[i] = i; + } + } + for (int i = totalCount - 1; i > 0; --i) { + Utils.swap(mOrder, i, mRandom.nextInt(i + 1)); + } + if (mOrder[0] == mLastIndex && totalCount > 1) { + Utils.swap(mOrder, 0, mRandom.nextInt(totalCount - 1) + 1); + } + } + + @Override + public void addContentListener(ContentListener listener) { + mMediaSet.addContentListener(listener); + } + + @Override + public void removeContentListener(ContentListener listener) { + mMediaSet.removeContentListener(listener); + } + } + + private static class SequentialSource implements SlideshowDataAdapter.SlideshowSource { + private static final int DATA_SIZE = 32; + + private ArrayList<MediaItem> mData = new ArrayList<MediaItem>(); + private int mDataStart = 0; + private long mDataVersion = MediaObject.INVALID_DATA_VERSION; + private final MediaSet mMediaSet; + private final boolean mRepeat; + + public SequentialSource(MediaSet mediaSet, boolean repeat) { + mMediaSet = mediaSet; + mRepeat = repeat; + } + + @Override + public int findItemIndex(Path path, int hint) { + return mMediaSet.getIndexOfItem(path, hint); + } + + @Override + public MediaItem getMediaItem(int index) { + int dataEnd = mDataStart + mData.size(); + + if (mRepeat) { + int count = mMediaSet.getMediaItemCount(); + if (count == 0) return null; + index = index % count; + } + if (index < mDataStart || index >= dataEnd) { + mData = mMediaSet.getMediaItem(index, DATA_SIZE); + mDataStart = index; + dataEnd = index + mData.size(); + } + + return (index < mDataStart || index >= dataEnd) ? null : mData.get(index - mDataStart); + } + + @Override + public long reload() { + long version = mMediaSet.reload(); + if (version != mDataVersion) { + mDataVersion = version; + mData.clear(); + } + return mDataVersion; + } + + @Override + public void addContentListener(ContentListener listener) { + mMediaSet.addContentListener(listener); + } + + @Override + public void removeContentListener(ContentListener listener) { + mMediaSet.removeContentListener(listener); + } + } +} diff --git a/src/com/android/gallery3d/app/StateManager.java b/src/com/android/gallery3d/app/StateManager.java new file mode 100644 index 000000000..53c3fc228 --- /dev/null +++ b/src/com/android/gallery3d/app/StateManager.java @@ -0,0 +1,339 @@ +/* + * 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.app; + +import android.app.Activity; +import android.content.Intent; +import android.content.res.Configuration; +import android.os.Bundle; +import android.os.Parcelable; +import android.view.Menu; +import android.view.MenuItem; + +import com.android.camera.CameraActivity; +import com.android.gallery3d.anim.StateTransitionAnimation; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.util.UsageStatistics; + +import java.util.Stack; + +public class StateManager { + @SuppressWarnings("unused") + private static final String TAG = "StateManager"; + private boolean mIsResumed = false; + + private static final String KEY_MAIN = "activity-state"; + private static final String KEY_DATA = "data"; + private static final String KEY_STATE = "bundle"; + private static final String KEY_CLASS = "class"; + + private AbstractGalleryActivity mActivity; + private Stack<StateEntry> mStack = new Stack<StateEntry>(); + private ActivityState.ResultEntry mResult; + + public StateManager(AbstractGalleryActivity activity) { + mActivity = activity; + } + + public void startState(Class<? extends ActivityState> klass, + Bundle data) { + Log.v(TAG, "startState " + klass); + ActivityState state = null; + try { + state = klass.newInstance(); + } catch (Exception e) { + throw new AssertionError(e); + } + if (!mStack.isEmpty()) { + ActivityState top = getTopState(); + top.transitionOnNextPause(top.getClass(), klass, + StateTransitionAnimation.Transition.Incoming); + if (mIsResumed) top.onPause(); + } + + UsageStatistics.onContentViewChanged( + UsageStatistics.COMPONENT_GALLERY, + klass.getSimpleName()); + state.initialize(mActivity, data); + + mStack.push(new StateEntry(data, state)); + state.onCreate(data, null); + if (mIsResumed) state.resume(); + } + + public void startStateForResult(Class<? extends ActivityState> klass, + int requestCode, Bundle data) { + Log.v(TAG, "startStateForResult " + klass + ", " + requestCode); + ActivityState state = null; + try { + state = klass.newInstance(); + } catch (Exception e) { + throw new AssertionError(e); + } + state.initialize(mActivity, data); + state.mResult = new ActivityState.ResultEntry(); + state.mResult.requestCode = requestCode; + + if (!mStack.isEmpty()) { + ActivityState as = getTopState(); + as.transitionOnNextPause(as.getClass(), klass, + StateTransitionAnimation.Transition.Incoming); + as.mReceivedResults = state.mResult; + if (mIsResumed) as.onPause(); + } else { + mResult = state.mResult; + } + UsageStatistics.onContentViewChanged(UsageStatistics.COMPONENT_GALLERY, + klass.getSimpleName()); + mStack.push(new StateEntry(data, state)); + state.onCreate(data, null); + if (mIsResumed) state.resume(); + } + + public boolean createOptionsMenu(Menu menu) { + if (mStack.isEmpty()) { + return false; + } else { + return getTopState().onCreateActionBar(menu); + } + } + + public void onConfigurationChange(Configuration config) { + for (StateEntry entry : mStack) { + entry.activityState.onConfigurationChanged(config); + } + } + + public void resume() { + if (mIsResumed) return; + mIsResumed = true; + if (!mStack.isEmpty()) getTopState().resume(); + } + + public void pause() { + if (!mIsResumed) return; + mIsResumed = false; + if (!mStack.isEmpty()) getTopState().onPause(); + } + + public void notifyActivityResult(int requestCode, int resultCode, Intent data) { + getTopState().onStateResult(requestCode, resultCode, data); + } + + public void clearActivityResult() { + if (!mStack.isEmpty()) { + getTopState().clearStateResult(); + } + } + + public int getStateCount() { + return mStack.size(); + } + + public boolean itemSelected(MenuItem item) { + if (!mStack.isEmpty()) { + if (getTopState().onItemSelected(item)) return true; + if (item.getItemId() == android.R.id.home) { + if (mStack.size() > 1) { + getTopState().onBackPressed(); + } + return true; + } + } + return false; + } + + public void onBackPressed() { + if (!mStack.isEmpty()) { + getTopState().onBackPressed(); + } + } + + void finishState(ActivityState state) { + finishState(state, true); + } + + public void clearTasks() { + // Remove all the states that are on top of the bottom PhotoPage state + while (mStack.size() > 1) { + mStack.pop().activityState.onDestroy(); + } + } + + void finishState(ActivityState state, boolean fireOnPause) { + // The finish() request could be rejected (only happens under Monkey), + // If it is rejected, we won't close the last page. + if (mStack.size() == 1) { + Activity activity = (Activity) mActivity.getAndroidContext(); + if (mResult != null) { + activity.setResult(mResult.resultCode, mResult.resultData); + } + activity.finish(); + if (!activity.isFinishing()) { + Log.w(TAG, "finish is rejected, keep the last state"); + return; + } + Log.v(TAG, "no more state, finish activity"); + } + + Log.v(TAG, "finishState " + state); + if (state != mStack.peek().activityState) { + if (state.isDestroyed()) { + Log.d(TAG, "The state is already destroyed"); + return; + } else { + throw new IllegalArgumentException("The stateview to be finished" + + " is not at the top of the stack: " + state + ", " + + mStack.peek().activityState); + } + } + + // Remove the top state. + mStack.pop(); + state.mIsFinishing = true; + ActivityState top = !mStack.isEmpty() ? mStack.peek().activityState : null; + if (mIsResumed && fireOnPause) { + if (top != null) { + state.transitionOnNextPause(state.getClass(), top.getClass(), + StateTransitionAnimation.Transition.Outgoing); + } + state.onPause(); + } + mActivity.getGLRoot().setContentPane(null); + state.onDestroy(); + + if (top != null && mIsResumed) top.resume(); + if (top != null) { + UsageStatistics.onContentViewChanged(UsageStatistics.COMPONENT_GALLERY, + top.getClass().getSimpleName()); + } + } + + public void switchState(ActivityState oldState, + Class<? extends ActivityState> klass, Bundle data) { + Log.v(TAG, "switchState " + oldState + ", " + klass); + if (oldState != mStack.peek().activityState) { + throw new IllegalArgumentException("The stateview to be finished" + + " is not at the top of the stack: " + oldState + ", " + + mStack.peek().activityState); + } + // Remove the top state. + mStack.pop(); + if (!data.containsKey(PhotoPage.KEY_APP_BRIDGE)) { + // Do not do the fade out stuff when we are switching camera modes + oldState.transitionOnNextPause(oldState.getClass(), klass, + StateTransitionAnimation.Transition.Incoming); + } + if (mIsResumed) oldState.onPause(); + oldState.onDestroy(); + + // Create new state. + ActivityState state = null; + try { + state = klass.newInstance(); + } catch (Exception e) { + throw new AssertionError(e); + } + state.initialize(mActivity, data); + mStack.push(new StateEntry(data, state)); + state.onCreate(data, null); + if (mIsResumed) state.resume(); + UsageStatistics.onContentViewChanged(UsageStatistics.COMPONENT_GALLERY, + klass.getSimpleName()); + } + + public void destroy() { + Log.v(TAG, "destroy"); + while (!mStack.isEmpty()) { + mStack.pop().activityState.onDestroy(); + } + mStack.clear(); + } + + @SuppressWarnings("unchecked") + public void restoreFromState(Bundle inState) { + Log.v(TAG, "restoreFromState"); + Parcelable list[] = inState.getParcelableArray(KEY_MAIN); + ActivityState topState = null; + for (Parcelable parcelable : list) { + Bundle bundle = (Bundle) parcelable; + Class<? extends ActivityState> klass = + (Class<? extends ActivityState>) bundle.getSerializable(KEY_CLASS); + + Bundle data = bundle.getBundle(KEY_DATA); + Bundle state = bundle.getBundle(KEY_STATE); + + ActivityState activityState; + try { + Log.v(TAG, "restoreFromState " + klass); + activityState = klass.newInstance(); + } catch (Exception e) { + throw new AssertionError(e); + } + activityState.initialize(mActivity, data); + activityState.onCreate(data, state); + mStack.push(new StateEntry(data, activityState)); + topState = activityState; + } + if (topState != null) { + UsageStatistics.onContentViewChanged(UsageStatistics.COMPONENT_GALLERY, + topState.getClass().getSimpleName()); + } + } + + public void saveState(Bundle outState) { + Log.v(TAG, "saveState"); + + Parcelable list[] = new Parcelable[mStack.size()]; + int i = 0; + for (StateEntry entry : mStack) { + Bundle bundle = new Bundle(); + bundle.putSerializable(KEY_CLASS, entry.activityState.getClass()); + bundle.putBundle(KEY_DATA, entry.data); + Bundle state = new Bundle(); + entry.activityState.onSaveState(state); + bundle.putBundle(KEY_STATE, state); + Log.v(TAG, "saveState " + entry.activityState.getClass()); + list[i++] = bundle; + } + outState.putParcelableArray(KEY_MAIN, list); + } + + public boolean hasStateClass(Class<? extends ActivityState> klass) { + for (StateEntry entry : mStack) { + if (klass.isInstance(entry.activityState)) { + return true; + } + } + return false; + } + + public ActivityState getTopState() { + Utils.assertTrue(!mStack.isEmpty()); + return mStack.peek().activityState; + } + + private static class StateEntry { + public Bundle data; + public ActivityState activityState; + + public StateEntry(Bundle data, ActivityState state) { + this.data = data; + this.activityState = state; + } + } +} diff --git a/src/com/android/gallery3d/app/StitchingChangeListener.java b/src/com/android/gallery3d/app/StitchingChangeListener.java new file mode 100644 index 000000000..0b8c2d6d6 --- /dev/null +++ b/src/com/android/gallery3d/app/StitchingChangeListener.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2012 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.app; + +import android.net.Uri; + +public interface StitchingChangeListener { + public void onStitchingQueued(Uri uri); + + public void onStitchingResult(Uri uri); + + public void onStitchingProgress(Uri uri, int progress); +} diff --git a/src/com/android/gallery3d/app/TimeBar.java b/src/com/android/gallery3d/app/TimeBar.java new file mode 100644 index 000000000..246346a56 --- /dev/null +++ b/src/com/android/gallery3d/app/TimeBar.java @@ -0,0 +1,266 @@ +/* + * Copyright (C) 2011 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.app; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.util.DisplayMetrics; +import android.view.MotionEvent; +import android.view.View; + +import com.android.gallery3d.R; +import com.android.gallery3d.common.Utils; + +/** + * The time bar view, which includes the current and total time, the progress + * bar, and the scrubber. + */ +public class TimeBar extends View { + + public interface Listener { + void onScrubbingStart(); + + void onScrubbingMove(int time); + + void onScrubbingEnd(int time, int start, int end); + } + + // Padding around the scrubber to increase its touch target + private static final int SCRUBBER_PADDING_IN_DP = 10; + + // The total padding, top plus bottom + private static final int V_PADDING_IN_DP = 30; + + private static final int TEXT_SIZE_IN_DP = 14; + + protected final Listener mListener; + + // the bars we use for displaying the progress + protected final Rect mProgressBar; + protected final Rect mPlayedBar; + + protected final Paint mProgressPaint; + protected final Paint mPlayedPaint; + protected final Paint mTimeTextPaint; + + protected final Bitmap mScrubber; + protected int mScrubberPadding; // adds some touch tolerance around the + // scrubber + + protected int mScrubberLeft; + protected int mScrubberTop; + protected int mScrubberCorrection; + protected boolean mScrubbing; + protected boolean mShowTimes; + protected boolean mShowScrubber; + + protected int mTotalTime; + protected int mCurrentTime; + + protected final Rect mTimeBounds; + + protected int mVPaddingInPx; + + public TimeBar(Context context, Listener listener) { + super(context); + mListener = Utils.checkNotNull(listener); + + mShowTimes = true; + mShowScrubber = true; + + mProgressBar = new Rect(); + mPlayedBar = new Rect(); + + mProgressPaint = new Paint(); + mProgressPaint.setColor(0xFF808080); + mPlayedPaint = new Paint(); + mPlayedPaint.setColor(0xFFFFFFFF); + + DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + float textSizeInPx = metrics.density * TEXT_SIZE_IN_DP; + mTimeTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mTimeTextPaint.setColor(0xFFCECECE); + mTimeTextPaint.setTextSize(textSizeInPx); + mTimeTextPaint.setTextAlign(Paint.Align.CENTER); + + mTimeBounds = new Rect(); + mTimeTextPaint.getTextBounds("0:00:00", 0, 7, mTimeBounds); + + mScrubber = BitmapFactory.decodeResource(getResources(), R.drawable.scrubber_knob); + mScrubberPadding = (int) (metrics.density * SCRUBBER_PADDING_IN_DP); + + mVPaddingInPx = (int) (metrics.density * V_PADDING_IN_DP); + } + + private void update() { + mPlayedBar.set(mProgressBar); + + if (mTotalTime > 0) { + mPlayedBar.right = + mPlayedBar.left + (int) ((mProgressBar.width() * (long) mCurrentTime) / mTotalTime); + } else { + mPlayedBar.right = mProgressBar.left; + } + + if (!mScrubbing) { + mScrubberLeft = mPlayedBar.right - mScrubber.getWidth() / 2; + } + invalidate(); + } + + /** + * @return the preferred height of this view, including invisible padding + */ + public int getPreferredHeight() { + return mTimeBounds.height() + mVPaddingInPx + mScrubberPadding; + } + + /** + * @return the height of the time bar, excluding invisible padding + */ + public int getBarHeight() { + return mTimeBounds.height() + mVPaddingInPx; + } + + public void setTime(int currentTime, int totalTime, + int trimStartTime, int trimEndTime) { + if (mCurrentTime == currentTime && mTotalTime == totalTime) { + return; + } + mCurrentTime = currentTime; + mTotalTime = totalTime; + update(); + } + + private boolean inScrubber(float x, float y) { + int scrubberRight = mScrubberLeft + mScrubber.getWidth(); + int scrubberBottom = mScrubberTop + mScrubber.getHeight(); + return mScrubberLeft - mScrubberPadding < x && x < scrubberRight + mScrubberPadding + && mScrubberTop - mScrubberPadding < y && y < scrubberBottom + mScrubberPadding; + } + + private void clampScrubber() { + int half = mScrubber.getWidth() / 2; + int max = mProgressBar.right - half; + int min = mProgressBar.left - half; + mScrubberLeft = Math.min(max, Math.max(min, mScrubberLeft)); + } + + private int getScrubberTime() { + return (int) ((long) (mScrubberLeft + mScrubber.getWidth() / 2 - mProgressBar.left) + * mTotalTime / mProgressBar.width()); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + int w = r - l; + int h = b - t; + if (!mShowTimes && !mShowScrubber) { + mProgressBar.set(0, 0, w, h); + } else { + int margin = mScrubber.getWidth() / 3; + if (mShowTimes) { + margin += mTimeBounds.width(); + } + int progressY = (h + mScrubberPadding) / 2; + mScrubberTop = progressY - mScrubber.getHeight() / 2 + 1; + mProgressBar.set( + getPaddingLeft() + margin, progressY, + w - getPaddingRight() - margin, progressY + 4); + } + update(); + } + + @Override + protected void onDraw(Canvas canvas) { + // draw progress bars + canvas.drawRect(mProgressBar, mProgressPaint); + canvas.drawRect(mPlayedBar, mPlayedPaint); + + // draw scrubber and timers + if (mShowScrubber) { + canvas.drawBitmap(mScrubber, mScrubberLeft, mScrubberTop, null); + } + if (mShowTimes) { + canvas.drawText( + stringForTime(mCurrentTime), + mTimeBounds.width() / 2 + getPaddingLeft(), + mTimeBounds.height() + mVPaddingInPx / 2 + mScrubberPadding + 1, + mTimeTextPaint); + canvas.drawText( + stringForTime(mTotalTime), + getWidth() - getPaddingRight() - mTimeBounds.width() / 2, + mTimeBounds.height() + mVPaddingInPx / 2 + mScrubberPadding + 1, + mTimeTextPaint); + } + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (mShowScrubber) { + int x = (int) event.getX(); + int y = (int) event.getY(); + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: { + mScrubberCorrection = inScrubber(x, y) + ? x - mScrubberLeft + : mScrubber.getWidth() / 2; + mScrubbing = true; + mListener.onScrubbingStart(); + } + // fall-through + case MotionEvent.ACTION_MOVE: { + mScrubberLeft = x - mScrubberCorrection; + clampScrubber(); + mCurrentTime = getScrubberTime(); + mListener.onScrubbingMove(mCurrentTime); + invalidate(); + return true; + } + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: { + mListener.onScrubbingEnd(getScrubberTime(), 0, 0); + mScrubbing = false; + return true; + } + } + } + return false; + } + + protected String stringForTime(long millis) { + int totalSeconds = (int) millis / 1000; + int seconds = totalSeconds % 60; + int minutes = (totalSeconds / 60) % 60; + int hours = totalSeconds / 3600; + if (hours > 0) { + return String.format("%d:%02d:%02d", hours, minutes, seconds).toString(); + } else { + return String.format("%02d:%02d", minutes, seconds).toString(); + } + } + + public void setSeekable(boolean canSeek) { + mShowScrubber = canSeek; + } + +} diff --git a/src/com/android/gallery3d/app/TransitionStore.java b/src/com/android/gallery3d/app/TransitionStore.java new file mode 100644 index 000000000..aa38ed77e --- /dev/null +++ b/src/com/android/gallery3d/app/TransitionStore.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2012 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.app; + +import java.util.HashMap; + +public class TransitionStore { + private HashMap<Object, Object> mStorage = new HashMap<Object, Object>(); + + public void put(Object key, Object value) { + mStorage.put(key, value); + } + + public <T> void putIfNotPresent(Object key, T valueIfNull) { + mStorage.put(key, get(key, valueIfNull)); + } + + @SuppressWarnings("unchecked") + public <T> T get(Object key) { + return (T) mStorage.get(key); + } + + @SuppressWarnings("unchecked") + public <T> T get(Object key, T valueIfNull) { + T value = (T) mStorage.get(key); + return value == null ? valueIfNull : value; + } + + public void clear() { + mStorage.clear(); + } +} diff --git a/src/com/android/gallery3d/app/TrimControllerOverlay.java b/src/com/android/gallery3d/app/TrimControllerOverlay.java new file mode 100644 index 000000000..cae016626 --- /dev/null +++ b/src/com/android/gallery3d/app/TrimControllerOverlay.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2012 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.app; + +import android.animation.Animator; +import android.animation.Animator.AnimatorListener; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.view.MotionEvent; +import android.view.View; + +import com.android.gallery3d.common.ApiHelper; + +/** + * The controller for the Trimming Video. + */ +public class TrimControllerOverlay extends CommonControllerOverlay { + + public TrimControllerOverlay(Context context) { + super(context); + } + + @Override + protected void createTimeBar(Context context) { + mTimeBar = new TrimTimeBar(context, this); + } + + private void hidePlayButtonIfPlaying() { + if (mState == State.PLAYING) { + mPlayPauseReplayView.setVisibility(View.INVISIBLE); + } + if (ApiHelper.HAS_OBJECT_ANIMATION) { + mPlayPauseReplayView.setAlpha(1f); + } + } + + @Override + public void showPlaying() { + super.showPlaying(); + if (ApiHelper.HAS_OBJECT_ANIMATION) { + // Add animation to hide the play button while playing. + ObjectAnimator anim = ObjectAnimator.ofFloat(mPlayPauseReplayView, "alpha", 1f, 0f); + anim.setDuration(200); + anim.start(); + anim.addListener(new AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + } + + @Override + public void onAnimationEnd(Animator animation) { + hidePlayButtonIfPlaying(); + } + + @Override + public void onAnimationCancel(Animator animation) { + hidePlayButtonIfPlaying(); + } + + @Override + public void onAnimationRepeat(Animator animation) { + } + }); + } else { + hidePlayButtonIfPlaying(); + } + } + + @Override + public void setTimes(int currentTime, int totalTime, int trimStartTime, int trimEndTime) { + mTimeBar.setTime(currentTime, totalTime, trimStartTime, trimEndTime); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (super.onTouchEvent(event)) { + return true; + } + + // The special thing here is that the State.ENDED include both cases of + // the video completed and current == trimEnd. Both request a replay. + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + if (mState == State.PLAYING || mState == State.PAUSED) { + mListener.onPlayPause(); + } else if (mState == State.ENDED) { + if (mCanReplay) { + mListener.onReplay(); + } + } + break; + case MotionEvent.ACTION_UP: + break; + } + return true; + } +} diff --git a/src/com/android/gallery3d/app/TrimTimeBar.java b/src/com/android/gallery3d/app/TrimTimeBar.java new file mode 100644 index 000000000..f8dbc749e --- /dev/null +++ b/src/com/android/gallery3d/app/TrimTimeBar.java @@ -0,0 +1,339 @@ +/* + * Copyright (C) 2012 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.app; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.view.MotionEvent; + +import com.android.gallery3d.R; + +/** + * The trim time bar view, which includes the current and total time, the progress + * bar, and the scrubbers for current time, start and end time for trimming. + */ +public class TrimTimeBar extends TimeBar { + + public static final int SCRUBBER_NONE = 0; + public static final int SCRUBBER_START = 1; + public static final int SCRUBBER_CURRENT = 2; + public static final int SCRUBBER_END = 3; + + private int mPressedThumb = SCRUBBER_NONE; + + // On touch event, the setting order is Scrubber Position -> Time -> + // PlayedBar. At the setTimes(), activity can update the Time directly, then + // PlayedBar will be updated too. + private int mTrimStartScrubberLeft; + private int mTrimEndScrubberLeft; + + private int mTrimStartScrubberTop; + private int mTrimEndScrubberTop; + + private int mTrimStartTime; + private int mTrimEndTime; + + private final Bitmap mTrimStartScrubber; + private final Bitmap mTrimEndScrubber; + public TrimTimeBar(Context context, Listener listener) { + super(context, listener); + + mTrimStartTime = 0; + mTrimEndTime = 0; + mTrimStartScrubberLeft = 0; + mTrimEndScrubberLeft = 0; + mTrimStartScrubberTop = 0; + mTrimEndScrubberTop = 0; + + mTrimStartScrubber = BitmapFactory.decodeResource(getResources(), + R.drawable.text_select_handle_left); + mTrimEndScrubber = BitmapFactory.decodeResource(getResources(), + R.drawable.text_select_handle_right); + // Increase the size of this trimTimeBar, but minimize the scrubber + // touch padding since we have 3 scrubbers now. + mScrubberPadding = 0; + mVPaddingInPx = mVPaddingInPx * 3 / 2; + } + + private int getBarPosFromTime(int time) { + return mProgressBar.left + + (int) ((mProgressBar.width() * (long) time) / mTotalTime); + } + + private int trimStartScrubberTipOffset() { + return mTrimStartScrubber.getWidth() * 3 / 4; + } + + private int trimEndScrubberTipOffset() { + return mTrimEndScrubber.getWidth() / 4; + } + + // Based on all the time info (current, total, trimStart, trimEnd), we + // decide the playedBar size. + private void updatePlayedBarAndScrubberFromTime() { + // According to the Time, update the Played Bar + mPlayedBar.set(mProgressBar); + if (mTotalTime > 0) { + // set playedBar according to the trim time. + mPlayedBar.left = getBarPosFromTime(mTrimStartTime); + mPlayedBar.right = getBarPosFromTime(mCurrentTime); + if (!mScrubbing) { + mScrubberLeft = mPlayedBar.right - mScrubber.getWidth() / 2; + mTrimStartScrubberLeft = mPlayedBar.left - trimStartScrubberTipOffset(); + mTrimEndScrubberLeft = getBarPosFromTime(mTrimEndTime) + - trimEndScrubberTipOffset(); + } + } else { + // If the video is not prepared, just show the scrubber at the end + // of progressBar + mPlayedBar.right = mProgressBar.left; + mScrubberLeft = mProgressBar.left - mScrubber.getWidth() / 2; + mTrimStartScrubberLeft = mProgressBar.left - trimStartScrubberTipOffset(); + mTrimEndScrubberLeft = mProgressBar.right - trimEndScrubberTipOffset(); + } + } + + private void initTrimTimeIfNeeded() { + if (mTotalTime > 0 && mTrimEndTime == 0) { + mTrimEndTime = mTotalTime; + } + } + + private void update() { + initTrimTimeIfNeeded(); + updatePlayedBarAndScrubberFromTime(); + invalidate(); + } + + @Override + public void setTime(int currentTime, int totalTime, + int trimStartTime, int trimEndTime) { + if (mCurrentTime == currentTime && mTotalTime == totalTime + && mTrimStartTime == trimStartTime && mTrimEndTime == trimEndTime) { + return; + } + mCurrentTime = currentTime; + mTotalTime = totalTime; + mTrimStartTime = trimStartTime; + mTrimEndTime = trimEndTime; + update(); + } + + private int whichScrubber(float x, float y) { + if (inScrubber(x, y, mTrimStartScrubberLeft, mTrimStartScrubberTop, mTrimStartScrubber)) { + return SCRUBBER_START; + } else if (inScrubber(x, y, mTrimEndScrubberLeft, mTrimEndScrubberTop, mTrimEndScrubber)) { + return SCRUBBER_END; + } else if (inScrubber(x, y, mScrubberLeft, mScrubberTop, mScrubber)) { + return SCRUBBER_CURRENT; + } + return SCRUBBER_NONE; + } + + private boolean inScrubber(float x, float y, int startX, int startY, Bitmap scrubber) { + int scrubberRight = startX + scrubber.getWidth(); + int scrubberBottom = startY + scrubber.getHeight(); + return startX < x && x < scrubberRight && startY < y && y < scrubberBottom; + } + + private int clampScrubber(int scrubberLeft, int offset, int lowerBound, int upperBound) { + int max = upperBound - offset; + int min = lowerBound - offset; + return Math.min(max, Math.max(min, scrubberLeft)); + } + + private int getScrubberTime(int scrubberLeft, int offset) { + return (int) ((long) (scrubberLeft + offset - mProgressBar.left) + * mTotalTime / mProgressBar.width()); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + int w = r - l; + int h = b - t; + if (!mShowTimes && !mShowScrubber) { + mProgressBar.set(0, 0, w, h); + } else { + int margin = mScrubber.getWidth() / 3; + if (mShowTimes) { + margin += mTimeBounds.width(); + } + int progressY = h / 4; + int scrubberY = progressY - mScrubber.getHeight() / 2 + 1; + mScrubberTop = scrubberY; + mTrimStartScrubberTop = progressY; + mTrimEndScrubberTop = progressY; + mProgressBar.set( + getPaddingLeft() + margin, progressY, + w - getPaddingRight() - margin, progressY + 4); + } + update(); + } + + @Override + protected void onDraw(Canvas canvas) { + // draw progress bars + canvas.drawRect(mProgressBar, mProgressPaint); + canvas.drawRect(mPlayedBar, mPlayedPaint); + + if (mShowTimes) { + canvas.drawText( + stringForTime(mCurrentTime), + mTimeBounds.width() / 2 + getPaddingLeft(), + mTimeBounds.height() / 2 + mTrimStartScrubberTop, + mTimeTextPaint); + canvas.drawText( + stringForTime(mTotalTime), + getWidth() - getPaddingRight() - mTimeBounds.width() / 2, + mTimeBounds.height() / 2 + mTrimStartScrubberTop, + mTimeTextPaint); + } + + // draw extra scrubbers + if (mShowScrubber) { + canvas.drawBitmap(mScrubber, mScrubberLeft, mScrubberTop, null); + canvas.drawBitmap(mTrimStartScrubber, mTrimStartScrubberLeft, + mTrimStartScrubberTop, null); + canvas.drawBitmap(mTrimEndScrubber, mTrimEndScrubberLeft, + mTrimEndScrubberTop, null); + } + } + + private void updateTimeFromPos() { + mCurrentTime = getScrubberTime(mScrubberLeft, mScrubber.getWidth() / 2); + mTrimStartTime = getScrubberTime(mTrimStartScrubberLeft, trimStartScrubberTipOffset()); + mTrimEndTime = getScrubberTime(mTrimEndScrubberLeft, trimEndScrubberTipOffset()); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (mShowScrubber) { + int x = (int) event.getX(); + int y = (int) event.getY(); + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + mPressedThumb = whichScrubber(x, y); + switch (mPressedThumb) { + case SCRUBBER_NONE: + break; + case SCRUBBER_CURRENT: + mScrubbing = true; + mScrubberCorrection = x - mScrubberLeft; + break; + case SCRUBBER_START: + mScrubbing = true; + mScrubberCorrection = x - mTrimStartScrubberLeft; + break; + case SCRUBBER_END: + mScrubbing = true; + mScrubberCorrection = x - mTrimEndScrubberLeft; + break; + } + if (mScrubbing == true) { + mListener.onScrubbingStart(); + return true; + } + break; + case MotionEvent.ACTION_MOVE: + if (mScrubbing) { + int seekToTime = -1; + int lowerBound = mTrimStartScrubberLeft + trimStartScrubberTipOffset(); + int upperBound = mTrimEndScrubberLeft + trimEndScrubberTipOffset(); + switch (mPressedThumb) { + case SCRUBBER_CURRENT: + mScrubberLeft = x - mScrubberCorrection; + mScrubberLeft = + clampScrubber(mScrubberLeft, + mScrubber.getWidth() / 2, + lowerBound, upperBound); + seekToTime = getScrubberTime(mScrubberLeft, + mScrubber.getWidth() / 2); + break; + case SCRUBBER_START: + mTrimStartScrubberLeft = x - mScrubberCorrection; + // Limit start <= end + if (mTrimStartScrubberLeft > mTrimEndScrubberLeft) { + mTrimStartScrubberLeft = mTrimEndScrubberLeft; + } + lowerBound = mProgressBar.left; + mTrimStartScrubberLeft = + clampScrubber(mTrimStartScrubberLeft, + trimStartScrubberTipOffset(), + lowerBound, upperBound); + seekToTime = getScrubberTime(mTrimStartScrubberLeft, + trimStartScrubberTipOffset()); + break; + case SCRUBBER_END: + mTrimEndScrubberLeft = x - mScrubberCorrection; + upperBound = mProgressBar.right; + mTrimEndScrubberLeft = + clampScrubber(mTrimEndScrubberLeft, + trimEndScrubberTipOffset(), + lowerBound, upperBound); + seekToTime = getScrubberTime(mTrimEndScrubberLeft, + trimEndScrubberTipOffset()); + break; + } + updateTimeFromPos(); + updatePlayedBarAndScrubberFromTime(); + if (seekToTime != -1) { + mListener.onScrubbingMove(seekToTime); + } + invalidate(); + return true; + } + break; + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + if (mScrubbing) { + int seekToTime = 0; + switch (mPressedThumb) { + case SCRUBBER_CURRENT: + seekToTime = getScrubberTime(mScrubberLeft, + mScrubber.getWidth() / 2); + break; + case SCRUBBER_START: + seekToTime = getScrubberTime(mTrimStartScrubberLeft, + trimStartScrubberTipOffset()); + mScrubberLeft = mTrimStartScrubberLeft + + trimStartScrubberTipOffset() - mScrubber.getWidth() / 2; + break; + case SCRUBBER_END: + seekToTime = getScrubberTime(mTrimEndScrubberLeft, + trimEndScrubberTipOffset()); + mScrubberLeft = mTrimEndScrubberLeft + + trimEndScrubberTipOffset() - mScrubber.getWidth() / 2; + break; + } + updateTimeFromPos(); + mListener.onScrubbingEnd(seekToTime, + getScrubberTime(mTrimStartScrubberLeft, + trimStartScrubberTipOffset()), + getScrubberTime(mTrimEndScrubberLeft, trimEndScrubberTipOffset())); + mScrubbing = false; + mPressedThumb = SCRUBBER_NONE; + return true; + } + break; + } + } + return false; + } +} diff --git a/src/com/android/gallery3d/app/TrimVideo.java b/src/com/android/gallery3d/app/TrimVideo.java new file mode 100644 index 000000000..1e7728162 --- /dev/null +++ b/src/com/android/gallery3d/app/TrimVideo.java @@ -0,0 +1,337 @@ +/* + * Copyright (C) 2012 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.app; + +import android.app.ActionBar; +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.Intent; +import android.media.MediaPlayer; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.provider.MediaStore; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.widget.TextView; +import android.widget.Toast; +import android.widget.VideoView; + +import com.android.gallery3d.R; +import com.android.gallery3d.util.SaveVideoFileInfo; +import com.android.gallery3d.util.SaveVideoFileUtils; + +import java.io.File; +import java.io.IOException; + +public class TrimVideo extends Activity implements + MediaPlayer.OnErrorListener, + MediaPlayer.OnCompletionListener, + ControllerOverlay.Listener { + + private VideoView mVideoView; + private TextView mSaveVideoTextView; + private TrimControllerOverlay mController; + private Context mContext; + private Uri mUri; + private final Handler mHandler = new Handler(); + public static final String TRIM_ACTION = "com.android.camera.action.TRIM"; + + public ProgressDialog mProgress; + + private int mTrimStartTime = 0; + private int mTrimEndTime = 0; + private int mVideoPosition = 0; + public static final String KEY_TRIM_START = "trim_start"; + public static final String KEY_TRIM_END = "trim_end"; + public static final String KEY_VIDEO_POSITION = "video_pos"; + private boolean mHasPaused = false; + + private String mSrcVideoPath = null; + private static final String TIME_STAMP_NAME = "'TRIM'_yyyyMMdd_HHmmss"; + private SaveVideoFileInfo mDstFileInfo = null; + + @Override + public void onCreate(Bundle savedInstanceState) { + mContext = getApplicationContext(); + super.onCreate(savedInstanceState); + + requestWindowFeature(Window.FEATURE_ACTION_BAR); + requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY); + + ActionBar actionBar = getActionBar(); + int displayOptions = ActionBar.DISPLAY_SHOW_HOME; + actionBar.setDisplayOptions(0, displayOptions); + displayOptions = ActionBar.DISPLAY_SHOW_CUSTOM; + actionBar.setDisplayOptions(displayOptions, displayOptions); + actionBar.setCustomView(R.layout.trim_menu); + + mSaveVideoTextView = (TextView) findViewById(R.id.start_trim); + mSaveVideoTextView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View arg0) { + trimVideo(); + } + }); + mSaveVideoTextView.setEnabled(false); + + Intent intent = getIntent(); + mUri = intent.getData(); + mSrcVideoPath = intent.getStringExtra(PhotoPage.KEY_MEDIA_ITEM_PATH); + setContentView(R.layout.trim_view); + View rootView = findViewById(R.id.trim_view_root); + + mVideoView = (VideoView) rootView.findViewById(R.id.surface_view); + + mController = new TrimControllerOverlay(mContext); + ((ViewGroup) rootView).addView(mController.getView()); + mController.setListener(this); + mController.setCanReplay(true); + + mVideoView.setOnErrorListener(this); + mVideoView.setOnCompletionListener(this); + mVideoView.setVideoURI(mUri); + + playVideo(); + } + + @Override + public void onResume() { + super.onResume(); + if (mHasPaused) { + mVideoView.seekTo(mVideoPosition); + mVideoView.resume(); + mHasPaused = false; + } + mHandler.post(mProgressChecker); + } + + @Override + public void onPause() { + mHasPaused = true; + mHandler.removeCallbacksAndMessages(null); + mVideoPosition = mVideoView.getCurrentPosition(); + mVideoView.suspend(); + super.onPause(); + } + + @Override + public void onStop() { + if (mProgress != null) { + mProgress.dismiss(); + mProgress = null; + } + super.onStop(); + } + + @Override + public void onDestroy() { + mVideoView.stopPlayback(); + super.onDestroy(); + } + + private final Runnable mProgressChecker = new Runnable() { + @Override + public void run() { + int pos = setProgress(); + mHandler.postDelayed(mProgressChecker, 200 - (pos % 200)); + } + }; + + @Override + public void onSaveInstanceState(Bundle savedInstanceState) { + savedInstanceState.putInt(KEY_TRIM_START, mTrimStartTime); + savedInstanceState.putInt(KEY_TRIM_END, mTrimEndTime); + savedInstanceState.putInt(KEY_VIDEO_POSITION, mVideoPosition); + super.onSaveInstanceState(savedInstanceState); + } + + @Override + public void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + mTrimStartTime = savedInstanceState.getInt(KEY_TRIM_START, 0); + mTrimEndTime = savedInstanceState.getInt(KEY_TRIM_END, 0); + mVideoPosition = savedInstanceState.getInt(KEY_VIDEO_POSITION, 0); + } + + // This updates the time bar display (if necessary). It is called by + // mProgressChecker and also from places where the time bar needs + // to be updated immediately. + private int setProgress() { + mVideoPosition = mVideoView.getCurrentPosition(); + // If the video position is smaller than the starting point of trimming, + // correct it. + if (mVideoPosition < mTrimStartTime) { + mVideoView.seekTo(mTrimStartTime); + mVideoPosition = mTrimStartTime; + } + // If the position is bigger than the end point of trimming, show the + // replay button and pause. + if (mVideoPosition >= mTrimEndTime && mTrimEndTime > 0) { + if (mVideoPosition > mTrimEndTime) { + mVideoView.seekTo(mTrimEndTime); + mVideoPosition = mTrimEndTime; + } + mController.showEnded(); + mVideoView.pause(); + } + + int duration = mVideoView.getDuration(); + if (duration > 0 && mTrimEndTime == 0) { + mTrimEndTime = duration; + } + mController.setTimes(mVideoPosition, duration, mTrimStartTime, mTrimEndTime); + return mVideoPosition; + } + + private void playVideo() { + mVideoView.start(); + mController.showPlaying(); + setProgress(); + } + + private void pauseVideo() { + mVideoView.pause(); + mController.showPaused(); + } + + + private boolean isModified() { + int delta = mTrimEndTime - mTrimStartTime; + + // Considering that we only trim at sync frame, we don't want to trim + // when the time interval is too short or too close to the origin. + if (delta < 100 || Math.abs(mVideoView.getDuration() - delta) < 100) { + return false; + } else { + return true; + } + } + + private void trimVideo() { + + mDstFileInfo = SaveVideoFileUtils.getDstMp4FileInfo(TIME_STAMP_NAME, + getContentResolver(), mUri, getString(R.string.folder_download)); + final File mSrcFile = new File(mSrcVideoPath); + + showProgressDialog(); + + new Thread(new Runnable() { + @Override + public void run() { + try { + VideoUtils.startTrim(mSrcFile, mDstFileInfo.mFile, + mTrimStartTime, mTrimEndTime); + // Update the database for adding a new video file. + SaveVideoFileUtils.insertContent(mDstFileInfo, + getContentResolver(), mUri); + } catch (IOException e) { + e.printStackTrace(); + } + // After trimming is done, trigger the UI changed. + mHandler.post(new Runnable() { + @Override + public void run() { + Toast.makeText(getApplicationContext(), + getString(R.string.save_into, mDstFileInfo.mFolderName), + Toast.LENGTH_SHORT) + .show(); + // TODO: change trimming into a service to avoid + // this progressDialog and add notification properly. + if (mProgress != null) { + mProgress.dismiss(); + mProgress = null; + // Show the result only when the activity not stopped. + Intent intent = new Intent(android.content.Intent.ACTION_VIEW); + intent.setDataAndType(Uri.fromFile(mDstFileInfo.mFile), "video/*"); + intent.putExtra(MediaStore.EXTRA_FINISH_ON_COMPLETION, false); + startActivity(intent); + finish(); + } + } + }); + } + }).start(); + } + + private void showProgressDialog() { + // create a background thread to trim the video. + // and show the progress. + mProgress = new ProgressDialog(this); + mProgress.setTitle(getString(R.string.trimming)); + mProgress.setMessage(getString(R.string.please_wait)); + // TODO: make this cancelable. + mProgress.setCancelable(false); + mProgress.setCanceledOnTouchOutside(false); + mProgress.show(); + } + + @Override + public void onPlayPause() { + if (mVideoView.isPlaying()) { + pauseVideo(); + } else { + playVideo(); + } + } + + @Override + public void onSeekStart() { + pauseVideo(); + } + + @Override + public void onSeekMove(int time) { + mVideoView.seekTo(time); + } + + @Override + public void onSeekEnd(int time, int start, int end) { + mVideoView.seekTo(time); + mTrimStartTime = start; + mTrimEndTime = end; + setProgress(); + // Enable save if there's modifications + mSaveVideoTextView.setEnabled(isModified()); + } + + @Override + public void onShown() { + } + + @Override + public void onHidden() { + } + + @Override + public void onReplay() { + mVideoView.seekTo(mTrimStartTime); + playVideo(); + } + + @Override + public void onCompletion(MediaPlayer mp) { + mController.showEnded(); + } + + @Override + public boolean onError(MediaPlayer mp, int what, int extra) { + return false; + } +} diff --git a/src/com/android/gallery3d/app/VideoUtils.java b/src/com/android/gallery3d/app/VideoUtils.java new file mode 100644 index 000000000..a3c3ef273 --- /dev/null +++ b/src/com/android/gallery3d/app/VideoUtils.java @@ -0,0 +1,328 @@ +/* + * Copyright (C) 2012 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. + */ + +// Modified example based on mp4parser google code open source project. +// http://code.google.com/p/mp4parser/source/browse/trunk/examples/src/main/java/com/googlecode/mp4parser/ShortenExample.java + +package com.android.gallery3d.app; + +import android.media.MediaCodec.BufferInfo; +import android.media.MediaExtractor; +import android.media.MediaFormat; +import android.media.MediaMetadataRetriever; +import android.media.MediaMuxer; +import android.util.Log; + +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.util.SaveVideoFileInfo; +import com.coremedia.iso.IsoFile; +import com.coremedia.iso.boxes.TimeToSampleBox; +import com.googlecode.mp4parser.authoring.Movie; +import com.googlecode.mp4parser.authoring.Track; +import com.googlecode.mp4parser.authoring.builder.DefaultMp4Builder; +import com.googlecode.mp4parser.authoring.container.mp4.MovieCreator; +import com.googlecode.mp4parser.authoring.tracks.CroppedTrack; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; + +public class VideoUtils { + private static final String LOGTAG = "VideoUtils"; + private static final int DEFAULT_BUFFER_SIZE = 1 * 1024 * 1024; + + /** + * Remove the sound track. + */ + public static void startMute(String filePath, SaveVideoFileInfo dstFileInfo) + throws IOException { + if (ApiHelper.HAS_MEDIA_MUXER) { + genVideoUsingMuxer(filePath, dstFileInfo.mFile.getPath(), -1, -1, + false, true); + } else { + startMuteUsingMp4Parser(filePath, dstFileInfo); + } + } + + /** + * Shortens/Crops tracks + */ + public static void startTrim(File src, File dst, int startMs, int endMs) + throws IOException { + if (ApiHelper.HAS_MEDIA_MUXER) { + genVideoUsingMuxer(src.getPath(), dst.getPath(), startMs, endMs, + true, true); + } else { + trimUsingMp4Parser(src, dst, startMs, endMs); + } + } + + private static void startMuteUsingMp4Parser(String filePath, + SaveVideoFileInfo dstFileInfo) throws FileNotFoundException, IOException { + File dst = dstFileInfo.mFile; + File src = new File(filePath); + RandomAccessFile randomAccessFile = new RandomAccessFile(src, "r"); + Movie movie = MovieCreator.build(randomAccessFile.getChannel()); + + // remove all tracks we will create new tracks from the old + List<Track> tracks = movie.getTracks(); + movie.setTracks(new LinkedList<Track>()); + + for (Track track : tracks) { + if (track.getHandler().equals("vide")) { + movie.addTrack(track); + } + } + writeMovieIntoFile(dst, movie); + randomAccessFile.close(); + } + + private static void writeMovieIntoFile(File dst, Movie movie) + throws IOException { + if (!dst.exists()) { + dst.createNewFile(); + } + + IsoFile out = new DefaultMp4Builder().build(movie); + FileOutputStream fos = new FileOutputStream(dst); + FileChannel fc = fos.getChannel(); + out.getBox(fc); // This one build up the memory. + + fc.close(); + fos.close(); + } + + /** + * @param srcPath the path of source video file. + * @param dstPath the path of destination video file. + * @param startMs starting time in milliseconds for trimming. Set to + * negative if starting from beginning. + * @param endMs end time for trimming in milliseconds. Set to negative if + * no trimming at the end. + * @param useAudio true if keep the audio track from the source. + * @param useVideo true if keep the video track from the source. + * @throws IOException + */ + private static void genVideoUsingMuxer(String srcPath, String dstPath, + int startMs, int endMs, boolean useAudio, boolean useVideo) + throws IOException { + // Set up MediaExtractor to read from the source. + MediaExtractor extractor = new MediaExtractor(); + extractor.setDataSource(srcPath); + + int trackCount = extractor.getTrackCount(); + + // Set up MediaMuxer for the destination. + MediaMuxer muxer; + muxer = new MediaMuxer(dstPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); + + // Set up the tracks and retrieve the max buffer size for selected + // tracks. + HashMap<Integer, Integer> indexMap = new HashMap<Integer, + Integer>(trackCount); + int bufferSize = -1; + for (int i = 0; i < trackCount; i++) { + MediaFormat format = extractor.getTrackFormat(i); + String mime = format.getString(MediaFormat.KEY_MIME); + + boolean selectCurrentTrack = false; + + if (mime.startsWith("audio/") && useAudio) { + selectCurrentTrack = true; + } else if (mime.startsWith("video/") && useVideo) { + selectCurrentTrack = true; + } + + if (selectCurrentTrack) { + extractor.selectTrack(i); + int dstIndex = muxer.addTrack(format); + indexMap.put(i, dstIndex); + if (format.containsKey(MediaFormat.KEY_MAX_INPUT_SIZE)) { + int newSize = format.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE); + bufferSize = newSize > bufferSize ? newSize : bufferSize; + } + } + } + + if (bufferSize < 0) { + bufferSize = DEFAULT_BUFFER_SIZE; + } + + // Set up the orientation and starting time for extractor. + MediaMetadataRetriever retrieverSrc = new MediaMetadataRetriever(); + retrieverSrc.setDataSource(srcPath); + String degreesString = retrieverSrc.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION); + if (degreesString != null) { + int degrees = Integer.parseInt(degreesString); + if (degrees >= 0) { + muxer.setOrientationHint(degrees); + } + } + + if (startMs > 0) { + extractor.seekTo(startMs * 1000, MediaExtractor.SEEK_TO_CLOSEST_SYNC); + } + + // Copy the samples from MediaExtractor to MediaMuxer. We will loop + // for copying each sample and stop when we get to the end of the source + // file or exceed the end time of the trimming. + int offset = 0; + int trackIndex = -1; + ByteBuffer dstBuf = ByteBuffer.allocate(bufferSize); + BufferInfo bufferInfo = new BufferInfo(); + + muxer.start(); + while (true) { + bufferInfo.offset = offset; + bufferInfo.size = extractor.readSampleData(dstBuf, offset); + if (bufferInfo.size < 0) { + Log.d(LOGTAG, "Saw input EOS."); + bufferInfo.size = 0; + break; + } else { + bufferInfo.presentationTimeUs = extractor.getSampleTime(); + if (endMs > 0 && bufferInfo.presentationTimeUs > (endMs * 1000)) { + Log.d(LOGTAG, "The current sample is over the trim end time."); + break; + } else { + bufferInfo.flags = extractor.getSampleFlags(); + trackIndex = extractor.getSampleTrackIndex(); + + muxer.writeSampleData(indexMap.get(trackIndex), dstBuf, + bufferInfo); + extractor.advance(); + } + } + } + + muxer.stop(); + muxer.release(); + return; + } + + private static void trimUsingMp4Parser(File src, File dst, int startMs, int endMs) + throws FileNotFoundException, IOException { + RandomAccessFile randomAccessFile = new RandomAccessFile(src, "r"); + Movie movie = MovieCreator.build(randomAccessFile.getChannel()); + + // remove all tracks we will create new tracks from the old + List<Track> tracks = movie.getTracks(); + movie.setTracks(new LinkedList<Track>()); + + double startTime = startMs / 1000; + double endTime = endMs / 1000; + + boolean timeCorrected = false; + + // Here we try to find a track that has sync samples. Since we can only + // start decoding at such a sample we SHOULD make sure that the start of + // the new fragment is exactly such a frame. + for (Track track : tracks) { + if (track.getSyncSamples() != null && track.getSyncSamples().length > 0) { + if (timeCorrected) { + // This exception here could be a false positive in case we + // have multiple tracks with sync samples at exactly the + // same positions. E.g. a single movie containing multiple + // qualities of the same video (Microsoft Smooth Streaming + // file) + throw new RuntimeException( + "The startTime has already been corrected by" + + " another track with SyncSample. Not Supported."); + } + startTime = correctTimeToSyncSample(track, startTime, false); + endTime = correctTimeToSyncSample(track, endTime, true); + timeCorrected = true; + } + } + + for (Track track : tracks) { + long currentSample = 0; + double currentTime = 0; + long startSample = -1; + long endSample = -1; + + for (int i = 0; i < track.getDecodingTimeEntries().size(); i++) { + TimeToSampleBox.Entry entry = track.getDecodingTimeEntries().get(i); + for (int j = 0; j < entry.getCount(); j++) { + // entry.getDelta() is the amount of time the current sample + // covers. + + if (currentTime <= startTime) { + // current sample is still before the new starttime + startSample = currentSample; + } + if (currentTime <= endTime) { + // current sample is after the new start time and still + // before the new endtime + endSample = currentSample; + } else { + // current sample is after the end of the cropped video + break; + } + currentTime += (double) entry.getDelta() + / (double) track.getTrackMetaData().getTimescale(); + currentSample++; + } + } + movie.addTrack(new CroppedTrack(track, startSample, endSample)); + } + writeMovieIntoFile(dst, movie); + randomAccessFile.close(); + } + + private static double correctTimeToSyncSample(Track track, double cutHere, + boolean next) { + double[] timeOfSyncSamples = new double[track.getSyncSamples().length]; + long currentSample = 0; + double currentTime = 0; + for (int i = 0; i < track.getDecodingTimeEntries().size(); i++) { + TimeToSampleBox.Entry entry = track.getDecodingTimeEntries().get(i); + for (int j = 0; j < entry.getCount(); j++) { + if (Arrays.binarySearch(track.getSyncSamples(), currentSample + 1) >= 0) { + // samples always start with 1 but we start with zero + // therefore +1 + timeOfSyncSamples[Arrays.binarySearch( + track.getSyncSamples(), currentSample + 1)] = currentTime; + } + currentTime += (double) entry.getDelta() + / (double) track.getTrackMetaData().getTimescale(); + currentSample++; + } + } + double previous = 0; + for (double timeOfSyncSample : timeOfSyncSamples) { + if (timeOfSyncSample > cutHere) { + if (next) { + return timeOfSyncSample; + } else { + return previous; + } + } + previous = timeOfSyncSample; + } + return timeOfSyncSamples[timeOfSyncSamples.length - 1]; + } + +} diff --git a/src/com/android/gallery3d/app/Wallpaper.java b/src/com/android/gallery3d/app/Wallpaper.java new file mode 100644 index 000000000..b0a26c236 --- /dev/null +++ b/src/com/android/gallery3d/app/Wallpaper.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2007 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.app; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Intent; +import android.graphics.Point; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.view.Display; + +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.filtershow.crop.CropActivity; +import com.android.gallery3d.filtershow.crop.CropExtras; + +/** + * Wallpaper picker for the gallery application. This just redirects to the + * standard pick action. + */ +public class Wallpaper extends Activity { + @SuppressWarnings("unused") + private static final String TAG = "Wallpaper"; + + private static final String IMAGE_TYPE = "image/*"; + private static final String KEY_STATE = "activity-state"; + private static final String KEY_PICKED_ITEM = "picked-item"; + + private static final int STATE_INIT = 0; + private static final int STATE_PHOTO_PICKED = 1; + + private int mState = STATE_INIT; + private Uri mPickedItem; + + @Override + protected void onCreate(Bundle bundle) { + super.onCreate(bundle); + if (bundle != null) { + mState = bundle.getInt(KEY_STATE); + mPickedItem = (Uri) bundle.getParcelable(KEY_PICKED_ITEM); + } + } + + @Override + protected void onSaveInstanceState(Bundle saveState) { + saveState.putInt(KEY_STATE, mState); + if (mPickedItem != null) { + saveState.putParcelable(KEY_PICKED_ITEM, mPickedItem); + } + } + + @SuppressWarnings("deprecation") + @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2) + private Point getDefaultDisplaySize(Point size) { + Display d = getWindowManager().getDefaultDisplay(); + if (Build.VERSION.SDK_INT >= ApiHelper.VERSION_CODES.HONEYCOMB_MR2) { + d.getSize(size); + } else { + size.set(d.getWidth(), d.getHeight()); + } + return size; + } + + @SuppressWarnings("fallthrough") + @Override + protected void onResume() { + super.onResume(); + Intent intent = getIntent(); + switch (mState) { + case STATE_INIT: { + mPickedItem = intent.getData(); + if (mPickedItem == null) { + Intent request = new Intent(Intent.ACTION_GET_CONTENT) + .setClass(this, DialogPicker.class) + .setType(IMAGE_TYPE); + startActivityForResult(request, STATE_PHOTO_PICKED); + return; + } + mState = STATE_PHOTO_PICKED; + // fall-through + } + case STATE_PHOTO_PICKED: { + int width = getWallpaperDesiredMinimumWidth(); + int height = getWallpaperDesiredMinimumHeight(); + Point size = getDefaultDisplaySize(new Point()); + float spotlightX = (float) size.x / width; + float spotlightY = (float) size.y / height; + Intent request = new Intent(CropActivity.CROP_ACTION) + .setDataAndType(mPickedItem, IMAGE_TYPE) + .addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT) + .putExtra(CropExtras.KEY_OUTPUT_X, width) + .putExtra(CropExtras.KEY_OUTPUT_Y, height) + .putExtra(CropExtras.KEY_ASPECT_X, width) + .putExtra(CropExtras.KEY_ASPECT_Y, height) + .putExtra(CropExtras.KEY_SPOTLIGHT_X, spotlightX) + .putExtra(CropExtras.KEY_SPOTLIGHT_Y, spotlightY) + .putExtra(CropExtras.KEY_SCALE, true) + .putExtra(CropExtras.KEY_SCALE_UP_IF_NEEDED, true) + .putExtra(CropExtras.KEY_SET_AS_WALLPAPER, true); + startActivity(request); + finish(); + } + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode != RESULT_OK) { + setResult(resultCode); + finish(); + return; + } + mState = requestCode; + if (mState == STATE_PHOTO_PICKED) { + mPickedItem = data.getData(); + } + + // onResume() would be called next + } +} diff --git a/src/com/android/gallery3d/data/ActionImage.java b/src/com/android/gallery3d/data/ActionImage.java new file mode 100644 index 000000000..58e30b146 --- /dev/null +++ b/src/com/android/gallery3d/data/ActionImage.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2012 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.data; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.BitmapRegionDecoder; +import android.net.Uri; + +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.common.BitmapUtils; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +public class ActionImage extends MediaItem { + @SuppressWarnings("unused") + private static final String TAG = "ActionImage"; + private GalleryApp mApplication; + private int mResourceId; + + public ActionImage(Path path, GalleryApp application, int resourceId) { + super(path, nextVersionNumber()); + mApplication = Utils.checkNotNull(application); + mResourceId = resourceId; + } + + @Override + public Job<Bitmap> requestImage(int type) { + return new BitmapJob(type); + } + + @Override + public Job<BitmapRegionDecoder> requestLargeImage() { + return null; + } + + private class BitmapJob implements Job<Bitmap> { + private int mType; + + protected BitmapJob(int type) { + mType = type; + } + + @Override + public Bitmap run(JobContext jc) { + int targetSize = MediaItem.getTargetSize(mType); + Bitmap bitmap = BitmapFactory.decodeResource(mApplication.getResources(), + mResourceId); + + if (mType == MediaItem.TYPE_MICROTHUMBNAIL) { + bitmap = BitmapUtils.resizeAndCropCenter(bitmap, targetSize, true); + } else { + bitmap = BitmapUtils.resizeDownBySideLength(bitmap, targetSize, true); + } + return bitmap; + } + } + + @Override + public int getSupportedOperations() { + return SUPPORT_ACTION; + } + + @Override + public int getMediaType() { + return MEDIA_TYPE_UNKNOWN; + } + + @Override + public Uri getContentUri() { + return null; + } + + @Override + public String getMimeType() { + return ""; + } + + @Override + public int getWidth() { + return 0; + } + + @Override + public int getHeight() { + return 0; + } +} diff --git a/src/com/android/gallery3d/data/BucketHelper.java b/src/com/android/gallery3d/data/BucketHelper.java new file mode 100644 index 000000000..3418dafb7 --- /dev/null +++ b/src/com/android/gallery3d/data/BucketHelper.java @@ -0,0 +1,241 @@ +package com.android.gallery3d.data; + +import android.annotation.TargetApi; +import android.content.ContentResolver; +import android.database.Cursor; +import android.net.Uri; +import android.provider.MediaStore.Files; +import android.provider.MediaStore.Files.FileColumns; +import android.provider.MediaStore.Images; +import android.provider.MediaStore.Images.ImageColumns; +import android.provider.MediaStore.Video; +import android.util.Log; + +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; + +class BucketHelper { + + private static final String TAG = "BucketHelper"; + private static final String EXTERNAL_MEDIA = "external"; + + // BUCKET_DISPLAY_NAME is a string like "Camera" which is the directory + // name of where an image or video is in. BUCKET_ID is a hash of the path + // name of that directory (see computeBucketValues() in MediaProvider for + // details). MEDIA_TYPE is video, image, audio, etc. + // + // The "albums" are not explicitly recorded in the database, but each image + // or video has the two columns (BUCKET_ID, MEDIA_TYPE). We define an + // "album" to be the collection of images/videos which have the same value + // for the two columns. + // + // The goal of the query (used in loadSubMediaSetsFromFilesTable()) is to + // find all albums, that is, all unique values for (BUCKET_ID, MEDIA_TYPE). + // In the meantime sort them by the timestamp of the latest image/video in + // each of the album. + // + // The order of columns below is important: it must match to the index in + // MediaStore. + private static final String[] PROJECTION_BUCKET = { + ImageColumns.BUCKET_ID, + FileColumns.MEDIA_TYPE, + ImageColumns.BUCKET_DISPLAY_NAME}; + + // The indices should match the above projections. + private static final int INDEX_BUCKET_ID = 0; + private static final int INDEX_MEDIA_TYPE = 1; + private static final int INDEX_BUCKET_NAME = 2; + + // We want to order the albums by reverse chronological order. We abuse the + // "WHERE" parameter to insert a "GROUP BY" clause into the SQL statement. + // The template for "WHERE" parameter is like: + // SELECT ... FROM ... WHERE (%s) + // and we make it look like: + // SELECT ... FROM ... WHERE (1) GROUP BY 1,(2) + // The "(1)" means true. The "1,(2)" means the first two columns specified + // after SELECT. Note that because there is a ")" in the template, we use + // "(2" to match it. + private static final String BUCKET_GROUP_BY = "1) GROUP BY 1,(2"; + + private static final String BUCKET_ORDER_BY = "MAX(datetaken) DESC"; + + // Before HoneyComb there is no Files table. Thus, we need to query the + // bucket info from the Images and Video tables and then merge them + // together. + // + // A bucket can exist in both tables. In this case, we need to find the + // latest timestamp from the two tables and sort ourselves. So we add the + // MAX(date_taken) to the projection and remove the media_type since we + // already know the media type from the table we query from. + private static final String[] PROJECTION_BUCKET_IN_ONE_TABLE = { + ImageColumns.BUCKET_ID, + "MAX(datetaken)", + ImageColumns.BUCKET_DISPLAY_NAME}; + + // We keep the INDEX_BUCKET_ID and INDEX_BUCKET_NAME the same as + // PROJECTION_BUCKET so we can reuse the values defined before. + private static final int INDEX_DATE_TAKEN = 1; + + // When query from the Images or Video tables, we only need to group by BUCKET_ID. + private static final String BUCKET_GROUP_BY_IN_ONE_TABLE = "1) GROUP BY (1"; + + public static BucketEntry[] loadBucketEntries( + JobContext jc, ContentResolver resolver, int type) { + if (ApiHelper.HAS_MEDIA_PROVIDER_FILES_TABLE) { + return loadBucketEntriesFromFilesTable(jc, resolver, type); + } else { + return loadBucketEntriesFromImagesAndVideoTable(jc, resolver, type); + } + } + + private static void updateBucketEntriesFromTable(JobContext jc, + ContentResolver resolver, Uri tableUri, HashMap<Integer, BucketEntry> buckets) { + Cursor cursor = resolver.query(tableUri, PROJECTION_BUCKET_IN_ONE_TABLE, + BUCKET_GROUP_BY_IN_ONE_TABLE, null, null); + if (cursor == null) { + Log.w(TAG, "cannot open media database: " + tableUri); + return; + } + try { + while (cursor.moveToNext()) { + int bucketId = cursor.getInt(INDEX_BUCKET_ID); + int dateTaken = cursor.getInt(INDEX_DATE_TAKEN); + BucketEntry entry = buckets.get(bucketId); + if (entry == null) { + entry = new BucketEntry(bucketId, cursor.getString(INDEX_BUCKET_NAME)); + buckets.put(bucketId, entry); + entry.dateTaken = dateTaken; + } else { + entry.dateTaken = Math.max(entry.dateTaken, dateTaken); + } + } + } finally { + Utils.closeSilently(cursor); + } + } + + private static BucketEntry[] loadBucketEntriesFromImagesAndVideoTable( + JobContext jc, ContentResolver resolver, int type) { + HashMap<Integer, BucketEntry> buckets = new HashMap<Integer, BucketEntry>(64); + if ((type & MediaObject.MEDIA_TYPE_IMAGE) != 0) { + updateBucketEntriesFromTable( + jc, resolver, Images.Media.EXTERNAL_CONTENT_URI, buckets); + } + if ((type & MediaObject.MEDIA_TYPE_VIDEO) != 0) { + updateBucketEntriesFromTable( + jc, resolver, Video.Media.EXTERNAL_CONTENT_URI, buckets); + } + BucketEntry[] entries = buckets.values().toArray(new BucketEntry[buckets.size()]); + Arrays.sort(entries, new Comparator<BucketEntry>() { + @Override + public int compare(BucketEntry a, BucketEntry b) { + // sorted by dateTaken in descending order + return b.dateTaken - a.dateTaken; + } + }); + return entries; + } + + private static BucketEntry[] loadBucketEntriesFromFilesTable( + JobContext jc, ContentResolver resolver, int type) { + Uri uri = getFilesContentUri(); + + Cursor cursor = resolver.query(uri, + PROJECTION_BUCKET, BUCKET_GROUP_BY, + null, BUCKET_ORDER_BY); + if (cursor == null) { + Log.w(TAG, "cannot open local database: " + uri); + return new BucketEntry[0]; + } + ArrayList<BucketEntry> buffer = new ArrayList<BucketEntry>(); + int typeBits = 0; + if ((type & MediaObject.MEDIA_TYPE_IMAGE) != 0) { + typeBits |= (1 << FileColumns.MEDIA_TYPE_IMAGE); + } + if ((type & MediaObject.MEDIA_TYPE_VIDEO) != 0) { + typeBits |= (1 << FileColumns.MEDIA_TYPE_VIDEO); + } + try { + while (cursor.moveToNext()) { + if ((typeBits & (1 << cursor.getInt(INDEX_MEDIA_TYPE))) != 0) { + BucketEntry entry = new BucketEntry( + cursor.getInt(INDEX_BUCKET_ID), + cursor.getString(INDEX_BUCKET_NAME)); + if (!buffer.contains(entry)) { + buffer.add(entry); + } + } + if (jc.isCancelled()) return null; + } + } finally { + Utils.closeSilently(cursor); + } + return buffer.toArray(new BucketEntry[buffer.size()]); + } + + private static String getBucketNameInTable( + ContentResolver resolver, Uri tableUri, int bucketId) { + String selectionArgs[] = new String[] {String.valueOf(bucketId)}; + Uri uri = tableUri.buildUpon() + .appendQueryParameter("limit", "1") + .build(); + Cursor cursor = resolver.query(uri, PROJECTION_BUCKET_IN_ONE_TABLE, + "bucket_id = ?", selectionArgs, null); + try { + if (cursor != null && cursor.moveToNext()) { + return cursor.getString(INDEX_BUCKET_NAME); + } + } finally { + Utils.closeSilently(cursor); + } + return null; + } + + @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) + private static Uri getFilesContentUri() { + return Files.getContentUri(EXTERNAL_MEDIA); + } + + public static String getBucketName(ContentResolver resolver, int bucketId) { + if (ApiHelper.HAS_MEDIA_PROVIDER_FILES_TABLE) { + String result = getBucketNameInTable(resolver, getFilesContentUri(), bucketId); + return result == null ? "" : result; + } else { + String result = getBucketNameInTable( + resolver, Images.Media.EXTERNAL_CONTENT_URI, bucketId); + if (result != null) return result; + result = getBucketNameInTable( + resolver, Video.Media.EXTERNAL_CONTENT_URI, bucketId); + return result == null ? "" : result; + } + } + + public static class BucketEntry { + public String bucketName; + public int bucketId; + public int dateTaken; + + public BucketEntry(int id, String name) { + bucketId = id; + bucketName = Utils.ensureNotNull(name); + } + + @Override + public int hashCode() { + return bucketId; + } + + @Override + public boolean equals(Object object) { + if (!(object instanceof BucketEntry)) return false; + BucketEntry entry = (BucketEntry) object; + return bucketId == entry.bucketId; + } + } +} diff --git a/src/com/android/gallery3d/data/BytesBufferPool.java b/src/com/android/gallery3d/data/BytesBufferPool.java new file mode 100644 index 000000000..d2da323fc --- /dev/null +++ b/src/com/android/gallery3d/data/BytesBufferPool.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2012 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.data; + +import com.android.gallery3d.util.ThreadPool.JobContext; + +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.ArrayList; + +public class BytesBufferPool { + + private static final int READ_STEP = 4096; + + public static class BytesBuffer { + public byte[] data; + public int offset; + public int length; + + private BytesBuffer(int capacity) { + this.data = new byte[capacity]; + } + + // an helper function to read content from FileDescriptor + public void readFrom(JobContext jc, FileDescriptor fd) throws IOException { + FileInputStream fis = new FileInputStream(fd); + length = 0; + try { + int capacity = data.length; + while (true) { + int step = Math.min(READ_STEP, capacity - length); + int rc = fis.read(data, length, step); + if (rc < 0 || jc.isCancelled()) return; + length += rc; + + if (length == capacity) { + byte[] newData = new byte[data.length * 2]; + System.arraycopy(data, 0, newData, 0, data.length); + data = newData; + capacity = data.length; + } + } + } finally { + fis.close(); + } + } + } + + private final int mPoolSize; + private final int mBufferSize; + private final ArrayList<BytesBuffer> mList; + + public BytesBufferPool(int poolSize, int bufferSize) { + mList = new ArrayList<BytesBuffer>(poolSize); + mPoolSize = poolSize; + mBufferSize = bufferSize; + } + + public synchronized BytesBuffer get() { + int n = mList.size(); + return n > 0 ? mList.remove(n - 1) : new BytesBuffer(mBufferSize); + } + + public synchronized void recycle(BytesBuffer buffer) { + if (buffer.data.length != mBufferSize) return; + if (mList.size() < mPoolSize) { + buffer.offset = 0; + buffer.length = 0; + mList.add(buffer); + } + } + + public synchronized void clear() { + mList.clear(); + } +} diff --git a/src/com/android/gallery3d/data/CameraShortcutImage.java b/src/com/android/gallery3d/data/CameraShortcutImage.java new file mode 100644 index 000000000..865270b4c --- /dev/null +++ b/src/com/android/gallery3d/data/CameraShortcutImage.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2012 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.data; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.GalleryApp; + +public class CameraShortcutImage extends ActionImage { + @SuppressWarnings("unused") + private static final String TAG = "CameraShortcutImage"; + + public CameraShortcutImage(Path path, GalleryApp application) { + super(path, application, R.drawable.placeholder_camera); + } + + @Override + public int getSupportedOperations() { + return super.getSupportedOperations() | SUPPORT_CAMERA_SHORTCUT; + } +} diff --git a/src/com/android/gallery3d/data/ChangeNotifier.java b/src/com/android/gallery3d/data/ChangeNotifier.java new file mode 100644 index 000000000..558a8648e --- /dev/null +++ b/src/com/android/gallery3d/data/ChangeNotifier.java @@ -0,0 +1,57 @@ +/* + * 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.data; + +import android.net.Uri; + +import com.android.gallery3d.app.GalleryApp; + +import java.util.concurrent.atomic.AtomicBoolean; + +// This handles change notification for media sets. +public class ChangeNotifier { + + private MediaSet mMediaSet; + private AtomicBoolean mContentDirty = new AtomicBoolean(true); + + public ChangeNotifier(MediaSet set, Uri uri, GalleryApp application) { + mMediaSet = set; + application.getDataManager().registerChangeNotifier(uri, this); + } + + public ChangeNotifier(MediaSet set, Uri[] uris, GalleryApp application) { + mMediaSet = set; + for (int i = 0; i < uris.length; i++) { + application.getDataManager().registerChangeNotifier(uris[i], this); + } + } + + // Returns the dirty flag and clear it. + public boolean isDirty() { + return mContentDirty.compareAndSet(true, false); + } + + public void fakeChange() { + onChange(false); + } + + protected void onChange(boolean selfChange) { + if (mContentDirty.compareAndSet(false, true)) { + mMediaSet.notifyContentChanged(); + } + } +}
\ No newline at end of file diff --git a/src/com/android/gallery3d/data/ClusterAlbum.java b/src/com/android/gallery3d/data/ClusterAlbum.java new file mode 100644 index 000000000..8681952bf --- /dev/null +++ b/src/com/android/gallery3d/data/ClusterAlbum.java @@ -0,0 +1,143 @@ +/* + * 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.data; + +import java.util.ArrayList; + +public class ClusterAlbum extends MediaSet implements ContentListener { + @SuppressWarnings("unused") + private static final String TAG = "ClusterAlbum"; + private ArrayList<Path> mPaths = new ArrayList<Path>(); + private String mName = ""; + private DataManager mDataManager; + private MediaSet mClusterAlbumSet; + private MediaItem mCover; + + public ClusterAlbum(Path path, DataManager dataManager, + MediaSet clusterAlbumSet) { + super(path, nextVersionNumber()); + mDataManager = dataManager; + mClusterAlbumSet = clusterAlbumSet; + mClusterAlbumSet.addContentListener(this); + } + + public void setCoverMediaItem(MediaItem cover) { + mCover = cover; + } + + @Override + public MediaItem getCoverMediaItem() { + return mCover != null ? mCover : super.getCoverMediaItem(); + } + + void setMediaItems(ArrayList<Path> paths) { + mPaths = paths; + } + + ArrayList<Path> getMediaItems() { + return mPaths; + } + + public void setName(String name) { + mName = name; + } + + @Override + public String getName() { + return mName; + } + + @Override + public int getMediaItemCount() { + return mPaths.size(); + } + + @Override + public ArrayList<MediaItem> getMediaItem(int start, int count) { + return getMediaItemFromPath(mPaths, start, count, mDataManager); + } + + public static ArrayList<MediaItem> getMediaItemFromPath( + ArrayList<Path> paths, int start, int count, + DataManager dataManager) { + if (start >= paths.size()) { + return new ArrayList<MediaItem>(); + } + int end = Math.min(start + count, paths.size()); + ArrayList<Path> subset = new ArrayList<Path>(paths.subList(start, end)); + final MediaItem[] buf = new MediaItem[end - start]; + ItemConsumer consumer = new ItemConsumer() { + @Override + public void consume(int index, MediaItem item) { + buf[index] = item; + } + }; + dataManager.mapMediaItems(subset, consumer, 0); + ArrayList<MediaItem> result = new ArrayList<MediaItem>(end - start); + for (int i = 0; i < buf.length; i++) { + result.add(buf[i]); + } + return result; + } + + @Override + protected int enumerateMediaItems(ItemConsumer consumer, int startIndex) { + mDataManager.mapMediaItems(mPaths, consumer, startIndex); + return mPaths.size(); + } + + @Override + public int getTotalMediaItemCount() { + return mPaths.size(); + } + + @Override + public long reload() { + if (mClusterAlbumSet.reload() > mDataVersion) { + mDataVersion = nextVersionNumber(); + } + return mDataVersion; + } + + @Override + public void onContentDirty() { + notifyContentChanged(); + } + + @Override + public int getSupportedOperations() { + return SUPPORT_SHARE | SUPPORT_DELETE | SUPPORT_INFO; + } + + @Override + public void delete() { + ItemConsumer consumer = new ItemConsumer() { + @Override + public void consume(int index, MediaItem item) { + if ((item.getSupportedOperations() & SUPPORT_DELETE) != 0) { + item.delete(); + } + } + }; + mDataManager.mapMediaItems(mPaths, consumer, 0); + } + + @Override + public boolean isLeafAlbum() { + return true; + } +} diff --git a/src/com/android/gallery3d/data/ClusterAlbumSet.java b/src/com/android/gallery3d/data/ClusterAlbumSet.java new file mode 100644 index 000000000..cb212ba36 --- /dev/null +++ b/src/com/android/gallery3d/data/ClusterAlbumSet.java @@ -0,0 +1,159 @@ +/* + * 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.data; + +import android.content.Context; +import android.net.Uri; + +import com.android.gallery3d.app.GalleryApp; + +import java.util.ArrayList; +import java.util.HashSet; + +public class ClusterAlbumSet extends MediaSet implements ContentListener { + @SuppressWarnings("unused") + private static final String TAG = "ClusterAlbumSet"; + private GalleryApp mApplication; + private MediaSet mBaseSet; + private int mKind; + private ArrayList<ClusterAlbum> mAlbums = new ArrayList<ClusterAlbum>(); + private boolean mFirstReloadDone; + + public ClusterAlbumSet(Path path, GalleryApp application, + MediaSet baseSet, int kind) { + super(path, INVALID_DATA_VERSION); + mApplication = application; + mBaseSet = baseSet; + mKind = kind; + baseSet.addContentListener(this); + } + + @Override + public MediaSet getSubMediaSet(int index) { + return mAlbums.get(index); + } + + @Override + public int getSubMediaSetCount() { + return mAlbums.size(); + } + + @Override + public String getName() { + return mBaseSet.getName(); + } + + @Override + public long reload() { + if (mBaseSet.reload() > mDataVersion) { + if (mFirstReloadDone) { + updateClustersContents(); + } else { + updateClusters(); + mFirstReloadDone = true; + } + mDataVersion = nextVersionNumber(); + } + return mDataVersion; + } + + @Override + public void onContentDirty() { + notifyContentChanged(); + } + + private void updateClusters() { + mAlbums.clear(); + Clustering clustering; + Context context = mApplication.getAndroidContext(); + switch (mKind) { + case ClusterSource.CLUSTER_ALBUMSET_TIME: + clustering = new TimeClustering(context); + break; + case ClusterSource.CLUSTER_ALBUMSET_LOCATION: + clustering = new LocationClustering(context); + break; + case ClusterSource.CLUSTER_ALBUMSET_TAG: + clustering = new TagClustering(context); + break; + case ClusterSource.CLUSTER_ALBUMSET_FACE: + clustering = new FaceClustering(context); + break; + default: /* CLUSTER_ALBUMSET_SIZE */ + clustering = new SizeClustering(context); + break; + } + + clustering.run(mBaseSet); + int n = clustering.getNumberOfClusters(); + DataManager dataManager = mApplication.getDataManager(); + for (int i = 0; i < n; i++) { + Path childPath; + String childName = clustering.getClusterName(i); + if (mKind == ClusterSource.CLUSTER_ALBUMSET_TAG) { + childPath = mPath.getChild(Uri.encode(childName)); + } else if (mKind == ClusterSource.CLUSTER_ALBUMSET_SIZE) { + long minSize = ((SizeClustering) clustering).getMinSize(i); + childPath = mPath.getChild(minSize); + } else { + childPath = mPath.getChild(i); + } + + ClusterAlbum album; + synchronized (DataManager.LOCK) { + album = (ClusterAlbum) dataManager.peekMediaObject(childPath); + if (album == null) { + album = new ClusterAlbum(childPath, dataManager, this); + } + } + album.setMediaItems(clustering.getCluster(i)); + album.setName(childName); + album.setCoverMediaItem(clustering.getClusterCover(i)); + mAlbums.add(album); + } + } + + private void updateClustersContents() { + final HashSet<Path> existing = new HashSet<Path>(); + mBaseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() { + @Override + public void consume(int index, MediaItem item) { + existing.add(item.getPath()); + } + }); + + int n = mAlbums.size(); + + // The loop goes backwards because we may remove empty albums from + // mAlbums. + for (int i = n - 1; i >= 0; i--) { + ArrayList<Path> oldPaths = mAlbums.get(i).getMediaItems(); + ArrayList<Path> newPaths = new ArrayList<Path>(); + int m = oldPaths.size(); + for (int j = 0; j < m; j++) { + Path p = oldPaths.get(j); + if (existing.contains(p)) { + newPaths.add(p); + } + } + mAlbums.get(i).setMediaItems(newPaths); + if (newPaths.isEmpty()) { + mAlbums.remove(i); + } + } + } +} diff --git a/src/com/android/gallery3d/data/ClusterSource.java b/src/com/android/gallery3d/data/ClusterSource.java new file mode 100644 index 000000000..a1f22e57a --- /dev/null +++ b/src/com/android/gallery3d/data/ClusterSource.java @@ -0,0 +1,86 @@ +/* + * 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.data; + +import com.android.gallery3d.app.GalleryApp; + +class ClusterSource extends MediaSource { + static final int CLUSTER_ALBUMSET_TIME = 0; + static final int CLUSTER_ALBUMSET_LOCATION = 1; + static final int CLUSTER_ALBUMSET_TAG = 2; + static final int CLUSTER_ALBUMSET_SIZE = 3; + static final int CLUSTER_ALBUMSET_FACE = 4; + + static final int CLUSTER_ALBUM_TIME = 0x100; + static final int CLUSTER_ALBUM_LOCATION = 0x101; + static final int CLUSTER_ALBUM_TAG = 0x102; + static final int CLUSTER_ALBUM_SIZE = 0x103; + static final int CLUSTER_ALBUM_FACE = 0x104; + + GalleryApp mApplication; + PathMatcher mMatcher; + + public ClusterSource(GalleryApp application) { + super("cluster"); + mApplication = application; + mMatcher = new PathMatcher(); + mMatcher.add("/cluster/*/time", CLUSTER_ALBUMSET_TIME); + mMatcher.add("/cluster/*/location", CLUSTER_ALBUMSET_LOCATION); + mMatcher.add("/cluster/*/tag", CLUSTER_ALBUMSET_TAG); + mMatcher.add("/cluster/*/size", CLUSTER_ALBUMSET_SIZE); + mMatcher.add("/cluster/*/face", CLUSTER_ALBUMSET_FACE); + + mMatcher.add("/cluster/*/time/*", CLUSTER_ALBUM_TIME); + mMatcher.add("/cluster/*/location/*", CLUSTER_ALBUM_LOCATION); + mMatcher.add("/cluster/*/tag/*", CLUSTER_ALBUM_TAG); + mMatcher.add("/cluster/*/size/*", CLUSTER_ALBUM_SIZE); + mMatcher.add("/cluster/*/face/*", CLUSTER_ALBUM_FACE); + } + + // The names we accept are: + // /cluster/{set}/time /cluster/{set}/time/k + // /cluster/{set}/location /cluster/{set}/location/k + // /cluster/{set}/tag /cluster/{set}/tag/encoded_tag + // /cluster/{set}/size /cluster/{set}/size/min_size + @Override + public MediaObject createMediaObject(Path path) { + int matchType = mMatcher.match(path); + String setsName = mMatcher.getVar(0); + DataManager dataManager = mApplication.getDataManager(); + MediaSet[] sets = dataManager.getMediaSetsFromString(setsName); + switch (matchType) { + case CLUSTER_ALBUMSET_TIME: + case CLUSTER_ALBUMSET_LOCATION: + case CLUSTER_ALBUMSET_TAG: + case CLUSTER_ALBUMSET_SIZE: + case CLUSTER_ALBUMSET_FACE: + return new ClusterAlbumSet(path, mApplication, sets[0], matchType); + case CLUSTER_ALBUM_TIME: + case CLUSTER_ALBUM_LOCATION: + case CLUSTER_ALBUM_TAG: + case CLUSTER_ALBUM_SIZE: + case CLUSTER_ALBUM_FACE: { + MediaSet parent = dataManager.getMediaSet(path.getParent()); + // The actual content in the ClusterAlbum will be filled later + // when the reload() method in the parent is run. + return new ClusterAlbum(path, dataManager, parent); + } + default: + throw new RuntimeException("bad path: " + path); + } + } +} diff --git a/src/com/android/gallery3d/data/Clustering.java b/src/com/android/gallery3d/data/Clustering.java new file mode 100644 index 000000000..4072bf57b --- /dev/null +++ b/src/com/android/gallery3d/data/Clustering.java @@ -0,0 +1,29 @@ +/* + * 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.data; + +import java.util.ArrayList; + +public abstract class Clustering { + public abstract void run(MediaSet baseSet); + public abstract int getNumberOfClusters(); + public abstract ArrayList<Path> getCluster(int index); + public abstract String getClusterName(int index); + public MediaItem getClusterCover(int index) { + return null; + } +} diff --git a/src/com/android/gallery3d/data/ComboAlbum.java b/src/com/android/gallery3d/data/ComboAlbum.java new file mode 100644 index 000000000..cadd9f8af --- /dev/null +++ b/src/com/android/gallery3d/data/ComboAlbum.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2011 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.data; + +import com.android.gallery3d.util.Future; + +import java.util.ArrayList; + +// ComboAlbum combines multiple media sets into one. It lists all media items +// from the input albums. +// This only handles SubMediaSets, not MediaItems. (That's all we need now) +public class ComboAlbum extends MediaSet implements ContentListener { + @SuppressWarnings("unused") + private static final String TAG = "ComboAlbum"; + private final MediaSet[] mSets; + private String mName; + + public ComboAlbum(Path path, MediaSet[] mediaSets, String name) { + super(path, nextVersionNumber()); + mSets = mediaSets; + for (MediaSet set : mSets) { + set.addContentListener(this); + } + mName = name; + } + + @Override + public ArrayList<MediaItem> getMediaItem(int start, int count) { + ArrayList<MediaItem> items = new ArrayList<MediaItem>(); + for (MediaSet set : mSets) { + int size = set.getMediaItemCount(); + if (count < 1) break; + if (start < size) { + int fetchCount = (start + count <= size) ? count : size - start; + ArrayList<MediaItem> fetchItems = set.getMediaItem(start, fetchCount); + items.addAll(fetchItems); + count -= fetchItems.size(); + start = 0; + } else { + start -= size; + } + } + return items; + } + + @Override + public int getMediaItemCount() { + int count = 0; + for (MediaSet set : mSets) { + count += set.getMediaItemCount(); + } + return count; + } + + @Override + public boolean isLeafAlbum() { + return true; + } + + @Override + public String getName() { + return mName; + } + + public void useNameOfChild(int i) { + if (i < mSets.length) mName = mSets[i].getName(); + } + + @Override + public long reload() { + boolean changed = false; + for (int i = 0, n = mSets.length; i < n; ++i) { + long version = mSets[i].reload(); + if (version > mDataVersion) changed = true; + } + if (changed) mDataVersion = nextVersionNumber(); + return mDataVersion; + } + + @Override + public void onContentDirty() { + notifyContentChanged(); + } + + @Override + public Future<Integer> requestSync(SyncListener listener) { + return requestSyncOnMultipleSets(mSets, listener); + } +} diff --git a/src/com/android/gallery3d/data/ComboAlbumSet.java b/src/com/android/gallery3d/data/ComboAlbumSet.java new file mode 100644 index 000000000..3f3674500 --- /dev/null +++ b/src/com/android/gallery3d/data/ComboAlbumSet.java @@ -0,0 +1,96 @@ +/* + * 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.data; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.util.Future; + +// ComboAlbumSet combines multiple media sets into one. It lists all sub +// media sets from the input album sets. +// This only handles SubMediaSets, not MediaItems. (That's all we need now) +public class ComboAlbumSet extends MediaSet implements ContentListener { + @SuppressWarnings("unused") + private static final String TAG = "ComboAlbumSet"; + private final MediaSet[] mSets; + private final String mName; + + public ComboAlbumSet(Path path, GalleryApp application, MediaSet[] mediaSets) { + super(path, nextVersionNumber()); + mSets = mediaSets; + for (MediaSet set : mSets) { + set.addContentListener(this); + } + mName = application.getResources().getString( + R.string.set_label_all_albums); + } + + @Override + public MediaSet getSubMediaSet(int index) { + for (MediaSet set : mSets) { + int size = set.getSubMediaSetCount(); + if (index < size) { + return set.getSubMediaSet(index); + } + index -= size; + } + return null; + } + + @Override + public int getSubMediaSetCount() { + int count = 0; + for (MediaSet set : mSets) { + count += set.getSubMediaSetCount(); + } + return count; + } + + @Override + public String getName() { + return mName; + } + + @Override + public boolean isLoading() { + for (int i = 0, n = mSets.length; i < n; ++i) { + if (mSets[i].isLoading()) return true; + } + return false; + } + + @Override + public long reload() { + boolean changed = false; + for (int i = 0, n = mSets.length; i < n; ++i) { + long version = mSets[i].reload(); + if (version > mDataVersion) changed = true; + } + if (changed) mDataVersion = nextVersionNumber(); + return mDataVersion; + } + + @Override + public void onContentDirty() { + notifyContentChanged(); + } + + @Override + public Future<Integer> requestSync(SyncListener listener) { + return requestSyncOnMultipleSets(mSets, listener); + } +} diff --git a/src/com/android/gallery3d/data/ComboSource.java b/src/com/android/gallery3d/data/ComboSource.java new file mode 100644 index 000000000..867d47e64 --- /dev/null +++ b/src/com/android/gallery3d/data/ComboSource.java @@ -0,0 +1,55 @@ +/* + * 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.data; + +import com.android.gallery3d.app.GalleryApp; + +class ComboSource extends MediaSource { + private static final int COMBO_ALBUMSET = 0; + private static final int COMBO_ALBUM = 1; + private GalleryApp mApplication; + private PathMatcher mMatcher; + + public ComboSource(GalleryApp application) { + super("combo"); + mApplication = application; + mMatcher = new PathMatcher(); + mMatcher.add("/combo/*", COMBO_ALBUMSET); + mMatcher.add("/combo/*/*", COMBO_ALBUM); + } + + // The only path we accept is "/combo/{set1, set2, ...} and /combo/item/{set1, set2, ...}" + @Override + public MediaObject createMediaObject(Path path) { + String[] segments = path.split(); + if (segments.length < 2) { + throw new RuntimeException("bad path: " + path); + } + + DataManager dataManager = mApplication.getDataManager(); + switch (mMatcher.match(path)) { + case COMBO_ALBUMSET: + return new ComboAlbumSet(path, mApplication, + dataManager.getMediaSetsFromString(segments[1])); + + case COMBO_ALBUM: + return new ComboAlbum(path, + dataManager.getMediaSetsFromString(segments[2]), segments[1]); + } + return null; + } +} diff --git a/src/com/android/gallery3d/data/ContentListener.java b/src/com/android/gallery3d/data/ContentListener.java new file mode 100644 index 000000000..5e2952685 --- /dev/null +++ b/src/com/android/gallery3d/data/ContentListener.java @@ -0,0 +1,21 @@ +/* + * 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.data; + +public interface ContentListener { + public void onContentDirty(); +}
\ No newline at end of file diff --git a/src/com/android/gallery3d/data/DataManager.java b/src/com/android/gallery3d/data/DataManager.java new file mode 100644 index 000000000..38865e9f1 --- /dev/null +++ b/src/com/android/gallery3d/data/DataManager.java @@ -0,0 +1,371 @@ +/* + * 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.data; + +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; + +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.app.StitchingChangeListener; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.MediaObject.PanoramaSupportCallback; +import com.android.gallery3d.data.MediaSet.ItemConsumer; +import com.android.gallery3d.data.MediaSource.PathId; +import com.android.gallery3d.picasasource.PicasaSource; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map.Entry; +import java.util.WeakHashMap; + +// DataManager manages all media sets and media items in the system. +// +// Each MediaSet and MediaItem has a unique 64 bits id. The most significant +// 32 bits represents its parent, and the least significant 32 bits represents +// the self id. For MediaSet the self id is is globally unique, but for +// MediaItem it's unique only relative to its parent. +// +// To make sure the id is the same when the MediaSet is re-created, a child key +// is provided to obtainSetId() to make sure the same self id will be used as +// when the parent and key are the same. A sequence of child keys is called a +// path. And it's used to identify a specific media set even if the process is +// killed and re-created, so child keys should be stable identifiers. + +public class DataManager implements StitchingChangeListener { + public static final int INCLUDE_IMAGE = 1; + public static final int INCLUDE_VIDEO = 2; + public static final int INCLUDE_ALL = INCLUDE_IMAGE | INCLUDE_VIDEO; + public static final int INCLUDE_LOCAL_ONLY = 4; + public static final int INCLUDE_LOCAL_IMAGE_ONLY = + INCLUDE_LOCAL_ONLY | INCLUDE_IMAGE; + public static final int INCLUDE_LOCAL_VIDEO_ONLY = + INCLUDE_LOCAL_ONLY | INCLUDE_VIDEO; + public static final int INCLUDE_LOCAL_ALL_ONLY = + INCLUDE_LOCAL_ONLY | INCLUDE_IMAGE | INCLUDE_VIDEO; + + // Any one who would like to access data should require this lock + // to prevent concurrency issue. + public static final Object LOCK = new Object(); + + public static DataManager from(Context context) { + GalleryApp app = (GalleryApp) context.getApplicationContext(); + return app.getDataManager(); + } + + private static final String TAG = "DataManager"; + + // This is the path for the media set seen by the user at top level. + private static final String TOP_SET_PATH = "/combo/{/local/all,/picasa/all}"; + + private static final String TOP_IMAGE_SET_PATH = "/combo/{/local/image,/picasa/image}"; + + private static final String TOP_VIDEO_SET_PATH = + "/combo/{/local/video,/picasa/video}"; + + private static final String TOP_LOCAL_SET_PATH = "/local/all"; + + private static final String TOP_LOCAL_IMAGE_SET_PATH = "/local/image"; + + private static final String TOP_LOCAL_VIDEO_SET_PATH = "/local/video"; + + public static final Comparator<MediaItem> sDateTakenComparator = + new DateTakenComparator(); + + private static class DateTakenComparator implements Comparator<MediaItem> { + @Override + public int compare(MediaItem item1, MediaItem item2) { + return -Utils.compare(item1.getDateInMs(), item2.getDateInMs()); + } + } + + private final Handler mDefaultMainHandler; + + private GalleryApp mApplication; + private int mActiveCount = 0; + + private HashMap<Uri, NotifyBroker> mNotifierMap = + new HashMap<Uri, NotifyBroker>(); + + + private HashMap<String, MediaSource> mSourceMap = + new LinkedHashMap<String, MediaSource>(); + + public DataManager(GalleryApp application) { + mApplication = application; + mDefaultMainHandler = new Handler(application.getMainLooper()); + } + + public synchronized void initializeSourceMap() { + if (!mSourceMap.isEmpty()) return; + + // the order matters, the UriSource must come last + addSource(new LocalSource(mApplication)); + addSource(new PicasaSource(mApplication)); + addSource(new ComboSource(mApplication)); + addSource(new ClusterSource(mApplication)); + addSource(new FilterSource(mApplication)); + addSource(new SecureSource(mApplication)); + addSource(new UriSource(mApplication)); + addSource(new SnailSource(mApplication)); + + if (mActiveCount > 0) { + for (MediaSource source : mSourceMap.values()) { + source.resume(); + } + } + } + + public String getTopSetPath(int typeBits) { + + switch (typeBits) { + case INCLUDE_IMAGE: return TOP_IMAGE_SET_PATH; + case INCLUDE_VIDEO: return TOP_VIDEO_SET_PATH; + case INCLUDE_ALL: return TOP_SET_PATH; + case INCLUDE_LOCAL_IMAGE_ONLY: return TOP_LOCAL_IMAGE_SET_PATH; + case INCLUDE_LOCAL_VIDEO_ONLY: return TOP_LOCAL_VIDEO_SET_PATH; + case INCLUDE_LOCAL_ALL_ONLY: return TOP_LOCAL_SET_PATH; + default: throw new IllegalArgumentException(); + } + } + + // open for debug + void addSource(MediaSource source) { + if (source == null) return; + mSourceMap.put(source.getPrefix(), source); + } + + // A common usage of this method is: + // synchronized (DataManager.LOCK) { + // MediaObject object = peekMediaObject(path); + // if (object == null) { + // object = createMediaObject(...); + // } + // } + public MediaObject peekMediaObject(Path path) { + return path.getObject(); + } + + public MediaObject getMediaObject(Path path) { + synchronized (LOCK) { + MediaObject obj = path.getObject(); + if (obj != null) return obj; + + MediaSource source = mSourceMap.get(path.getPrefix()); + if (source == null) { + Log.w(TAG, "cannot find media source for path: " + path); + return null; + } + + try { + MediaObject object = source.createMediaObject(path); + if (object == null) { + Log.w(TAG, "cannot create media object: " + path); + } + return object; + } catch (Throwable t) { + Log.w(TAG, "exception in creating media object: " + path, t); + return null; + } + } + } + + public MediaObject getMediaObject(String s) { + return getMediaObject(Path.fromString(s)); + } + + public MediaSet getMediaSet(Path path) { + return (MediaSet) getMediaObject(path); + } + + public MediaSet getMediaSet(String s) { + return (MediaSet) getMediaObject(s); + } + + public MediaSet[] getMediaSetsFromString(String segment) { + String[] seq = Path.splitSequence(segment); + int n = seq.length; + MediaSet[] sets = new MediaSet[n]; + for (int i = 0; i < n; i++) { + sets[i] = getMediaSet(seq[i]); + } + return sets; + } + + // Maps a list of Paths to MediaItems, and invoke consumer.consume() + // for each MediaItem (may not be in the same order as the input list). + // An index number is also passed to consumer.consume() to identify + // the original position in the input list of the corresponding Path (plus + // startIndex). + public void mapMediaItems(ArrayList<Path> list, ItemConsumer consumer, + int startIndex) { + HashMap<String, ArrayList<PathId>> map = + new HashMap<String, ArrayList<PathId>>(); + + // Group the path by the prefix. + int n = list.size(); + for (int i = 0; i < n; i++) { + Path path = list.get(i); + String prefix = path.getPrefix(); + ArrayList<PathId> group = map.get(prefix); + if (group == null) { + group = new ArrayList<PathId>(); + map.put(prefix, group); + } + group.add(new PathId(path, i + startIndex)); + } + + // For each group, ask the corresponding media source to map it. + for (Entry<String, ArrayList<PathId>> entry : map.entrySet()) { + String prefix = entry.getKey(); + MediaSource source = mSourceMap.get(prefix); + source.mapMediaItems(entry.getValue(), consumer); + } + } + + // The following methods forward the request to the proper object. + public int getSupportedOperations(Path path) { + return getMediaObject(path).getSupportedOperations(); + } + + public void getPanoramaSupport(Path path, PanoramaSupportCallback callback) { + getMediaObject(path).getPanoramaSupport(callback); + } + + public void delete(Path path) { + getMediaObject(path).delete(); + } + + public void rotate(Path path, int degrees) { + getMediaObject(path).rotate(degrees); + } + + public Uri getContentUri(Path path) { + return getMediaObject(path).getContentUri(); + } + + public int getMediaType(Path path) { + return getMediaObject(path).getMediaType(); + } + + public Path findPathByUri(Uri uri, String type) { + if (uri == null) return null; + for (MediaSource source : mSourceMap.values()) { + Path path = source.findPathByUri(uri, type); + if (path != null) return path; + } + return null; + } + + public Path getDefaultSetOf(Path item) { + MediaSource source = mSourceMap.get(item.getPrefix()); + return source == null ? null : source.getDefaultSetOf(item); + } + + // Returns number of bytes used by cached pictures currently downloaded. + public long getTotalUsedCacheSize() { + long sum = 0; + for (MediaSource source : mSourceMap.values()) { + sum += source.getTotalUsedCacheSize(); + } + return sum; + } + + // Returns number of bytes used by cached pictures if all pending + // downloads and removals are completed. + public long getTotalTargetCacheSize() { + long sum = 0; + for (MediaSource source : mSourceMap.values()) { + sum += source.getTotalTargetCacheSize(); + } + return sum; + } + + public void registerChangeNotifier(Uri uri, ChangeNotifier notifier) { + NotifyBroker broker = null; + synchronized (mNotifierMap) { + broker = mNotifierMap.get(uri); + if (broker == null) { + broker = new NotifyBroker(mDefaultMainHandler); + mApplication.getContentResolver() + .registerContentObserver(uri, true, broker); + mNotifierMap.put(uri, broker); + } + } + broker.registerNotifier(notifier); + } + + public void resume() { + if (++mActiveCount == 1) { + for (MediaSource source : mSourceMap.values()) { + source.resume(); + } + } + } + + public void pause() { + if (--mActiveCount == 0) { + for (MediaSource source : mSourceMap.values()) { + source.pause(); + } + } + } + + private static class NotifyBroker extends ContentObserver { + private WeakHashMap<ChangeNotifier, Object> mNotifiers = + new WeakHashMap<ChangeNotifier, Object>(); + + public NotifyBroker(Handler handler) { + super(handler); + } + + public synchronized void registerNotifier(ChangeNotifier notifier) { + mNotifiers.put(notifier, null); + } + + @Override + public synchronized void onChange(boolean selfChange) { + for(ChangeNotifier notifier : mNotifiers.keySet()) { + notifier.onChange(selfChange); + } + } + } + + @Override + public void onStitchingQueued(Uri uri) { + // Do nothing. + } + + @Override + public void onStitchingResult(Uri uri) { + Path path = findPathByUri(uri, null); + if (path != null) { + MediaObject mediaObject = getMediaObject(path); + if (mediaObject != null) { + mediaObject.clearCachedPanoramaSupport(); + } + } + } + + @Override + public void onStitchingProgress(Uri uri, int progress) { + // Do nothing. + } +} diff --git a/src/com/android/gallery3d/data/DataSourceType.java b/src/com/android/gallery3d/data/DataSourceType.java new file mode 100644 index 000000000..ab534d0c3 --- /dev/null +++ b/src/com/android/gallery3d/data/DataSourceType.java @@ -0,0 +1,45 @@ +/* + * 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.data; + +import com.android.gallery3d.util.MediaSetUtils; + +public final class DataSourceType { + public static final int TYPE_NOT_CATEGORIZED = 0; + public static final int TYPE_LOCAL = 1; + public static final int TYPE_PICASA = 2; + public static final int TYPE_CAMERA = 3; + + private static final Path PICASA_ROOT = Path.fromString("/picasa"); + private static final Path LOCAL_ROOT = Path.fromString("/local"); + + public static int identifySourceType(MediaSet set) { + if (set == null) { + return TYPE_NOT_CATEGORIZED; + } + + Path path = set.getPath(); + if (MediaSetUtils.isCameraSource(path)) return TYPE_CAMERA; + + Path prefix = path.getPrefixPath(); + + if (prefix == PICASA_ROOT) return TYPE_PICASA; + if (prefix == LOCAL_ROOT) return TYPE_LOCAL; + + return TYPE_NOT_CATEGORIZED; + } +} diff --git a/src/com/android/gallery3d/data/DecodeUtils.java b/src/com/android/gallery3d/data/DecodeUtils.java new file mode 100644 index 000000000..fa709157d --- /dev/null +++ b/src/com/android/gallery3d/data/DecodeUtils.java @@ -0,0 +1,312 @@ +/* + * 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.data; + +import android.annotation.TargetApi; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.BitmapFactory; +import android.graphics.BitmapFactory.Options; +import android.graphics.BitmapRegionDecoder; +import android.os.Build; +import android.util.FloatMath; + +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.common.BitmapUtils; +import com.android.gallery3d.common.Utils; +import com.android.photos.data.GalleryBitmapPool; +import com.android.gallery3d.ui.Log; +import com.android.gallery3d.util.ThreadPool.CancelListener; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.InputStream; + +public class DecodeUtils { + private static final String TAG = "DecodeUtils"; + + private static class DecodeCanceller implements CancelListener { + Options mOptions; + + public DecodeCanceller(Options options) { + mOptions = options; + } + + @Override + public void onCancel() { + mOptions.requestCancelDecode(); + } + } + + @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) + public static void setOptionsMutable(Options options) { + if (ApiHelper.HAS_OPTIONS_IN_MUTABLE) options.inMutable = true; + } + + public static Bitmap decode(JobContext jc, FileDescriptor fd, Options options) { + if (options == null) options = new Options(); + jc.setCancelListener(new DecodeCanceller(options)); + setOptionsMutable(options); + return ensureGLCompatibleBitmap( + BitmapFactory.decodeFileDescriptor(fd, null, options)); + } + + public static void decodeBounds(JobContext jc, FileDescriptor fd, + Options options) { + Utils.assertTrue(options != null); + options.inJustDecodeBounds = true; + jc.setCancelListener(new DecodeCanceller(options)); + BitmapFactory.decodeFileDescriptor(fd, null, options); + options.inJustDecodeBounds = false; + } + + public static Bitmap decode(JobContext jc, byte[] bytes, Options options) { + return decode(jc, bytes, 0, bytes.length, options); + } + + public static Bitmap decode(JobContext jc, byte[] bytes, int offset, + int length, Options options) { + if (options == null) options = new Options(); + jc.setCancelListener(new DecodeCanceller(options)); + setOptionsMutable(options); + return ensureGLCompatibleBitmap( + BitmapFactory.decodeByteArray(bytes, offset, length, options)); + } + + public static void decodeBounds(JobContext jc, byte[] bytes, int offset, + int length, Options options) { + Utils.assertTrue(options != null); + options.inJustDecodeBounds = true; + jc.setCancelListener(new DecodeCanceller(options)); + BitmapFactory.decodeByteArray(bytes, offset, length, options); + options.inJustDecodeBounds = false; + } + + public static Bitmap decodeThumbnail( + JobContext jc, String filePath, Options options, int targetSize, int type) { + FileInputStream fis = null; + try { + fis = new FileInputStream(filePath); + FileDescriptor fd = fis.getFD(); + return decodeThumbnail(jc, fd, options, targetSize, type); + } catch (Exception ex) { + Log.w(TAG, ex); + return null; + } finally { + Utils.closeSilently(fis); + } + } + + public static Bitmap decodeThumbnail( + JobContext jc, FileDescriptor fd, Options options, int targetSize, int type) { + if (options == null) options = new Options(); + jc.setCancelListener(new DecodeCanceller(options)); + + options.inJustDecodeBounds = true; + BitmapFactory.decodeFileDescriptor(fd, null, options); + if (jc.isCancelled()) return null; + + int w = options.outWidth; + int h = options.outHeight; + + if (type == MediaItem.TYPE_MICROTHUMBNAIL) { + // We center-crop the original image as it's micro thumbnail. In this case, + // we want to make sure the shorter side >= "targetSize". + float scale = (float) targetSize / Math.min(w, h); + options.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale); + + // For an extremely wide image, e.g. 300x30000, we may got OOM when decoding + // it for TYPE_MICROTHUMBNAIL. So we add a max number of pixels limit here. + final int MAX_PIXEL_COUNT = 640000; // 400 x 1600 + if ((w / options.inSampleSize) * (h / options.inSampleSize) > MAX_PIXEL_COUNT) { + options.inSampleSize = BitmapUtils.computeSampleSize( + FloatMath.sqrt((float) MAX_PIXEL_COUNT / (w * h))); + } + } else { + // For screen nail, we only want to keep the longer side >= targetSize. + float scale = (float) targetSize / Math.max(w, h); + options.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale); + } + + options.inJustDecodeBounds = false; + setOptionsMutable(options); + + Bitmap result = BitmapFactory.decodeFileDescriptor(fd, null, options); + if (result == null) return null; + + // We need to resize down if the decoder does not support inSampleSize + // (For example, GIF images) + float scale = (float) targetSize / (type == MediaItem.TYPE_MICROTHUMBNAIL + ? Math.min(result.getWidth(), result.getHeight()) + : Math.max(result.getWidth(), result.getHeight())); + + if (scale <= 0.5) result = BitmapUtils.resizeBitmapByScale(result, scale, true); + return ensureGLCompatibleBitmap(result); + } + + /** + * Decodes the bitmap from the given byte array if the image size is larger than the given + * requirement. + * + * Note: The returned image may be resized down. However, both width and height must be + * larger than the <code>targetSize</code>. + */ + public static Bitmap decodeIfBigEnough(JobContext jc, byte[] data, + Options options, int targetSize) { + if (options == null) options = new Options(); + jc.setCancelListener(new DecodeCanceller(options)); + + options.inJustDecodeBounds = true; + BitmapFactory.decodeByteArray(data, 0, data.length, options); + if (jc.isCancelled()) return null; + if (options.outWidth < targetSize || options.outHeight < targetSize) { + return null; + } + options.inSampleSize = BitmapUtils.computeSampleSizeLarger( + options.outWidth, options.outHeight, targetSize); + options.inJustDecodeBounds = false; + setOptionsMutable(options); + + return ensureGLCompatibleBitmap( + BitmapFactory.decodeByteArray(data, 0, data.length, options)); + } + + // TODO: This function should not be called directly from + // DecodeUtils.requestDecode(...), since we don't have the knowledge + // if the bitmap will be uploaded to GL. + public static Bitmap ensureGLCompatibleBitmap(Bitmap bitmap) { + if (bitmap == null || bitmap.getConfig() != null) return bitmap; + Bitmap newBitmap = bitmap.copy(Config.ARGB_8888, false); + bitmap.recycle(); + return newBitmap; + } + + public static BitmapRegionDecoder createBitmapRegionDecoder( + JobContext jc, byte[] bytes, int offset, int length, + boolean shareable) { + if (offset < 0 || length <= 0 || offset + length > bytes.length) { + throw new IllegalArgumentException(String.format( + "offset = %s, length = %s, bytes = %s", + offset, length, bytes.length)); + } + + try { + return BitmapRegionDecoder.newInstance( + bytes, offset, length, shareable); + } catch (Throwable t) { + Log.w(TAG, t); + return null; + } + } + + public static BitmapRegionDecoder createBitmapRegionDecoder( + JobContext jc, String filePath, boolean shareable) { + try { + return BitmapRegionDecoder.newInstance(filePath, shareable); + } catch (Throwable t) { + Log.w(TAG, t); + return null; + } + } + + public static BitmapRegionDecoder createBitmapRegionDecoder( + JobContext jc, FileDescriptor fd, boolean shareable) { + try { + return BitmapRegionDecoder.newInstance(fd, shareable); + } catch (Throwable t) { + Log.w(TAG, t); + return null; + } + } + + public static BitmapRegionDecoder createBitmapRegionDecoder( + JobContext jc, InputStream is, boolean shareable) { + try { + return BitmapRegionDecoder.newInstance(is, shareable); + } catch (Throwable t) { + // We often cancel the creating of bitmap region decoder, + // so just log one line. + Log.w(TAG, "requestCreateBitmapRegionDecoder: " + t); + return null; + } + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public static Bitmap decodeUsingPool(JobContext jc, byte[] data, int offset, + int length, BitmapFactory.Options options) { + if (options == null) options = new BitmapFactory.Options(); + if (options.inSampleSize < 1) options.inSampleSize = 1; + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + options.inBitmap = (options.inSampleSize == 1) + ? findCachedBitmap(jc, data, offset, length, options) : null; + try { + Bitmap bitmap = decode(jc, data, offset, length, options); + if (options.inBitmap != null && options.inBitmap != bitmap) { + GalleryBitmapPool.getInstance().put(options.inBitmap); + options.inBitmap = null; + } + return bitmap; + } catch (IllegalArgumentException e) { + if (options.inBitmap == null) throw e; + + Log.w(TAG, "decode fail with a given bitmap, try decode to a new bitmap"); + GalleryBitmapPool.getInstance().put(options.inBitmap); + options.inBitmap = null; + return decode(jc, data, offset, length, options); + } + } + + // This is the same as the method above except the source data comes + // from a file descriptor instead of a byte array. + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public static Bitmap decodeUsingPool(JobContext jc, + FileDescriptor fileDescriptor, Options options) { + if (options == null) options = new BitmapFactory.Options(); + if (options.inSampleSize < 1) options.inSampleSize = 1; + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + options.inBitmap = (options.inSampleSize == 1) + ? findCachedBitmap(jc, fileDescriptor, options) : null; + try { + Bitmap bitmap = DecodeUtils.decode(jc, fileDescriptor, options); + if (options.inBitmap != null && options.inBitmap != bitmap) { + GalleryBitmapPool.getInstance().put(options.inBitmap); + options.inBitmap = null; + } + return bitmap; + } catch (IllegalArgumentException e) { + if (options.inBitmap == null) throw e; + + Log.w(TAG, "decode fail with a given bitmap, try decode to a new bitmap"); + GalleryBitmapPool.getInstance().put(options.inBitmap); + options.inBitmap = null; + return decode(jc, fileDescriptor, options); + } + } + + private static Bitmap findCachedBitmap(JobContext jc, byte[] data, + int offset, int length, Options options) { + decodeBounds(jc, data, offset, length, options); + return GalleryBitmapPool.getInstance().get(options.outWidth, options.outHeight); + } + + private static Bitmap findCachedBitmap(JobContext jc, FileDescriptor fileDescriptor, + Options options) { + decodeBounds(jc, fileDescriptor, options); + return GalleryBitmapPool.getInstance().get(options.outWidth, options.outHeight); + } +} diff --git a/src/com/android/gallery3d/data/DownloadCache.java b/src/com/android/gallery3d/data/DownloadCache.java new file mode 100644 index 000000000..be7820b01 --- /dev/null +++ b/src/com/android/gallery3d/data/DownloadCache.java @@ -0,0 +1,370 @@ +/* + * 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.data; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.common.LruCache; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.DownloadEntry.Columns; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.FutureListener; +import com.android.gallery3d.util.ThreadPool; +import com.android.gallery3d.util.ThreadPool.CancelListener; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import java.io.File; +import java.net.URL; +import java.util.HashMap; +import java.util.HashSet; + +public class DownloadCache { + private static final String TAG = "DownloadCache"; + private static final int MAX_DELETE_COUNT = 16; + private static final int LRU_CAPACITY = 4; + + private static final String TABLE_NAME = DownloadEntry.SCHEMA.getTableName(); + + private static final String QUERY_PROJECTION[] = {Columns.ID, Columns.DATA}; + private static final String WHERE_HASH_AND_URL = String.format( + "%s = ? AND %s = ?", Columns.HASH_CODE, Columns.CONTENT_URL); + private static final int QUERY_INDEX_ID = 0; + private static final int QUERY_INDEX_DATA = 1; + + private static final String FREESPACE_PROJECTION[] = { + Columns.ID, Columns.DATA, Columns.CONTENT_URL, Columns.CONTENT_SIZE}; + private static final String FREESPACE_ORDER_BY = + String.format("%s ASC", Columns.LAST_ACCESS); + private static final int FREESPACE_IDNEX_ID = 0; + private static final int FREESPACE_IDNEX_DATA = 1; + private static final int FREESPACE_INDEX_CONTENT_URL = 2; + private static final int FREESPACE_INDEX_CONTENT_SIZE = 3; + + private static final String ID_WHERE = Columns.ID + " = ?"; + + private static final String SUM_PROJECTION[] = + {String.format("sum(%s)", Columns.CONTENT_SIZE)}; + private static final int SUM_INDEX_SUM = 0; + + private final LruCache<String, Entry> mEntryMap = + new LruCache<String, Entry>(LRU_CAPACITY); + private final HashMap<String, DownloadTask> mTaskMap = + new HashMap<String, DownloadTask>(); + private final File mRoot; + private final GalleryApp mApplication; + private final SQLiteDatabase mDatabase; + private final long mCapacity; + + private long mTotalBytes = 0; + private boolean mInitialized = false; + + public DownloadCache(GalleryApp application, File root, long capacity) { + mRoot = Utils.checkNotNull(root); + mApplication = Utils.checkNotNull(application); + mCapacity = capacity; + mDatabase = new DatabaseHelper(application.getAndroidContext()) + .getWritableDatabase(); + } + + private Entry findEntryInDatabase(String stringUrl) { + long hash = Utils.crc64Long(stringUrl); + String whereArgs[] = {String.valueOf(hash), stringUrl}; + Cursor cursor = mDatabase.query(TABLE_NAME, QUERY_PROJECTION, + WHERE_HASH_AND_URL, whereArgs, null, null, null); + try { + if (cursor.moveToNext()) { + File file = new File(cursor.getString(QUERY_INDEX_DATA)); + long id = cursor.getInt(QUERY_INDEX_ID); + Entry entry = null; + synchronized (mEntryMap) { + entry = mEntryMap.get(stringUrl); + if (entry == null) { + entry = new Entry(id, file); + mEntryMap.put(stringUrl, entry); + } + } + return entry; + } + } finally { + cursor.close(); + } + return null; + } + + public Entry download(JobContext jc, URL url) { + if (!mInitialized) initialize(); + + String stringUrl = url.toString(); + + // First find in the entry-pool + synchronized (mEntryMap) { + Entry entry = mEntryMap.get(stringUrl); + if (entry != null) { + updateLastAccess(entry.mId); + return entry; + } + } + + // Then, find it in database + TaskProxy proxy = new TaskProxy(); + synchronized (mTaskMap) { + Entry entry = findEntryInDatabase(stringUrl); + if (entry != null) { + updateLastAccess(entry.mId); + return entry; + } + + // Finally, we need to download the file .... + // First check if we are downloading it now ... + DownloadTask task = mTaskMap.get(stringUrl); + if (task == null) { // if not, start the download task now + task = new DownloadTask(stringUrl); + mTaskMap.put(stringUrl, task); + task.mFuture = mApplication.getThreadPool().submit(task, task); + } + task.addProxy(proxy); + } + + return proxy.get(jc); + } + + private void updateLastAccess(long id) { + ContentValues values = new ContentValues(); + values.put(Columns.LAST_ACCESS, System.currentTimeMillis()); + mDatabase.update(TABLE_NAME, values, + ID_WHERE, new String[] {String.valueOf(id)}); + } + + private synchronized void freeSomeSpaceIfNeed(int maxDeleteFileCount) { + if (mTotalBytes <= mCapacity) return; + Cursor cursor = mDatabase.query(TABLE_NAME, + FREESPACE_PROJECTION, null, null, null, null, FREESPACE_ORDER_BY); + try { + while (maxDeleteFileCount > 0 + && mTotalBytes > mCapacity && cursor.moveToNext()) { + long id = cursor.getLong(FREESPACE_IDNEX_ID); + String url = cursor.getString(FREESPACE_INDEX_CONTENT_URL); + long size = cursor.getLong(FREESPACE_INDEX_CONTENT_SIZE); + String path = cursor.getString(FREESPACE_IDNEX_DATA); + boolean containsKey; + synchronized (mEntryMap) { + containsKey = mEntryMap.containsKey(url); + } + if (!containsKey) { + --maxDeleteFileCount; + mTotalBytes -= size; + new File(path).delete(); + mDatabase.delete(TABLE_NAME, + ID_WHERE, new String[]{String.valueOf(id)}); + } else { + // skip delete, since it is being used + } + } + } finally { + cursor.close(); + } + } + + private synchronized long insertEntry(String url, File file) { + long size = file.length(); + mTotalBytes += size; + + ContentValues values = new ContentValues(); + String hashCode = String.valueOf(Utils.crc64Long(url)); + values.put(Columns.DATA, file.getAbsolutePath()); + values.put(Columns.HASH_CODE, hashCode); + values.put(Columns.CONTENT_URL, url); + values.put(Columns.CONTENT_SIZE, size); + values.put(Columns.LAST_UPDATED, System.currentTimeMillis()); + return mDatabase.insert(TABLE_NAME, "", values); + } + + private synchronized void initialize() { + if (mInitialized) return; + mInitialized = true; + if (!mRoot.isDirectory()) mRoot.mkdirs(); + if (!mRoot.isDirectory()) { + throw new RuntimeException("cannot create " + mRoot.getAbsolutePath()); + } + + Cursor cursor = mDatabase.query( + TABLE_NAME, SUM_PROJECTION, null, null, null, null, null); + mTotalBytes = 0; + try { + if (cursor.moveToNext()) { + mTotalBytes = cursor.getLong(SUM_INDEX_SUM); + } + } finally { + cursor.close(); + } + if (mTotalBytes > mCapacity) freeSomeSpaceIfNeed(MAX_DELETE_COUNT); + } + + private final class DatabaseHelper extends SQLiteOpenHelper { + public static final String DATABASE_NAME = "download.db"; + public static final int DATABASE_VERSION = 2; + + public DatabaseHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + DownloadEntry.SCHEMA.createTables(db); + // Delete old files + for (File file : mRoot.listFiles()) { + if (!file.delete()) { + Log.w(TAG, "fail to remove: " + file.getAbsolutePath()); + } + } + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + //reset everything + DownloadEntry.SCHEMA.dropTables(db); + onCreate(db); + } + } + + public class Entry { + public File cacheFile; + protected long mId; + + Entry(long id, File cacheFile) { + mId = id; + this.cacheFile = Utils.checkNotNull(cacheFile); + } + } + + private class DownloadTask implements Job<File>, FutureListener<File> { + private HashSet<TaskProxy> mProxySet = new HashSet<TaskProxy>(); + private Future<File> mFuture; + private final String mUrl; + + public DownloadTask(String url) { + mUrl = Utils.checkNotNull(url); + } + + public void removeProxy(TaskProxy proxy) { + synchronized (mTaskMap) { + Utils.assertTrue(mProxySet.remove(proxy)); + if (mProxySet.isEmpty()) { + mFuture.cancel(); + mTaskMap.remove(mUrl); + } + } + } + + // should be used in synchronized block of mDatabase + public void addProxy(TaskProxy proxy) { + proxy.mTask = this; + mProxySet.add(proxy); + } + + @Override + public void onFutureDone(Future<File> future) { + File file = future.get(); + long id = 0; + if (file != null) { // insert to database + id = insertEntry(mUrl, file); + } + + if (future.isCancelled()) { + Utils.assertTrue(mProxySet.isEmpty()); + return; + } + + synchronized (mTaskMap) { + Entry entry = null; + synchronized (mEntryMap) { + if (file != null) { + entry = new Entry(id, file); + Utils.assertTrue(mEntryMap.put(mUrl, entry) == null); + } + } + for (TaskProxy proxy : mProxySet) { + proxy.setResult(entry); + } + mTaskMap.remove(mUrl); + freeSomeSpaceIfNeed(MAX_DELETE_COUNT); + } + } + + @Override + public File run(JobContext jc) { + // TODO: utilize etag + jc.setMode(ThreadPool.MODE_NETWORK); + File tempFile = null; + try { + URL url = new URL(mUrl); + tempFile = File.createTempFile("cache", ".tmp", mRoot); + // download from url to tempFile + jc.setMode(ThreadPool.MODE_NETWORK); + boolean downloaded = DownloadUtils.requestDownload(jc, url, tempFile); + jc.setMode(ThreadPool.MODE_NONE); + if (downloaded) return tempFile; + } catch (Exception e) { + Log.e(TAG, String.format("fail to download %s", mUrl), e); + } finally { + jc.setMode(ThreadPool.MODE_NONE); + } + if (tempFile != null) tempFile.delete(); + return null; + } + } + + public static class TaskProxy { + private DownloadTask mTask; + private boolean mIsCancelled = false; + private Entry mEntry; + + synchronized void setResult(Entry entry) { + if (mIsCancelled) return; + mEntry = entry; + notifyAll(); + } + + public synchronized Entry get(JobContext jc) { + jc.setCancelListener(new CancelListener() { + @Override + public void onCancel() { + mTask.removeProxy(TaskProxy.this); + synchronized (TaskProxy.this) { + mIsCancelled = true; + TaskProxy.this.notifyAll(); + } + } + }); + while (!mIsCancelled && mEntry == null) { + try { + wait(); + } catch (InterruptedException e) { + Log.w(TAG, "ignore interrupt", e); + } + } + jc.setCancelListener(null); + return mEntry; + } + } +} diff --git a/src/com/android/gallery3d/data/DownloadEntry.java b/src/com/android/gallery3d/data/DownloadEntry.java new file mode 100644 index 000000000..578523f73 --- /dev/null +++ b/src/com/android/gallery3d/data/DownloadEntry.java @@ -0,0 +1,72 @@ +/* + * 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.data; + +import com.android.gallery3d.common.Entry; +import com.android.gallery3d.common.EntrySchema; + + +@Entry.Table("download") +public class DownloadEntry extends Entry { + public static final EntrySchema SCHEMA = new EntrySchema(DownloadEntry.class); + + public static interface Columns extends Entry.Columns { + public static final String HASH_CODE = "hash_code"; + public static final String CONTENT_URL = "content_url"; + public static final String CONTENT_SIZE = "_size"; + public static final String ETAG = "etag"; + public static final String LAST_ACCESS = "last_access"; + public static final String LAST_UPDATED = "last_updated"; + public static final String DATA = "_data"; + } + + @Column(value = "hash_code", indexed = true) + public long hashCode; + + @Column("content_url") + public String contentUrl; + + @Column("_size") + public long contentSize; + + @Column("etag") + public String eTag; + + @Column(value = "last_access", indexed = true) + public long lastAccessTime; + + @Column(value = "last_updated") + public long lastUpdatedTime; + + @Column("_data") + public String path; + + @Override + public String toString() { + // Note: THIS IS REQUIRED. We used all the fields here. Otherwise, + // ProGuard will remove these UNUSED fields. However, these + // fields are needed to generate database. + return new StringBuilder() + .append("hash_code: ").append(hashCode).append(", ") + .append("content_url").append(contentUrl).append(", ") + .append("_size").append(contentSize).append(", ") + .append("etag").append(eTag).append(", ") + .append("last_access").append(lastAccessTime).append(", ") + .append("last_updated").append(lastUpdatedTime).append(",") + .append("_data").append(path) + .toString(); + } +} diff --git a/src/com/android/gallery3d/data/DownloadUtils.java b/src/com/android/gallery3d/data/DownloadUtils.java new file mode 100644 index 000000000..137898e91 --- /dev/null +++ b/src/com/android/gallery3d/data/DownloadUtils.java @@ -0,0 +1,79 @@ +/* + * 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.data; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.util.ThreadPool.CancelListener; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.io.OutputStream; +import java.net.URL; + +public class DownloadUtils { + private static final String TAG = "DownloadService"; + + public static boolean requestDownload(JobContext jc, URL url, File file) { + FileOutputStream fos = null; + try { + fos = new FileOutputStream(file); + return download(jc, url, fos); + } catch (Throwable t) { + return false; + } finally { + Utils.closeSilently(fos); + } + } + + public static void dump(JobContext jc, InputStream is, OutputStream os) + throws IOException { + byte buffer[] = new byte[4096]; + int rc = is.read(buffer, 0, buffer.length); + final Thread thread = Thread.currentThread(); + jc.setCancelListener(new CancelListener() { + @Override + public void onCancel() { + thread.interrupt(); + } + }); + while (rc > 0) { + if (jc.isCancelled()) throw new InterruptedIOException(); + os.write(buffer, 0, rc); + rc = is.read(buffer, 0, buffer.length); + } + jc.setCancelListener(null); + Thread.interrupted(); // consume the interrupt signal + } + + public static boolean download(JobContext jc, URL url, OutputStream output) { + InputStream input = null; + try { + input = url.openStream(); + dump(jc, input, output); + return true; + } catch (Throwable t) { + Log.w(TAG, "fail to download", t); + return false; + } finally { + Utils.closeSilently(input); + } + } +}
\ No newline at end of file diff --git a/src/com/android/gallery3d/data/EmptyAlbumImage.java b/src/com/android/gallery3d/data/EmptyAlbumImage.java new file mode 100644 index 000000000..6f8c37c6b --- /dev/null +++ b/src/com/android/gallery3d/data/EmptyAlbumImage.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2012 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.data; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.GalleryApp; + +public class EmptyAlbumImage extends ActionImage { + @SuppressWarnings("unused") + private static final String TAG = "EmptyAlbumImage"; + + public EmptyAlbumImage(Path path, GalleryApp application) { + super(path, application, R.drawable.placeholder_empty); + } + + @Override + public int getSupportedOperations() { + return super.getSupportedOperations() | SUPPORT_BACK; + } +} diff --git a/src/com/android/gallery3d/data/Exif.java b/src/com/android/gallery3d/data/Exif.java new file mode 100644 index 000000000..950e7de18 --- /dev/null +++ b/src/com/android/gallery3d/data/Exif.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2011 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.data; + +import android.util.Log; + +import com.android.gallery3d.exif.ExifInterface; + +import java.io.IOException; +import java.io.InputStream; + +public class Exif { + private static final String TAG = "CameraExif"; + + // Returns the degrees in clockwise. Values are 0, 90, 180, or 270. + public static int getOrientation(InputStream is) { + if (is == null) { + return 0; + } + ExifInterface exif = new ExifInterface(); + try { + exif.readExif(is); + Integer val = exif.getTagIntValue(ExifInterface.TAG_ORIENTATION); + if (val == null) { + return 0; + } else { + return ExifInterface.getRotationForOrientationValue(val.shortValue()); + } + } catch (IOException e) { + Log.w(TAG, "Failed to read EXIF orientation", e); + return 0; + } + } +} diff --git a/src/com/android/gallery3d/data/Face.java b/src/com/android/gallery3d/data/Face.java new file mode 100644 index 000000000..d2dc22bfc --- /dev/null +++ b/src/com/android/gallery3d/data/Face.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2011 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.data; + +import android.graphics.Rect; + +import com.android.gallery3d.common.Utils; + +import java.util.StringTokenizer; + +public class Face implements Comparable<Face> { + private String mName; + private String mPersonId; + private Rect mPosition; + + public Face(String name, String personId, String rect) { + mName = name; + mPersonId = personId; + Utils.assertTrue(mName != null && mPersonId != null && rect != null); + StringTokenizer tokenizer = new StringTokenizer(rect); + mPosition = new Rect(); + while (tokenizer.hasMoreElements()) { + mPosition.left = Integer.parseInt(tokenizer.nextToken()); + mPosition.top = Integer.parseInt(tokenizer.nextToken()); + mPosition.right = Integer.parseInt(tokenizer.nextToken()); + mPosition.bottom = Integer.parseInt(tokenizer.nextToken()); + } + } + + public Rect getPosition() { + return mPosition; + } + + public String getName() { + return mName; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Face) { + Face face = (Face) obj; + return mPersonId.equals(face.mPersonId); + } + return false; + } + + @Override + public int compareTo(Face another) { + return mName.compareTo(another.mName); + } +} diff --git a/src/com/android/gallery3d/data/FaceClustering.java b/src/com/android/gallery3d/data/FaceClustering.java new file mode 100644 index 000000000..819915edb --- /dev/null +++ b/src/com/android/gallery3d/data/FaceClustering.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2011 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.data; + +import android.content.Context; +import android.graphics.Rect; + +import com.android.gallery3d.R; +import com.android.gallery3d.picasasource.PicasaSource; + +import java.util.ArrayList; +import java.util.TreeMap; + +public class FaceClustering extends Clustering { + @SuppressWarnings("unused") + private static final String TAG = "FaceClustering"; + + private FaceCluster[] mClusters; + private String mUntaggedString; + private Context mContext; + + private class FaceCluster { + ArrayList<Path> mPaths = new ArrayList<Path>(); + String mName; + MediaItem mCoverItem; + Rect mCoverRegion; + int mCoverFaceIndex; + + public FaceCluster(String name) { + mName = name; + } + + public void add(MediaItem item, int faceIndex) { + Path path = item.getPath(); + mPaths.add(path); + Face[] faces = item.getFaces(); + if (faces != null) { + Face face = faces[faceIndex]; + if (mCoverItem == null) { + mCoverItem = item; + mCoverRegion = face.getPosition(); + mCoverFaceIndex = faceIndex; + } else { + Rect region = face.getPosition(); + if (mCoverRegion.width() < region.width() && + mCoverRegion.height() < region.height()) { + mCoverItem = item; + mCoverRegion = face.getPosition(); + mCoverFaceIndex = faceIndex; + } + } + } + } + + public int size() { + return mPaths.size(); + } + + public MediaItem getCover() { + if (mCoverItem != null) { + if (PicasaSource.isPicasaImage(mCoverItem)) { + return PicasaSource.getFaceItem(mContext, mCoverItem, mCoverFaceIndex); + } else { + return mCoverItem; + } + } + return null; + } + } + + public FaceClustering(Context context) { + mUntaggedString = context.getResources().getString(R.string.untagged); + mContext = context; + } + + @Override + public void run(MediaSet baseSet) { + final TreeMap<Face, FaceCluster> map = + new TreeMap<Face, FaceCluster>(); + final FaceCluster untagged = new FaceCluster(mUntaggedString); + + baseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() { + @Override + public void consume(int index, MediaItem item) { + Face[] faces = item.getFaces(); + if (faces == null || faces.length == 0) { + untagged.add(item, -1); + return; + } + for (int j = 0; j < faces.length; j++) { + Face face = faces[j]; + FaceCluster cluster = map.get(face); + if (cluster == null) { + cluster = new FaceCluster(face.getName()); + map.put(face, cluster); + } + cluster.add(item, j); + } + } + }); + + int m = map.size(); + mClusters = map.values().toArray(new FaceCluster[m + ((untagged.size() > 0) ? 1 : 0)]); + if (untagged.size() > 0) { + mClusters[m] = untagged; + } + } + + @Override + public int getNumberOfClusters() { + return mClusters.length; + } + + @Override + public ArrayList<Path> getCluster(int index) { + return mClusters[index].mPaths; + } + + @Override + public String getClusterName(int index) { + return mClusters[index].mName; + } + + @Override + public MediaItem getClusterCover(int index) { + return mClusters[index].getCover(); + } +} diff --git a/src/com/android/gallery3d/data/FilterDeleteSet.java b/src/com/android/gallery3d/data/FilterDeleteSet.java new file mode 100644 index 000000000..c76412ff8 --- /dev/null +++ b/src/com/android/gallery3d/data/FilterDeleteSet.java @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2012 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.data; + +import java.util.ArrayList; + +// FilterDeleteSet filters a base MediaSet to remove some deletion items (we +// expect the number to be small). The user can use the following methods to +// add/remove deletion items: +// +// void addDeletion(Path path, int index); +// void removeDelection(Path path); +// void clearDeletion(); +// int getNumberOfDeletions(); +// +public class FilterDeleteSet extends MediaSet implements ContentListener { + @SuppressWarnings("unused") + private static final String TAG = "FilterDeleteSet"; + + private static final int REQUEST_ADD = 1; + private static final int REQUEST_REMOVE = 2; + private static final int REQUEST_CLEAR = 3; + + private static class Request { + int type; // one of the REQUEST_* constants + Path path; + int indexHint; + public Request(int type, Path path, int indexHint) { + this.type = type; + this.path = path; + this.indexHint = indexHint; + } + } + + private static class Deletion { + Path path; + int index; + public Deletion(Path path, int index) { + this.path = path; + this.index = index; + } + } + + // The underlying MediaSet + private final MediaSet mBaseSet; + + // Pending Requests + private ArrayList<Request> mRequests = new ArrayList<Request>(); + + // Deletions currently in effect, ordered by index + private ArrayList<Deletion> mCurrent = new ArrayList<Deletion>(); + + public FilterDeleteSet(Path path, MediaSet baseSet) { + super(path, INVALID_DATA_VERSION); + mBaseSet = baseSet; + mBaseSet.addContentListener(this); + } + + @Override + public boolean isCameraRoll() { + return mBaseSet.isCameraRoll(); + } + + @Override + public String getName() { + return mBaseSet.getName(); + } + + @Override + public int getMediaItemCount() { + return mBaseSet.getMediaItemCount() - mCurrent.size(); + } + + // Gets the MediaItems whose (post-deletion) index are in the range [start, + // start + count). Because we remove some of the MediaItems, the index need + // to be adjusted. + // + // For example, if there are 12 items in total. The deleted items are 3, 5, + // 10, and the the requested range is [3, 7]: + // + // The original index: 0 1 2 3 4 5 6 7 8 9 A B C + // The deleted items: X X X + // The new index: 0 1 2 3 4 5 6 7 8 9 + // Requested: * * * * * + // + // We need to figure out the [3, 7] actually maps to the original index 4, + // 6, 7, 8, 9. + // + // We can break the MediaItems into segments, each segment other than the + // last one ends in a deleted item. The difference between the new index and + // the original index increases with each segment: + // + // 0 1 2 X (new index = old index) + // 4 X (new index = old index - 1) + // 6 7 8 9 X (new index = old index - 2) + // B C (new index = old index - 3) + // + @Override + public ArrayList<MediaItem> getMediaItem(int start, int count) { + if (count <= 0) return new ArrayList<MediaItem>(); + + int end = start + count - 1; + int n = mCurrent.size(); + // Find the segment that "start" falls into. Count the number of items + // not yet deleted until it reaches "start". + int i = 0; + for (i = 0; i < n; i++) { + Deletion d = mCurrent.get(i); + if (d.index - i > start) break; + } + // Find the segment that "end" falls into. + int j = i; + for (; j < n; j++) { + Deletion d = mCurrent.get(j); + if (d.index - j > end) break; + } + + // Now get enough to cover deleted items in [start, end] + ArrayList<MediaItem> base = mBaseSet.getMediaItem(start + i, count + (j - i)); + + // Remove the deleted items. + for (int m = j - 1; m >= i; m--) { + Deletion d = mCurrent.get(m); + int k = d.index - (start + i); + base.remove(k); + } + return base; + } + + // We apply the pending requests in the mRequests to construct mCurrent in reload(). + @Override + public long reload() { + boolean newData = mBaseSet.reload() > mDataVersion; + synchronized (mRequests) { + if (!newData && mRequests.isEmpty()) { + return mDataVersion; + } + for (int i = 0; i < mRequests.size(); i++) { + Request r = mRequests.get(i); + switch (r.type) { + case REQUEST_ADD: { + // Add the path into mCurrent if there is no duplicate. + int n = mCurrent.size(); + int j; + for (j = 0; j < n; j++) { + if (mCurrent.get(j).path == r.path) break; + } + if (j == n) { + mCurrent.add(new Deletion(r.path, r.indexHint)); + } + break; + } + case REQUEST_REMOVE: { + // Remove the path from mCurrent. + int n = mCurrent.size(); + for (int j = 0; j < n; j++) { + if (mCurrent.get(j).path == r.path) { + mCurrent.remove(j); + break; + } + } + break; + } + case REQUEST_CLEAR: { + mCurrent.clear(); + break; + } + } + } + mRequests.clear(); + } + + if (!mCurrent.isEmpty()) { + // See if the elements in mCurrent can be found in the MediaSet. We + // don't want to search the whole mBaseSet, so we just search a + // small window that contains the index hints (plus some margin). + int minIndex = mCurrent.get(0).index; + int maxIndex = minIndex; + for (int i = 1; i < mCurrent.size(); i++) { + Deletion d = mCurrent.get(i); + minIndex = Math.min(d.index, minIndex); + maxIndex = Math.max(d.index, maxIndex); + } + + int n = mBaseSet.getMediaItemCount(); + int from = Math.max(minIndex - 5, 0); + int to = Math.min(maxIndex + 5, n); + ArrayList<MediaItem> items = mBaseSet.getMediaItem(from, to - from); + ArrayList<Deletion> result = new ArrayList<Deletion>(); + for (int i = 0; i < items.size(); i++) { + MediaItem item = items.get(i); + if (item == null) continue; + Path p = item.getPath(); + // Find the matching path in mCurrent, if found move it to result + for (int j = 0; j < mCurrent.size(); j++) { + Deletion d = mCurrent.get(j); + if (d.path == p) { + d.index = from + i; + result.add(d); + mCurrent.remove(j); + break; + } + } + } + mCurrent = result; + } + + mDataVersion = nextVersionNumber(); + return mDataVersion; + } + + private void sendRequest(int type, Path path, int indexHint) { + Request r = new Request(type, path, indexHint); + synchronized (mRequests) { + mRequests.add(r); + } + notifyContentChanged(); + } + + @Override + public void onContentDirty() { + notifyContentChanged(); + } + + public void addDeletion(Path path, int indexHint) { + sendRequest(REQUEST_ADD, path, indexHint); + } + + public void removeDeletion(Path path) { + sendRequest(REQUEST_REMOVE, path, 0 /* unused */); + } + + public void clearDeletion() { + sendRequest(REQUEST_CLEAR, null /* unused */ , 0 /* unused */); + } + + // Returns number of deletions _in effect_ (the number will only gets + // updated after a reload()). + public int getNumberOfDeletions() { + return mCurrent.size(); + } +} diff --git a/src/com/android/gallery3d/data/FilterEmptyPromptSet.java b/src/com/android/gallery3d/data/FilterEmptyPromptSet.java new file mode 100644 index 000000000..b576e06d4 --- /dev/null +++ b/src/com/android/gallery3d/data/FilterEmptyPromptSet.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2012 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.data; + +import java.util.ArrayList; + +public class FilterEmptyPromptSet extends MediaSet implements ContentListener { + @SuppressWarnings("unused") + private static final String TAG = "FilterEmptyPromptSet"; + + private ArrayList<MediaItem> mEmptyItem; + private MediaSet mBaseSet; + + public FilterEmptyPromptSet(Path path, MediaSet baseSet, MediaItem emptyItem) { + super(path, INVALID_DATA_VERSION); + mEmptyItem = new ArrayList<MediaItem>(1); + mEmptyItem.add(emptyItem); + mBaseSet = baseSet; + mBaseSet.addContentListener(this); + } + + @Override + public int getMediaItemCount() { + int itemCount = mBaseSet.getMediaItemCount(); + if (itemCount > 0) { + return itemCount; + } else { + return 1; + } + } + + @Override + public ArrayList<MediaItem> getMediaItem(int start, int count) { + int itemCount = mBaseSet.getMediaItemCount(); + if (itemCount > 0) { + return mBaseSet.getMediaItem(start, count); + } else if (start == 0 && count == 1) { + return mEmptyItem; + } else { + throw new ArrayIndexOutOfBoundsException(); + } + } + + @Override + public void onContentDirty() { + notifyContentChanged(); + } + + @Override + public boolean isLeafAlbum() { + return true; + } + + @Override + public boolean isCameraRoll() { + return mBaseSet.isCameraRoll(); + } + + @Override + public long reload() { + return mBaseSet.reload(); + } + + @Override + public String getName() { + return mBaseSet.getName(); + } +} diff --git a/src/com/android/gallery3d/data/FilterSource.java b/src/com/android/gallery3d/data/FilterSource.java new file mode 100644 index 000000000..d689fe336 --- /dev/null +++ b/src/com/android/gallery3d/data/FilterSource.java @@ -0,0 +1,94 @@ +/* + * 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.data; + +import com.android.gallery3d.app.GalleryApp; + +public class FilterSource extends MediaSource { + @SuppressWarnings("unused") + private static final String TAG = "FilterSource"; + private static final int FILTER_BY_MEDIATYPE = 0; + private static final int FILTER_BY_DELETE = 1; + private static final int FILTER_BY_EMPTY = 2; + private static final int FILTER_BY_EMPTY_ITEM = 3; + private static final int FILTER_BY_CAMERA_SHORTCUT = 4; + private static final int FILTER_BY_CAMERA_SHORTCUT_ITEM = 5; + + public static final String FILTER_EMPTY_ITEM = "/filter/empty_prompt"; + public static final String FILTER_CAMERA_SHORTCUT = "/filter/camera_shortcut"; + private static final String FILTER_CAMERA_SHORTCUT_ITEM = "/filter/camera_shortcut_item"; + + private GalleryApp mApplication; + private PathMatcher mMatcher; + private MediaItem mEmptyItem; + private MediaItem mCameraShortcutItem; + + public FilterSource(GalleryApp application) { + super("filter"); + mApplication = application; + mMatcher = new PathMatcher(); + mMatcher.add("/filter/mediatype/*/*", FILTER_BY_MEDIATYPE); + mMatcher.add("/filter/delete/*", FILTER_BY_DELETE); + mMatcher.add("/filter/empty/*", FILTER_BY_EMPTY); + mMatcher.add(FILTER_EMPTY_ITEM, FILTER_BY_EMPTY_ITEM); + mMatcher.add(FILTER_CAMERA_SHORTCUT, FILTER_BY_CAMERA_SHORTCUT); + mMatcher.add(FILTER_CAMERA_SHORTCUT_ITEM, FILTER_BY_CAMERA_SHORTCUT_ITEM); + + mEmptyItem = new EmptyAlbumImage(Path.fromString(FILTER_EMPTY_ITEM), + mApplication); + mCameraShortcutItem = new CameraShortcutImage( + Path.fromString(FILTER_CAMERA_SHORTCUT_ITEM), mApplication); + } + + // The name we accept are: + // /filter/mediatype/k/{set} where k is the media type we want. + // /filter/delete/{set} + @Override + public MediaObject createMediaObject(Path path) { + int matchType = mMatcher.match(path); + DataManager dataManager = mApplication.getDataManager(); + switch (matchType) { + case FILTER_BY_MEDIATYPE: { + int mediaType = mMatcher.getIntVar(0); + String setsName = mMatcher.getVar(1); + MediaSet[] sets = dataManager.getMediaSetsFromString(setsName); + return new FilterTypeSet(path, dataManager, sets[0], mediaType); + } + case FILTER_BY_DELETE: { + String setsName = mMatcher.getVar(0); + MediaSet[] sets = dataManager.getMediaSetsFromString(setsName); + return new FilterDeleteSet(path, sets[0]); + } + case FILTER_BY_EMPTY: { + String setsName = mMatcher.getVar(0); + MediaSet[] sets = dataManager.getMediaSetsFromString(setsName); + return new FilterEmptyPromptSet(path, sets[0], mEmptyItem); + } + case FILTER_BY_EMPTY_ITEM: { + return mEmptyItem; + } + case FILTER_BY_CAMERA_SHORTCUT: { + return new SingleItemAlbum(path, mCameraShortcutItem); + } + case FILTER_BY_CAMERA_SHORTCUT_ITEM: { + return mCameraShortcutItem; + } + default: + throw new RuntimeException("bad path: " + path); + } + } +} diff --git a/src/com/android/gallery3d/data/FilterTypeSet.java b/src/com/android/gallery3d/data/FilterTypeSet.java new file mode 100644 index 000000000..477ef73ad --- /dev/null +++ b/src/com/android/gallery3d/data/FilterTypeSet.java @@ -0,0 +1,137 @@ +/* + * 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.data; + +import java.util.ArrayList; + +// FilterTypeSet filters a base MediaSet according to a matching media type. +public class FilterTypeSet extends MediaSet implements ContentListener { + @SuppressWarnings("unused") + private static final String TAG = "FilterTypeSet"; + + private final DataManager mDataManager; + private final MediaSet mBaseSet; + private final int mMediaType; + private final ArrayList<Path> mPaths = new ArrayList<Path>(); + private final ArrayList<MediaSet> mAlbums = new ArrayList<MediaSet>(); + + public FilterTypeSet(Path path, DataManager dataManager, MediaSet baseSet, + int mediaType) { + super(path, INVALID_DATA_VERSION); + mDataManager = dataManager; + mBaseSet = baseSet; + mMediaType = mediaType; + mBaseSet.addContentListener(this); + } + + @Override + public String getName() { + return mBaseSet.getName(); + } + + @Override + public MediaSet getSubMediaSet(int index) { + return mAlbums.get(index); + } + + @Override + public int getSubMediaSetCount() { + return mAlbums.size(); + } + + @Override + public int getMediaItemCount() { + return mPaths.size(); + } + + @Override + public ArrayList<MediaItem> getMediaItem(int start, int count) { + return ClusterAlbum.getMediaItemFromPath( + mPaths, start, count, mDataManager); + } + + @Override + public long reload() { + if (mBaseSet.reload() > mDataVersion) { + updateData(); + mDataVersion = nextVersionNumber(); + } + return mDataVersion; + } + + @Override + public void onContentDirty() { + notifyContentChanged(); + } + + private void updateData() { + // Albums + mAlbums.clear(); + String basePath = "/filter/mediatype/" + mMediaType; + + for (int i = 0, n = mBaseSet.getSubMediaSetCount(); i < n; i++) { + MediaSet set = mBaseSet.getSubMediaSet(i); + String filteredPath = basePath + "/{" + set.getPath().toString() + "}"; + MediaSet filteredSet = mDataManager.getMediaSet(filteredPath); + filteredSet.reload(); + if (filteredSet.getMediaItemCount() > 0 + || filteredSet.getSubMediaSetCount() > 0) { + mAlbums.add(filteredSet); + } + } + + // Items + mPaths.clear(); + final int total = mBaseSet.getMediaItemCount(); + final Path[] buf = new Path[total]; + + mBaseSet.enumerateMediaItems(new MediaSet.ItemConsumer() { + @Override + public void consume(int index, MediaItem item) { + if (item.getMediaType() == mMediaType) { + if (index < 0 || index >= total) return; + Path path = item.getPath(); + buf[index] = path; + } + } + }); + + for (int i = 0; i < total; i++) { + if (buf[i] != null) { + mPaths.add(buf[i]); + } + } + } + + @Override + public int getSupportedOperations() { + return SUPPORT_SHARE | SUPPORT_DELETE; + } + + @Override + public void delete() { + ItemConsumer consumer = new ItemConsumer() { + @Override + public void consume(int index, MediaItem item) { + if ((item.getSupportedOperations() & SUPPORT_DELETE) != 0) { + item.delete(); + } + } + }; + mDataManager.mapMediaItems(mPaths, consumer, 0); + } +} diff --git a/src/com/android/gallery3d/data/ImageCacheRequest.java b/src/com/android/gallery3d/data/ImageCacheRequest.java new file mode 100644 index 000000000..6cbc5c5ea --- /dev/null +++ b/src/com/android/gallery3d/data/ImageCacheRequest.java @@ -0,0 +1,102 @@ +/* + * 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.data; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; + +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.common.BitmapUtils; +import com.android.gallery3d.data.BytesBufferPool.BytesBuffer; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +abstract class ImageCacheRequest implements Job<Bitmap> { + private static final String TAG = "ImageCacheRequest"; + + protected GalleryApp mApplication; + private Path mPath; + private int mType; + private int mTargetSize; + private long mTimeModified; + + public ImageCacheRequest(GalleryApp application, + Path path, long timeModified, int type, int targetSize) { + mApplication = application; + mPath = path; + mType = type; + mTargetSize = targetSize; + mTimeModified = timeModified; + } + + private String debugTag() { + return mPath + "," + mTimeModified + "," + + ((mType == MediaItem.TYPE_THUMBNAIL) ? "THUMB" : + (mType == MediaItem.TYPE_MICROTHUMBNAIL) ? "MICROTHUMB" : "?"); + } + + @Override + public Bitmap run(JobContext jc) { + ImageCacheService cacheService = mApplication.getImageCacheService(); + + BytesBuffer buffer = MediaItem.getBytesBufferPool().get(); + try { + boolean found = cacheService.getImageData(mPath, mTimeModified, mType, buffer); + if (jc.isCancelled()) return null; + if (found) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + Bitmap bitmap; + if (mType == MediaItem.TYPE_MICROTHUMBNAIL) { + bitmap = DecodeUtils.decodeUsingPool(jc, + buffer.data, buffer.offset, buffer.length, options); + } else { + bitmap = DecodeUtils.decodeUsingPool(jc, + buffer.data, buffer.offset, buffer.length, options); + } + if (bitmap == null && !jc.isCancelled()) { + Log.w(TAG, "decode cached failed " + debugTag()); + } + return bitmap; + } + } finally { + MediaItem.getBytesBufferPool().recycle(buffer); + } + Bitmap bitmap = onDecodeOriginal(jc, mType); + if (jc.isCancelled()) return null; + + if (bitmap == null) { + Log.w(TAG, "decode orig failed " + debugTag()); + return null; + } + + if (mType == MediaItem.TYPE_MICROTHUMBNAIL) { + bitmap = BitmapUtils.resizeAndCropCenter(bitmap, mTargetSize, true); + } else { + bitmap = BitmapUtils.resizeDownBySideLength(bitmap, mTargetSize, true); + } + if (jc.isCancelled()) return null; + + byte[] array = BitmapUtils.compressToBytes(bitmap); + if (jc.isCancelled()) return null; + + cacheService.putImageData(mPath, mTimeModified, mType, array); + return bitmap; + } + + public abstract Bitmap onDecodeOriginal(JobContext jc, int targetSize); +} diff --git a/src/com/android/gallery3d/data/ImageCacheService.java b/src/com/android/gallery3d/data/ImageCacheService.java new file mode 100644 index 000000000..1c7cb8c5e --- /dev/null +++ b/src/com/android/gallery3d/data/ImageCacheService.java @@ -0,0 +1,123 @@ +/* + * 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.data; + +import android.content.Context; + +import com.android.gallery3d.common.BlobCache; +import com.android.gallery3d.common.BlobCache.LookupRequest; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.BytesBufferPool.BytesBuffer; +import com.android.gallery3d.util.CacheManager; +import com.android.gallery3d.util.GalleryUtils; + +import java.io.IOException; +import java.nio.ByteBuffer; + +public class ImageCacheService { + @SuppressWarnings("unused") + private static final String TAG = "ImageCacheService"; + + private static final String IMAGE_CACHE_FILE = "imgcache"; + private static final int IMAGE_CACHE_MAX_ENTRIES = 5000; + private static final int IMAGE_CACHE_MAX_BYTES = 200 * 1024 * 1024; + private static final int IMAGE_CACHE_VERSION = 7; + + private BlobCache mCache; + + public ImageCacheService(Context context) { + mCache = CacheManager.getCache(context, IMAGE_CACHE_FILE, + IMAGE_CACHE_MAX_ENTRIES, IMAGE_CACHE_MAX_BYTES, + IMAGE_CACHE_VERSION); + } + + /** + * Gets the cached image data for the given <code>path</code>, + * <code>timeModified</code> and <code>type</code>. + * + * The image data will be stored in <code>buffer.data</code>, started from + * <code>buffer.offset</code> for <code>buffer.length</code> bytes. If the + * buffer.data is not big enough, a new byte array will be allocated and returned. + * + * @return true if the image data is found; false if not found. + */ + public boolean getImageData(Path path, long timeModified, int type, BytesBuffer buffer) { + byte[] key = makeKey(path, timeModified, type); + long cacheKey = Utils.crc64Long(key); + try { + LookupRequest request = new LookupRequest(); + request.key = cacheKey; + request.buffer = buffer.data; + synchronized (mCache) { + if (!mCache.lookup(request)) return false; + } + if (isSameKey(key, request.buffer)) { + buffer.data = request.buffer; + buffer.offset = key.length; + buffer.length = request.length - buffer.offset; + return true; + } + } catch (IOException ex) { + // ignore. + } + return false; + } + + public void putImageData(Path path, long timeModified, int type, byte[] value) { + byte[] key = makeKey(path, timeModified, type); + long cacheKey = Utils.crc64Long(key); + ByteBuffer buffer = ByteBuffer.allocate(key.length + value.length); + buffer.put(key); + buffer.put(value); + synchronized (mCache) { + try { + mCache.insert(cacheKey, buffer.array()); + } catch (IOException ex) { + // ignore. + } + } + } + + public void clearImageData(Path path, long timeModified, int type) { + byte[] key = makeKey(path, timeModified, type); + long cacheKey = Utils.crc64Long(key); + synchronized (mCache) { + try { + mCache.clearEntry(cacheKey); + } catch (IOException ex) { + // ignore. + } + } + } + + private static byte[] makeKey(Path path, long timeModified, int type) { + return GalleryUtils.getBytes(path.toString() + "+" + timeModified + "+" + type); + } + + private static boolean isSameKey(byte[] key, byte[] buffer) { + int n = key.length; + if (buffer.length < n) { + return false; + } + for (int i = 0; i < n; ++i) { + if (key[i] != buffer[i]) { + return false; + } + } + return true; + } +} diff --git a/src/com/android/gallery3d/data/LocalAlbum.java b/src/com/android/gallery3d/data/LocalAlbum.java new file mode 100644 index 000000000..7b7015af6 --- /dev/null +++ b/src/com/android/gallery3d/data/LocalAlbum.java @@ -0,0 +1,325 @@ +/* + * 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.data; + +import android.content.ContentResolver; +import android.content.res.Resources; +import android.database.Cursor; +import android.net.Uri; +import android.os.Environment; +import android.provider.MediaStore; +import android.provider.MediaStore.Images; +import android.provider.MediaStore.Images.ImageColumns; +import android.provider.MediaStore.Video; +import android.provider.MediaStore.Video.VideoColumns; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.util.BucketNames; +import com.android.gallery3d.util.GalleryUtils; +import com.android.gallery3d.util.MediaSetUtils; + +import java.io.File; +import java.util.ArrayList; + +// LocalAlbumSet lists all media items in one bucket on local storage. +// The media items need to be all images or all videos, but not both. +public class LocalAlbum extends MediaSet { + private static final String TAG = "LocalAlbum"; + private static final String[] COUNT_PROJECTION = { "count(*)" }; + + private static final int INVALID_COUNT = -1; + private final String mWhereClause; + private final String mOrderClause; + private final Uri mBaseUri; + private final String[] mProjection; + + private final GalleryApp mApplication; + private final ContentResolver mResolver; + private final int mBucketId; + private final String mName; + private final boolean mIsImage; + private final ChangeNotifier mNotifier; + private final Path mItemPath; + private int mCachedCount = INVALID_COUNT; + + public LocalAlbum(Path path, GalleryApp application, int bucketId, + boolean isImage, String name) { + super(path, nextVersionNumber()); + mApplication = application; + mResolver = application.getContentResolver(); + mBucketId = bucketId; + mName = name; + mIsImage = isImage; + + if (isImage) { + mWhereClause = ImageColumns.BUCKET_ID + " = ?"; + mOrderClause = ImageColumns.DATE_TAKEN + " DESC, " + + ImageColumns._ID + " DESC"; + mBaseUri = Images.Media.EXTERNAL_CONTENT_URI; + mProjection = LocalImage.PROJECTION; + mItemPath = LocalImage.ITEM_PATH; + } else { + mWhereClause = VideoColumns.BUCKET_ID + " = ?"; + mOrderClause = VideoColumns.DATE_TAKEN + " DESC, " + + VideoColumns._ID + " DESC"; + mBaseUri = Video.Media.EXTERNAL_CONTENT_URI; + mProjection = LocalVideo.PROJECTION; + mItemPath = LocalVideo.ITEM_PATH; + } + + mNotifier = new ChangeNotifier(this, mBaseUri, application); + } + + public LocalAlbum(Path path, GalleryApp application, int bucketId, + boolean isImage) { + this(path, application, bucketId, isImage, + BucketHelper.getBucketName( + application.getContentResolver(), bucketId)); + } + + @Override + public boolean isCameraRoll() { + return mBucketId == MediaSetUtils.CAMERA_BUCKET_ID; + } + + @Override + public Uri getContentUri() { + if (mIsImage) { + return MediaStore.Images.Media.EXTERNAL_CONTENT_URI.buildUpon() + .appendQueryParameter(LocalSource.KEY_BUCKET_ID, + String.valueOf(mBucketId)).build(); + } else { + return MediaStore.Video.Media.EXTERNAL_CONTENT_URI.buildUpon() + .appendQueryParameter(LocalSource.KEY_BUCKET_ID, + String.valueOf(mBucketId)).build(); + } + } + + @Override + public ArrayList<MediaItem> getMediaItem(int start, int count) { + DataManager dataManager = mApplication.getDataManager(); + Uri uri = mBaseUri.buildUpon() + .appendQueryParameter("limit", start + "," + count).build(); + ArrayList<MediaItem> list = new ArrayList<MediaItem>(); + GalleryUtils.assertNotInRenderThread(); + Cursor cursor = mResolver.query( + uri, mProjection, mWhereClause, + new String[]{String.valueOf(mBucketId)}, + mOrderClause); + if (cursor == null) { + Log.w(TAG, "query fail: " + uri); + return list; + } + + try { + while (cursor.moveToNext()) { + int id = cursor.getInt(0); // _id must be in the first column + Path childPath = mItemPath.getChild(id); + MediaItem item = loadOrUpdateItem(childPath, cursor, + dataManager, mApplication, mIsImage); + list.add(item); + } + } finally { + cursor.close(); + } + return list; + } + + private static MediaItem loadOrUpdateItem(Path path, Cursor cursor, + DataManager dataManager, GalleryApp app, boolean isImage) { + synchronized (DataManager.LOCK) { + LocalMediaItem item = (LocalMediaItem) dataManager.peekMediaObject(path); + if (item == null) { + if (isImage) { + item = new LocalImage(path, app, cursor); + } else { + item = new LocalVideo(path, app, cursor); + } + } else { + item.updateContent(cursor); + } + return item; + } + } + + // The pids array are sorted by the (path) id. + public static MediaItem[] getMediaItemById( + GalleryApp application, boolean isImage, ArrayList<Integer> ids) { + // get the lower and upper bound of (path) id + MediaItem[] result = new MediaItem[ids.size()]; + if (ids.isEmpty()) return result; + int idLow = ids.get(0); + int idHigh = ids.get(ids.size() - 1); + + // prepare the query parameters + Uri baseUri; + String[] projection; + Path itemPath; + if (isImage) { + baseUri = Images.Media.EXTERNAL_CONTENT_URI; + projection = LocalImage.PROJECTION; + itemPath = LocalImage.ITEM_PATH; + } else { + baseUri = Video.Media.EXTERNAL_CONTENT_URI; + projection = LocalVideo.PROJECTION; + itemPath = LocalVideo.ITEM_PATH; + } + + ContentResolver resolver = application.getContentResolver(); + DataManager dataManager = application.getDataManager(); + Cursor cursor = resolver.query(baseUri, projection, "_id BETWEEN ? AND ?", + new String[]{String.valueOf(idLow), String.valueOf(idHigh)}, + "_id"); + if (cursor == null) { + Log.w(TAG, "query fail" + baseUri); + return result; + } + try { + int n = ids.size(); + int i = 0; + + while (i < n && cursor.moveToNext()) { + int id = cursor.getInt(0); // _id must be in the first column + + // Match id with the one on the ids list. + if (ids.get(i) > id) { + continue; + } + + while (ids.get(i) < id) { + if (++i >= n) { + return result; + } + } + + Path childPath = itemPath.getChild(id); + MediaItem item = loadOrUpdateItem(childPath, cursor, dataManager, + application, isImage); + result[i] = item; + ++i; + } + return result; + } finally { + cursor.close(); + } + } + + public static Cursor getItemCursor(ContentResolver resolver, Uri uri, + String[] projection, int id) { + return resolver.query(uri, projection, "_id=?", + new String[]{String.valueOf(id)}, null); + } + + @Override + public int getMediaItemCount() { + if (mCachedCount == INVALID_COUNT) { + Cursor cursor = mResolver.query( + mBaseUri, COUNT_PROJECTION, mWhereClause, + new String[]{String.valueOf(mBucketId)}, null); + if (cursor == null) { + Log.w(TAG, "query fail"); + return 0; + } + try { + Utils.assertTrue(cursor.moveToNext()); + mCachedCount = cursor.getInt(0); + } finally { + cursor.close(); + } + } + return mCachedCount; + } + + @Override + public String getName() { + return getLocalizedName(mApplication.getResources(), mBucketId, mName); + } + + @Override + public long reload() { + if (mNotifier.isDirty()) { + mDataVersion = nextVersionNumber(); + mCachedCount = INVALID_COUNT; + } + return mDataVersion; + } + + @Override + public int getSupportedOperations() { + return SUPPORT_DELETE | SUPPORT_SHARE | SUPPORT_INFO; + } + + @Override + public void delete() { + GalleryUtils.assertNotInRenderThread(); + mResolver.delete(mBaseUri, mWhereClause, + new String[]{String.valueOf(mBucketId)}); + } + + @Override + public boolean isLeafAlbum() { + return true; + } + + public static String getLocalizedName(Resources res, int bucketId, + String name) { + if (bucketId == MediaSetUtils.CAMERA_BUCKET_ID) { + return res.getString(R.string.folder_camera); + } else if (bucketId == MediaSetUtils.DOWNLOAD_BUCKET_ID) { + return res.getString(R.string.folder_download); + } else if (bucketId == MediaSetUtils.IMPORTED_BUCKET_ID) { + return res.getString(R.string.folder_imported); + } else if (bucketId == MediaSetUtils.SNAPSHOT_BUCKET_ID) { + return res.getString(R.string.folder_screenshot); + } else if (bucketId == MediaSetUtils.EDITED_ONLINE_PHOTOS_BUCKET_ID) { + return res.getString(R.string.folder_edited_online_photos); + } else { + return name; + } + } + + // Relative path is the absolute path minus external storage path + public static String getRelativePath(int bucketId) { + String relativePath = "/"; + if (bucketId == MediaSetUtils.CAMERA_BUCKET_ID) { + relativePath += BucketNames.CAMERA; + } else if (bucketId == MediaSetUtils.DOWNLOAD_BUCKET_ID) { + relativePath += BucketNames.DOWNLOAD; + } else if (bucketId == MediaSetUtils.IMPORTED_BUCKET_ID) { + relativePath += BucketNames.IMPORTED; + } else if (bucketId == MediaSetUtils.SNAPSHOT_BUCKET_ID) { + relativePath += BucketNames.SCREENSHOTS; + } else if (bucketId == MediaSetUtils.EDITED_ONLINE_PHOTOS_BUCKET_ID) { + relativePath += BucketNames.EDITED_ONLINE_PHOTOS; + } else { + // If the first few cases didn't hit the matching path, do a + // thorough search in the local directories. + File extStorage = Environment.getExternalStorageDirectory(); + String path = GalleryUtils.searchDirForPath(extStorage, bucketId); + if (path == null) { + Log.w(TAG, "Relative path for bucket id: " + bucketId + " is not found."); + relativePath = null; + } else { + relativePath = path.substring(extStorage.getAbsolutePath().length()); + } + } + return relativePath; + } + +} diff --git a/src/com/android/gallery3d/data/LocalAlbumSet.java b/src/com/android/gallery3d/data/LocalAlbumSet.java new file mode 100644 index 000000000..b2b4b8c5d --- /dev/null +++ b/src/com/android/gallery3d/data/LocalAlbumSet.java @@ -0,0 +1,211 @@ +/* + * 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.data; + +import android.net.Uri; +import android.os.Handler; +import android.provider.MediaStore.Images; +import android.provider.MediaStore.Video; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.data.BucketHelper.BucketEntry; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.FutureListener; +import com.android.gallery3d.util.MediaSetUtils; +import com.android.gallery3d.util.ThreadPool; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import java.util.ArrayList; +import java.util.Comparator; + +// LocalAlbumSet lists all image or video albums in the local storage. +// The path should be "/local/image", "local/video" or "/local/all" +public class LocalAlbumSet extends MediaSet + implements FutureListener<ArrayList<MediaSet>> { + @SuppressWarnings("unused") + private static final String TAG = "LocalAlbumSet"; + + public static final Path PATH_ALL = Path.fromString("/local/all"); + public static final Path PATH_IMAGE = Path.fromString("/local/image"); + public static final Path PATH_VIDEO = Path.fromString("/local/video"); + + private static final Uri[] mWatchUris = + {Images.Media.EXTERNAL_CONTENT_URI, Video.Media.EXTERNAL_CONTENT_URI}; + + private final GalleryApp mApplication; + private final int mType; + private ArrayList<MediaSet> mAlbums = new ArrayList<MediaSet>(); + private final ChangeNotifier mNotifier; + private final String mName; + private final Handler mHandler; + private boolean mIsLoading; + + private Future<ArrayList<MediaSet>> mLoadTask; + private ArrayList<MediaSet> mLoadBuffer; + + public LocalAlbumSet(Path path, GalleryApp application) { + super(path, nextVersionNumber()); + mApplication = application; + mHandler = new Handler(application.getMainLooper()); + mType = getTypeFromPath(path); + mNotifier = new ChangeNotifier(this, mWatchUris, application); + mName = application.getResources().getString( + R.string.set_label_local_albums); + } + + private static int getTypeFromPath(Path path) { + String name[] = path.split(); + if (name.length < 2) { + throw new IllegalArgumentException(path.toString()); + } + return getTypeFromString(name[1]); + } + + @Override + public MediaSet getSubMediaSet(int index) { + return mAlbums.get(index); + } + + @Override + public int getSubMediaSetCount() { + return mAlbums.size(); + } + + @Override + public String getName() { + return mName; + } + + private static int findBucket(BucketEntry entries[], int bucketId) { + for (int i = 0, n = entries.length; i < n; ++i) { + if (entries[i].bucketId == bucketId) return i; + } + return -1; + } + + private class AlbumsLoader implements ThreadPool.Job<ArrayList<MediaSet>> { + + @Override + @SuppressWarnings("unchecked") + public ArrayList<MediaSet> run(JobContext jc) { + // Note: it will be faster if we only select media_type and bucket_id. + // need to test the performance if that is worth + BucketEntry[] entries = BucketHelper.loadBucketEntries( + jc, mApplication.getContentResolver(), mType); + + if (jc.isCancelled()) return null; + + int offset = 0; + // Move camera and download bucket to the front, while keeping the + // order of others. + int index = findBucket(entries, MediaSetUtils.CAMERA_BUCKET_ID); + if (index != -1) { + circularShiftRight(entries, offset++, index); + } + index = findBucket(entries, MediaSetUtils.DOWNLOAD_BUCKET_ID); + if (index != -1) { + circularShiftRight(entries, offset++, index); + } + + ArrayList<MediaSet> albums = new ArrayList<MediaSet>(); + DataManager dataManager = mApplication.getDataManager(); + for (BucketEntry entry : entries) { + MediaSet album = getLocalAlbum(dataManager, + mType, mPath, entry.bucketId, entry.bucketName); + albums.add(album); + } + return albums; + } + } + + private MediaSet getLocalAlbum( + DataManager manager, int type, Path parent, int id, String name) { + synchronized (DataManager.LOCK) { + Path path = parent.getChild(id); + MediaObject object = manager.peekMediaObject(path); + if (object != null) return (MediaSet) object; + switch (type) { + case MEDIA_TYPE_IMAGE: + return new LocalAlbum(path, mApplication, id, true, name); + case MEDIA_TYPE_VIDEO: + return new LocalAlbum(path, mApplication, id, false, name); + case MEDIA_TYPE_ALL: + Comparator<MediaItem> comp = DataManager.sDateTakenComparator; + return new LocalMergeAlbum(path, comp, new MediaSet[] { + getLocalAlbum(manager, MEDIA_TYPE_IMAGE, PATH_IMAGE, id, name), + getLocalAlbum(manager, MEDIA_TYPE_VIDEO, PATH_VIDEO, id, name)}, id); + } + throw new IllegalArgumentException(String.valueOf(type)); + } + } + + @Override + public synchronized boolean isLoading() { + return mIsLoading; + } + + @Override + // synchronized on this function for + // 1. Prevent calling reload() concurrently. + // 2. Prevent calling onFutureDone() and reload() concurrently + public synchronized long reload() { + if (mNotifier.isDirty()) { + if (mLoadTask != null) mLoadTask.cancel(); + mIsLoading = true; + mLoadTask = mApplication.getThreadPool().submit(new AlbumsLoader(), this); + } + if (mLoadBuffer != null) { + mAlbums = mLoadBuffer; + mLoadBuffer = null; + for (MediaSet album : mAlbums) { + album.reload(); + } + mDataVersion = nextVersionNumber(); + } + return mDataVersion; + } + + @Override + public synchronized void onFutureDone(Future<ArrayList<MediaSet>> future) { + if (mLoadTask != future) return; // ignore, wait for the latest task + mLoadBuffer = future.get(); + mIsLoading = false; + if (mLoadBuffer == null) mLoadBuffer = new ArrayList<MediaSet>(); + mHandler.post(new Runnable() { + @Override + public void run() { + notifyContentChanged(); + } + }); + } + + // For debug only. Fake there is a ContentObserver.onChange() event. + void fakeChange() { + mNotifier.fakeChange(); + } + + // Circular shift the array range from a[i] to a[j] (inclusive). That is, + // a[i] -> a[i+1] -> a[i+2] -> ... -> a[j], and a[j] -> a[i] + private static <T> void circularShiftRight(T[] array, int i, int j) { + T temp = array[j]; + for (int k = j; k > i; k--) { + array[k] = array[k - 1]; + } + array[i] = temp; + } +} diff --git a/src/com/android/gallery3d/data/LocalImage.java b/src/com/android/gallery3d/data/LocalImage.java new file mode 100644 index 000000000..cc70dd457 --- /dev/null +++ b/src/com/android/gallery3d/data/LocalImage.java @@ -0,0 +1,355 @@ +/* + * 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.data; + +import android.annotation.TargetApi; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.BitmapRegionDecoder; +import android.net.Uri; +import android.os.Build; +import android.provider.MediaStore.Images; +import android.provider.MediaStore.Images.ImageColumns; +import android.provider.MediaStore.MediaColumns; +import android.util.Log; + +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.app.PanoramaMetadataSupport; +import com.android.gallery3d.app.StitchingProgressManager; +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.common.BitmapUtils; +import com.android.gallery3d.exif.ExifInterface; +import com.android.gallery3d.exif.ExifTag; +import com.android.gallery3d.filtershow.tools.SaveImage; +import com.android.gallery3d.util.GalleryUtils; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; +import com.android.gallery3d.util.UpdateHelper; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; + +// LocalImage represents an image in the local storage. +public class LocalImage extends LocalMediaItem { + private static final String TAG = "LocalImage"; + + static final Path ITEM_PATH = Path.fromString("/local/image/item"); + + // Must preserve order between these indices and the order of the terms in + // the following PROJECTION array. + private static final int INDEX_ID = 0; + private static final int INDEX_CAPTION = 1; + private static final int INDEX_MIME_TYPE = 2; + private static final int INDEX_LATITUDE = 3; + private static final int INDEX_LONGITUDE = 4; + private static final int INDEX_DATE_TAKEN = 5; + private static final int INDEX_DATE_ADDED = 6; + private static final int INDEX_DATE_MODIFIED = 7; + private static final int INDEX_DATA = 8; + private static final int INDEX_ORIENTATION = 9; + private static final int INDEX_BUCKET_ID = 10; + private static final int INDEX_SIZE = 11; + private static final int INDEX_WIDTH = 12; + private static final int INDEX_HEIGHT = 13; + + static final String[] PROJECTION = { + ImageColumns._ID, // 0 + ImageColumns.TITLE, // 1 + ImageColumns.MIME_TYPE, // 2 + ImageColumns.LATITUDE, // 3 + ImageColumns.LONGITUDE, // 4 + ImageColumns.DATE_TAKEN, // 5 + ImageColumns.DATE_ADDED, // 6 + ImageColumns.DATE_MODIFIED, // 7 + ImageColumns.DATA, // 8 + ImageColumns.ORIENTATION, // 9 + ImageColumns.BUCKET_ID, // 10 + ImageColumns.SIZE, // 11 + "0", // 12 + "0" // 13 + }; + + static { + updateWidthAndHeightProjection(); + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + private static void updateWidthAndHeightProjection() { + if (ApiHelper.HAS_MEDIA_COLUMNS_WIDTH_AND_HEIGHT) { + PROJECTION[INDEX_WIDTH] = MediaColumns.WIDTH; + PROJECTION[INDEX_HEIGHT] = MediaColumns.HEIGHT; + } + } + + private final GalleryApp mApplication; + + public int rotation; + + private PanoramaMetadataSupport mPanoramaMetadata = new PanoramaMetadataSupport(this); + + public LocalImage(Path path, GalleryApp application, Cursor cursor) { + super(path, nextVersionNumber()); + mApplication = application; + loadFromCursor(cursor); + } + + public LocalImage(Path path, GalleryApp application, int id) { + super(path, nextVersionNumber()); + mApplication = application; + ContentResolver resolver = mApplication.getContentResolver(); + Uri uri = Images.Media.EXTERNAL_CONTENT_URI; + Cursor cursor = LocalAlbum.getItemCursor(resolver, uri, PROJECTION, id); + if (cursor == null) { + throw new RuntimeException("cannot get cursor for: " + path); + } + try { + if (cursor.moveToNext()) { + loadFromCursor(cursor); + } else { + throw new RuntimeException("cannot find data for: " + path); + } + } finally { + cursor.close(); + } + } + + private void loadFromCursor(Cursor cursor) { + id = cursor.getInt(INDEX_ID); + caption = cursor.getString(INDEX_CAPTION); + mimeType = cursor.getString(INDEX_MIME_TYPE); + latitude = cursor.getDouble(INDEX_LATITUDE); + longitude = cursor.getDouble(INDEX_LONGITUDE); + dateTakenInMs = cursor.getLong(INDEX_DATE_TAKEN); + dateAddedInSec = cursor.getLong(INDEX_DATE_ADDED); + dateModifiedInSec = cursor.getLong(INDEX_DATE_MODIFIED); + filePath = cursor.getString(INDEX_DATA); + rotation = cursor.getInt(INDEX_ORIENTATION); + bucketId = cursor.getInt(INDEX_BUCKET_ID); + fileSize = cursor.getLong(INDEX_SIZE); + width = cursor.getInt(INDEX_WIDTH); + height = cursor.getInt(INDEX_HEIGHT); + } + + @Override + protected boolean updateFromCursor(Cursor cursor) { + UpdateHelper uh = new UpdateHelper(); + id = uh.update(id, cursor.getInt(INDEX_ID)); + caption = uh.update(caption, cursor.getString(INDEX_CAPTION)); + mimeType = uh.update(mimeType, cursor.getString(INDEX_MIME_TYPE)); + latitude = uh.update(latitude, cursor.getDouble(INDEX_LATITUDE)); + longitude = uh.update(longitude, cursor.getDouble(INDEX_LONGITUDE)); + dateTakenInMs = uh.update( + dateTakenInMs, cursor.getLong(INDEX_DATE_TAKEN)); + dateAddedInSec = uh.update( + dateAddedInSec, cursor.getLong(INDEX_DATE_ADDED)); + dateModifiedInSec = uh.update( + dateModifiedInSec, cursor.getLong(INDEX_DATE_MODIFIED)); + filePath = uh.update(filePath, cursor.getString(INDEX_DATA)); + rotation = uh.update(rotation, cursor.getInt(INDEX_ORIENTATION)); + bucketId = uh.update(bucketId, cursor.getInt(INDEX_BUCKET_ID)); + fileSize = uh.update(fileSize, cursor.getLong(INDEX_SIZE)); + width = uh.update(width, cursor.getInt(INDEX_WIDTH)); + height = uh.update(height, cursor.getInt(INDEX_HEIGHT)); + return uh.isUpdated(); + } + + @Override + public Job<Bitmap> requestImage(int type) { + return new LocalImageRequest(mApplication, mPath, dateModifiedInSec, + type, filePath); + } + + public static class LocalImageRequest extends ImageCacheRequest { + private String mLocalFilePath; + + LocalImageRequest(GalleryApp application, Path path, long timeModified, + int type, String localFilePath) { + super(application, path, timeModified, type, + MediaItem.getTargetSize(type)); + mLocalFilePath = localFilePath; + } + + @Override + public Bitmap onDecodeOriginal(JobContext jc, final int type) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + int targetSize = MediaItem.getTargetSize(type); + + // try to decode from JPEG EXIF + if (type == MediaItem.TYPE_MICROTHUMBNAIL) { + ExifInterface exif = new ExifInterface(); + byte[] thumbData = null; + try { + exif.readExif(mLocalFilePath); + thumbData = exif.getThumbnail(); + } catch (FileNotFoundException e) { + Log.w(TAG, "failed to find file to read thumbnail: " + mLocalFilePath); + } catch (IOException e) { + Log.w(TAG, "failed to get thumbnail from: " + mLocalFilePath); + } + if (thumbData != null) { + Bitmap bitmap = DecodeUtils.decodeIfBigEnough( + jc, thumbData, options, targetSize); + if (bitmap != null) return bitmap; + } + } + + return DecodeUtils.decodeThumbnail(jc, mLocalFilePath, options, targetSize, type); + } + } + + @Override + public Job<BitmapRegionDecoder> requestLargeImage() { + return new LocalLargeImageRequest(filePath); + } + + public static class LocalLargeImageRequest + implements Job<BitmapRegionDecoder> { + String mLocalFilePath; + + public LocalLargeImageRequest(String localFilePath) { + mLocalFilePath = localFilePath; + } + + @Override + public BitmapRegionDecoder run(JobContext jc) { + return DecodeUtils.createBitmapRegionDecoder(jc, mLocalFilePath, false); + } + } + + @Override + public int getSupportedOperations() { + StitchingProgressManager progressManager = mApplication.getStitchingProgressManager(); + if (progressManager != null && progressManager.getProgress(getContentUri()) != null) { + return 0; // doesn't support anything while stitching! + } + int operation = SUPPORT_DELETE | SUPPORT_SHARE | SUPPORT_CROP + | SUPPORT_SETAS | SUPPORT_EDIT | SUPPORT_INFO; + if (BitmapUtils.isSupportedByRegionDecoder(mimeType)) { + operation |= SUPPORT_FULL_IMAGE; + } + + if (BitmapUtils.isRotationSupported(mimeType)) { + operation |= SUPPORT_ROTATE; + } + + if (GalleryUtils.isValidLocation(latitude, longitude)) { + operation |= SUPPORT_SHOW_ON_MAP; + } + return operation; + } + + @Override + public void getPanoramaSupport(PanoramaSupportCallback callback) { + mPanoramaMetadata.getPanoramaSupport(mApplication, callback); + } + + @Override + public void clearCachedPanoramaSupport() { + mPanoramaMetadata.clearCachedValues(); + } + + @Override + public void delete() { + GalleryUtils.assertNotInRenderThread(); + Uri baseUri = Images.Media.EXTERNAL_CONTENT_URI; + ContentResolver contentResolver = mApplication.getContentResolver(); + SaveImage.deleteAuxFiles(contentResolver, getContentUri()); + contentResolver.delete(baseUri, "_id=?", + new String[]{String.valueOf(id)}); + } + + @Override + public void rotate(int degrees) { + GalleryUtils.assertNotInRenderThread(); + Uri baseUri = Images.Media.EXTERNAL_CONTENT_URI; + ContentValues values = new ContentValues(); + int rotation = (this.rotation + degrees) % 360; + if (rotation < 0) rotation += 360; + + if (mimeType.equalsIgnoreCase("image/jpeg")) { + ExifInterface exifInterface = new ExifInterface(); + ExifTag tag = exifInterface.buildTag(ExifInterface.TAG_ORIENTATION, + ExifInterface.getOrientationValueForRotation(rotation)); + if(tag != null) { + exifInterface.setTag(tag); + try { + exifInterface.forceRewriteExif(filePath); + fileSize = new File(filePath).length(); + values.put(Images.Media.SIZE, fileSize); + } catch (FileNotFoundException e) { + Log.w(TAG, "cannot find file to set exif: " + filePath); + } catch (IOException e) { + Log.w(TAG, "cannot set exif data: " + filePath); + } + } else { + Log.w(TAG, "Could not build tag: " + ExifInterface.TAG_ORIENTATION); + } + } + + values.put(Images.Media.ORIENTATION, rotation); + mApplication.getContentResolver().update(baseUri, values, "_id=?", + new String[]{String.valueOf(id)}); + } + + @Override + public Uri getContentUri() { + Uri baseUri = Images.Media.EXTERNAL_CONTENT_URI; + return baseUri.buildUpon().appendPath(String.valueOf(id)).build(); + } + + @Override + public int getMediaType() { + return MEDIA_TYPE_IMAGE; + } + + @Override + public MediaDetails getDetails() { + MediaDetails details = super.getDetails(); + details.addDetail(MediaDetails.INDEX_ORIENTATION, Integer.valueOf(rotation)); + if (MIME_TYPE_JPEG.equals(mimeType)) { + // ExifInterface returns incorrect values for photos in other format. + // For example, the width and height of an webp images is always '0'. + MediaDetails.extractExifInfo(details, filePath); + } + return details; + } + + @Override + public int getRotation() { + return rotation; + } + + @Override + public int getWidth() { + return width; + } + + @Override + public int getHeight() { + return height; + } + + @Override + public String getFilePath() { + return filePath; + } +} diff --git a/src/com/android/gallery3d/data/LocalMediaItem.java b/src/com/android/gallery3d/data/LocalMediaItem.java new file mode 100644 index 000000000..7e003cd3a --- /dev/null +++ b/src/com/android/gallery3d/data/LocalMediaItem.java @@ -0,0 +1,109 @@ +/* + * 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.data; + +import android.database.Cursor; + +import com.android.gallery3d.util.GalleryUtils; + +import java.text.DateFormat; +import java.util.Date; + +// +// LocalMediaItem is an abstract class captures those common fields +// in LocalImage and LocalVideo. +// +public abstract class LocalMediaItem extends MediaItem { + + @SuppressWarnings("unused") + private static final String TAG = "LocalMediaItem"; + + // database fields + public int id; + public String caption; + public String mimeType; + public long fileSize; + public double latitude = INVALID_LATLNG; + public double longitude = INVALID_LATLNG; + public long dateTakenInMs; + public long dateAddedInSec; + public long dateModifiedInSec; + public String filePath; + public int bucketId; + public int width; + public int height; + + public LocalMediaItem(Path path, long version) { + super(path, version); + } + + @Override + public long getDateInMs() { + return dateTakenInMs; + } + + @Override + public String getName() { + return caption; + } + + @Override + public void getLatLong(double[] latLong) { + latLong[0] = latitude; + latLong[1] = longitude; + } + + abstract protected boolean updateFromCursor(Cursor cursor); + + public int getBucketId() { + return bucketId; + } + + protected void updateContent(Cursor cursor) { + if (updateFromCursor(cursor)) { + mDataVersion = nextVersionNumber(); + } + } + + @Override + public MediaDetails getDetails() { + MediaDetails details = super.getDetails(); + details.addDetail(MediaDetails.INDEX_PATH, filePath); + details.addDetail(MediaDetails.INDEX_TITLE, caption); + DateFormat formater = DateFormat.getDateTimeInstance(); + details.addDetail(MediaDetails.INDEX_DATETIME, + formater.format(new Date(dateModifiedInSec * 1000))); + details.addDetail(MediaDetails.INDEX_WIDTH, width); + details.addDetail(MediaDetails.INDEX_HEIGHT, height); + + if (GalleryUtils.isValidLocation(latitude, longitude)) { + details.addDetail(MediaDetails.INDEX_LOCATION, new double[] {latitude, longitude}); + } + if (fileSize > 0) details.addDetail(MediaDetails.INDEX_SIZE, fileSize); + return details; + } + + @Override + public String getMimeType() { + return mimeType; + } + + @Override + public long getSize() { + return fileSize; + } +} diff --git a/src/com/android/gallery3d/data/LocalMergeAlbum.java b/src/com/android/gallery3d/data/LocalMergeAlbum.java new file mode 100644 index 000000000..f0b5e5726 --- /dev/null +++ b/src/com/android/gallery3d/data/LocalMergeAlbum.java @@ -0,0 +1,257 @@ +/* + * 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.data; + +import android.net.Uri; +import android.provider.MediaStore; + +import com.android.gallery3d.common.ApiHelper; + +import java.lang.ref.SoftReference; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.NoSuchElementException; +import java.util.SortedMap; +import java.util.TreeMap; + +// MergeAlbum merges items from two or more MediaSets. It uses a Comparator to +// determine the order of items. The items are assumed to be sorted in the input +// media sets (with the same order that the Comparator uses). +// +// This only handles MediaItems, not SubMediaSets. +public class LocalMergeAlbum extends MediaSet implements ContentListener { + @SuppressWarnings("unused") + private static final String TAG = "LocalMergeAlbum"; + private static final int PAGE_SIZE = 64; + + private final Comparator<MediaItem> mComparator; + private final MediaSet[] mSources; + + private FetchCache[] mFetcher; + private int mSupportedOperation; + private int mBucketId; + + // mIndex maps global position to the position of each underlying media sets. + private TreeMap<Integer, int[]> mIndex = new TreeMap<Integer, int[]>(); + + public LocalMergeAlbum( + Path path, Comparator<MediaItem> comparator, MediaSet[] sources, int bucketId) { + super(path, INVALID_DATA_VERSION); + mComparator = comparator; + mSources = sources; + mBucketId = bucketId; + for (MediaSet set : mSources) { + set.addContentListener(this); + } + reload(); + } + + @Override + public boolean isCameraRoll() { + if (mSources.length == 0) return false; + for(MediaSet set : mSources) { + if (!set.isCameraRoll()) return false; + } + return true; + } + + private void updateData() { + ArrayList<MediaSet> matches = new ArrayList<MediaSet>(); + int supported = mSources.length == 0 ? 0 : MediaItem.SUPPORT_ALL; + mFetcher = new FetchCache[mSources.length]; + for (int i = 0, n = mSources.length; i < n; ++i) { + mFetcher[i] = new FetchCache(mSources[i]); + supported &= mSources[i].getSupportedOperations(); + } + mSupportedOperation = supported; + mIndex.clear(); + mIndex.put(0, new int[mSources.length]); + } + + private void invalidateCache() { + for (int i = 0, n = mSources.length; i < n; i++) { + mFetcher[i].invalidate(); + } + mIndex.clear(); + mIndex.put(0, new int[mSources.length]); + } + + @Override + public Uri getContentUri() { + String bucketId = String.valueOf(mBucketId); + if (ApiHelper.HAS_MEDIA_PROVIDER_FILES_TABLE) { + return MediaStore.Files.getContentUri("external").buildUpon() + .appendQueryParameter(LocalSource.KEY_BUCKET_ID, bucketId) + .build(); + } else { + // We don't have a single URL for a merged image before ICS + // So we used the image's URL as a substitute. + return MediaStore.Images.Media.EXTERNAL_CONTENT_URI.buildUpon() + .appendQueryParameter(LocalSource.KEY_BUCKET_ID, bucketId) + .build(); + } + } + + @Override + public String getName() { + return mSources.length == 0 ? "" : mSources[0].getName(); + } + + @Override + public int getMediaItemCount() { + return getTotalMediaItemCount(); + } + + @Override + public ArrayList<MediaItem> getMediaItem(int start, int count) { + + // First find the nearest mark position <= start. + SortedMap<Integer, int[]> head = mIndex.headMap(start + 1); + int markPos = head.lastKey(); + int[] subPos = head.get(markPos).clone(); + MediaItem[] slot = new MediaItem[mSources.length]; + + int size = mSources.length; + + // fill all slots + for (int i = 0; i < size; i++) { + slot[i] = mFetcher[i].getItem(subPos[i]); + } + + ArrayList<MediaItem> result = new ArrayList<MediaItem>(); + + for (int i = markPos; i < start + count; i++) { + int k = -1; // k points to the best slot up to now. + for (int j = 0; j < size; j++) { + if (slot[j] != null) { + if (k == -1 || mComparator.compare(slot[j], slot[k]) < 0) { + k = j; + } + } + } + + // If we don't have anything, all streams are exhausted. + if (k == -1) break; + + // Pick the best slot and refill it. + subPos[k]++; + if (i >= start) { + result.add(slot[k]); + } + slot[k] = mFetcher[k].getItem(subPos[k]); + + // Periodically leave a mark in the index, so we can come back later. + if ((i + 1) % PAGE_SIZE == 0) { + mIndex.put(i + 1, subPos.clone()); + } + } + + return result; + } + + @Override + public int getTotalMediaItemCount() { + int count = 0; + for (MediaSet set : mSources) { + count += set.getTotalMediaItemCount(); + } + return count; + } + + @Override + public long reload() { + boolean changed = false; + for (int i = 0, n = mSources.length; i < n; ++i) { + if (mSources[i].reload() > mDataVersion) changed = true; + } + if (changed) { + mDataVersion = nextVersionNumber(); + updateData(); + invalidateCache(); + } + return mDataVersion; + } + + @Override + public void onContentDirty() { + notifyContentChanged(); + } + + @Override + public int getSupportedOperations() { + return mSupportedOperation; + } + + @Override + public void delete() { + for (MediaSet set : mSources) { + set.delete(); + } + } + + @Override + public void rotate(int degrees) { + for (MediaSet set : mSources) { + set.rotate(degrees); + } + } + + private static class FetchCache { + private MediaSet mBaseSet; + private SoftReference<ArrayList<MediaItem>> mCacheRef; + private int mStartPos; + + public FetchCache(MediaSet baseSet) { + mBaseSet = baseSet; + } + + public void invalidate() { + mCacheRef = null; + } + + public MediaItem getItem(int index) { + boolean needLoading = false; + ArrayList<MediaItem> cache = null; + if (mCacheRef == null + || index < mStartPos || index >= mStartPos + PAGE_SIZE) { + needLoading = true; + } else { + cache = mCacheRef.get(); + if (cache == null) { + needLoading = true; + } + } + + if (needLoading) { + cache = mBaseSet.getMediaItem(index, PAGE_SIZE); + mCacheRef = new SoftReference<ArrayList<MediaItem>>(cache); + mStartPos = index; + } + + if (index < mStartPos || index >= mStartPos + cache.size()) { + return null; + } + + return cache.get(index - mStartPos); + } + } + + @Override + public boolean isLeafAlbum() { + return true; + } +} diff --git a/src/com/android/gallery3d/data/LocalSource.java b/src/com/android/gallery3d/data/LocalSource.java new file mode 100644 index 000000000..a2e3d1443 --- /dev/null +++ b/src/com/android/gallery3d/data/LocalSource.java @@ -0,0 +1,275 @@ +/* + * 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.data; + +import android.content.ContentProviderClient; +import android.content.ContentUris; +import android.content.UriMatcher; +import android.net.Uri; +import android.provider.MediaStore; + +import com.android.gallery3d.app.Gallery; +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.data.MediaSet.ItemConsumer; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; + +class LocalSource extends MediaSource { + + public static final String KEY_BUCKET_ID = "bucketId"; + + private GalleryApp mApplication; + private PathMatcher mMatcher; + private static final int NO_MATCH = -1; + private final UriMatcher mUriMatcher = new UriMatcher(NO_MATCH); + public static final Comparator<PathId> sIdComparator = new IdComparator(); + + private static final int LOCAL_IMAGE_ALBUMSET = 0; + private static final int LOCAL_VIDEO_ALBUMSET = 1; + private static final int LOCAL_IMAGE_ALBUM = 2; + private static final int LOCAL_VIDEO_ALBUM = 3; + private static final int LOCAL_IMAGE_ITEM = 4; + private static final int LOCAL_VIDEO_ITEM = 5; + private static final int LOCAL_ALL_ALBUMSET = 6; + private static final int LOCAL_ALL_ALBUM = 7; + + private static final String TAG = "LocalSource"; + + private ContentProviderClient mClient; + + public LocalSource(GalleryApp context) { + super("local"); + mApplication = context; + mMatcher = new PathMatcher(); + mMatcher.add("/local/image", LOCAL_IMAGE_ALBUMSET); + mMatcher.add("/local/video", LOCAL_VIDEO_ALBUMSET); + mMatcher.add("/local/all", LOCAL_ALL_ALBUMSET); + + mMatcher.add("/local/image/*", LOCAL_IMAGE_ALBUM); + mMatcher.add("/local/video/*", LOCAL_VIDEO_ALBUM); + mMatcher.add("/local/all/*", LOCAL_ALL_ALBUM); + mMatcher.add("/local/image/item/*", LOCAL_IMAGE_ITEM); + mMatcher.add("/local/video/item/*", LOCAL_VIDEO_ITEM); + + mUriMatcher.addURI(MediaStore.AUTHORITY, + "external/images/media/#", LOCAL_IMAGE_ITEM); + mUriMatcher.addURI(MediaStore.AUTHORITY, + "external/video/media/#", LOCAL_VIDEO_ITEM); + mUriMatcher.addURI(MediaStore.AUTHORITY, + "external/images/media", LOCAL_IMAGE_ALBUM); + mUriMatcher.addURI(MediaStore.AUTHORITY, + "external/video/media", LOCAL_VIDEO_ALBUM); + mUriMatcher.addURI(MediaStore.AUTHORITY, + "external/file", LOCAL_ALL_ALBUM); + } + + @Override + public MediaObject createMediaObject(Path path) { + GalleryApp app = mApplication; + switch (mMatcher.match(path)) { + case LOCAL_ALL_ALBUMSET: + case LOCAL_IMAGE_ALBUMSET: + case LOCAL_VIDEO_ALBUMSET: + return new LocalAlbumSet(path, mApplication); + case LOCAL_IMAGE_ALBUM: + return new LocalAlbum(path, app, mMatcher.getIntVar(0), true); + case LOCAL_VIDEO_ALBUM: + return new LocalAlbum(path, app, mMatcher.getIntVar(0), false); + case LOCAL_ALL_ALBUM: { + int bucketId = mMatcher.getIntVar(0); + DataManager dataManager = app.getDataManager(); + MediaSet imageSet = (MediaSet) dataManager.getMediaObject( + LocalAlbumSet.PATH_IMAGE.getChild(bucketId)); + MediaSet videoSet = (MediaSet) dataManager.getMediaObject( + LocalAlbumSet.PATH_VIDEO.getChild(bucketId)); + Comparator<MediaItem> comp = DataManager.sDateTakenComparator; + return new LocalMergeAlbum( + path, comp, new MediaSet[] {imageSet, videoSet}, bucketId); + } + case LOCAL_IMAGE_ITEM: + return new LocalImage(path, mApplication, mMatcher.getIntVar(0)); + case LOCAL_VIDEO_ITEM: + return new LocalVideo(path, mApplication, mMatcher.getIntVar(0)); + default: + throw new RuntimeException("bad path: " + path); + } + } + + private static int getMediaType(String type, int defaultType) { + if (type == null) return defaultType; + try { + int value = Integer.parseInt(type); + if ((value & (MEDIA_TYPE_IMAGE + | MEDIA_TYPE_VIDEO)) != 0) return value; + } catch (NumberFormatException e) { + Log.w(TAG, "invalid type: " + type, e); + } + return defaultType; + } + + // The media type bit passed by the intent + private static final int MEDIA_TYPE_ALL = 0; + private static final int MEDIA_TYPE_IMAGE = 1; + private static final int MEDIA_TYPE_VIDEO = 4; + + private Path getAlbumPath(Uri uri, int defaultType) { + int mediaType = getMediaType( + uri.getQueryParameter(Gallery.KEY_MEDIA_TYPES), + defaultType); + String bucketId = uri.getQueryParameter(KEY_BUCKET_ID); + int id = 0; + try { + id = Integer.parseInt(bucketId); + } catch (NumberFormatException e) { + Log.w(TAG, "invalid bucket id: " + bucketId, e); + return null; + } + switch (mediaType) { + case MEDIA_TYPE_IMAGE: + return Path.fromString("/local/image").getChild(id); + case MEDIA_TYPE_VIDEO: + return Path.fromString("/local/video").getChild(id); + default: + return Path.fromString("/local/all").getChild(id); + } + } + + @Override + public Path findPathByUri(Uri uri, String type) { + try { + switch (mUriMatcher.match(uri)) { + case LOCAL_IMAGE_ITEM: { + long id = ContentUris.parseId(uri); + return id >= 0 ? LocalImage.ITEM_PATH.getChild(id) : null; + } + case LOCAL_VIDEO_ITEM: { + long id = ContentUris.parseId(uri); + return id >= 0 ? LocalVideo.ITEM_PATH.getChild(id) : null; + } + case LOCAL_IMAGE_ALBUM: { + return getAlbumPath(uri, MEDIA_TYPE_IMAGE); + } + case LOCAL_VIDEO_ALBUM: { + return getAlbumPath(uri, MEDIA_TYPE_VIDEO); + } + case LOCAL_ALL_ALBUM: { + return getAlbumPath(uri, MEDIA_TYPE_ALL); + } + } + } catch (NumberFormatException e) { + Log.w(TAG, "uri: " + uri.toString(), e); + } + return null; + } + + @Override + public Path getDefaultSetOf(Path item) { + MediaObject object = mApplication.getDataManager().getMediaObject(item); + if (object instanceof LocalMediaItem) { + return Path.fromString("/local/all").getChild( + String.valueOf(((LocalMediaItem) object).getBucketId())); + } + return null; + } + + @Override + public void mapMediaItems(ArrayList<PathId> list, ItemConsumer consumer) { + ArrayList<PathId> imageList = new ArrayList<PathId>(); + ArrayList<PathId> videoList = new ArrayList<PathId>(); + int n = list.size(); + for (int i = 0; i < n; i++) { + PathId pid = list.get(i); + // We assume the form is: "/local/{image,video}/item/#" + // We don't use mMatcher for efficiency's reason. + Path parent = pid.path.getParent(); + if (parent == LocalImage.ITEM_PATH) { + imageList.add(pid); + } else if (parent == LocalVideo.ITEM_PATH) { + videoList.add(pid); + } + } + // TODO: use "files" table so we can merge the two cases. + processMapMediaItems(imageList, consumer, true); + processMapMediaItems(videoList, consumer, false); + } + + private void processMapMediaItems(ArrayList<PathId> list, + ItemConsumer consumer, boolean isImage) { + // Sort path by path id + Collections.sort(list, sIdComparator); + int n = list.size(); + for (int i = 0; i < n; ) { + PathId pid = list.get(i); + + // Find a range of items. + ArrayList<Integer> ids = new ArrayList<Integer>(); + int startId = Integer.parseInt(pid.path.getSuffix()); + ids.add(startId); + + int j; + for (j = i + 1; j < n; j++) { + PathId pid2 = list.get(j); + int curId = Integer.parseInt(pid2.path.getSuffix()); + if (curId - startId >= MediaSet.MEDIAITEM_BATCH_FETCH_COUNT) { + break; + } + ids.add(curId); + } + + MediaItem[] items = LocalAlbum.getMediaItemById( + mApplication, isImage, ids); + for(int k = i ; k < j; k++) { + PathId pid2 = list.get(k); + consumer.consume(pid2.id, items[k - i]); + } + + i = j; + } + } + + // This is a comparator which compares the suffix number in two Paths. + private static class IdComparator implements Comparator<PathId> { + @Override + public int compare(PathId p1, PathId p2) { + String s1 = p1.path.getSuffix(); + String s2 = p2.path.getSuffix(); + int len1 = s1.length(); + int len2 = s2.length(); + if (len1 < len2) { + return -1; + } else if (len1 > len2) { + return 1; + } else { + return s1.compareTo(s2); + } + } + } + + @Override + public void resume() { + mClient = mApplication.getContentResolver() + .acquireContentProviderClient(MediaStore.AUTHORITY); + } + + @Override + public void pause() { + mClient.release(); + mClient = null; + } +} diff --git a/src/com/android/gallery3d/data/LocalVideo.java b/src/com/android/gallery3d/data/LocalVideo.java new file mode 100644 index 000000000..4b8774ca4 --- /dev/null +++ b/src/com/android/gallery3d/data/LocalVideo.java @@ -0,0 +1,242 @@ +/* + * 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.data; + +import android.content.ContentResolver; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapRegionDecoder; +import android.net.Uri; +import android.provider.MediaStore.Video; +import android.provider.MediaStore.Video.VideoColumns; + +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.common.BitmapUtils; +import com.android.gallery3d.util.GalleryUtils; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; +import com.android.gallery3d.util.UpdateHelper; + +// LocalVideo represents a video in the local storage. +public class LocalVideo extends LocalMediaItem { + private static final String TAG = "LocalVideo"; + static final Path ITEM_PATH = Path.fromString("/local/video/item"); + + // Must preserve order between these indices and the order of the terms in + // the following PROJECTION array. + private static final int INDEX_ID = 0; + private static final int INDEX_CAPTION = 1; + private static final int INDEX_MIME_TYPE = 2; + private static final int INDEX_LATITUDE = 3; + private static final int INDEX_LONGITUDE = 4; + private static final int INDEX_DATE_TAKEN = 5; + private static final int INDEX_DATE_ADDED = 6; + private static final int INDEX_DATE_MODIFIED = 7; + private static final int INDEX_DATA = 8; + private static final int INDEX_DURATION = 9; + private static final int INDEX_BUCKET_ID = 10; + private static final int INDEX_SIZE = 11; + private static final int INDEX_RESOLUTION = 12; + + static final String[] PROJECTION = new String[] { + VideoColumns._ID, + VideoColumns.TITLE, + VideoColumns.MIME_TYPE, + VideoColumns.LATITUDE, + VideoColumns.LONGITUDE, + VideoColumns.DATE_TAKEN, + VideoColumns.DATE_ADDED, + VideoColumns.DATE_MODIFIED, + VideoColumns.DATA, + VideoColumns.DURATION, + VideoColumns.BUCKET_ID, + VideoColumns.SIZE, + VideoColumns.RESOLUTION, + }; + + private final GalleryApp mApplication; + + public int durationInSec; + + public LocalVideo(Path path, GalleryApp application, Cursor cursor) { + super(path, nextVersionNumber()); + mApplication = application; + loadFromCursor(cursor); + } + + public LocalVideo(Path path, GalleryApp context, int id) { + super(path, nextVersionNumber()); + mApplication = context; + ContentResolver resolver = mApplication.getContentResolver(); + Uri uri = Video.Media.EXTERNAL_CONTENT_URI; + Cursor cursor = LocalAlbum.getItemCursor(resolver, uri, PROJECTION, id); + if (cursor == null) { + throw new RuntimeException("cannot get cursor for: " + path); + } + try { + if (cursor.moveToNext()) { + loadFromCursor(cursor); + } else { + throw new RuntimeException("cannot find data for: " + path); + } + } finally { + cursor.close(); + } + } + + private void loadFromCursor(Cursor cursor) { + id = cursor.getInt(INDEX_ID); + caption = cursor.getString(INDEX_CAPTION); + mimeType = cursor.getString(INDEX_MIME_TYPE); + latitude = cursor.getDouble(INDEX_LATITUDE); + longitude = cursor.getDouble(INDEX_LONGITUDE); + dateTakenInMs = cursor.getLong(INDEX_DATE_TAKEN); + dateAddedInSec = cursor.getLong(INDEX_DATE_ADDED); + dateModifiedInSec = cursor.getLong(INDEX_DATE_MODIFIED); + filePath = cursor.getString(INDEX_DATA); + durationInSec = cursor.getInt(INDEX_DURATION) / 1000; + bucketId = cursor.getInt(INDEX_BUCKET_ID); + fileSize = cursor.getLong(INDEX_SIZE); + parseResolution(cursor.getString(INDEX_RESOLUTION)); + } + + private void parseResolution(String resolution) { + if (resolution == null) return; + int m = resolution.indexOf('x'); + if (m == -1) return; + try { + int w = Integer.parseInt(resolution.substring(0, m)); + int h = Integer.parseInt(resolution.substring(m + 1)); + width = w; + height = h; + } catch (Throwable t) { + Log.w(TAG, t); + } + } + + @Override + protected boolean updateFromCursor(Cursor cursor) { + UpdateHelper uh = new UpdateHelper(); + id = uh.update(id, cursor.getInt(INDEX_ID)); + caption = uh.update(caption, cursor.getString(INDEX_CAPTION)); + mimeType = uh.update(mimeType, cursor.getString(INDEX_MIME_TYPE)); + latitude = uh.update(latitude, cursor.getDouble(INDEX_LATITUDE)); + longitude = uh.update(longitude, cursor.getDouble(INDEX_LONGITUDE)); + dateTakenInMs = uh.update( + dateTakenInMs, cursor.getLong(INDEX_DATE_TAKEN)); + dateAddedInSec = uh.update( + dateAddedInSec, cursor.getLong(INDEX_DATE_ADDED)); + dateModifiedInSec = uh.update( + dateModifiedInSec, cursor.getLong(INDEX_DATE_MODIFIED)); + filePath = uh.update(filePath, cursor.getString(INDEX_DATA)); + durationInSec = uh.update( + durationInSec, cursor.getInt(INDEX_DURATION) / 1000); + bucketId = uh.update(bucketId, cursor.getInt(INDEX_BUCKET_ID)); + fileSize = uh.update(fileSize, cursor.getLong(INDEX_SIZE)); + return uh.isUpdated(); + } + + @Override + public Job<Bitmap> requestImage(int type) { + return new LocalVideoRequest(mApplication, getPath(), dateModifiedInSec, + type, filePath); + } + + public static class LocalVideoRequest extends ImageCacheRequest { + private String mLocalFilePath; + + LocalVideoRequest(GalleryApp application, Path path, long timeModified, + int type, String localFilePath) { + super(application, path, timeModified, type, + MediaItem.getTargetSize(type)); + mLocalFilePath = localFilePath; + } + + @Override + public Bitmap onDecodeOriginal(JobContext jc, int type) { + Bitmap bitmap = BitmapUtils.createVideoThumbnail(mLocalFilePath); + if (bitmap == null || jc.isCancelled()) return null; + return bitmap; + } + } + + @Override + public Job<BitmapRegionDecoder> requestLargeImage() { + throw new UnsupportedOperationException("Cannot regquest a large image" + + " to a local video!"); + } + + @Override + public int getSupportedOperations() { + return SUPPORT_DELETE | SUPPORT_SHARE | SUPPORT_PLAY | SUPPORT_INFO | SUPPORT_TRIM | SUPPORT_MUTE; + } + + @Override + public void delete() { + GalleryUtils.assertNotInRenderThread(); + Uri baseUri = Video.Media.EXTERNAL_CONTENT_URI; + mApplication.getContentResolver().delete(baseUri, "_id=?", + new String[]{String.valueOf(id)}); + } + + @Override + public void rotate(int degrees) { + // TODO + } + + @Override + public Uri getContentUri() { + Uri baseUri = Video.Media.EXTERNAL_CONTENT_URI; + return baseUri.buildUpon().appendPath(String.valueOf(id)).build(); + } + + @Override + public Uri getPlayUri() { + return getContentUri(); + } + + @Override + public int getMediaType() { + return MEDIA_TYPE_VIDEO; + } + + @Override + public MediaDetails getDetails() { + MediaDetails details = super.getDetails(); + int s = durationInSec; + if (s > 0) { + details.addDetail(MediaDetails.INDEX_DURATION, GalleryUtils.formatDuration( + mApplication.getAndroidContext(), durationInSec)); + } + return details; + } + + @Override + public int getWidth() { + return width; + } + + @Override + public int getHeight() { + return height; + } + + @Override + public String getFilePath() { + return filePath; + } +} diff --git a/src/com/android/gallery3d/data/LocationClustering.java b/src/com/android/gallery3d/data/LocationClustering.java new file mode 100644 index 000000000..540322a33 --- /dev/null +++ b/src/com/android/gallery3d/data/LocationClustering.java @@ -0,0 +1,316 @@ +/* + * 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.data; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.util.FloatMath; +import android.widget.Toast; + +import com.android.gallery3d.R; +import com.android.gallery3d.util.GalleryUtils; +import com.android.gallery3d.util.ReverseGeocoder; + +import java.util.ArrayList; + +class LocationClustering extends Clustering { + @SuppressWarnings("unused") + private static final String TAG = "LocationClustering"; + + private static final int MIN_GROUPS = 1; + private static final int MAX_GROUPS = 20; + private static final int MAX_ITERATIONS = 30; + + // If the total distance change is less than this ratio, stop iterating. + private static final float STOP_CHANGE_RATIO = 0.01f; + private Context mContext; + private ArrayList<ArrayList<SmallItem>> mClusters; + private ArrayList<String> mNames; + private String mNoLocationString; + private Handler mHandler; + + private static class Point { + public Point(double lat, double lng) { + latRad = Math.toRadians(lat); + lngRad = Math.toRadians(lng); + } + public Point() {} + public double latRad, lngRad; + } + + private static class SmallItem { + Path path; + double lat, lng; + } + + public LocationClustering(Context context) { + mContext = context; + mNoLocationString = mContext.getResources().getString(R.string.no_location); + mHandler = new Handler(Looper.getMainLooper()); + } + + @Override + public void run(MediaSet baseSet) { + final int total = baseSet.getTotalMediaItemCount(); + final SmallItem[] buf = new SmallItem[total]; + // Separate items to two sets: with or without lat-long. + final double[] latLong = new double[2]; + baseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() { + @Override + public void consume(int index, MediaItem item) { + if (index < 0 || index >= total) return; + SmallItem s = new SmallItem(); + s.path = item.getPath(); + item.getLatLong(latLong); + s.lat = latLong[0]; + s.lng = latLong[1]; + buf[index] = s; + } + }); + + final ArrayList<SmallItem> withLatLong = new ArrayList<SmallItem>(); + final ArrayList<SmallItem> withoutLatLong = new ArrayList<SmallItem>(); + final ArrayList<Point> points = new ArrayList<Point>(); + for (int i = 0; i < total; i++) { + SmallItem s = buf[i]; + if (s == null) continue; + if (GalleryUtils.isValidLocation(s.lat, s.lng)) { + withLatLong.add(s); + points.add(new Point(s.lat, s.lng)); + } else { + withoutLatLong.add(s); + } + } + + ArrayList<ArrayList<SmallItem>> clusters = new ArrayList<ArrayList<SmallItem>>(); + + int m = withLatLong.size(); + if (m > 0) { + // cluster the items with lat-long + Point[] pointsArray = new Point[m]; + pointsArray = points.toArray(pointsArray); + int[] bestK = new int[1]; + int[] index = kMeans(pointsArray, bestK); + + for (int i = 0; i < bestK[0]; i++) { + clusters.add(new ArrayList<SmallItem>()); + } + + for (int i = 0; i < m; i++) { + clusters.get(index[i]).add(withLatLong.get(i)); + } + } + + ReverseGeocoder geocoder = new ReverseGeocoder(mContext); + mNames = new ArrayList<String>(); + boolean hasUnresolvedAddress = false; + mClusters = new ArrayList<ArrayList<SmallItem>>(); + for (ArrayList<SmallItem> cluster : clusters) { + String name = generateName(cluster, geocoder); + if (name != null) { + mNames.add(name); + mClusters.add(cluster); + } else { + // move cluster-i to no location cluster + withoutLatLong.addAll(cluster); + hasUnresolvedAddress = true; + } + } + + if (withoutLatLong.size() > 0) { + mNames.add(mNoLocationString); + mClusters.add(withoutLatLong); + } + + if (hasUnresolvedAddress) { + mHandler.post(new Runnable() { + @Override + public void run() { + Toast.makeText(mContext, R.string.no_connectivity, + Toast.LENGTH_LONG).show(); + } + }); + } + } + + private static String generateName(ArrayList<SmallItem> items, + ReverseGeocoder geocoder) { + ReverseGeocoder.SetLatLong set = new ReverseGeocoder.SetLatLong(); + + int n = items.size(); + for (int i = 0; i < n; i++) { + SmallItem item = items.get(i); + double itemLatitude = item.lat; + double itemLongitude = item.lng; + + if (set.mMinLatLatitude > itemLatitude) { + set.mMinLatLatitude = itemLatitude; + set.mMinLatLongitude = itemLongitude; + } + if (set.mMaxLatLatitude < itemLatitude) { + set.mMaxLatLatitude = itemLatitude; + set.mMaxLatLongitude = itemLongitude; + } + if (set.mMinLonLongitude > itemLongitude) { + set.mMinLonLatitude = itemLatitude; + set.mMinLonLongitude = itemLongitude; + } + if (set.mMaxLonLongitude < itemLongitude) { + set.mMaxLonLatitude = itemLatitude; + set.mMaxLonLongitude = itemLongitude; + } + } + + return geocoder.computeAddress(set); + } + + @Override + public int getNumberOfClusters() { + return mClusters.size(); + } + + @Override + public ArrayList<Path> getCluster(int index) { + ArrayList<SmallItem> items = mClusters.get(index); + ArrayList<Path> result = new ArrayList<Path>(items.size()); + for (int i = 0, n = items.size(); i < n; i++) { + result.add(items.get(i).path); + } + return result; + } + + @Override + public String getClusterName(int index) { + return mNames.get(index); + } + + // Input: n points + // Output: the best k is stored in bestK[0], and the return value is the + // an array which specifies the group that each point belongs (0 to k - 1). + private static int[] kMeans(Point points[], int[] bestK) { + int n = points.length; + + // min and max number of groups wanted + int minK = Math.min(n, MIN_GROUPS); + int maxK = Math.min(n, MAX_GROUPS); + + Point[] center = new Point[maxK]; // center of each group. + Point[] groupSum = new Point[maxK]; // sum of points in each group. + int[] groupCount = new int[maxK]; // number of points in each group. + int[] grouping = new int[n]; // The group assignment for each point. + + for (int i = 0; i < maxK; i++) { + center[i] = new Point(); + groupSum[i] = new Point(); + } + + // The score we want to minimize is: + // (sum of distance from each point to its group center) * sqrt(k). + float bestScore = Float.MAX_VALUE; + // The best group assignment up to now. + int[] bestGrouping = new int[n]; + // The best K up to now. + bestK[0] = 1; + + float lastDistance = 0; + float totalDistance = 0; + + for (int k = minK; k <= maxK; k++) { + // step 1: (arbitrarily) pick k points as the initial centers. + int delta = n / k; + for (int i = 0; i < k; i++) { + Point p = points[i * delta]; + center[i].latRad = p.latRad; + center[i].lngRad = p.lngRad; + } + + for (int iter = 0; iter < MAX_ITERATIONS; iter++) { + // step 2: assign each point to the nearest center. + for (int i = 0; i < k; i++) { + groupSum[i].latRad = 0; + groupSum[i].lngRad = 0; + groupCount[i] = 0; + } + totalDistance = 0; + + for (int i = 0; i < n; i++) { + Point p = points[i]; + float bestDistance = Float.MAX_VALUE; + int bestIndex = 0; + for (int j = 0; j < k; j++) { + float distance = (float) GalleryUtils.fastDistanceMeters( + p.latRad, p.lngRad, center[j].latRad, center[j].lngRad); + // We may have small non-zero distance introduced by + // floating point calculation, so zero out small + // distances less than 1 meter. + if (distance < 1) { + distance = 0; + } + if (distance < bestDistance) { + bestDistance = distance; + bestIndex = j; + } + } + grouping[i] = bestIndex; + groupCount[bestIndex]++; + groupSum[bestIndex].latRad += p.latRad; + groupSum[bestIndex].lngRad += p.lngRad; + totalDistance += bestDistance; + } + + // step 3: calculate new centers + for (int i = 0; i < k; i++) { + if (groupCount[i] > 0) { + center[i].latRad = groupSum[i].latRad / groupCount[i]; + center[i].lngRad = groupSum[i].lngRad / groupCount[i]; + } + } + + if (totalDistance == 0 || (Math.abs(lastDistance - totalDistance) + / totalDistance) < STOP_CHANGE_RATIO) { + break; + } + lastDistance = totalDistance; + } + + // step 4: remove empty groups and reassign group number + int reassign[] = new int[k]; + int realK = 0; + for (int i = 0; i < k; i++) { + if (groupCount[i] > 0) { + reassign[i] = realK++; + } + } + + // step 5: calculate the final score + float score = totalDistance * FloatMath.sqrt(realK); + + if (score < bestScore) { + bestScore = score; + bestK[0] = realK; + for (int i = 0; i < n; i++) { + bestGrouping[i] = reassign[grouping[i]]; + } + if (score == 0) { + break; + } + } + } + return bestGrouping; + } +} diff --git a/src/com/android/gallery3d/data/Log.java b/src/com/android/gallery3d/data/Log.java new file mode 100644 index 000000000..3384eb66c --- /dev/null +++ b/src/com/android/gallery3d/data/Log.java @@ -0,0 +1,53 @@ +/* + * 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.data; + +public class Log { + public static int v(String tag, String msg) { + return android.util.Log.v(tag, msg); + } + public static int v(String tag, String msg, Throwable tr) { + return android.util.Log.v(tag, msg, tr); + } + public static int d(String tag, String msg) { + return android.util.Log.d(tag, msg); + } + public static int d(String tag, String msg, Throwable tr) { + return android.util.Log.d(tag, msg, tr); + } + public static int i(String tag, String msg) { + return android.util.Log.i(tag, msg); + } + public static int i(String tag, String msg, Throwable tr) { + return android.util.Log.i(tag, msg, tr); + } + public static int w(String tag, String msg) { + return android.util.Log.w(tag, msg); + } + public static int w(String tag, String msg, Throwable tr) { + return android.util.Log.w(tag, msg, tr); + } + public static int w(String tag, Throwable tr) { + return android.util.Log.w(tag, tr); + } + public static int e(String tag, String msg) { + return android.util.Log.e(tag, msg); + } + public static int e(String tag, String msg, Throwable tr) { + return android.util.Log.e(tag, msg, tr); + } +} diff --git a/src/com/android/gallery3d/data/MediaDetails.java b/src/com/android/gallery3d/data/MediaDetails.java new file mode 100644 index 000000000..cac524b88 --- /dev/null +++ b/src/com/android/gallery3d/data/MediaDetails.java @@ -0,0 +1,170 @@ +/* + * 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.data; + +import com.android.gallery3d.R; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.exif.ExifInterface; +import com.android.gallery3d.exif.ExifTag; +import com.android.gallery3d.exif.Rational; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map.Entry; +import java.util.TreeMap; + +public class MediaDetails implements Iterable<Entry<Integer, Object>> { + @SuppressWarnings("unused") + private static final String TAG = "MediaDetails"; + + private TreeMap<Integer, Object> mDetails = new TreeMap<Integer, Object>(); + private HashMap<Integer, Integer> mUnits = new HashMap<Integer, Integer>(); + + public static final int INDEX_TITLE = 1; + public static final int INDEX_DESCRIPTION = 2; + public static final int INDEX_DATETIME = 3; + public static final int INDEX_LOCATION = 4; + public static final int INDEX_WIDTH = 5; + public static final int INDEX_HEIGHT = 6; + public static final int INDEX_ORIENTATION = 7; + public static final int INDEX_DURATION = 8; + public static final int INDEX_MIMETYPE = 9; + public static final int INDEX_SIZE = 10; + + // for EXIF + public static final int INDEX_MAKE = 100; + public static final int INDEX_MODEL = 101; + public static final int INDEX_FLASH = 102; + public static final int INDEX_FOCAL_LENGTH = 103; + public static final int INDEX_WHITE_BALANCE = 104; + public static final int INDEX_APERTURE = 105; + public static final int INDEX_SHUTTER_SPEED = 106; + public static final int INDEX_EXPOSURE_TIME = 107; + public static final int INDEX_ISO = 108; + + // Put this last because it may be long. + public static final int INDEX_PATH = 200; + + public static class FlashState { + private static int FLASH_FIRED_MASK = 1; + private static int FLASH_RETURN_MASK = 2 | 4; + private static int FLASH_MODE_MASK = 8 | 16; + private static int FLASH_FUNCTION_MASK = 32; + private static int FLASH_RED_EYE_MASK = 64; + private int mState; + + public FlashState(int state) { + mState = state; + } + + public boolean isFlashFired() { + return (mState & FLASH_FIRED_MASK) != 0; + } + } + + public void addDetail(int index, Object value) { + mDetails.put(index, value); + } + + public Object getDetail(int index) { + return mDetails.get(index); + } + + public int size() { + return mDetails.size(); + } + + @Override + public Iterator<Entry<Integer, Object>> iterator() { + return mDetails.entrySet().iterator(); + } + + public void setUnit(int index, int unit) { + mUnits.put(index, unit); + } + + public boolean hasUnit(int index) { + return mUnits.containsKey(index); + } + + public int getUnit(int index) { + return mUnits.get(index); + } + + private static void setExifData(MediaDetails details, ExifTag tag, + int key) { + if (tag != null) { + String value = null; + int type = tag.getDataType(); + if (type == ExifTag.TYPE_UNSIGNED_RATIONAL || type == ExifTag.TYPE_RATIONAL) { + value = String.valueOf(tag.getValueAsRational(0).toDouble()); + } else if (type == ExifTag.TYPE_ASCII) { + value = tag.getValueAsString(); + } else { + value = String.valueOf(tag.forceGetValueAsLong(0)); + } + if (key == MediaDetails.INDEX_FLASH) { + MediaDetails.FlashState state = new MediaDetails.FlashState( + Integer.valueOf(value.toString())); + details.addDetail(key, state); + } else { + details.addDetail(key, value); + } + } + } + + public static void extractExifInfo(MediaDetails details, String filePath) { + + ExifInterface exif = new ExifInterface(); + try { + exif.readExif(filePath); + } catch (FileNotFoundException e) { + Log.w(TAG, "Could not find file to read exif: " + filePath, e); + } catch (IOException e) { + Log.w(TAG, "Could not read exif from file: " + filePath, e); + } + + setExifData(details, exif.getTag(ExifInterface.TAG_FLASH), + MediaDetails.INDEX_FLASH); + setExifData(details, exif.getTag(ExifInterface.TAG_IMAGE_WIDTH), + MediaDetails.INDEX_WIDTH); + setExifData(details, exif.getTag(ExifInterface.TAG_IMAGE_LENGTH), + MediaDetails.INDEX_HEIGHT); + setExifData(details, exif.getTag(ExifInterface.TAG_MAKE), + MediaDetails.INDEX_MAKE); + setExifData(details, exif.getTag(ExifInterface.TAG_MODEL), + MediaDetails.INDEX_MODEL); + setExifData(details, exif.getTag(ExifInterface.TAG_APERTURE_VALUE), + MediaDetails.INDEX_APERTURE); + setExifData(details, exif.getTag(ExifInterface.TAG_ISO_SPEED_RATINGS), + MediaDetails.INDEX_ISO); + setExifData(details, exif.getTag(ExifInterface.TAG_WHITE_BALANCE), + MediaDetails.INDEX_WHITE_BALANCE); + setExifData(details, exif.getTag(ExifInterface.TAG_EXPOSURE_TIME), + MediaDetails.INDEX_EXPOSURE_TIME); + ExifTag focalTag = exif.getTag(ExifInterface.TAG_FOCAL_LENGTH); + if (focalTag != null) { + details.addDetail(MediaDetails.INDEX_FOCAL_LENGTH, + focalTag.getValueAsRational(0).toDouble()); + details.setUnit(MediaDetails.INDEX_FOCAL_LENGTH, R.string.unit_mm); + } + } +} diff --git a/src/com/android/gallery3d/data/MediaItem.java b/src/com/android/gallery3d/data/MediaItem.java new file mode 100644 index 000000000..59ea86551 --- /dev/null +++ b/src/com/android/gallery3d/data/MediaItem.java @@ -0,0 +1,134 @@ +/* + * 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.data; + +import android.graphics.Bitmap; +import android.graphics.BitmapRegionDecoder; + +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.ui.ScreenNail; +import com.android.gallery3d.util.ThreadPool.Job; + +// MediaItem represents an image or a video item. +public abstract class MediaItem extends MediaObject { + // NOTE: These type numbers are stored in the image cache, so it should not + // not be changed without resetting the cache. + public static final int TYPE_THUMBNAIL = 1; + public static final int TYPE_MICROTHUMBNAIL = 2; + + public static final int CACHED_IMAGE_QUALITY = 95; + + public static final int IMAGE_READY = 0; + public static final int IMAGE_WAIT = 1; + public static final int IMAGE_ERROR = -1; + + public static final String MIME_TYPE_JPEG = "image/jpeg"; + + private static final int BYTESBUFFE_POOL_SIZE = 4; + private static final int BYTESBUFFER_SIZE = 200 * 1024; + + private static int sMicrothumbnailTargetSize = 200; + private static final BytesBufferPool sMicroThumbBufferPool = + new BytesBufferPool(BYTESBUFFE_POOL_SIZE, BYTESBUFFER_SIZE); + + private static int sThumbnailTargetSize = 640; + + // TODO: fix default value for latlng and change this. + public static final double INVALID_LATLNG = 0f; + + public abstract Job<Bitmap> requestImage(int type); + public abstract Job<BitmapRegionDecoder> requestLargeImage(); + + public MediaItem(Path path, long version) { + super(path, version); + } + + public long getDateInMs() { + return 0; + } + + public String getName() { + return null; + } + + public void getLatLong(double[] latLong) { + latLong[0] = INVALID_LATLNG; + latLong[1] = INVALID_LATLNG; + } + + public String[] getTags() { + return null; + } + + public Face[] getFaces() { + return null; + } + + // The rotation of the full-resolution image. By default, it returns the value of + // getRotation(). + public int getFullImageRotation() { + return getRotation(); + } + + public int getRotation() { + return 0; + } + + public long getSize() { + return 0; + } + + public abstract String getMimeType(); + + public String getFilePath() { + return ""; + } + + // Returns width and height of the media item. + // Returns 0, 0 if the information is not available. + public abstract int getWidth(); + public abstract int getHeight(); + + // This is an alternative for requestImage() in PhotoPage. If this + // is implemented, you don't need to implement requestImage(). + public ScreenNail getScreenNail() { + return null; + } + + public static int getTargetSize(int type) { + switch (type) { + case TYPE_THUMBNAIL: + return sThumbnailTargetSize; + case TYPE_MICROTHUMBNAIL: + return sMicrothumbnailTargetSize; + default: + throw new RuntimeException( + "should only request thumb/microthumb from cache"); + } + } + + public static BytesBufferPool getBytesBufferPool() { + return sMicroThumbBufferPool; + } + + public static void setThumbnailSizes(int size, int microSize) { + sThumbnailTargetSize = size; + if (sMicrothumbnailTargetSize != microSize) { + sMicrothumbnailTargetSize = microSize; + } + } +} diff --git a/src/com/android/gallery3d/data/MediaObject.java b/src/com/android/gallery3d/data/MediaObject.java new file mode 100644 index 000000000..270d4cf0b --- /dev/null +++ b/src/com/android/gallery3d/data/MediaObject.java @@ -0,0 +1,166 @@ +/* + * 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.data; + +import android.net.Uri; + +public abstract class MediaObject { + @SuppressWarnings("unused") + private static final String TAG = "MediaObject"; + public static final long INVALID_DATA_VERSION = -1; + + // These are the bits returned from getSupportedOperations(): + public static final int SUPPORT_DELETE = 1 << 0; + public static final int SUPPORT_ROTATE = 1 << 1; + public static final int SUPPORT_SHARE = 1 << 2; + public static final int SUPPORT_CROP = 1 << 3; + public static final int SUPPORT_SHOW_ON_MAP = 1 << 4; + public static final int SUPPORT_SETAS = 1 << 5; + public static final int SUPPORT_FULL_IMAGE = 1 << 6; + public static final int SUPPORT_PLAY = 1 << 7; + public static final int SUPPORT_CACHE = 1 << 8; + public static final int SUPPORT_EDIT = 1 << 9; + public static final int SUPPORT_INFO = 1 << 10; + public static final int SUPPORT_TRIM = 1 << 11; + public static final int SUPPORT_UNLOCK = 1 << 12; + public static final int SUPPORT_BACK = 1 << 13; + public static final int SUPPORT_ACTION = 1 << 14; + public static final int SUPPORT_CAMERA_SHORTCUT = 1 << 15; + public static final int SUPPORT_MUTE = 1 << 16; + public static final int SUPPORT_ALL = 0xffffffff; + + // These are the bits returned from getMediaType(): + public static final int MEDIA_TYPE_UNKNOWN = 1; + public static final int MEDIA_TYPE_IMAGE = 2; + public static final int MEDIA_TYPE_VIDEO = 4; + public static final int MEDIA_TYPE_ALL = MEDIA_TYPE_IMAGE | MEDIA_TYPE_VIDEO; + + public static final String MEDIA_TYPE_IMAGE_STRING = "image"; + public static final String MEDIA_TYPE_VIDEO_STRING = "video"; + public static final String MEDIA_TYPE_ALL_STRING = "all"; + + // These are flags for cache() and return values for getCacheFlag(): + public static final int CACHE_FLAG_NO = 0; + public static final int CACHE_FLAG_SCREENNAIL = 1; + public static final int CACHE_FLAG_FULL = 2; + + // These are return values for getCacheStatus(): + public static final int CACHE_STATUS_NOT_CACHED = 0; + public static final int CACHE_STATUS_CACHING = 1; + public static final int CACHE_STATUS_CACHED_SCREENNAIL = 2; + public static final int CACHE_STATUS_CACHED_FULL = 3; + + private static long sVersionSerial = 0; + + protected long mDataVersion; + + protected final Path mPath; + + public interface PanoramaSupportCallback { + void panoramaInfoAvailable(MediaObject mediaObject, boolean isPanorama, + boolean isPanorama360); + } + + public MediaObject(Path path, long version) { + path.setObject(this); + mPath = path; + mDataVersion = version; + } + + public Path getPath() { + return mPath; + } + + public int getSupportedOperations() { + return 0; + } + + public void getPanoramaSupport(PanoramaSupportCallback callback) { + callback.panoramaInfoAvailable(this, false, false); + } + + public void clearCachedPanoramaSupport() { + } + + public void delete() { + throw new UnsupportedOperationException(); + } + + public void rotate(int degrees) { + throw new UnsupportedOperationException(); + } + + public Uri getContentUri() { + String className = getClass().getName(); + Log.e(TAG, "Class " + className + "should implement getContentUri."); + Log.e(TAG, "The object was created from path: " + getPath()); + throw new UnsupportedOperationException(); + } + + public Uri getPlayUri() { + throw new UnsupportedOperationException(); + } + + public int getMediaType() { + return MEDIA_TYPE_UNKNOWN; + } + + public MediaDetails getDetails() { + MediaDetails details = new MediaDetails(); + return details; + } + + public long getDataVersion() { + return mDataVersion; + } + + public int getCacheFlag() { + return CACHE_FLAG_NO; + } + + public int getCacheStatus() { + throw new UnsupportedOperationException(); + } + + public long getCacheSize() { + throw new UnsupportedOperationException(); + } + + public void cache(int flag) { + throw new UnsupportedOperationException(); + } + + public static synchronized long nextVersionNumber() { + return ++MediaObject.sVersionSerial; + } + + public static int getTypeFromString(String s) { + if (MEDIA_TYPE_ALL_STRING.equals(s)) return MediaObject.MEDIA_TYPE_ALL; + if (MEDIA_TYPE_IMAGE_STRING.equals(s)) return MediaObject.MEDIA_TYPE_IMAGE; + if (MEDIA_TYPE_VIDEO_STRING.equals(s)) return MediaObject.MEDIA_TYPE_VIDEO; + throw new IllegalArgumentException(s); + } + + public static String getTypeString(int type) { + switch (type) { + case MEDIA_TYPE_IMAGE: return MEDIA_TYPE_IMAGE_STRING; + case MEDIA_TYPE_VIDEO: return MEDIA_TYPE_VIDEO_STRING; + case MEDIA_TYPE_ALL: return MEDIA_TYPE_ALL_STRING; + } + throw new IllegalArgumentException(); + } +} diff --git a/src/com/android/gallery3d/data/MediaSet.java b/src/com/android/gallery3d/data/MediaSet.java new file mode 100644 index 000000000..683aa6b32 --- /dev/null +++ b/src/com/android/gallery3d/data/MediaSet.java @@ -0,0 +1,348 @@ +/* + * 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.data; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.util.Future; + +import java.util.ArrayList; +import java.util.WeakHashMap; + +// MediaSet is a directory-like data structure. +// It contains MediaItems and sub-MediaSets. +// +// The primary interface are: +// getMediaItemCount(), getMediaItem() and +// getSubMediaSetCount(), getSubMediaSet(). +// +// getTotalMediaItemCount() returns the number of all MediaItems, including +// those in sub-MediaSets. +public abstract class MediaSet extends MediaObject { + @SuppressWarnings("unused") + private static final String TAG = "MediaSet"; + + public static final int MEDIAITEM_BATCH_FETCH_COUNT = 500; + public static final int INDEX_NOT_FOUND = -1; + + public static final int SYNC_RESULT_SUCCESS = 0; + public static final int SYNC_RESULT_CANCELLED = 1; + public static final int SYNC_RESULT_ERROR = 2; + + /** Listener to be used with requestSync(SyncListener). */ + public static interface SyncListener { + /** + * Called when the sync task completed. Completion may be due to normal termination, + * an exception, or cancellation. + * + * @param mediaSet the MediaSet that's done with sync + * @param resultCode one of the SYNC_RESULT_* constants + */ + void onSyncDone(MediaSet mediaSet, int resultCode); + } + + public MediaSet(Path path, long version) { + super(path, version); + } + + public int getMediaItemCount() { + return 0; + } + + // Returns the media items in the range [start, start + count). + // + // The number of media items returned may be less than the specified count + // if there are not enough media items available. The number of + // media items available may not be consistent with the return value of + // getMediaItemCount() because the contents of database may have already + // changed. + public ArrayList<MediaItem> getMediaItem(int start, int count) { + return new ArrayList<MediaItem>(); + } + + public MediaItem getCoverMediaItem() { + ArrayList<MediaItem> items = getMediaItem(0, 1); + if (items.size() > 0) return items.get(0); + for (int i = 0, n = getSubMediaSetCount(); i < n; i++) { + MediaItem cover = getSubMediaSet(i).getCoverMediaItem(); + if (cover != null) return cover; + } + return null; + } + + public int getSubMediaSetCount() { + return 0; + } + + public MediaSet getSubMediaSet(int index) { + throw new IndexOutOfBoundsException(); + } + + public boolean isLeafAlbum() { + return false; + } + + public boolean isCameraRoll() { + return false; + } + + /** + * Method {@link #reload()} may process the loading task in background, this method tells + * its client whether the loading is still in process or not. + */ + public boolean isLoading() { + return false; + } + + public int getTotalMediaItemCount() { + int total = getMediaItemCount(); + for (int i = 0, n = getSubMediaSetCount(); i < n; i++) { + total += getSubMediaSet(i).getTotalMediaItemCount(); + } + return total; + } + + // TODO: we should have better implementation of sub classes + public int getIndexOfItem(Path path, int hint) { + // hint < 0 is handled below + // first, try to find it around the hint + int start = Math.max(0, + hint - MEDIAITEM_BATCH_FETCH_COUNT / 2); + ArrayList<MediaItem> list = getMediaItem( + start, MEDIAITEM_BATCH_FETCH_COUNT); + int index = getIndexOf(path, list); + if (index != INDEX_NOT_FOUND) return start + index; + + // try to find it globally + start = start == 0 ? MEDIAITEM_BATCH_FETCH_COUNT : 0; + list = getMediaItem(start, MEDIAITEM_BATCH_FETCH_COUNT); + while (true) { + index = getIndexOf(path, list); + if (index != INDEX_NOT_FOUND) return start + index; + if (list.size() < MEDIAITEM_BATCH_FETCH_COUNT) return INDEX_NOT_FOUND; + start += MEDIAITEM_BATCH_FETCH_COUNT; + list = getMediaItem(start, MEDIAITEM_BATCH_FETCH_COUNT); + } + } + + protected int getIndexOf(Path path, ArrayList<MediaItem> list) { + for (int i = 0, n = list.size(); i < n; ++i) { + // item could be null only in ClusterAlbum + MediaObject item = list.get(i); + if (item != null && item.mPath == path) return i; + } + return INDEX_NOT_FOUND; + } + + public abstract String getName(); + + private WeakHashMap<ContentListener, Object> mListeners = + new WeakHashMap<ContentListener, Object>(); + + // NOTE: The MediaSet only keeps a weak reference to the listener. The + // listener is automatically removed when there is no other reference to + // the listener. + public void addContentListener(ContentListener listener) { + mListeners.put(listener, null); + } + + public void removeContentListener(ContentListener listener) { + mListeners.remove(listener); + } + + // This should be called by subclasses when the content is changed. + public void notifyContentChanged() { + for (ContentListener listener : mListeners.keySet()) { + listener.onContentDirty(); + } + } + + // Reload the content. Return the current data version. reload() should be called + // in the same thread as getMediaItem(int, int) and getSubMediaSet(int). + public abstract long reload(); + + @Override + public MediaDetails getDetails() { + MediaDetails details = super.getDetails(); + details.addDetail(MediaDetails.INDEX_TITLE, getName()); + return details; + } + + // Enumerate all media items in this media set (including the ones in sub + // media sets), in an efficient order. ItemConsumer.consumer() will be + // called for each media item with its index. + public void enumerateMediaItems(ItemConsumer consumer) { + enumerateMediaItems(consumer, 0); + } + + public void enumerateTotalMediaItems(ItemConsumer consumer) { + enumerateTotalMediaItems(consumer, 0); + } + + public static interface ItemConsumer { + void consume(int index, MediaItem item); + } + + // The default implementation uses getMediaItem() for enumerateMediaItems(). + // Subclasses may override this and use more efficient implementations. + // Returns the number of items enumerated. + protected int enumerateMediaItems(ItemConsumer consumer, int startIndex) { + int total = getMediaItemCount(); + int start = 0; + while (start < total) { + int count = Math.min(MEDIAITEM_BATCH_FETCH_COUNT, total - start); + ArrayList<MediaItem> items = getMediaItem(start, count); + for (int i = 0, n = items.size(); i < n; i++) { + MediaItem item = items.get(i); + consumer.consume(startIndex + start + i, item); + } + start += count; + } + return total; + } + + // Recursively enumerate all media items under this set. + // Returns the number of items enumerated. + protected int enumerateTotalMediaItems( + ItemConsumer consumer, int startIndex) { + int start = 0; + start += enumerateMediaItems(consumer, startIndex); + int m = getSubMediaSetCount(); + for (int i = 0; i < m; i++) { + start += getSubMediaSet(i).enumerateTotalMediaItems( + consumer, startIndex + start); + } + return start; + } + + /** + * Requests sync on this MediaSet. It returns a Future object that can be used by the caller + * to query the status of the sync. The sync result code is one of the SYNC_RESULT_* constants + * defined in this class and can be obtained by Future.get(). + * + * Subclasses should perform sync on a different thread. + * + * The default implementation here returns a Future stub that does nothing and returns + * SYNC_RESULT_SUCCESS by get(). + */ + public Future<Integer> requestSync(SyncListener listener) { + listener.onSyncDone(this, SYNC_RESULT_SUCCESS); + return FUTURE_STUB; + } + + private static final Future<Integer> FUTURE_STUB = new Future<Integer>() { + @Override + public void cancel() {} + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean isDone() { + return true; + } + + @Override + public Integer get() { + return SYNC_RESULT_SUCCESS; + } + + @Override + public void waitDone() {} + }; + + protected Future<Integer> requestSyncOnMultipleSets(MediaSet[] sets, SyncListener listener) { + return new MultiSetSyncFuture(sets, listener); + } + + private class MultiSetSyncFuture implements Future<Integer>, SyncListener { + @SuppressWarnings("hiding") + private static final String TAG = "Gallery.MultiSetSync"; + + private final SyncListener mListener; + private final Future<Integer> mFutures[]; + + private boolean mIsCancelled = false; + private int mResult = -1; + private int mPendingCount; + + @SuppressWarnings("unchecked") + MultiSetSyncFuture(MediaSet[] sets, SyncListener listener) { + mListener = listener; + mPendingCount = sets.length; + mFutures = new Future[sets.length]; + + synchronized (this) { + for (int i = 0, n = sets.length; i < n; ++i) { + mFutures[i] = sets[i].requestSync(this); + Log.d(TAG, " request sync: " + Utils.maskDebugInfo(sets[i].getName())); + } + } + } + + @Override + public synchronized void cancel() { + if (mIsCancelled) return; + mIsCancelled = true; + for (Future<Integer> future : mFutures) future.cancel(); + if (mResult < 0) mResult = SYNC_RESULT_CANCELLED; + } + + @Override + public synchronized boolean isCancelled() { + return mIsCancelled; + } + + @Override + public synchronized boolean isDone() { + return mPendingCount == 0; + } + + @Override + public synchronized Integer get() { + waitDone(); + return mResult; + } + + @Override + public synchronized void waitDone() { + try { + while (!isDone()) wait(); + } catch (InterruptedException e) { + Log.d(TAG, "waitDone() interrupted"); + } + } + + // SyncListener callback + @Override + public void onSyncDone(MediaSet mediaSet, int resultCode) { + SyncListener listener = null; + synchronized (this) { + if (resultCode == SYNC_RESULT_ERROR) mResult = SYNC_RESULT_ERROR; + --mPendingCount; + if (mPendingCount == 0) { + listener = mListener; + notifyAll(); + } + Log.d(TAG, "onSyncDone: " + Utils.maskDebugInfo(mediaSet.getName()) + + " #pending=" + mPendingCount); + } + if (listener != null) listener.onSyncDone(MediaSet.this, mResult); + } + } +} diff --git a/src/com/android/gallery3d/data/MediaSource.java b/src/com/android/gallery3d/data/MediaSource.java new file mode 100644 index 000000000..95901283b --- /dev/null +++ b/src/com/android/gallery3d/data/MediaSource.java @@ -0,0 +1,96 @@ +/* + * 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.data; + +import android.net.Uri; + +import com.android.gallery3d.data.MediaSet.ItemConsumer; + +import java.util.ArrayList; + +public abstract class MediaSource { + private static final String TAG = "MediaSource"; + private String mPrefix; + + protected MediaSource(String prefix) { + mPrefix = prefix; + } + + public String getPrefix() { + return mPrefix; + } + + public Path findPathByUri(Uri uri, String type) { + return null; + } + + public abstract MediaObject createMediaObject(Path path); + + public void pause() { + } + + public void resume() { + } + + public Path getDefaultSetOf(Path item) { + return null; + } + + public long getTotalUsedCacheSize() { + return 0; + } + + public long getTotalTargetCacheSize() { + return 0; + } + + public static class PathId { + public PathId(Path path, int id) { + this.path = path; + this.id = id; + } + public Path path; + public int id; + } + + // Maps a list of Paths (all belong to this MediaSource) to MediaItems, + // and invoke consumer.consume() for each MediaItem with the given id. + // + // This default implementation uses getMediaObject for each Path. Subclasses + // may override this and provide more efficient implementation (like + // batching the database query). + public void mapMediaItems(ArrayList<PathId> list, ItemConsumer consumer) { + int n = list.size(); + for (int i = 0; i < n; i++) { + PathId pid = list.get(i); + MediaObject obj; + synchronized (DataManager.LOCK) { + obj = pid.path.getObject(); + if (obj == null) { + try { + obj = createMediaObject(pid.path); + } catch (Throwable th) { + Log.w(TAG, "cannot create media object: " + pid.path, th); + } + } + } + if (obj != null) { + consumer.consume(pid.id, (MediaItem) obj); + } + } + } +} diff --git a/src/com/android/gallery3d/data/MtpClient.java b/src/com/android/gallery3d/data/MtpClient.java new file mode 100644 index 000000000..737b5b60d --- /dev/null +++ b/src/com/android/gallery3d/data/MtpClient.java @@ -0,0 +1,443 @@ +/* + * 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.data; + +import android.annotation.TargetApi; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.hardware.usb.UsbConstants; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbDeviceConnection; +import android.hardware.usb.UsbInterface; +import android.hardware.usb.UsbManager; +import android.mtp.MtpDevice; +import android.mtp.MtpObjectInfo; +import android.mtp.MtpStorageInfo; +import android.util.Log; + +import com.android.gallery3d.common.ApiHelper; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +/** + * This class helps an application manage a list of connected MTP or PTP devices. + * It listens for MTP devices being attached and removed from the USB host bus + * and notifies the application when the MTP device list changes. + */ +@TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB_MR1) +public class MtpClient { + + private static final String TAG = "MtpClient"; + + private static final String ACTION_USB_PERMISSION = + "android.mtp.MtpClient.action.USB_PERMISSION"; + + private final Context mContext; + private final UsbManager mUsbManager; + private final ArrayList<Listener> mListeners = new ArrayList<Listener>(); + // mDevices contains all MtpDevices that have been seen by our client, + // so we can inform when the device has been detached. + // mDevices is also used for synchronization in this class. + private final HashMap<String, MtpDevice> mDevices = new HashMap<String, MtpDevice>(); + // List of MTP devices we should not try to open for which we are currently + // asking for permission to open. + private final ArrayList<String> mRequestPermissionDevices = new ArrayList<String>(); + // List of MTP devices we should not try to open. + // We add devices to this list if the user canceled a permission request or we were + // unable to open the device. + private final ArrayList<String> mIgnoredDevices = new ArrayList<String>(); + + private final PendingIntent mPermissionIntent; + + private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + UsbDevice usbDevice = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); + String deviceName = usbDevice.getDeviceName(); + + synchronized (mDevices) { + MtpDevice mtpDevice = mDevices.get(deviceName); + + if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) { + if (mtpDevice == null) { + mtpDevice = openDeviceLocked(usbDevice); + } + if (mtpDevice != null) { + for (Listener listener : mListeners) { + listener.deviceAdded(mtpDevice); + } + } + } else if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) { + if (mtpDevice != null) { + mDevices.remove(deviceName); + mRequestPermissionDevices.remove(deviceName); + mIgnoredDevices.remove(deviceName); + for (Listener listener : mListeners) { + listener.deviceRemoved(mtpDevice); + } + } + } else if (ACTION_USB_PERMISSION.equals(action)) { + mRequestPermissionDevices.remove(deviceName); + boolean permission = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, + false); + Log.d(TAG, "ACTION_USB_PERMISSION: " + permission); + if (permission) { + if (mtpDevice == null) { + mtpDevice = openDeviceLocked(usbDevice); + } + if (mtpDevice != null) { + for (Listener listener : mListeners) { + listener.deviceAdded(mtpDevice); + } + } + } else { + // so we don't ask for permission again + mIgnoredDevices.add(deviceName); + } + } + } + } + }; + + /** + * An interface for being notified when MTP or PTP devices are attached + * or removed. In the current implementation, only PTP devices are supported. + */ + public interface Listener { + /** + * Called when a new device has been added + * + * @param device the new device that was added + */ + public void deviceAdded(MtpDevice device); + + /** + * Called when a new device has been removed + * + * @param device the device that was removed + */ + public void deviceRemoved(MtpDevice device); + } + + /** + * Tests to see if a {@link android.hardware.usb.UsbDevice} + * supports the PTP protocol (typically used by digital cameras) + * + * @param device the device to test + * @return true if the device is a PTP device. + */ + static public boolean isCamera(UsbDevice device) { + int count = device.getInterfaceCount(); + for (int i = 0; i < count; i++) { + UsbInterface intf = device.getInterface(i); + if (intf.getInterfaceClass() == UsbConstants.USB_CLASS_STILL_IMAGE && + intf.getInterfaceSubclass() == 1 && + intf.getInterfaceProtocol() == 1) { + return true; + } + } + return false; + } + + /** + * MtpClient constructor + * + * @param context the {@link android.content.Context} to use for the MtpClient + */ + public MtpClient(Context context) { + mContext = context; + mUsbManager = (UsbManager)context.getSystemService(Context.USB_SERVICE); + mPermissionIntent = PendingIntent.getBroadcast(mContext, 0, new Intent(ACTION_USB_PERMISSION), 0); + IntentFilter filter = new IntentFilter(); + filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED); + filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED); + filter.addAction(ACTION_USB_PERMISSION); + context.registerReceiver(mUsbReceiver, filter); + } + + /** + * Opens the {@link android.hardware.usb.UsbDevice} for an MTP or PTP + * device and return an {@link android.mtp.MtpDevice} for it. + * + * @param usbDevice the device to open + * @return an MtpDevice for the device. + */ + private MtpDevice openDeviceLocked(UsbDevice usbDevice) { + String deviceName = usbDevice.getDeviceName(); + + // don't try to open devices that we have decided to ignore + // or are currently asking permission for + if (isCamera(usbDevice) && !mIgnoredDevices.contains(deviceName) + && !mRequestPermissionDevices.contains(deviceName)) { + if (!mUsbManager.hasPermission(usbDevice)) { + mUsbManager.requestPermission(usbDevice, mPermissionIntent); + mRequestPermissionDevices.add(deviceName); + } else { + UsbDeviceConnection connection = mUsbManager.openDevice(usbDevice); + if (connection != null) { + MtpDevice mtpDevice = new MtpDevice(usbDevice); + if (mtpDevice.open(connection)) { + mDevices.put(usbDevice.getDeviceName(), mtpDevice); + return mtpDevice; + } else { + // so we don't try to open it again + mIgnoredDevices.add(deviceName); + } + } else { + // so we don't try to open it again + mIgnoredDevices.add(deviceName); + } + } + } + return null; + } + + /** + * Closes all resources related to the MtpClient object + */ + public void close() { + mContext.unregisterReceiver(mUsbReceiver); + } + + /** + * Registers a {@link com.android.gallery3d.data.MtpClient.Listener} interface to receive + * notifications when MTP or PTP devices are added or removed. + * + * @param listener the listener to register + */ + public void addListener(Listener listener) { + synchronized (mDevices) { + if (!mListeners.contains(listener)) { + mListeners.add(listener); + } + } + } + + /** + * Unregisters a {@link com.android.gallery3d.data.MtpClient.Listener} interface. + * + * @param listener the listener to unregister + */ + public void removeListener(Listener listener) { + synchronized (mDevices) { + mListeners.remove(listener); + } + } + + /** + * Retrieves an {@link android.mtp.MtpDevice} object for the USB device + * with the given name. + * + * @param deviceName the name of the USB device + * @return the MtpDevice, or null if it does not exist + */ + public MtpDevice getDevice(String deviceName) { + synchronized (mDevices) { + return mDevices.get(deviceName); + } + } + + /** + * Retrieves an {@link android.mtp.MtpDevice} object for the USB device + * with the given ID. + * + * @param id the ID of the USB device + * @return the MtpDevice, or null if it does not exist + */ + public MtpDevice getDevice(int id) { + synchronized (mDevices) { + return mDevices.get(UsbDevice.getDeviceName(id)); + } + } + + /** + * Retrieves a list of all currently connected {@link android.mtp.MtpDevice}. + * + * @return the list of MtpDevices + */ + public List<MtpDevice> getDeviceList() { + synchronized (mDevices) { + // Query the USB manager since devices might have attached + // before we added our listener. + for (UsbDevice usbDevice : mUsbManager.getDeviceList().values()) { + if (mDevices.get(usbDevice.getDeviceName()) == null) { + openDeviceLocked(usbDevice); + } + } + + return new ArrayList<MtpDevice>(mDevices.values()); + } + } + + /** + * Retrieves a list of all {@link android.mtp.MtpStorageInfo} + * for the MTP or PTP device with the given USB device name + * + * @param deviceName the name of the USB device + * @return the list of MtpStorageInfo + */ + public List<MtpStorageInfo> getStorageList(String deviceName) { + MtpDevice device = getDevice(deviceName); + if (device == null) { + return null; + } + int[] storageIds = device.getStorageIds(); + if (storageIds == null) { + return null; + } + + int length = storageIds.length; + ArrayList<MtpStorageInfo> storageList = new ArrayList<MtpStorageInfo>(length); + for (int i = 0; i < length; i++) { + MtpStorageInfo info = device.getStorageInfo(storageIds[i]); + if (info == null) { + Log.w(TAG, "getStorageInfo failed"); + } else { + storageList.add(info); + } + } + return storageList; + } + + /** + * Retrieves the {@link android.mtp.MtpObjectInfo} for an object on + * the MTP or PTP device with the given USB device name with the given + * object handle + * + * @param deviceName the name of the USB device + * @param objectHandle handle of the object to query + * @return the MtpObjectInfo + */ + public MtpObjectInfo getObjectInfo(String deviceName, int objectHandle) { + MtpDevice device = getDevice(deviceName); + if (device == null) { + return null; + } + return device.getObjectInfo(objectHandle); + } + + /** + * Deletes an object on the MTP or PTP device with the given USB device name. + * + * @param deviceName the name of the USB device + * @param objectHandle handle of the object to delete + * @return true if the deletion succeeds + */ + public boolean deleteObject(String deviceName, int objectHandle) { + MtpDevice device = getDevice(deviceName); + if (device == null) { + return false; + } + return device.deleteObject(objectHandle); + } + + /** + * Retrieves a list of {@link android.mtp.MtpObjectInfo} for all objects + * on the MTP or PTP device with the given USB device name and given storage ID + * and/or object handle. + * If the object handle is zero, then all objects in the root of the storage unit + * will be returned. Otherwise, all immediate children of the object will be returned. + * If the storage ID is also zero, then all objects on all storage units will be returned. + * + * @param deviceName the name of the USB device + * @param storageId the ID of the storage unit to query, or zero for all + * @param objectHandle the handle of the parent object to query, or zero for the storage root + * @return the list of MtpObjectInfo + */ + public List<MtpObjectInfo> getObjectList(String deviceName, int storageId, int objectHandle) { + MtpDevice device = getDevice(deviceName); + if (device == null) { + return null; + } + if (objectHandle == 0) { + // all objects in root of storage + objectHandle = 0xFFFFFFFF; + } + int[] handles = device.getObjectHandles(storageId, 0, objectHandle); + if (handles == null) { + return null; + } + + int length = handles.length; + ArrayList<MtpObjectInfo> objectList = new ArrayList<MtpObjectInfo>(length); + for (int i = 0; i < length; i++) { + MtpObjectInfo info = device.getObjectInfo(handles[i]); + if (info == null) { + Log.w(TAG, "getObjectInfo failed"); + } else { + objectList.add(info); + } + } + return objectList; + } + + /** + * Returns the data for an object as a byte array. + * + * @param deviceName the name of the USB device containing the object + * @param objectHandle handle of the object to read + * @param objectSize the size of the object (this should match + * {@link android.mtp.MtpObjectInfo#getCompressedSize} + * @return the object's data, or null if reading fails + */ + public byte[] getObject(String deviceName, int objectHandle, int objectSize) { + MtpDevice device = getDevice(deviceName); + if (device == null) { + return null; + } + return device.getObject(objectHandle, objectSize); + } + + /** + * Returns the thumbnail data for an object as a byte array. + * + * @param deviceName the name of the USB device containing the object + * @param objectHandle handle of the object to read + * @return the object's thumbnail, or null if reading fails + */ + public byte[] getThumbnail(String deviceName, int objectHandle) { + MtpDevice device = getDevice(deviceName); + if (device == null) { + return null; + } + return device.getThumbnail(objectHandle); + } + + /** + * Copies the data for an object to a file in external storage. + * + * @param deviceName the name of the USB device containing the object + * @param objectHandle handle of the object to read + * @param destPath path to destination for the file transfer. + * This path should be in the external storage as defined by + * {@link android.os.Environment#getExternalStorageDirectory} + * @return true if the file transfer succeeds + */ + public boolean importFile(String deviceName, int objectHandle, String destPath) { + MtpDevice device = getDevice(deviceName); + if (device == null) { + return false; + } + return device.importFile(objectHandle, destPath); + } +} diff --git a/src/com/android/gallery3d/data/PanoramaMetadataJob.java b/src/com/android/gallery3d/data/PanoramaMetadataJob.java new file mode 100644 index 000000000..ab99d6a81 --- /dev/null +++ b/src/com/android/gallery3d/data/PanoramaMetadataJob.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2012 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.data; + +import android.content.Context; +import android.net.Uri; + +import com.android.gallery3d.util.LightCycleHelper; +import com.android.gallery3d.util.LightCycleHelper.PanoramaMetadata; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +public class PanoramaMetadataJob implements Job<PanoramaMetadata> { + Context mContext; + Uri mUri; + + public PanoramaMetadataJob(Context context, Uri uri) { + mContext = context; + mUri = uri; + } + + @Override + public PanoramaMetadata run(JobContext jc) { + return LightCycleHelper.getPanoramaMetadata(mContext, mUri); + } +} diff --git a/src/com/android/gallery3d/data/Path.java b/src/com/android/gallery3d/data/Path.java new file mode 100644 index 000000000..fcae65e66 --- /dev/null +++ b/src/com/android/gallery3d/data/Path.java @@ -0,0 +1,241 @@ +/* + * 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.data; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.util.IdentityCache; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; + +public class Path { + private static final String TAG = "Path"; + private static Path sRoot = new Path(null, "ROOT"); + + private final Path mParent; + private final String mSegment; + private WeakReference<MediaObject> mObject; + private IdentityCache<String, Path> mChildren; + + private Path(Path parent, String segment) { + mParent = parent; + mSegment = segment; + } + + public Path getChild(String segment) { + synchronized (Path.class) { + if (mChildren == null) { + mChildren = new IdentityCache<String, Path>(); + } else { + Path p = mChildren.get(segment); + if (p != null) return p; + } + + Path p = new Path(this, segment); + mChildren.put(segment, p); + return p; + } + } + + public Path getParent() { + synchronized (Path.class) { + return mParent; + } + } + + public Path getChild(int segment) { + return getChild(String.valueOf(segment)); + } + + public Path getChild(long segment) { + return getChild(String.valueOf(segment)); + } + + public void setObject(MediaObject object) { + synchronized (Path.class) { + Utils.assertTrue(mObject == null || mObject.get() == null); + mObject = new WeakReference<MediaObject>(object); + } + } + + MediaObject getObject() { + synchronized (Path.class) { + return (mObject == null) ? null : mObject.get(); + } + } + + @Override + // TODO: toString() should be more efficient, will fix it later + public String toString() { + synchronized (Path.class) { + StringBuilder sb = new StringBuilder(); + String[] segments = split(); + for (int i = 0; i < segments.length; i++) { + sb.append("/"); + sb.append(segments[i]); + } + return sb.toString(); + } + } + + public boolean equalsIgnoreCase (String p) { + String path = toString(); + return path.equalsIgnoreCase(p); + } + + public static Path fromString(String s) { + synchronized (Path.class) { + String[] segments = split(s); + Path current = sRoot; + for (int i = 0; i < segments.length; i++) { + current = current.getChild(segments[i]); + } + return current; + } + } + + public String[] split() { + synchronized (Path.class) { + int n = 0; + for (Path p = this; p != sRoot; p = p.mParent) { + n++; + } + String[] segments = new String[n]; + int i = n - 1; + for (Path p = this; p != sRoot; p = p.mParent) { + segments[i--] = p.mSegment; + } + return segments; + } + } + + public static String[] split(String s) { + int n = s.length(); + if (n == 0) return new String[0]; + if (s.charAt(0) != '/') { + throw new RuntimeException("malformed path:" + s); + } + ArrayList<String> segments = new ArrayList<String>(); + int i = 1; + while (i < n) { + int brace = 0; + int j; + for (j = i; j < n; j++) { + char c = s.charAt(j); + if (c == '{') ++brace; + else if (c == '}') --brace; + else if (brace == 0 && c == '/') break; + } + if (brace != 0) { + throw new RuntimeException("unbalanced brace in path:" + s); + } + segments.add(s.substring(i, j)); + i = j + 1; + } + String[] result = new String[segments.size()]; + segments.toArray(result); + return result; + } + + // Splits a string to an array of strings. + // For example, "{foo,bar,baz}" -> {"foo","bar","baz"}. + public static String[] splitSequence(String s) { + int n = s.length(); + if (s.charAt(0) != '{' || s.charAt(n-1) != '}') { + throw new RuntimeException("bad sequence: " + s); + } + ArrayList<String> segments = new ArrayList<String>(); + int i = 1; + while (i < n - 1) { + int brace = 0; + int j; + for (j = i; j < n - 1; j++) { + char c = s.charAt(j); + if (c == '{') ++brace; + else if (c == '}') --brace; + else if (brace == 0 && c == ',') break; + } + if (brace != 0) { + throw new RuntimeException("unbalanced brace in path:" + s); + } + segments.add(s.substring(i, j)); + i = j + 1; + } + String[] result = new String[segments.size()]; + segments.toArray(result); + return result; + } + + public String getPrefix() { + if (this == sRoot) return ""; + return getPrefixPath().mSegment; + } + + public Path getPrefixPath() { + synchronized (Path.class) { + Path current = this; + if (current == sRoot) { + throw new IllegalStateException(); + } + while (current.mParent != sRoot) { + current = current.mParent; + } + return current; + } + } + + public String getSuffix() { + // We don't need lock because mSegment is final. + return mSegment; + } + + // Below are for testing/debugging only + static void clearAll() { + synchronized (Path.class) { + sRoot = new Path(null, ""); + } + } + + static void dumpAll() { + dumpAll(sRoot, "", ""); + } + + static void dumpAll(Path p, String prefix1, String prefix2) { + synchronized (Path.class) { + MediaObject obj = p.getObject(); + Log.d(TAG, prefix1 + p.mSegment + ":" + + (obj == null ? "null" : obj.getClass().getSimpleName())); + if (p.mChildren != null) { + ArrayList<String> childrenKeys = p.mChildren.keys(); + int i = 0, n = childrenKeys.size(); + for (String key : childrenKeys) { + Path child = p.mChildren.get(key); + if (child == null) { + ++i; + continue; + } + Log.d(TAG, prefix2 + "|"); + if (++i < n) { + dumpAll(child, prefix2 + "+-- ", prefix2 + "| "); + } else { + dumpAll(child, prefix2 + "+-- ", prefix2 + " "); + } + } + } + } + } +} diff --git a/src/com/android/gallery3d/data/PathMatcher.java b/src/com/android/gallery3d/data/PathMatcher.java new file mode 100644 index 000000000..9c6b840d5 --- /dev/null +++ b/src/com/android/gallery3d/data/PathMatcher.java @@ -0,0 +1,102 @@ +/* + * 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.data; + +import java.util.ArrayList; +import java.util.HashMap; + +public class PathMatcher { + public static final int NOT_FOUND = -1; + + private ArrayList<String> mVariables = new ArrayList<String>(); + private Node mRoot = new Node(); + + public PathMatcher() { + mRoot = new Node(); + } + + public void add(String pattern, int kind) { + String[] segments = Path.split(pattern); + Node current = mRoot; + for (int i = 0; i < segments.length; i++) { + current = current.addChild(segments[i]); + } + current.setKind(kind); + } + + public int match(Path path) { + String[] segments = path.split(); + mVariables.clear(); + Node current = mRoot; + for (int i = 0; i < segments.length; i++) { + Node next = current.getChild(segments[i]); + if (next == null) { + next = current.getChild("*"); + if (next != null) { + mVariables.add(segments[i]); + } else { + return NOT_FOUND; + } + } + current = next; + } + return current.getKind(); + } + + public String getVar(int index) { + return mVariables.get(index); + } + + public int getIntVar(int index) { + return Integer.parseInt(mVariables.get(index)); + } + + public long getLongVar(int index) { + return Long.parseLong(mVariables.get(index)); + } + + private static class Node { + private HashMap<String, Node> mMap; + private int mKind = NOT_FOUND; + + Node addChild(String segment) { + if (mMap == null) { + mMap = new HashMap<String, Node>(); + } else { + Node node = mMap.get(segment); + if (node != null) return node; + } + + Node n = new Node(); + mMap.put(segment, n); + return n; + } + + Node getChild(String segment) { + if (mMap == null) return null; + return mMap.get(segment); + } + + void setKind(int kind) { + mKind = kind; + } + + int getKind() { + return mKind; + } + } +} diff --git a/src/com/android/gallery3d/data/SecureAlbum.java b/src/com/android/gallery3d/data/SecureAlbum.java new file mode 100644 index 000000000..204f848f8 --- /dev/null +++ b/src/com/android/gallery3d/data/SecureAlbum.java @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2012 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.data; + +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.MediaStore.Images; +import android.provider.MediaStore.MediaColumns; +import android.provider.MediaStore.Video; + +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.app.StitchingChangeListener; +import com.android.gallery3d.util.MediaSetUtils; + +import java.util.ArrayList; + +// This class lists all media items added by the client. +public class SecureAlbum extends MediaSet implements StitchingChangeListener { + @SuppressWarnings("unused") + private static final String TAG = "SecureAlbum"; + private static final String[] PROJECTION = {MediaColumns._ID}; + private int mMinImageId = Integer.MAX_VALUE; // the smallest id of images + private int mMaxImageId = Integer.MIN_VALUE; // the biggest id in images + private int mMinVideoId = Integer.MAX_VALUE; // the smallest id of videos + private int mMaxVideoId = Integer.MIN_VALUE; // the biggest id of videos + // All the media items added by the client. + private ArrayList<Path> mAllItems = new ArrayList<Path>(); + // The types of items in mAllItems. True is video and false is image. + private ArrayList<Boolean> mAllItemTypes = new ArrayList<Boolean>(); + private ArrayList<Path> mExistingItems = new ArrayList<Path>(); + private Context mContext; + private DataManager mDataManager; + private static final Uri[] mWatchUris = + {Images.Media.EXTERNAL_CONTENT_URI, Video.Media.EXTERNAL_CONTENT_URI}; + private final ChangeNotifier mNotifier; + // A placeholder image in the end of secure album. When it is tapped, it + // will take the user to the lock screen. + private MediaItem mUnlockItem; + private boolean mShowUnlockItem; + + public SecureAlbum(Path path, GalleryApp application, MediaItem unlock) { + super(path, nextVersionNumber()); + mContext = application.getAndroidContext(); + mDataManager = application.getDataManager(); + mNotifier = new ChangeNotifier(this, mWatchUris, application); + mUnlockItem = unlock; + mShowUnlockItem = (!isCameraBucketEmpty(Images.Media.EXTERNAL_CONTENT_URI) + || !isCameraBucketEmpty(Video.Media.EXTERNAL_CONTENT_URI)); + } + + public void addMediaItem(boolean isVideo, int id) { + Path pathBase; + if (isVideo) { + pathBase = LocalVideo.ITEM_PATH; + mMinVideoId = Math.min(mMinVideoId, id); + mMaxVideoId = Math.max(mMaxVideoId, id); + } else { + pathBase = LocalImage.ITEM_PATH; + mMinImageId = Math.min(mMinImageId, id); + mMaxImageId = Math.max(mMaxImageId, id); + } + Path path = pathBase.getChild(id); + if (!mAllItems.contains(path)) { + mAllItems.add(path); + mAllItemTypes.add(isVideo); + mNotifier.fakeChange(); + } + } + + // The sequence is stitching items, local media items, and unlock image. + @Override + public ArrayList<MediaItem> getMediaItem(int start, int count) { + int existingCount = mExistingItems.size(); + if (start >= existingCount + 1) { + return new ArrayList<MediaItem>(); + } + + // Add paths of requested stitching items. + int end = Math.min(start + count, existingCount); + ArrayList<Path> subset = new ArrayList<Path>(mExistingItems.subList(start, end)); + + // Convert paths to media items. + final MediaItem[] buf = new MediaItem[end - start]; + ItemConsumer consumer = new ItemConsumer() { + @Override + public void consume(int index, MediaItem item) { + buf[index] = item; + } + }; + mDataManager.mapMediaItems(subset, consumer, 0); + ArrayList<MediaItem> result = new ArrayList<MediaItem>(end - start); + for (int i = 0; i < buf.length; i++) { + result.add(buf[i]); + } + if (mShowUnlockItem) result.add(mUnlockItem); + return result; + } + + @Override + public int getMediaItemCount() { + return (mExistingItems.size() + (mShowUnlockItem ? 1 : 0)); + } + + @Override + public String getName() { + return "secure"; + } + + @Override + public long reload() { + if (mNotifier.isDirty()) { + mDataVersion = nextVersionNumber(); + updateExistingItems(); + } + return mDataVersion; + } + + private ArrayList<Integer> queryExistingIds(Uri uri, int minId, int maxId) { + ArrayList<Integer> ids = new ArrayList<Integer>(); + if (minId == Integer.MAX_VALUE || maxId == Integer.MIN_VALUE) return ids; + + String[] selectionArgs = {String.valueOf(minId), String.valueOf(maxId)}; + Cursor cursor = mContext.getContentResolver().query(uri, PROJECTION, + "_id BETWEEN ? AND ?", selectionArgs, null); + if (cursor == null) return ids; + try { + while (cursor.moveToNext()) { + ids.add(cursor.getInt(0)); + } + } finally { + cursor.close(); + } + return ids; + } + + private boolean isCameraBucketEmpty(Uri baseUri) { + Uri uri = baseUri.buildUpon() + .appendQueryParameter("limit", "1").build(); + String[] selection = {String.valueOf(MediaSetUtils.CAMERA_BUCKET_ID)}; + Cursor cursor = mContext.getContentResolver().query(uri, PROJECTION, + "bucket_id = ?", selection, null); + if (cursor == null) return true; + try { + return (cursor.getCount() == 0); + } finally { + cursor.close(); + } + } + + private void updateExistingItems() { + if (mAllItems.size() == 0) return; + + // Query existing ids. + ArrayList<Integer> imageIds = queryExistingIds( + Images.Media.EXTERNAL_CONTENT_URI, mMinImageId, mMaxImageId); + ArrayList<Integer> videoIds = queryExistingIds( + Video.Media.EXTERNAL_CONTENT_URI, mMinVideoId, mMaxVideoId); + + // Construct the existing items list. + mExistingItems.clear(); + for (int i = mAllItems.size() - 1; i >= 0; i--) { + Path path = mAllItems.get(i); + boolean isVideo = mAllItemTypes.get(i); + int id = Integer.parseInt(path.getSuffix()); + if (isVideo) { + if (videoIds.contains(id)) mExistingItems.add(path); + } else { + if (imageIds.contains(id)) mExistingItems.add(path); + } + } + } + + @Override + public boolean isLeafAlbum() { + return true; + } + + @Override + public void onStitchingQueued(Uri uri) { + int id = Integer.parseInt(uri.getLastPathSegment()); + addMediaItem(false, id); + } + + @Override + public void onStitchingResult(Uri uri) { + } + + @Override + public void onStitchingProgress(Uri uri, final int progress) { + } +} diff --git a/src/com/android/gallery3d/data/SecureSource.java b/src/com/android/gallery3d/data/SecureSource.java new file mode 100644 index 000000000..6bc8cc295 --- /dev/null +++ b/src/com/android/gallery3d/data/SecureSource.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2012 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.data; + +import com.android.gallery3d.app.GalleryApp; + +public class SecureSource extends MediaSource { + private GalleryApp mApplication; + private static PathMatcher mMatcher = new PathMatcher(); + private static final int SECURE_ALBUM = 0; + private static final int SECURE_UNLOCK = 1; + + static { + mMatcher.add("/secure/all/*", SECURE_ALBUM); + mMatcher.add("/secure/unlock", SECURE_UNLOCK); + } + + public SecureSource(GalleryApp context) { + super("secure"); + mApplication = context; + } + + public static boolean isSecurePath(String path) { + return (SECURE_ALBUM == mMatcher.match(Path.fromString(path))); + } + + @Override + public MediaObject createMediaObject(Path path) { + switch (mMatcher.match(path)) { + case SECURE_ALBUM: { + DataManager dataManager = mApplication.getDataManager(); + MediaItem unlock = (MediaItem) dataManager.getMediaObject( + "/secure/unlock"); + return new SecureAlbum(path, mApplication, unlock); + } + case SECURE_UNLOCK: + return new UnlockImage(path, mApplication); + default: + throw new RuntimeException("bad path: " + path); + } + } +} diff --git a/src/com/android/gallery3d/data/SingleItemAlbum.java b/src/com/android/gallery3d/data/SingleItemAlbum.java new file mode 100644 index 000000000..a0093e0c3 --- /dev/null +++ b/src/com/android/gallery3d/data/SingleItemAlbum.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2012 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.data; + +import java.util.ArrayList; + +public class SingleItemAlbum extends MediaSet { + @SuppressWarnings("unused") + private static final String TAG = "SingleItemAlbum"; + private final MediaItem mItem; + private final String mName; + + public SingleItemAlbum(Path path, MediaItem item) { + super(path, nextVersionNumber()); + mItem = item; + mName = "SingleItemAlbum("+mItem.getClass().getSimpleName()+")"; + } + + @Override + public int getMediaItemCount() { + return 1; + } + + @Override + public ArrayList<MediaItem> getMediaItem(int start, int count) { + ArrayList<MediaItem> result = new ArrayList<MediaItem>(); + + // If [start, start+count) contains the index 0, return the item. + if (start <= 0 && start + count > 0) { + result.add(mItem); + } + + return result; + } + + public MediaItem getItem() { + return mItem; + } + + @Override + public boolean isLeafAlbum() { + return true; + } + + @Override + public String getName() { + return mName; + } + + @Override + public long reload() { + return mDataVersion; + } +} diff --git a/src/com/android/gallery3d/data/SizeClustering.java b/src/com/android/gallery3d/data/SizeClustering.java new file mode 100644 index 000000000..b809c841b --- /dev/null +++ b/src/com/android/gallery3d/data/SizeClustering.java @@ -0,0 +1,141 @@ +/* + * 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.data; + +import android.content.Context; +import android.content.res.Resources; + +import com.android.gallery3d.R; + +import java.util.ArrayList; + +public class SizeClustering extends Clustering { + @SuppressWarnings("unused") + private static final String TAG = "SizeClustering"; + + private Context mContext; + private ArrayList<Path>[] mClusters; + private String[] mNames; + private long mMinSizes[]; + + private static final long MEGA_BYTES = 1024L*1024; + private static final long GIGA_BYTES = 1024L*1024*1024; + + private static final long[] SIZE_LEVELS = { + 0, + 1 * MEGA_BYTES, + 10 * MEGA_BYTES, + 100 * MEGA_BYTES, + 1 * GIGA_BYTES, + 2 * GIGA_BYTES, + 4 * GIGA_BYTES, + }; + + public SizeClustering(Context context) { + mContext = context; + } + + @SuppressWarnings("unchecked") + @Override + public void run(MediaSet baseSet) { + @SuppressWarnings("unchecked") + final ArrayList<Path>[] group = new ArrayList[SIZE_LEVELS.length]; + baseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() { + @Override + public void consume(int index, MediaItem item) { + // Find the cluster this item belongs to. + long size = item.getSize(); + int i; + for (i = 0; i < SIZE_LEVELS.length - 1; i++) { + if (size < SIZE_LEVELS[i + 1]) { + break; + } + } + + ArrayList<Path> list = group[i]; + if (list == null) { + list = new ArrayList<Path>(); + group[i] = list; + } + list.add(item.getPath()); + } + }); + + int count = 0; + for (int i = 0; i < group.length; i++) { + if (group[i] != null) { + count++; + } + } + + mClusters = new ArrayList[count]; + mNames = new String[count]; + mMinSizes = new long[count]; + + Resources res = mContext.getResources(); + int k = 0; + // Go through group in the reverse order, so the group with the largest + // size will show first. + for (int i = group.length - 1; i >= 0; i--) { + if (group[i] == null) continue; + + mClusters[k] = group[i]; + if (i == 0) { + mNames[k] = String.format( + res.getString(R.string.size_below), getSizeString(i + 1)); + } else if (i == group.length - 1) { + mNames[k] = String.format( + res.getString(R.string.size_above), getSizeString(i)); + } else { + String minSize = getSizeString(i); + String maxSize = getSizeString(i + 1); + mNames[k] = String.format( + res.getString(R.string.size_between), minSize, maxSize); + } + mMinSizes[k] = SIZE_LEVELS[i]; + k++; + } + } + + private String getSizeString(int index) { + long bytes = SIZE_LEVELS[index]; + if (bytes >= GIGA_BYTES) { + return (bytes / GIGA_BYTES) + "GB"; + } else { + return (bytes / MEGA_BYTES) + "MB"; + } + } + + @Override + public int getNumberOfClusters() { + return mClusters.length; + } + + @Override + public ArrayList<Path> getCluster(int index) { + return mClusters[index]; + } + + @Override + public String getClusterName(int index) { + return mNames[index]; + } + + public long getMinSize(int index) { + return mMinSizes[index]; + } +} diff --git a/src/com/android/gallery3d/data/SnailAlbum.java b/src/com/android/gallery3d/data/SnailAlbum.java new file mode 100644 index 000000000..7bce7a695 --- /dev/null +++ b/src/com/android/gallery3d/data/SnailAlbum.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2012 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.data; + +import java.util.concurrent.atomic.AtomicBoolean; + +// This is a simple MediaSet which contains only one MediaItem -- a SnailItem. +public class SnailAlbum extends SingleItemAlbum { + @SuppressWarnings("unused") + private static final String TAG = "SnailAlbum"; + private AtomicBoolean mDirty = new AtomicBoolean(false); + + public SnailAlbum(Path path, SnailItem item) { + super(path, item); + } + + @Override + public long reload() { + if (mDirty.compareAndSet(true, false)) { + ((SnailItem) getItem()).updateVersion(); + mDataVersion = nextVersionNumber(); + } + return mDataVersion; + } + + public void notifyChange() { + mDirty.set(true); + notifyContentChanged(); + } +} diff --git a/src/com/android/gallery3d/data/SnailItem.java b/src/com/android/gallery3d/data/SnailItem.java new file mode 100644 index 000000000..3586d2cab --- /dev/null +++ b/src/com/android/gallery3d/data/SnailItem.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2012 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.data; + +import android.graphics.Bitmap; +import android.graphics.BitmapRegionDecoder; + +import com.android.gallery3d.ui.ScreenNail; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +// SnailItem is a MediaItem which can provide a ScreenNail. This is +// used so we can show an foreign component (like an +// android.view.View) instead of a Bitmap. +public class SnailItem extends MediaItem { + @SuppressWarnings("unused") + private static final String TAG = "SnailItem"; + private ScreenNail mScreenNail; + + public SnailItem(Path path) { + super(path, nextVersionNumber()); + } + + @Override + public Job<Bitmap> requestImage(int type) { + // nothing to return + return new Job<Bitmap>() { + @Override + public Bitmap run(JobContext jc) { + return null; + } + }; + } + + @Override + public Job<BitmapRegionDecoder> requestLargeImage() { + // nothing to return + return new Job<BitmapRegionDecoder>() { + @Override + public BitmapRegionDecoder run(JobContext jc) { + return null; + } + }; + } + + // We do not provide requestImage or requestLargeImage, instead we + // provide a ScreenNail. + @Override + public ScreenNail getScreenNail() { + return mScreenNail; + } + + @Override + public String getMimeType() { + return ""; + } + + // Returns width and height of the media item. + // Returns 0, 0 if the information is not available. + @Override + public int getWidth() { + return 0; + } + + @Override + public int getHeight() { + return 0; + } + + ////////////////////////////////////////////////////////////////////////// + // Extra methods for SnailItem + ////////////////////////////////////////////////////////////////////////// + + public void setScreenNail(ScreenNail screenNail) { + mScreenNail = screenNail; + } + + public void updateVersion() { + mDataVersion = nextVersionNumber(); + } +} diff --git a/src/com/android/gallery3d/data/SnailSource.java b/src/com/android/gallery3d/data/SnailSource.java new file mode 100644 index 000000000..5c690ccdb --- /dev/null +++ b/src/com/android/gallery3d/data/SnailSource.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2012 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.data; + +import com.android.gallery3d.app.GalleryApp; + +public class SnailSource extends MediaSource { + @SuppressWarnings("unused") + private static final String TAG = "SnailSource"; + private static final int SNAIL_ALBUM = 0; + private static final int SNAIL_ITEM = 1; + + private GalleryApp mApplication; + private PathMatcher mMatcher; + private static int sNextId; + + public SnailSource(GalleryApp application) { + super("snail"); + mApplication = application; + mMatcher = new PathMatcher(); + mMatcher.add("/snail/set/*", SNAIL_ALBUM); + mMatcher.add("/snail/item/*", SNAIL_ITEM); + } + + // The only path we accept is "/snail/set/id" and "/snail/item/id" + @Override + public MediaObject createMediaObject(Path path) { + DataManager dataManager = mApplication.getDataManager(); + switch (mMatcher.match(path)) { + case SNAIL_ALBUM: + String itemPath = "/snail/item/" + mMatcher.getVar(0); + SnailItem item = + (SnailItem) dataManager.getMediaObject(itemPath); + return new SnailAlbum(path, item); + case SNAIL_ITEM: { + int id = mMatcher.getIntVar(0); + return new SnailItem(path); + } + } + return null; + } + + // Registers a new SnailAlbum containing a SnailItem and returns the id of + // them. You can obtain the Path of the SnailAlbum and SnailItem associated + // with the id by getSetPath and getItemPath(). + public static synchronized int newId() { + return sNextId++; + } + + public static Path getSetPath(int id) { + return Path.fromString("/snail/set").getChild(id); + } + + public static Path getItemPath(int id) { + return Path.fromString("/snail/item").getChild(id); + } +} diff --git a/src/com/android/gallery3d/data/TagClustering.java b/src/com/android/gallery3d/data/TagClustering.java new file mode 100644 index 000000000..407ca84c4 --- /dev/null +++ b/src/com/android/gallery3d/data/TagClustering.java @@ -0,0 +1,95 @@ +/* + * 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.data; + +import android.content.Context; + +import com.android.gallery3d.R; + +import java.util.ArrayList; +import java.util.Map; +import java.util.TreeMap; + +public class TagClustering extends Clustering { + @SuppressWarnings("unused") + private static final String TAG = "TagClustering"; + + private ArrayList<ArrayList<Path>> mClusters; + private String[] mNames; + private String mUntaggedString; + + public TagClustering(Context context) { + mUntaggedString = context.getResources().getString(R.string.untagged); + } + + @Override + public void run(MediaSet baseSet) { + final TreeMap<String, ArrayList<Path>> map = + new TreeMap<String, ArrayList<Path>>(); + final ArrayList<Path> untagged = new ArrayList<Path>(); + + baseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() { + @Override + public void consume(int index, MediaItem item) { + Path path = item.getPath(); + + String[] tags = item.getTags(); + if (tags == null || tags.length == 0) { + untagged.add(path); + return; + } + for (int j = 0; j < tags.length; j++) { + String key = tags[j]; + ArrayList<Path> list = map.get(key); + if (list == null) { + list = new ArrayList<Path>(); + map.put(key, list); + } + list.add(path); + } + } + }); + + int m = map.size(); + mClusters = new ArrayList<ArrayList<Path>>(); + mNames = new String[m + ((untagged.size() > 0) ? 1 : 0)]; + int i = 0; + for (Map.Entry<String, ArrayList<Path>> entry : map.entrySet()) { + mNames[i++] = entry.getKey(); + mClusters.add(entry.getValue()); + } + if (untagged.size() > 0) { + mNames[i++] = mUntaggedString; + mClusters.add(untagged); + } + } + + @Override + public int getNumberOfClusters() { + return mClusters.size(); + } + + @Override + public ArrayList<Path> getCluster(int index) { + return mClusters.get(index); + } + + @Override + public String getClusterName(int index) { + return mNames[index]; + } +} diff --git a/src/com/android/gallery3d/data/TimeClustering.java b/src/com/android/gallery3d/data/TimeClustering.java new file mode 100644 index 000000000..35cbab1ee --- /dev/null +++ b/src/com/android/gallery3d/data/TimeClustering.java @@ -0,0 +1,439 @@ +/* + * 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.data; + +import android.content.Context; +import android.text.format.DateFormat; +import android.text.format.DateUtils; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.util.GalleryUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; + +public class TimeClustering extends Clustering { + @SuppressWarnings("unused") + private static final String TAG = "TimeClustering"; + + // If 2 items are greater than 25 miles apart, they will be in different + // clusters. + private static final int GEOGRAPHIC_DISTANCE_CUTOFF_IN_MILES = 20; + + // Do not want to split based on anything under 1 min. + private static final long MIN_CLUSTER_SPLIT_TIME_IN_MS = 60000L; + + // Disregard a cluster split time of anything over 2 hours. + private static final long MAX_CLUSTER_SPLIT_TIME_IN_MS = 7200000L; + + // Try and get around 9 clusters (best-effort for the common case). + private static final int NUM_CLUSTERS_TARGETED = 9; + + // Try and merge 2 clusters if they are both smaller than min cluster size. + // The min cluster size can range from 8 to 15. + private static final int MIN_MIN_CLUSTER_SIZE = 8; + private static final int MAX_MIN_CLUSTER_SIZE = 15; + + // Try and split a cluster if it is bigger than max cluster size. + // The max cluster size can range from 20 to 50. + private static final int MIN_MAX_CLUSTER_SIZE = 20; + private static final int MAX_MAX_CLUSTER_SIZE = 50; + + // Initially put 2 items in the same cluster as long as they are within + // 3 cluster frequencies of each other. + private static int CLUSTER_SPLIT_MULTIPLIER = 3; + + // The minimum change factor in the time between items to consider a + // partition. + // Example: (Item 3 - Item 2) / (Item 2 - Item 1). + private static final int MIN_PARTITION_CHANGE_FACTOR = 2; + + // Make the cluster split time of a large cluster half that of a regular + // cluster. + private static final int PARTITION_CLUSTER_SPLIT_TIME_FACTOR = 2; + + private Context mContext; + private ArrayList<Cluster> mClusters; + private String[] mNames; + private Cluster mCurrCluster; + + private long mClusterSplitTime = + (MIN_CLUSTER_SPLIT_TIME_IN_MS + MAX_CLUSTER_SPLIT_TIME_IN_MS) / 2; + private long mLargeClusterSplitTime = + mClusterSplitTime / PARTITION_CLUSTER_SPLIT_TIME_FACTOR; + private int mMinClusterSize = (MIN_MIN_CLUSTER_SIZE + MAX_MIN_CLUSTER_SIZE) / 2; + private int mMaxClusterSize = (MIN_MAX_CLUSTER_SIZE + MAX_MAX_CLUSTER_SIZE) / 2; + + + private static final Comparator<SmallItem> sDateComparator = + new DateComparator(); + + private static class DateComparator implements Comparator<SmallItem> { + @Override + public int compare(SmallItem item1, SmallItem item2) { + return -Utils.compare(item1.dateInMs, item2.dateInMs); + } + } + + public TimeClustering(Context context) { + mContext = context; + mClusters = new ArrayList<Cluster>(); + mCurrCluster = new Cluster(); + } + + @Override + public void run(MediaSet baseSet) { + final int total = baseSet.getTotalMediaItemCount(); + final SmallItem[] buf = new SmallItem[total]; + final double[] latLng = new double[2]; + + baseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() { + @Override + public void consume(int index, MediaItem item) { + if (index < 0 || index >= total) return; + SmallItem s = new SmallItem(); + s.path = item.getPath(); + s.dateInMs = item.getDateInMs(); + item.getLatLong(latLng); + s.lat = latLng[0]; + s.lng = latLng[1]; + buf[index] = s; + } + }); + + ArrayList<SmallItem> items = new ArrayList<SmallItem>(total); + for (int i = 0; i < total; i++) { + if (buf[i] != null) { + items.add(buf[i]); + } + } + + Collections.sort(items, sDateComparator); + + int n = items.size(); + long minTime = 0; + long maxTime = 0; + for (int i = 0; i < n; i++) { + long t = items.get(i).dateInMs; + if (t == 0) continue; + if (minTime == 0) { + minTime = maxTime = t; + } else { + minTime = Math.min(minTime, t); + maxTime = Math.max(maxTime, t); + } + } + + setTimeRange(maxTime - minTime, n); + + for (int i = 0; i < n; i++) { + compute(items.get(i)); + } + + compute(null); + + int m = mClusters.size(); + mNames = new String[m]; + for (int i = 0; i < m; i++) { + mNames[i] = mClusters.get(i).generateCaption(mContext); + } + } + + @Override + public int getNumberOfClusters() { + return mClusters.size(); + } + + @Override + public ArrayList<Path> getCluster(int index) { + ArrayList<SmallItem> items = mClusters.get(index).getItems(); + ArrayList<Path> result = new ArrayList<Path>(items.size()); + for (int i = 0, n = items.size(); i < n; i++) { + result.add(items.get(i).path); + } + return result; + } + + @Override + public String getClusterName(int index) { + return mNames[index]; + } + + private void setTimeRange(long timeRange, int numItems) { + if (numItems != 0) { + int meanItemsPerCluster = numItems / NUM_CLUSTERS_TARGETED; + // Heuristic to get min and max cluster size - half and double the + // desired items per cluster. + mMinClusterSize = meanItemsPerCluster / 2; + mMaxClusterSize = meanItemsPerCluster * 2; + mClusterSplitTime = timeRange / numItems * CLUSTER_SPLIT_MULTIPLIER; + } + mClusterSplitTime = Utils.clamp(mClusterSplitTime, MIN_CLUSTER_SPLIT_TIME_IN_MS, MAX_CLUSTER_SPLIT_TIME_IN_MS); + mLargeClusterSplitTime = mClusterSplitTime / PARTITION_CLUSTER_SPLIT_TIME_FACTOR; + mMinClusterSize = Utils.clamp(mMinClusterSize, MIN_MIN_CLUSTER_SIZE, MAX_MIN_CLUSTER_SIZE); + mMaxClusterSize = Utils.clamp(mMaxClusterSize, MIN_MAX_CLUSTER_SIZE, MAX_MAX_CLUSTER_SIZE); + } + + private void compute(SmallItem currentItem) { + if (currentItem != null) { + int numClusters = mClusters.size(); + int numCurrClusterItems = mCurrCluster.size(); + boolean geographicallySeparateItem = false; + boolean itemAddedToCurrentCluster = false; + + // Determine if this item should go in the current cluster or be the + // start of a new cluster. + if (numCurrClusterItems == 0) { + mCurrCluster.addItem(currentItem); + } else { + SmallItem prevItem = mCurrCluster.getLastItem(); + if (isGeographicallySeparated(prevItem, currentItem)) { + mClusters.add(mCurrCluster); + geographicallySeparateItem = true; + } else if (numCurrClusterItems > mMaxClusterSize) { + splitAndAddCurrentCluster(); + } else if (timeDistance(prevItem, currentItem) < mClusterSplitTime) { + mCurrCluster.addItem(currentItem); + itemAddedToCurrentCluster = true; + } else if (numClusters > 0 && numCurrClusterItems < mMinClusterSize + && !mCurrCluster.mGeographicallySeparatedFromPrevCluster) { + mergeAndAddCurrentCluster(); + } else { + mClusters.add(mCurrCluster); + } + + // Creating a new cluster and adding the current item to it. + if (!itemAddedToCurrentCluster) { + mCurrCluster = new Cluster(); + if (geographicallySeparateItem) { + mCurrCluster.mGeographicallySeparatedFromPrevCluster = true; + } + mCurrCluster.addItem(currentItem); + } + } + } else { + if (mCurrCluster.size() > 0) { + int numClusters = mClusters.size(); + int numCurrClusterItems = mCurrCluster.size(); + + // The last cluster may potentially be too big or too small. + if (numCurrClusterItems > mMaxClusterSize) { + splitAndAddCurrentCluster(); + } else if (numClusters > 0 && numCurrClusterItems < mMinClusterSize + && !mCurrCluster.mGeographicallySeparatedFromPrevCluster) { + mergeAndAddCurrentCluster(); + } else { + mClusters.add(mCurrCluster); + } + mCurrCluster = new Cluster(); + } + } + } + + private void splitAndAddCurrentCluster() { + ArrayList<SmallItem> currClusterItems = mCurrCluster.getItems(); + int numCurrClusterItems = mCurrCluster.size(); + int secondPartitionStartIndex = getPartitionIndexForCurrentCluster(); + if (secondPartitionStartIndex != -1) { + Cluster partitionedCluster = new Cluster(); + for (int j = 0; j < secondPartitionStartIndex; j++) { + partitionedCluster.addItem(currClusterItems.get(j)); + } + mClusters.add(partitionedCluster); + partitionedCluster = new Cluster(); + for (int j = secondPartitionStartIndex; j < numCurrClusterItems; j++) { + partitionedCluster.addItem(currClusterItems.get(j)); + } + mClusters.add(partitionedCluster); + } else { + mClusters.add(mCurrCluster); + } + } + + private int getPartitionIndexForCurrentCluster() { + int partitionIndex = -1; + float largestChange = MIN_PARTITION_CHANGE_FACTOR; + ArrayList<SmallItem> currClusterItems = mCurrCluster.getItems(); + int numCurrClusterItems = mCurrCluster.size(); + int minClusterSize = mMinClusterSize; + + // Could be slightly more efficient here but this code seems cleaner. + if (numCurrClusterItems > minClusterSize + 1) { + for (int i = minClusterSize; i < numCurrClusterItems - minClusterSize; i++) { + SmallItem prevItem = currClusterItems.get(i - 1); + SmallItem currItem = currClusterItems.get(i); + SmallItem nextItem = currClusterItems.get(i + 1); + + long timeNext = nextItem.dateInMs; + long timeCurr = currItem.dateInMs; + long timePrev = prevItem.dateInMs; + + if (timeNext == 0 || timeCurr == 0 || timePrev == 0) continue; + + long diff1 = Math.abs(timeNext - timeCurr); + long diff2 = Math.abs(timeCurr - timePrev); + + float change = Math.max(diff1 / (diff2 + 0.01f), diff2 / (diff1 + 0.01f)); + if (change > largestChange) { + if (timeDistance(currItem, prevItem) > mLargeClusterSplitTime) { + partitionIndex = i; + largestChange = change; + } else if (timeDistance(nextItem, currItem) > mLargeClusterSplitTime) { + partitionIndex = i + 1; + largestChange = change; + } + } + } + } + return partitionIndex; + } + + private void mergeAndAddCurrentCluster() { + int numClusters = mClusters.size(); + Cluster prevCluster = mClusters.get(numClusters - 1); + ArrayList<SmallItem> currClusterItems = mCurrCluster.getItems(); + int numCurrClusterItems = mCurrCluster.size(); + if (prevCluster.size() < mMinClusterSize) { + for (int i = 0; i < numCurrClusterItems; i++) { + prevCluster.addItem(currClusterItems.get(i)); + } + mClusters.set(numClusters - 1, prevCluster); + } else { + mClusters.add(mCurrCluster); + } + } + + // Returns true if a, b are sufficiently geographically separated. + private static boolean isGeographicallySeparated(SmallItem itemA, SmallItem itemB) { + if (!GalleryUtils.isValidLocation(itemA.lat, itemA.lng) + || !GalleryUtils.isValidLocation(itemB.lat, itemB.lng)) { + return false; + } + + double distance = GalleryUtils.fastDistanceMeters( + Math.toRadians(itemA.lat), + Math.toRadians(itemA.lng), + Math.toRadians(itemB.lat), + Math.toRadians(itemB.lng)); + return (GalleryUtils.toMile(distance) > GEOGRAPHIC_DISTANCE_CUTOFF_IN_MILES); + } + + // Returns the time interval between the two items in milliseconds. + private static long timeDistance(SmallItem a, SmallItem b) { + return Math.abs(a.dateInMs - b.dateInMs); + } +} + +class SmallItem { + Path path; + long dateInMs; + double lat, lng; +} + +class Cluster { + @SuppressWarnings("unused") + private static final String TAG = "Cluster"; + private static final String MMDDYY_FORMAT = "MMddyy"; + + // This is for TimeClustering only. + public boolean mGeographicallySeparatedFromPrevCluster = false; + + private ArrayList<SmallItem> mItems = new ArrayList<SmallItem>(); + + public Cluster() { + } + + public void addItem(SmallItem item) { + mItems.add(item); + } + + public int size() { + return mItems.size(); + } + + public SmallItem getLastItem() { + int n = mItems.size(); + return (n == 0) ? null : mItems.get(n - 1); + } + + public ArrayList<SmallItem> getItems() { + return mItems; + } + + public String generateCaption(Context context) { + int n = mItems.size(); + long minTimestamp = 0; + long maxTimestamp = 0; + + for (int i = 0; i < n; i++) { + long t = mItems.get(i).dateInMs; + if (t == 0) continue; + if (minTimestamp == 0) { + minTimestamp = maxTimestamp = t; + } else { + minTimestamp = Math.min(minTimestamp, t); + maxTimestamp = Math.max(maxTimestamp, t); + } + } + if (minTimestamp == 0) return ""; + + String caption; + String minDay = DateFormat.format(MMDDYY_FORMAT, minTimestamp) + .toString(); + String maxDay = DateFormat.format(MMDDYY_FORMAT, maxTimestamp) + .toString(); + + if (minDay.substring(4).equals(maxDay.substring(4))) { + // The items are from the same year - show at least as + // much granularity as abbrev_all allows. + caption = DateUtils.formatDateRange(context, minTimestamp, + maxTimestamp, DateUtils.FORMAT_ABBREV_ALL); + + // Get a more granular date range string if the min and + // max timestamp are on the same day and from the + // current year. + if (minDay.equals(maxDay)) { + int flags = DateUtils.FORMAT_ABBREV_MONTH | DateUtils.FORMAT_SHOW_DATE; + // Contains the year only if the date does not + // correspond to the current year. + String dateRangeWithOptionalYear = DateUtils.formatDateTime( + context, minTimestamp, flags); + String dateRangeWithYear = DateUtils.formatDateTime( + context, minTimestamp, flags | DateUtils.FORMAT_SHOW_YEAR); + if (!dateRangeWithOptionalYear.equals(dateRangeWithYear)) { + // This means both dates are from the same year + // - show the time. + // Not enough room to display the time range. + // Pick the mid-point. + long midTimestamp = (minTimestamp + maxTimestamp) / 2; + caption = DateUtils.formatDateRange(context, midTimestamp, + midTimestamp, DateUtils.FORMAT_SHOW_TIME | flags); + } + } + } else { + // The items are not from the same year - only show + // month and year. + int flags = DateUtils.FORMAT_NO_MONTH_DAY + | DateUtils.FORMAT_ABBREV_MONTH | DateUtils.FORMAT_SHOW_DATE; + caption = DateUtils.formatDateRange(context, minTimestamp, + maxTimestamp, flags); + } + + return caption; + } +} diff --git a/src/com/android/gallery3d/data/UnlockImage.java b/src/com/android/gallery3d/data/UnlockImage.java new file mode 100644 index 000000000..ed3b485c4 --- /dev/null +++ b/src/com/android/gallery3d/data/UnlockImage.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2012 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.data; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.GalleryApp; + +public class UnlockImage extends ActionImage { + @SuppressWarnings("unused") + private static final String TAG = "UnlockImage"; + + public UnlockImage(Path path, GalleryApp application) { + super(path, application, R.drawable.placeholder_locked); + } + + @Override + public int getSupportedOperations() { + return super.getSupportedOperations() | SUPPORT_UNLOCK; + } +} diff --git a/src/com/android/gallery3d/data/UriImage.java b/src/com/android/gallery3d/data/UriImage.java new file mode 100644 index 000000000..e8875b572 --- /dev/null +++ b/src/com/android/gallery3d/data/UriImage.java @@ -0,0 +1,298 @@ +/* + * 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.data; + +import android.content.ContentResolver; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.BitmapFactory.Options; +import android.graphics.BitmapRegionDecoder; +import android.net.Uri; +import android.os.ParcelFileDescriptor; + +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.app.PanoramaMetadataSupport; +import com.android.gallery3d.common.BitmapUtils; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.util.ThreadPool.CancelListener; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.net.URI; +import java.net.URL; + +public class UriImage extends MediaItem { + private static final String TAG = "UriImage"; + + private static final int STATE_INIT = 0; + private static final int STATE_DOWNLOADING = 1; + private static final int STATE_DOWNLOADED = 2; + private static final int STATE_ERROR = -1; + + private final Uri mUri; + private final String mContentType; + + private DownloadCache.Entry mCacheEntry; + private ParcelFileDescriptor mFileDescriptor; + private int mState = STATE_INIT; + private int mWidth; + private int mHeight; + private int mRotation; + private PanoramaMetadataSupport mPanoramaMetadata = new PanoramaMetadataSupport(this); + + private GalleryApp mApplication; + + public UriImage(GalleryApp application, Path path, Uri uri, String contentType) { + super(path, nextVersionNumber()); + mUri = uri; + mApplication = Utils.checkNotNull(application); + mContentType = contentType; + } + + @Override + public Job<Bitmap> requestImage(int type) { + return new BitmapJob(type); + } + + @Override + public Job<BitmapRegionDecoder> requestLargeImage() { + return new RegionDecoderJob(); + } + + private void openFileOrDownloadTempFile(JobContext jc) { + int state = openOrDownloadInner(jc); + synchronized (this) { + mState = state; + if (mState != STATE_DOWNLOADED) { + if (mFileDescriptor != null) { + Utils.closeSilently(mFileDescriptor); + mFileDescriptor = null; + } + } + notifyAll(); + } + } + + private int openOrDownloadInner(JobContext jc) { + String scheme = mUri.getScheme(); + if (ContentResolver.SCHEME_CONTENT.equals(scheme) + || ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme) + || ContentResolver.SCHEME_FILE.equals(scheme)) { + try { + if (MIME_TYPE_JPEG.equalsIgnoreCase(mContentType)) { + InputStream is = mApplication.getContentResolver() + .openInputStream(mUri); + mRotation = Exif.getOrientation(is); + Utils.closeSilently(is); + } + mFileDescriptor = mApplication.getContentResolver() + .openFileDescriptor(mUri, "r"); + if (jc.isCancelled()) return STATE_INIT; + return STATE_DOWNLOADED; + } catch (FileNotFoundException e) { + Log.w(TAG, "fail to open: " + mUri, e); + return STATE_ERROR; + } + } else { + try { + URL url = new URI(mUri.toString()).toURL(); + mCacheEntry = mApplication.getDownloadCache().download(jc, url); + if (jc.isCancelled()) return STATE_INIT; + if (mCacheEntry == null) { + Log.w(TAG, "download failed " + url); + return STATE_ERROR; + } + if (MIME_TYPE_JPEG.equalsIgnoreCase(mContentType)) { + InputStream is = new FileInputStream(mCacheEntry.cacheFile); + mRotation = Exif.getOrientation(is); + Utils.closeSilently(is); + } + mFileDescriptor = ParcelFileDescriptor.open( + mCacheEntry.cacheFile, ParcelFileDescriptor.MODE_READ_ONLY); + return STATE_DOWNLOADED; + } catch (Throwable t) { + Log.w(TAG, "download error", t); + return STATE_ERROR; + } + } + } + + private boolean prepareInputFile(JobContext jc) { + jc.setCancelListener(new CancelListener() { + @Override + public void onCancel() { + synchronized (this) { + notifyAll(); + } + } + }); + + while (true) { + synchronized (this) { + if (jc.isCancelled()) return false; + if (mState == STATE_INIT) { + mState = STATE_DOWNLOADING; + // Then leave the synchronized block and continue. + } else if (mState == STATE_ERROR) { + return false; + } else if (mState == STATE_DOWNLOADED) { + return true; + } else /* if (mState == STATE_DOWNLOADING) */ { + try { + wait(); + } catch (InterruptedException ex) { + // ignored. + } + continue; + } + } + // This is only reached for STATE_INIT->STATE_DOWNLOADING + openFileOrDownloadTempFile(jc); + } + } + + private class RegionDecoderJob implements Job<BitmapRegionDecoder> { + @Override + public BitmapRegionDecoder run(JobContext jc) { + if (!prepareInputFile(jc)) return null; + BitmapRegionDecoder decoder = DecodeUtils.createBitmapRegionDecoder( + jc, mFileDescriptor.getFileDescriptor(), false); + mWidth = decoder.getWidth(); + mHeight = decoder.getHeight(); + return decoder; + } + } + + private class BitmapJob implements Job<Bitmap> { + private int mType; + + protected BitmapJob(int type) { + mType = type; + } + + @Override + public Bitmap run(JobContext jc) { + if (!prepareInputFile(jc)) return null; + int targetSize = MediaItem.getTargetSize(mType); + Options options = new Options(); + options.inPreferredConfig = Config.ARGB_8888; + Bitmap bitmap = DecodeUtils.decodeThumbnail(jc, + mFileDescriptor.getFileDescriptor(), options, targetSize, mType); + + if (jc.isCancelled() || bitmap == null) { + return null; + } + + if (mType == MediaItem.TYPE_MICROTHUMBNAIL) { + bitmap = BitmapUtils.resizeAndCropCenter(bitmap, targetSize, true); + } else { + bitmap = BitmapUtils.resizeDownBySideLength(bitmap, targetSize, true); + } + return bitmap; + } + } + + @Override + public int getSupportedOperations() { + int supported = SUPPORT_EDIT | SUPPORT_SETAS; + if (isSharable()) supported |= SUPPORT_SHARE; + if (BitmapUtils.isSupportedByRegionDecoder(mContentType)) { + supported |= SUPPORT_FULL_IMAGE; + } + return supported; + } + + @Override + public void getPanoramaSupport(PanoramaSupportCallback callback) { + mPanoramaMetadata.getPanoramaSupport(mApplication, callback); + } + + @Override + public void clearCachedPanoramaSupport() { + mPanoramaMetadata.clearCachedValues(); + } + + private boolean isSharable() { + // We cannot grant read permission to the receiver since we put + // the data URI in EXTRA_STREAM instead of the data part of an intent + // And there are issues in MediaUploader and Bluetooth file sender to + // share a general image data. So, we only share for local file. + return ContentResolver.SCHEME_FILE.equals(mUri.getScheme()); + } + + @Override + public int getMediaType() { + return MEDIA_TYPE_IMAGE; + } + + @Override + public Uri getContentUri() { + return mUri; + } + + @Override + public MediaDetails getDetails() { + MediaDetails details = super.getDetails(); + if (mWidth != 0 && mHeight != 0) { + details.addDetail(MediaDetails.INDEX_WIDTH, mWidth); + details.addDetail(MediaDetails.INDEX_HEIGHT, mHeight); + } + if (mContentType != null) { + details.addDetail(MediaDetails.INDEX_MIMETYPE, mContentType); + } + if (ContentResolver.SCHEME_FILE.equals(mUri.getScheme())) { + String filePath = mUri.getPath(); + details.addDetail(MediaDetails.INDEX_PATH, filePath); + MediaDetails.extractExifInfo(details, filePath); + } + return details; + } + + @Override + public String getMimeType() { + return mContentType; + } + + @Override + protected void finalize() throws Throwable { + try { + if (mFileDescriptor != null) { + Utils.closeSilently(mFileDescriptor); + } + } finally { + super.finalize(); + } + } + + @Override + public int getWidth() { + return 0; + } + + @Override + public int getHeight() { + return 0; + } + + @Override + public int getRotation() { + return mRotation; + } +} diff --git a/src/com/android/gallery3d/data/UriSource.java b/src/com/android/gallery3d/data/UriSource.java new file mode 100644 index 000000000..f66bacd7b --- /dev/null +++ b/src/com/android/gallery3d/data/UriSource.java @@ -0,0 +1,95 @@ +/* + * 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.data; + +import android.content.ContentResolver; +import android.net.Uri; +import android.webkit.MimeTypeMap; + +import com.android.gallery3d.app.GalleryApp; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; + +class UriSource extends MediaSource { + @SuppressWarnings("unused") + private static final String TAG = "UriSource"; + private static final String IMAGE_TYPE_PREFIX = "image/"; + private static final String IMAGE_TYPE_ANY = "image/*"; + private static final String CHARSET_UTF_8 = "utf-8"; + + private GalleryApp mApplication; + + public UriSource(GalleryApp context) { + super("uri"); + mApplication = context; + } + + @Override + public MediaObject createMediaObject(Path path) { + String segment[] = path.split(); + if (segment.length != 3) { + throw new RuntimeException("bad path: " + path); + } + try { + String uri = URLDecoder.decode(segment[1], CHARSET_UTF_8); + String type = URLDecoder.decode(segment[2], CHARSET_UTF_8); + return new UriImage(mApplication, path, Uri.parse(uri), type); + } catch (UnsupportedEncodingException e) { + throw new AssertionError(e); + } + } + + private String getMimeType(Uri uri) { + if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { + String extension = + MimeTypeMap.getFileExtensionFromUrl(uri.toString()); + String type = MimeTypeMap.getSingleton() + .getMimeTypeFromExtension(extension.toLowerCase()); + if (type != null) return type; + } + // Assume the type is image if the type cannot be resolved + // This could happen for "http" URI. + String type = mApplication.getContentResolver().getType(uri); + if (type == null) type = "image/*"; + return type; + } + + @Override + public Path findPathByUri(Uri uri, String type) { + String mimeType = getMimeType(uri); + + // Try to find a most specific type but it has to be started with "image/" + if ((type == null) || (IMAGE_TYPE_ANY.equals(type) + && mimeType.startsWith(IMAGE_TYPE_PREFIX))) { + type = mimeType; + } + + if (type.startsWith(IMAGE_TYPE_PREFIX)) { + try { + return Path.fromString("/uri/" + + URLEncoder.encode(uri.toString(), CHARSET_UTF_8) + + "/" +URLEncoder.encode(type, CHARSET_UTF_8)); + } catch (UnsupportedEncodingException e) { + throw new AssertionError(e); + } + } + // We have no clues that it is an image + return null; + } +} diff --git a/src/com/android/gallery3d/filtershow/CenteredLinearLayout.java b/src/com/android/gallery3d/filtershow/CenteredLinearLayout.java new file mode 100644 index 000000000..bc9342d6f --- /dev/null +++ b/src/com/android/gallery3d/filtershow/CenteredLinearLayout.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2012 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.filtershow; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.widget.LinearLayout; + +import com.android.gallery3d.R; + +public class CenteredLinearLayout extends LinearLayout { + private final int mMaxWidth; + + public CenteredLinearLayout(Context context, AttributeSet attrs) { + super(context, attrs); + TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.CenteredLinearLayout); + mMaxWidth = a.getDimensionPixelSize(R.styleable.CenteredLinearLayout_max_width, 0); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int parentWidth = MeasureSpec.getSize(widthMeasureSpec); + int parentHeight = MeasureSpec.getSize(heightMeasureSpec); + Resources r = getContext().getResources(); + float value = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, parentWidth, + r.getDisplayMetrics()); + if (mMaxWidth > 0 && parentWidth > mMaxWidth) { + int measureMode = MeasureSpec.getMode(widthMeasureSpec); + widthMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxWidth, measureMode); + } + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + +} diff --git a/src/com/android/gallery3d/filtershow/EditorPlaceHolder.java b/src/com/android/gallery3d/filtershow/EditorPlaceHolder.java new file mode 100644 index 000000000..95abce114 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/EditorPlaceHolder.java @@ -0,0 +1,82 @@ +package com.android.gallery3d.filtershow; + +import android.view.View; +import android.view.ViewParent; +import android.widget.FrameLayout; + +import com.android.gallery3d.filtershow.cache.ImageLoader; +import com.android.gallery3d.filtershow.editors.Editor; +import com.android.gallery3d.filtershow.imageshow.ImageShow; + +import java.util.HashMap; +import java.util.Vector; + +public class EditorPlaceHolder { + private static final String LOGTAG = "EditorPlaceHolder"; + + private FilterShowActivity mActivity = null; + private FrameLayout mContainer = null; + private HashMap<Integer, Editor> mEditors = new HashMap<Integer, Editor>(); + private Vector<ImageShow> mOldViews = new Vector<ImageShow>(); + + public EditorPlaceHolder(FilterShowActivity activity) { + mActivity = activity; + } + + public void setContainer(FrameLayout container) { + mContainer = container; + } + + public void addEditor(Editor c) { + mEditors.put(c.getID(), c); + } + + public boolean contains(int type) { + if (mEditors.get(type) != null) { + return true; + } + return false; + } + + public Editor showEditor(int type) { + Editor editor = mEditors.get(type); + if (editor == null) { + return null; + } + + editor.createEditor(mActivity, mContainer); + editor.getImageShow().bindAsImageLoadListener(); + mContainer.setVisibility(View.VISIBLE); + mContainer.removeAllViews(); + View eview = editor.getTopLevelView(); + ViewParent parent = eview.getParent(); + + if (parent != null && parent instanceof FrameLayout) { + ((FrameLayout) parent).removeAllViews(); + } + + mContainer.addView(eview); + hideOldViews(); + editor.setVisibility(View.VISIBLE); + return editor; + } + + public void setOldViews(Vector<ImageShow> views) { + mOldViews = views; + } + + public void hide() { + mContainer.setVisibility(View.GONE); + } + + public void hideOldViews() { + for (View view : mOldViews) { + view.setVisibility(View.GONE); + } + } + + public Editor getEditor(int editorId) { + return mEditors.get(editorId); + } + +} diff --git a/src/com/android/gallery3d/filtershow/FilterShowActivity.java b/src/com/android/gallery3d/filtershow/FilterShowActivity.java new file mode 100644 index 000000000..4700fccfe --- /dev/null +++ b/src/com/android/gallery3d/filtershow/FilterShowActivity.java @@ -0,0 +1,1121 @@ +/* + * Copyright (C) 2012 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.filtershow; + +import android.app.ActionBar; +import android.app.AlertDialog; +import android.app.ProgressDialog; +import android.content.ComponentName; +import android.content.ContentValues; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.pm.ActivityInfo; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.support.v4.app.DialogFragment; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentTransaction; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.TypedValue; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewPropertyAnimator; +import android.view.WindowManager; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.FrameLayout; +import android.widget.ShareActionProvider; +import android.widget.ShareActionProvider.OnShareTargetSelectedListener; +import android.widget.Toast; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.PhotoPage; +import com.android.gallery3d.data.LocalAlbum; +import com.android.gallery3d.filtershow.cache.ImageLoader; +import com.android.gallery3d.filtershow.category.Action; +import com.android.gallery3d.filtershow.category.CategoryAdapter; +import com.android.gallery3d.filtershow.category.MainPanel; +import com.android.gallery3d.filtershow.data.UserPresetsManager; +import com.android.gallery3d.filtershow.editors.BasicEditor; +import com.android.gallery3d.filtershow.editors.Editor; +import com.android.gallery3d.filtershow.editors.EditorChanSat; +import com.android.gallery3d.filtershow.editors.EditorCrop; +import com.android.gallery3d.filtershow.editors.EditorDraw; +import com.android.gallery3d.filtershow.editors.EditorGrad; +import com.android.gallery3d.filtershow.editors.EditorManager; +import com.android.gallery3d.filtershow.editors.EditorMirror; +import com.android.gallery3d.filtershow.editors.EditorPanel; +import com.android.gallery3d.filtershow.editors.EditorRedEye; +import com.android.gallery3d.filtershow.editors.EditorRotate; +import com.android.gallery3d.filtershow.editors.EditorStraighten; +import com.android.gallery3d.filtershow.editors.EditorTinyPlanet; +import com.android.gallery3d.filtershow.editors.ImageOnlyEditor; +import com.android.gallery3d.filtershow.filters.FilterRepresentation; +import com.android.gallery3d.filtershow.filters.FilterUserPresetRepresentation; +import com.android.gallery3d.filtershow.filters.FiltersManager; +import com.android.gallery3d.filtershow.filters.ImageFilter; +import com.android.gallery3d.filtershow.history.HistoryItem; +import com.android.gallery3d.filtershow.history.HistoryManager; +import com.android.gallery3d.filtershow.imageshow.ImageShow; +import com.android.gallery3d.filtershow.imageshow.MasterImage; +import com.android.gallery3d.filtershow.imageshow.Spline; +import com.android.gallery3d.filtershow.pipeline.CachingPipeline; +import com.android.gallery3d.filtershow.pipeline.ImagePreset; +import com.android.gallery3d.filtershow.pipeline.ProcessingService; +import com.android.gallery3d.filtershow.presets.PresetManagementDialog; +import com.android.gallery3d.filtershow.presets.UserPresetsAdapter; +import com.android.gallery3d.filtershow.provider.SharedImageProvider; +import com.android.gallery3d.filtershow.state.StateAdapter; +import com.android.gallery3d.filtershow.tools.SaveImage; +import com.android.gallery3d.filtershow.tools.XmpPresets; +import com.android.gallery3d.filtershow.tools.XmpPresets.XMresults; +import com.android.gallery3d.filtershow.ui.ExportDialog; +import com.android.gallery3d.filtershow.ui.FramedTextButton; +import com.android.gallery3d.util.GalleryUtils; +import com.android.gallery3d.util.UsageStatistics; +import com.android.photos.data.GalleryBitmapPool; + +import java.io.File; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Vector; + +public class FilterShowActivity extends FragmentActivity implements OnItemClickListener, + OnShareTargetSelectedListener { + + private String mAction = ""; + MasterImage mMasterImage = null; + + private static final long LIMIT_SUPPORTS_HIGHRES = 134217728; // 128Mb + + public static final String TINY_PLANET_ACTION = "com.android.camera.action.TINY_PLANET"; + public static final String LAUNCH_FULLSCREEN = "launch-fullscreen"; + private ImageShow mImageShow = null; + + private View mSaveButton = null; + + private EditorPlaceHolder mEditorPlaceHolder = new EditorPlaceHolder(this); + + private static final int SELECT_PICTURE = 1; + private static final String LOGTAG = "FilterShowActivity"; + + private boolean mShowingTinyPlanet = false; + private boolean mShowingImageStatePanel = false; + + private final Vector<ImageShow> mImageViews = new Vector<ImageShow>(); + + private ShareActionProvider mShareActionProvider; + private File mSharedOutputFile = null; + + private boolean mSharingImage = false; + + private WeakReference<ProgressDialog> mSavingProgressDialog; + + private LoadBitmapTask mLoadBitmapTask; + + private Uri mOriginalImageUri = null; + private ImagePreset mOriginalPreset = null; + + private Uri mSelectedImageUri = null; + + private UserPresetsManager mUserPresetsManager = null; + private UserPresetsAdapter mUserPresetsAdapter = null; + private CategoryAdapter mCategoryLooksAdapter = null; + private CategoryAdapter mCategoryBordersAdapter = null; + private CategoryAdapter mCategoryGeometryAdapter = null; + private CategoryAdapter mCategoryFiltersAdapter = null; + private int mCurrentPanel = MainPanel.LOOKS; + + private ProcessingService mBoundService; + private boolean mIsBound = false; + + public ProcessingService getProcessingService() { + return mBoundService; + } + + public boolean isSimpleEditAction() { + return !PhotoPage.ACTION_NEXTGEN_EDIT.equalsIgnoreCase(mAction); + } + + private ServiceConnection mConnection = new ServiceConnection() { + public void onServiceConnected(ComponentName className, IBinder service) { + /* + * This is called when the connection with the service has been + * established, giving us the service object we can use to + * interact with the service. Because we have bound to a explicit + * service that we know is running in our own process, we can + * cast its IBinder to a concrete class and directly access it. + */ + mBoundService = ((ProcessingService.LocalBinder)service).getService(); + mBoundService.setFiltershowActivity(FilterShowActivity.this); + mBoundService.onStart(); + } + + public void onServiceDisconnected(ComponentName className) { + /* + * This is called when the connection with the service has been + * unexpectedly disconnected -- that is, its process crashed. + * Because it is running in our same process, we should never + * see this happen. + */ + mBoundService = null; + } + }; + + void doBindService() { + /* + * Establish a connection with the service. We use an explicit + * class name because we want a specific service implementation that + * we know will be running in our own process (and thus won't be + * supporting component replacement by other applications). + */ + bindService(new Intent(FilterShowActivity.this, ProcessingService.class), + mConnection, Context.BIND_AUTO_CREATE); + mIsBound = true; + } + + void doUnbindService() { + if (mIsBound) { + // Detach our existing connection. + unbindService(mConnection); + mIsBound = false; + } + } + + private void setupPipeline() { + doBindService(); + ImageFilter.setActivityForMemoryToasts(this); + mUserPresetsManager = new UserPresetsManager(this); + mUserPresetsAdapter = new UserPresetsAdapter(this); + mCategoryLooksAdapter = new CategoryAdapter(this); + } + + public void updateUIAfterServiceStarted() { + fillCategories(); + loadMainPanel(); + setDefaultPreset(); + extractXMPData(); + processIntent(); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + boolean onlyUsePortrait = getResources().getBoolean(R.bool.only_use_portrait); + if (onlyUsePortrait) { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + } + MasterImage.setMaster(mMasterImage); + + clearGalleryBitmapPool(); + setupPipeline(); + + setupMasterImage(); + setDefaultValues(); + fillEditors(); + + loadXML(); + UsageStatistics.onContentViewChanged(UsageStatistics.COMPONENT_EDITOR, "Main"); + UsageStatistics.onEvent(UsageStatistics.COMPONENT_EDITOR, + UsageStatistics.CATEGORY_LIFECYCLE, UsageStatistics.LIFECYCLE_START); + } + + public boolean isShowingImageStatePanel() { + return mShowingImageStatePanel; + } + + public void loadMainPanel() { + if (findViewById(R.id.main_panel_container) == null) { + return; + } + MainPanel panel = new MainPanel(); + FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); + transaction.replace(R.id.main_panel_container, panel, MainPanel.FRAGMENT_TAG); + transaction.commit(); + } + + public void loadEditorPanel(FilterRepresentation representation, + final Editor currentEditor) { + if (representation.getEditorId() == ImageOnlyEditor.ID) { + currentEditor.reflectCurrentFilter(); + return; + } + final int currentId = currentEditor.getID(); + Runnable showEditor = new Runnable() { + @Override + public void run() { + EditorPanel panel = new EditorPanel(); + panel.setEditor(currentId); + FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); + transaction.remove(getSupportFragmentManager().findFragmentByTag(MainPanel.FRAGMENT_TAG)); + transaction.replace(R.id.main_panel_container, panel, MainPanel.FRAGMENT_TAG); + transaction.commit(); + } + }; + Fragment main = getSupportFragmentManager().findFragmentByTag(MainPanel.FRAGMENT_TAG); + boolean doAnimation = false; + if (mShowingImageStatePanel + && getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) { + doAnimation = true; + } + if (doAnimation && main != null && main instanceof MainPanel) { + MainPanel mainPanel = (MainPanel) main; + View container = mainPanel.getView().findViewById(R.id.category_panel_container); + View bottom = mainPanel.getView().findViewById(R.id.bottom_panel); + int panelHeight = container.getHeight() + bottom.getHeight(); + ViewPropertyAnimator anim = mainPanel.getView().animate(); + anim.translationY(panelHeight).start(); + final Handler handler = new Handler(); + handler.postDelayed(showEditor, anim.getDuration()); + } else { + showEditor.run(); + } + } + + private void loadXML() { + setContentView(R.layout.filtershow_activity); + + ActionBar actionBar = getActionBar(); + actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM); + actionBar.setCustomView(R.layout.filtershow_actionbar); + + mSaveButton = actionBar.getCustomView(); + mSaveButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + saveImage(); + } + }); + + mImageShow = (ImageShow) findViewById(R.id.imageShow); + mImageViews.add(mImageShow); + + setupEditors(); + + mEditorPlaceHolder.hide(); + mImageShow.bindAsImageLoadListener(); + + setupStatePanel(); + } + + public void fillCategories() { + fillLooks(); + loadUserPresets(); + fillBorders(); + fillTools(); + fillEffects(); + } + + public void setupStatePanel() { + MasterImage.getImage().setHistoryManager(mMasterImage.getHistory()); + } + + private void fillEffects() { + FiltersManager filtersManager = FiltersManager.getManager(); + ArrayList<FilterRepresentation> filtersRepresentations = filtersManager.getEffects(); + mCategoryFiltersAdapter = new CategoryAdapter(this); + for (FilterRepresentation representation : filtersRepresentations) { + if (representation.getTextId() != 0) { + representation.setName(getString(representation.getTextId())); + } + mCategoryFiltersAdapter.add(new Action(this, representation)); + } + } + + private void fillTools() { + FiltersManager filtersManager = FiltersManager.getManager(); + ArrayList<FilterRepresentation> filtersRepresentations = filtersManager.getTools(); + mCategoryGeometryAdapter = new CategoryAdapter(this); + for (FilterRepresentation representation : filtersRepresentations) { + mCategoryGeometryAdapter.add(new Action(this, representation)); + } + } + + private void processIntent() { + Intent intent = getIntent(); + if (intent.getBooleanExtra(LAUNCH_FULLSCREEN, false)) { + getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + } + + mAction = intent.getAction(); + mSelectedImageUri = intent.getData(); + Uri loadUri = mSelectedImageUri; + if (mOriginalImageUri != null) { + loadUri = mOriginalImageUri; + } + if (loadUri != null) { + startLoadBitmap(loadUri); + } else { + pickImage(); + } + } + + private void setupEditors() { + mEditorPlaceHolder.setContainer((FrameLayout) findViewById(R.id.editorContainer)); + EditorManager.addEditors(mEditorPlaceHolder); + mEditorPlaceHolder.setOldViews(mImageViews); + } + + private void fillEditors() { + mEditorPlaceHolder.addEditor(new EditorChanSat()); + mEditorPlaceHolder.addEditor(new EditorGrad()); + mEditorPlaceHolder.addEditor(new EditorDraw()); + mEditorPlaceHolder.addEditor(new BasicEditor()); + mEditorPlaceHolder.addEditor(new ImageOnlyEditor()); + mEditorPlaceHolder.addEditor(new EditorTinyPlanet()); + mEditorPlaceHolder.addEditor(new EditorRedEye()); + mEditorPlaceHolder.addEditor(new EditorCrop()); + mEditorPlaceHolder.addEditor(new EditorMirror()); + mEditorPlaceHolder.addEditor(new EditorRotate()); + mEditorPlaceHolder.addEditor(new EditorStraighten()); + } + + private void setDefaultValues() { + Resources res = getResources(); + + // TODO: get those values from XML. + FramedTextButton.setTextSize((int) getPixelsFromDip(14)); + FramedTextButton.setTrianglePadding((int) getPixelsFromDip(4)); + FramedTextButton.setTriangleSize((int) getPixelsFromDip(10)); + + Drawable curveHandle = res.getDrawable(R.drawable.camera_crop); + int curveHandleSize = (int) res.getDimension(R.dimen.crop_indicator_size); + Spline.setCurveHandle(curveHandle, curveHandleSize); + Spline.setCurveWidth((int) getPixelsFromDip(3)); + } + + private void startLoadBitmap(Uri uri) { + final View loading = findViewById(R.id.loading); + final View imageShow = findViewById(R.id.imageShow); + imageShow.setVisibility(View.INVISIBLE); + loading.setVisibility(View.VISIBLE); + mShowingTinyPlanet = false; + mLoadBitmapTask = new LoadBitmapTask(); + mLoadBitmapTask.execute(uri); + } + + private void fillBorders() { + FiltersManager filtersManager = FiltersManager.getManager(); + ArrayList<FilterRepresentation> borders = filtersManager.getBorders(); + + for (int i = 0; i < borders.size(); i++) { + FilterRepresentation filter = borders.get(i); + filter.setName(getString(R.string.borders)); + if (i == 0) { + filter.setName(getString(R.string.none)); + } + } + + mCategoryBordersAdapter = new CategoryAdapter(this); + for (FilterRepresentation representation : borders) { + if (representation.getTextId() != 0) { + representation.setName(getString(representation.getTextId())); + } + mCategoryBordersAdapter.add(new Action(this, representation, Action.FULL_VIEW)); + } + } + + public UserPresetsAdapter getUserPresetsAdapter() { + return mUserPresetsAdapter; + } + + public CategoryAdapter getCategoryLooksAdapter() { + return mCategoryLooksAdapter; + } + + public CategoryAdapter getCategoryBordersAdapter() { + return mCategoryBordersAdapter; + } + + public CategoryAdapter getCategoryGeometryAdapter() { + return mCategoryGeometryAdapter; + } + + public CategoryAdapter getCategoryFiltersAdapter() { + return mCategoryFiltersAdapter; + } + + public void removeFilterRepresentation(FilterRepresentation filterRepresentation) { + if (filterRepresentation == null) { + return; + } + ImagePreset oldPreset = MasterImage.getImage().getPreset(); + ImagePreset copy = new ImagePreset(oldPreset); + copy.removeFilter(filterRepresentation); + MasterImage.getImage().setPreset(copy, copy.getLastRepresentation(), true); + if (MasterImage.getImage().getCurrentFilterRepresentation() == filterRepresentation) { + FilterRepresentation lastRepresentation = copy.getLastRepresentation(); + MasterImage.getImage().setCurrentFilterRepresentation(lastRepresentation); + } + } + + public void useFilterRepresentation(FilterRepresentation filterRepresentation) { + if (filterRepresentation == null) { + return; + } + if (MasterImage.getImage().getCurrentFilterRepresentation() == filterRepresentation) { + return; + } + ImagePreset oldPreset = MasterImage.getImage().getPreset(); + ImagePreset copy = new ImagePreset(oldPreset); + FilterRepresentation representation = copy.getRepresentation(filterRepresentation); + if (representation == null) { + copy.addFilter(filterRepresentation); + } else if (filterRepresentation.getFilterType() == FilterRepresentation.TYPE_GEOMETRY) { + filterRepresentation = representation; + } else { + if (filterRepresentation.allowsSingleInstanceOnly()) { + // Don't just update the filter representation. Centralize the + // logic in the addFilter(), such that we can keep "None" as + // null. + copy.removeFilter(representation); + copy.addFilter(filterRepresentation); + } + } + MasterImage.getImage().setPreset(copy, filterRepresentation, true); + MasterImage.getImage().setCurrentFilterRepresentation(filterRepresentation); + } + + public void showRepresentation(FilterRepresentation representation) { + if (representation == null) { + return; + } + + useFilterRepresentation(representation); + + // show representation + Editor mCurrentEditor = mEditorPlaceHolder.showEditor(representation.getEditorId()); + loadEditorPanel(representation, mCurrentEditor); + } + + public Editor getEditor(int editorID) { + return mEditorPlaceHolder.getEditor(editorID); + } + + public void setCurrentPanel(int currentPanel) { + mCurrentPanel = currentPanel; + } + + public int getCurrentPanel() { + return mCurrentPanel; + } + + public void updateCategories() { + ImagePreset preset = mMasterImage.getPreset(); + mCategoryLooksAdapter.reflectImagePreset(preset); + mCategoryBordersAdapter.reflectImagePreset(preset); + } + + private class LoadHighresBitmapTask extends AsyncTask<Void, Void, Boolean> { + @Override + protected Boolean doInBackground(Void... params) { + MasterImage master = MasterImage.getImage(); + Rect originalBounds = master.getOriginalBounds(); + if (master.supportsHighRes()) { + int highresPreviewSize = master.getOriginalBitmapLarge().getWidth() * 2; + if (highresPreviewSize > originalBounds.width()) { + highresPreviewSize = originalBounds.width(); + } + Rect bounds = new Rect(); + Bitmap originalHires = ImageLoader.loadOrientedConstrainedBitmap(master.getUri(), + master.getActivity(), highresPreviewSize, + master.getOrientation(), bounds); + master.setOriginalBounds(bounds); + master.setOriginalBitmapHighres(originalHires); + mBoundService.setOriginalBitmapHighres(originalHires); + master.warnListeners(); + } + return true; + } + + @Override + protected void onPostExecute(Boolean result) { + Bitmap highresBitmap = MasterImage.getImage().getOriginalBitmapHighres(); + if (highresBitmap != null) { + float highResPreviewScale = (float) highresBitmap.getWidth() + / (float) MasterImage.getImage().getOriginalBounds().width(); + mBoundService.setHighresPreviewScaleFactor(highResPreviewScale); + } + } + } + + private class LoadBitmapTask extends AsyncTask<Uri, Boolean, Boolean> { + int mBitmapSize; + + public LoadBitmapTask() { + mBitmapSize = getScreenImageSize(); + } + + @Override + protected Boolean doInBackground(Uri... params) { + if (!MasterImage.getImage().loadBitmap(params[0], mBitmapSize)) { + return false; + } + publishProgress(ImageLoader.queryLightCycle360(MasterImage.getImage().getActivity())); + return true; + } + + @Override + protected void onProgressUpdate(Boolean... values) { + super.onProgressUpdate(values); + if (isCancelled()) { + return; + } + if (values[0]) { + mShowingTinyPlanet = true; + } + } + + @Override + protected void onPostExecute(Boolean result) { + MasterImage.setMaster(mMasterImage); + if (isCancelled()) { + return; + } + + if (!result) { + cannotLoadImage(); + } + + if (null == CachingPipeline.getRenderScriptContext()){ + Log.v(LOGTAG,"RenderScript context destroyed during load"); + return; + } + final View loading = findViewById(R.id.loading); + loading.setVisibility(View.GONE); + final View imageShow = findViewById(R.id.imageShow); + imageShow.setVisibility(View.VISIBLE); + + Bitmap largeBitmap = MasterImage.getImage().getOriginalBitmapLarge(); + mBoundService.setOriginalBitmap(largeBitmap); + + float previewScale = (float) largeBitmap.getWidth() + / (float) MasterImage.getImage().getOriginalBounds().width(); + mBoundService.setPreviewScaleFactor(previewScale); + if (!mShowingTinyPlanet) { + mCategoryFiltersAdapter.removeTinyPlanet(); + } + mCategoryLooksAdapter.imageLoaded(); + mCategoryBordersAdapter.imageLoaded(); + mCategoryGeometryAdapter.imageLoaded(); + mCategoryFiltersAdapter.imageLoaded(); + mLoadBitmapTask = null; + + if (mOriginalPreset != null) { + MasterImage.getImage().setLoadedPreset(mOriginalPreset); + MasterImage.getImage().setPreset(mOriginalPreset, + mOriginalPreset.getLastRepresentation(), true); + mOriginalPreset = null; + } + + if (mAction == TINY_PLANET_ACTION) { + showRepresentation(mCategoryFiltersAdapter.getTinyPlanet()); + } + LoadHighresBitmapTask highresLoad = new LoadHighresBitmapTask(); + highresLoad.execute(); + super.onPostExecute(result); + } + + } + + private void clearGalleryBitmapPool() { + (new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... params) { + // Free memory held in Gallery's Bitmap pool. May be O(n) for n bitmaps. + GalleryBitmapPool.getInstance().clear(); + return null; + } + }).execute(); + } + + @Override + protected void onDestroy() { + if (mLoadBitmapTask != null) { + mLoadBitmapTask.cancel(false); + } + mUserPresetsManager.close(); + doUnbindService(); + super.onDestroy(); + } + + // TODO: find a more robust way of handling image size selection + // for high screen densities. + private int getScreenImageSize() { + DisplayMetrics outMetrics = new DisplayMetrics(); + getWindowManager().getDefaultDisplay().getMetrics(outMetrics); + return (int) Math.max(outMetrics.heightPixels, outMetrics.widthPixels); + } + + private void showSavingProgress(String albumName) { + ProgressDialog progress; + if (mSavingProgressDialog != null) { + progress = mSavingProgressDialog.get(); + if (progress != null) { + progress.show(); + return; + } + } + // TODO: Allow cancellation of the saving process + String progressText; + if (albumName == null) { + progressText = getString(R.string.saving_image); + } else { + progressText = getString(R.string.filtershow_saving_image, albumName); + } + progress = ProgressDialog.show(this, "", progressText, true, false); + mSavingProgressDialog = new WeakReference<ProgressDialog>(progress); + } + + private void hideSavingProgress() { + if (mSavingProgressDialog != null) { + ProgressDialog progress = mSavingProgressDialog.get(); + if (progress != null) + progress.dismiss(); + } + } + + public void completeSaveImage(Uri saveUri) { + if (mSharingImage && mSharedOutputFile != null) { + // Image saved, we unblock the content provider + Uri uri = Uri.withAppendedPath(SharedImageProvider.CONTENT_URI, + Uri.encode(mSharedOutputFile.getAbsolutePath())); + ContentValues values = new ContentValues(); + values.put(SharedImageProvider.PREPARE, false); + getContentResolver().insert(uri, values); + } + setResult(RESULT_OK, new Intent().setData(saveUri)); + hideSavingProgress(); + finish(); + } + + @Override + public boolean onShareTargetSelected(ShareActionProvider arg0, Intent arg1) { + // First, let's tell the SharedImageProvider that it will need to wait + // for the image + Uri uri = Uri.withAppendedPath(SharedImageProvider.CONTENT_URI, + Uri.encode(mSharedOutputFile.getAbsolutePath())); + ContentValues values = new ContentValues(); + values.put(SharedImageProvider.PREPARE, true); + getContentResolver().insert(uri, values); + mSharingImage = true; + + // Process and save the image in the background. + showSavingProgress(null); + mImageShow.saveImage(this, mSharedOutputFile); + return true; + } + + private Intent getDefaultShareIntent() { + Intent intent = new Intent(Intent.ACTION_SEND); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.setType(SharedImageProvider.MIME_TYPE); + mSharedOutputFile = SaveImage.getNewFile(this, MasterImage.getImage().getUri()); + Uri uri = Uri.withAppendedPath(SharedImageProvider.CONTENT_URI, + Uri.encode(mSharedOutputFile.getAbsolutePath())); + intent.putExtra(Intent.EXTRA_STREAM, uri); + return intent; + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.filtershow_activity_menu, menu); + MenuItem showState = menu.findItem(R.id.showImageStateButton); + if (mShowingImageStatePanel) { + showState.setTitle(R.string.hide_imagestate_panel); + } else { + showState.setTitle(R.string.show_imagestate_panel); + } + mShareActionProvider = (ShareActionProvider) menu.findItem(R.id.menu_share) + .getActionProvider(); + mShareActionProvider.setShareIntent(getDefaultShareIntent()); + mShareActionProvider.setOnShareTargetSelectedListener(this); + + MenuItem undoItem = menu.findItem(R.id.undoButton); + MenuItem redoItem = menu.findItem(R.id.redoButton); + MenuItem resetItem = menu.findItem(R.id.resetHistoryButton); + mMasterImage.getHistory().setMenuItems(undoItem, redoItem, resetItem); + return true; + } + + @Override + public void onPause() { + super.onPause(); + if (mShareActionProvider != null) { + mShareActionProvider.setOnShareTargetSelectedListener(null); + } + } + + @Override + public void onResume() { + super.onResume(); + if (mShareActionProvider != null) { + mShareActionProvider.setOnShareTargetSelectedListener(this); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.undoButton: { + HistoryManager adapter = mMasterImage.getHistory(); + int position = adapter.undo(); + mMasterImage.onHistoryItemClick(position); + backToMain(); + invalidateViews(); + UsageStatistics.onEvent(UsageStatistics.COMPONENT_EDITOR, + UsageStatistics.CATEGORY_BUTTON_PRESS, "Undo"); + return true; + } + case R.id.redoButton: { + HistoryManager adapter = mMasterImage.getHistory(); + int position = adapter.redo(); + mMasterImage.onHistoryItemClick(position); + invalidateViews(); + UsageStatistics.onEvent(UsageStatistics.COMPONENT_EDITOR, + UsageStatistics.CATEGORY_BUTTON_PRESS, "Redo"); + return true; + } + case R.id.resetHistoryButton: { + resetHistory(); + UsageStatistics.onEvent(UsageStatistics.COMPONENT_EDITOR, + UsageStatistics.CATEGORY_BUTTON_PRESS, "ResetHistory"); + return true; + } + case R.id.showImageStateButton: { + toggleImageStatePanel(); + UsageStatistics.onEvent(UsageStatistics.COMPONENT_EDITOR, + UsageStatistics.CATEGORY_BUTTON_PRESS, + mShowingImageStatePanel ? "ShowPanel" : "HidePanel"); + return true; + } + case R.id.exportFlattenButton: { + showExportOptionsDialog(); + return true; + } + case android.R.id.home: { + saveImage(); + return true; + } + case R.id.manageUserPresets: { + manageUserPresets(); + return true; + } + } + return false; + } + + private void manageUserPresets() { + DialogFragment dialog = new PresetManagementDialog(); + dialog.show(getSupportFragmentManager(), "NoticeDialogFragment"); + } + + private void showExportOptionsDialog() { + DialogFragment dialog = new ExportDialog(); + dialog.show(getSupportFragmentManager(), "ExportDialogFragment"); + } + + public void updateUserPresetsFromAdapter(UserPresetsAdapter adapter) { + ArrayList<FilterUserPresetRepresentation> representations = + adapter.getDeletedRepresentations(); + for (FilterUserPresetRepresentation representation : representations) { + deletePreset(representation.getId()); + } + ArrayList<FilterUserPresetRepresentation> changedRepresentations = + adapter.getChangedRepresentations(); + for (FilterUserPresetRepresentation representation : changedRepresentations) { + updatePreset(representation); + } + adapter.clearDeletedRepresentations(); + adapter.clearChangedRepresentations(); + loadUserPresets(); + } + + public void loadUserPresets() { + mUserPresetsManager.load(); + } + + public void updateUserPresetsFromManager() { + ArrayList<FilterUserPresetRepresentation> presets = mUserPresetsManager.getRepresentations(); + if (presets == null) { + return; + } + if (mCategoryLooksAdapter != null) { + fillLooks(); + } + mUserPresetsAdapter.clear(); + for (int i = 0; i < presets.size(); i++) { + FilterUserPresetRepresentation representation = presets.get(i); + mCategoryLooksAdapter.add( + new Action(this, representation, Action.FULL_VIEW)); + mUserPresetsAdapter.add(new Action(this, representation, Action.FULL_VIEW)); + } + mCategoryLooksAdapter.notifyDataSetInvalidated(); + + } + + public void saveCurrentImagePreset() { + mUserPresetsManager.save(MasterImage.getImage().getPreset()); + } + + private void deletePreset(int id) { + mUserPresetsManager.delete(id); + } + + private void updatePreset(FilterUserPresetRepresentation representation) { + mUserPresetsManager.update(representation); + } + + public void enableSave(boolean enable) { + if (mSaveButton != null) { + mSaveButton.setEnabled(enable); + } + } + + private void fillLooks() { + FiltersManager filtersManager = FiltersManager.getManager(); + ArrayList<FilterRepresentation> filtersRepresentations = filtersManager.getLooks(); + + mCategoryLooksAdapter.clear(); + int verticalItemHeight = (int) getResources().getDimension(R.dimen.action_item_height); + mCategoryLooksAdapter.setItemHeight(verticalItemHeight); + for (FilterRepresentation representation : filtersRepresentations) { + mCategoryLooksAdapter.add(new Action(this, representation, Action.FULL_VIEW)); + } + } + + public void setDefaultPreset() { + // Default preset (original) + ImagePreset preset = new ImagePreset(); // empty + mMasterImage.setPreset(preset, preset.getLastRepresentation(), true); + } + + // ////////////////////////////////////////////////////////////////////////////// + // Some utility functions + // TODO: finish the cleanup. + + public void invalidateViews() { + for (ImageShow views : mImageViews) { + views.updateImage(); + } + } + + public void hideImageViews() { + for (View view : mImageViews) { + view.setVisibility(View.GONE); + } + mEditorPlaceHolder.hide(); + } + + // ////////////////////////////////////////////////////////////////////////////// + // imageState panel... + + public void toggleImageStatePanel() { + invalidateOptionsMenu(); + mShowingImageStatePanel = !mShowingImageStatePanel; + Fragment panel = getSupportFragmentManager().findFragmentByTag(MainPanel.FRAGMENT_TAG); + if (panel != null) { + if (panel instanceof EditorPanel) { + EditorPanel editorPanel = (EditorPanel) panel; + editorPanel.showImageStatePanel(mShowingImageStatePanel); + } else if (panel instanceof MainPanel) { + MainPanel mainPanel = (MainPanel) panel; + mainPanel.showImageStatePanel(mShowingImageStatePanel); + } + } + } + + @Override + public void onConfigurationChanged(Configuration newConfig) + { + super.onConfigurationChanged(newConfig); + setDefaultValues(); + loadXML(); + fillCategories(); + loadMainPanel(); + + // mLoadBitmapTask==null implies you have looked at the intent + if (!mShowingTinyPlanet && (mLoadBitmapTask == null)) { + mCategoryFiltersAdapter.removeTinyPlanet(); + } + final View loading = findViewById(R.id.loading); + loading.setVisibility(View.GONE); + } + + public void setupMasterImage() { + + HistoryManager historyManager = new HistoryManager(); + StateAdapter imageStateAdapter = new StateAdapter(this, 0); + MasterImage.reset(); + mMasterImage = MasterImage.getImage(); + mMasterImage.setHistoryManager(historyManager); + mMasterImage.setStateAdapter(imageStateAdapter); + mMasterImage.setActivity(this); + + if (Runtime.getRuntime().maxMemory() > LIMIT_SUPPORTS_HIGHRES) { + mMasterImage.setSupportsHighRes(true); + } else { + mMasterImage.setSupportsHighRes(false); + } + } + + void resetHistory() { + HistoryManager adapter = mMasterImage.getHistory(); + adapter.reset(); + HistoryItem historyItem = adapter.getItem(0); + ImagePreset original = new ImagePreset(historyItem.getImagePreset()); + mMasterImage.setPreset(original, historyItem.getFilterRepresentation(), true); + invalidateViews(); + backToMain(); + } + + public void showDefaultImageView() { + mEditorPlaceHolder.hide(); + mImageShow.setVisibility(View.VISIBLE); + MasterImage.getImage().setCurrentFilter(null); + MasterImage.getImage().setCurrentFilterRepresentation(null); + } + + public void backToMain() { + Fragment currentPanel = getSupportFragmentManager().findFragmentByTag(MainPanel.FRAGMENT_TAG); + if (currentPanel instanceof MainPanel) { + return; + } + loadMainPanel(); + showDefaultImageView(); + } + + @Override + public void onBackPressed() { + Fragment currentPanel = getSupportFragmentManager().findFragmentByTag(MainPanel.FRAGMENT_TAG); + if (currentPanel instanceof MainPanel) { + if (!mImageShow.hasModifications()) { + done(); + } else { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setMessage(R.string.unsaved).setTitle(R.string.save_before_exit); + builder.setPositiveButton(R.string.save_and_exit, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int id) { + saveImage(); + } + }); + builder.setNegativeButton(R.string.exit, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int id) { + done(); + } + }); + builder.show(); + } + } else { + backToMain(); + } + } + + public void cannotLoadImage() { + Toast.makeText(this, R.string.cannot_load_image, Toast.LENGTH_SHORT).show(); + finish(); + } + + // ////////////////////////////////////////////////////////////////////////////// + + public float getPixelsFromDip(float value) { + Resources r = getResources(); + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, + r.getDisplayMetrics()); + } + + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, + long id) { + mMasterImage.onHistoryItemClick(position); + invalidateViews(); + } + + public void pickImage() { + Intent intent = new Intent(); + intent.setType("image/*"); + intent.setAction(Intent.ACTION_GET_CONTENT); + startActivityForResult(Intent.createChooser(intent, getString(R.string.select_image)), + SELECT_PICTURE); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode == RESULT_OK) { + if (requestCode == SELECT_PICTURE) { + Uri selectedImageUri = data.getData(); + startLoadBitmap(selectedImageUri); + } + } + } + + + public void saveImage() { + if (mImageShow.hasModifications()) { + // Get the name of the album, to which the image will be saved + File saveDir = SaveImage.getFinalSaveDirectory(this, mSelectedImageUri); + int bucketId = GalleryUtils.getBucketId(saveDir.getPath()); + String albumName = LocalAlbum.getLocalizedName(getResources(), bucketId, null); + showSavingProgress(albumName); + mImageShow.saveImage(this, null); + } else { + done(); + } + } + + + public void done() { + hideSavingProgress(); + if (mLoadBitmapTask != null) { + mLoadBitmapTask.cancel(false); + } + finish(); + } + + private void extractXMPData() { + XMresults res = XmpPresets.extractXMPData( + getBaseContext(), mMasterImage, getIntent().getData()); + if (res == null) + return; + + mOriginalImageUri = res.originalimage; + mOriginalPreset = res.preset; + } + + public Uri getSelectedImageUri() { + return mSelectedImageUri; + } + +} diff --git a/src/com/android/gallery3d/filtershow/cache/ImageLoader.java b/src/com/android/gallery3d/filtershow/cache/ImageLoader.java new file mode 100644 index 000000000..b6c72fd9d --- /dev/null +++ b/src/com/android/gallery3d/filtershow/cache/ImageLoader.java @@ -0,0 +1,502 @@ +/* + * Copyright (C) 2012 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.filtershow.cache; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.database.Cursor; +import android.database.sqlite.SQLiteException; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.BitmapRegionDecoder; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.net.Uri; +import android.provider.MediaStore; +import android.util.Log; +import android.webkit.MimeTypeMap; + +import com.adobe.xmp.XMPException; +import com.adobe.xmp.XMPMeta; +import com.android.gallery3d.R; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.exif.ExifInterface; +import com.android.gallery3d.filtershow.imageshow.MasterImage; +import com.android.gallery3d.util.XmpUtilHelper; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +public final class ImageLoader { + + private static final String LOGTAG = "ImageLoader"; + + public static final String JPEG_MIME_TYPE = "image/jpeg"; + public static final int DEFAULT_COMPRESS_QUALITY = 95; + + public static final int ORI_NORMAL = ExifInterface.Orientation.TOP_LEFT; + public static final int ORI_ROTATE_90 = ExifInterface.Orientation.RIGHT_TOP; + public static final int ORI_ROTATE_180 = ExifInterface.Orientation.BOTTOM_LEFT; + public static final int ORI_ROTATE_270 = ExifInterface.Orientation.RIGHT_BOTTOM; + public static final int ORI_FLIP_HOR = ExifInterface.Orientation.TOP_RIGHT; + public static final int ORI_FLIP_VERT = ExifInterface.Orientation.BOTTOM_RIGHT; + public static final int ORI_TRANSPOSE = ExifInterface.Orientation.LEFT_TOP; + public static final int ORI_TRANSVERSE = ExifInterface.Orientation.LEFT_BOTTOM; + + private static final int BITMAP_LOAD_BACKOUT_ATTEMPTS = 5; + + private ImageLoader() {} + + /** + * Returns the Mime type for a Url. Safe to use with Urls that do not + * come from Gallery's content provider. + */ + public static String getMimeType(Uri src) { + String postfix = MimeTypeMap.getFileExtensionFromUrl(src.toString()); + String ret = null; + if (postfix != null) { + ret = MimeTypeMap.getSingleton().getMimeTypeFromExtension(postfix); + } + return ret; + } + + /** + * Returns the image's orientation flag. Defaults to ORI_NORMAL if no valid + * orientation was found. + */ + public static int getMetadataOrientation(Context context, Uri uri) { + if (uri == null || context == null) { + throw new IllegalArgumentException("bad argument to getOrientation"); + } + + // First try to find orientation data in Gallery's ContentProvider. + Cursor cursor = null; + try { + cursor = context.getContentResolver().query(uri, + new String[] { MediaStore.Images.ImageColumns.ORIENTATION }, + null, null, null); + if (cursor != null && cursor.moveToNext()) { + int ori = cursor.getInt(0); + switch (ori) { + case 90: + return ORI_ROTATE_90; + case 270: + return ORI_ROTATE_270; + case 180: + return ORI_ROTATE_180; + default: + return ORI_NORMAL; + } + } + } catch (SQLiteException e) { + // Do nothing + } catch (IllegalArgumentException e) { + // Do nothing + } finally { + Utils.closeSilently(cursor); + } + + // Fall back to checking EXIF tags in file. + if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { + String mimeType = getMimeType(uri); + if (!JPEG_MIME_TYPE.equals(mimeType)) { + return ORI_NORMAL; + } + String path = uri.getPath(); + ExifInterface exif = new ExifInterface(); + try { + exif.readExif(path); + Integer tagval = exif.getTagIntValue(ExifInterface.TAG_ORIENTATION); + if (tagval != null) { + int orientation = tagval; + switch(orientation) { + case ORI_NORMAL: + case ORI_ROTATE_90: + case ORI_ROTATE_180: + case ORI_ROTATE_270: + case ORI_FLIP_HOR: + case ORI_FLIP_VERT: + case ORI_TRANSPOSE: + case ORI_TRANSVERSE: + return orientation; + default: + return ORI_NORMAL; + } + } + } catch (IOException e) { + Log.w(LOGTAG, "Failed to read EXIF orientation", e); + } + } + return ORI_NORMAL; + } + + /** + * Returns the rotation of image at the given URI as one of 0, 90, 180, + * 270. Defaults to 0. + */ + public static int getMetadataRotation(Context context, Uri uri) { + int orientation = getMetadataOrientation(context, uri); + switch(orientation) { + case ORI_ROTATE_90: + return 90; + case ORI_ROTATE_180: + return 180; + case ORI_ROTATE_270: + return 270; + default: + return 0; + } + } + + /** + * Takes an orientation and a bitmap, and returns the bitmap transformed + * to that orientation. + */ + public static Bitmap orientBitmap(Bitmap bitmap, int ori) { + Matrix matrix = new Matrix(); + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + if (ori == ORI_ROTATE_90 || + ori == ORI_ROTATE_270 || + ori == ORI_TRANSPOSE || + ori == ORI_TRANSVERSE) { + int tmp = w; + w = h; + h = tmp; + } + switch (ori) { + case ORI_ROTATE_90: + matrix.setRotate(90, w / 2f, h / 2f); + break; + case ORI_ROTATE_180: + matrix.setRotate(180, w / 2f, h / 2f); + break; + case ORI_ROTATE_270: + matrix.setRotate(270, w / 2f, h / 2f); + break; + case ORI_FLIP_HOR: + matrix.preScale(-1, 1); + break; + case ORI_FLIP_VERT: + matrix.preScale(1, -1); + break; + case ORI_TRANSPOSE: + matrix.setRotate(90, w / 2f, h / 2f); + matrix.preScale(1, -1); + break; + case ORI_TRANSVERSE: + matrix.setRotate(270, w / 2f, h / 2f); + matrix.preScale(1, -1); + break; + case ORI_NORMAL: + default: + return bitmap; + } + return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), + bitmap.getHeight(), matrix, true); + } + + /** + * Returns the bitmap for the rectangular region given by "bounds" + * if it is a subset of the bitmap stored at uri. Otherwise returns + * null. + */ + public static Bitmap loadRegionBitmap(Context context, Uri uri, BitmapFactory.Options options, + Rect bounds) { + InputStream is = null; + try { + is = context.getContentResolver().openInputStream(uri); + BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false); + Rect r = new Rect(0, 0, decoder.getWidth(), decoder.getHeight()); + // return null if bounds are not entirely within the bitmap + if (!r.contains(bounds)) { + return null; + } + return decoder.decodeRegion(bounds, options); + } catch (FileNotFoundException e) { + Log.e(LOGTAG, "FileNotFoundException for " + uri, e); + } catch (IOException e) { + Log.e(LOGTAG, "FileNotFoundException for " + uri, e); + } finally { + Utils.closeSilently(is); + } + return null; + } + + /** + * Returns the bounds of the bitmap stored at a given Url. + */ + public static Rect loadBitmapBounds(Context context, Uri uri) { + BitmapFactory.Options o = new BitmapFactory.Options(); + loadBitmap(context, uri, o); + return new Rect(0, 0, o.outWidth, o.outHeight); + } + + /** + * Loads a bitmap that has been downsampled using sampleSize from a given url. + */ + public static Bitmap loadDownsampledBitmap(Context context, Uri uri, int sampleSize) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inMutable = true; + options.inSampleSize = sampleSize; + return loadBitmap(context, uri, options); + } + + + /** + * Returns the bitmap from the given uri loaded using the given options. + * Returns null on failure. + */ + public static Bitmap loadBitmap(Context context, Uri uri, BitmapFactory.Options o) { + if (uri == null || context == null) { + throw new IllegalArgumentException("bad argument to loadBitmap"); + } + InputStream is = null; + try { + is = context.getContentResolver().openInputStream(uri); + return BitmapFactory.decodeStream(is, null, o); + } catch (FileNotFoundException e) { + Log.e(LOGTAG, "FileNotFoundException for " + uri, e); + } finally { + Utils.closeSilently(is); + } + return null; + } + + /** + * Loads a bitmap at a given URI that is downsampled so that both sides are + * smaller than maxSideLength. The Bitmap's original dimensions are stored + * in the rect originalBounds. + * + * @param uri URI of image to open. + * @param context context whose ContentResolver to use. + * @param maxSideLength max side length of returned bitmap. + * @param originalBounds If not null, set to the actual bounds of the stored bitmap. + * @param useMin use min or max side of the original image + * @return downsampled bitmap or null if this operation failed. + */ + public static Bitmap loadConstrainedBitmap(Uri uri, Context context, int maxSideLength, + Rect originalBounds, boolean useMin) { + if (maxSideLength <= 0 || uri == null || context == null) { + throw new IllegalArgumentException("bad argument to getScaledBitmap"); + } + // Get width and height of stored bitmap + Rect storedBounds = loadBitmapBounds(context, uri); + if (originalBounds != null) { + originalBounds.set(storedBounds); + } + int w = storedBounds.width(); + int h = storedBounds.height(); + + // If bitmap cannot be decoded, return null + if (w <= 0 || h <= 0) { + return null; + } + + // Find best downsampling size + int imageSide = 0; + if (useMin) { + imageSide = Math.min(w, h); + } else { + imageSide = Math.max(w, h); + } + int sampleSize = 1; + while (imageSide > maxSideLength) { + imageSide >>>= 1; + sampleSize <<= 1; + } + + // Make sure sample size is reasonable + if (sampleSize <= 0 || + 0 >= (int) (Math.min(w, h) / sampleSize)) { + return null; + } + return loadDownsampledBitmap(context, uri, sampleSize); + } + + /** + * Loads a bitmap at a given URI that is downsampled so that both sides are + * smaller than maxSideLength. The Bitmap's original dimensions are stored + * in the rect originalBounds. The output is also transformed to the given + * orientation. + * + * @param uri URI of image to open. + * @param context context whose ContentResolver to use. + * @param maxSideLength max side length of returned bitmap. + * @param orientation the orientation to transform the bitmap to. + * @param originalBounds set to the actual bounds of the stored bitmap. + * @return downsampled bitmap or null if this operation failed. + */ + public static Bitmap loadOrientedConstrainedBitmap(Uri uri, Context context, int maxSideLength, + int orientation, Rect originalBounds) { + Bitmap bmap = loadConstrainedBitmap(uri, context, maxSideLength, originalBounds, false); + if (bmap != null) { + bmap = orientBitmap(bmap, orientation); + } + return bmap; + } + + public static Bitmap getScaleOneImageForPreset(Context context, Uri uri, Rect bounds, + Rect destination) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inMutable = true; + if (destination != null) { + if (bounds.width() > destination.width()) { + int sampleSize = 1; + int w = bounds.width(); + while (w > destination.width()) { + sampleSize *= 2; + w /= sampleSize; + } + options.inSampleSize = sampleSize; + } + } + Bitmap bmp = loadRegionBitmap(context, uri, options, bounds); + return bmp; + } + + /** + * Loads a bitmap that is downsampled by at least the input sample size. In + * low-memory situations, the bitmap may be downsampled further. + */ + public static Bitmap loadBitmapWithBackouts(Context context, Uri sourceUri, int sampleSize) { + boolean noBitmap = true; + int num_tries = 0; + if (sampleSize <= 0) { + sampleSize = 1; + } + Bitmap bmap = null; + while (noBitmap) { + try { + // Try to decode, downsample if low-memory. + bmap = loadDownsampledBitmap(context, sourceUri, sampleSize); + noBitmap = false; + } catch (java.lang.OutOfMemoryError e) { + // Try with more downsampling before failing for good. + if (++num_tries >= BITMAP_LOAD_BACKOUT_ATTEMPTS) { + throw e; + } + bmap = null; + System.gc(); + sampleSize *= 2; + } + } + return bmap; + } + + /** + * Loads an oriented bitmap that is downsampled by at least the input sample + * size. In low-memory situations, the bitmap may be downsampled further. + */ + public static Bitmap loadOrientedBitmapWithBackouts(Context context, Uri sourceUri, + int sampleSize) { + Bitmap bitmap = loadBitmapWithBackouts(context, sourceUri, sampleSize); + if (bitmap == null) { + return null; + } + int orientation = getMetadataOrientation(context, sourceUri); + bitmap = orientBitmap(bitmap, orientation); + return bitmap; + } + + /** + * Loads bitmap from a resource that may be downsampled in low-memory situations. + */ + public static Bitmap decodeResourceWithBackouts(Resources res, BitmapFactory.Options options, + int id) { + boolean noBitmap = true; + int num_tries = 0; + if (options.inSampleSize < 1) { + options.inSampleSize = 1; + } + // Stopgap fix for low-memory devices. + Bitmap bmap = null; + while (noBitmap) { + try { + // Try to decode, downsample if low-memory. + bmap = BitmapFactory.decodeResource( + res, id, options); + noBitmap = false; + } catch (java.lang.OutOfMemoryError e) { + // Retry before failing for good. + if (++num_tries >= BITMAP_LOAD_BACKOUT_ATTEMPTS) { + throw e; + } + bmap = null; + System.gc(); + options.inSampleSize *= 2; + } + } + return bmap; + } + + public static XMPMeta getXmpObject(Context context) { + try { + InputStream is = context.getContentResolver().openInputStream( + MasterImage.getImage().getUri()); + return XmpUtilHelper.extractXMPMeta(is); + } catch (FileNotFoundException e) { + return null; + } + } + + /** + * Determine if this is a light cycle 360 image + * + * @return true if it is a light Cycle image that is full 360 + */ + public static boolean queryLightCycle360(Context context) { + InputStream is = null; + try { + is = context.getContentResolver().openInputStream(MasterImage.getImage().getUri()); + XMPMeta meta = XmpUtilHelper.extractXMPMeta(is); + if (meta == null) { + return false; + } + String namespace = "http://ns.google.com/photos/1.0/panorama/"; + String cropWidthName = "GPano:CroppedAreaImageWidthPixels"; + String fullWidthName = "GPano:FullPanoWidthPixels"; + + if (!meta.doesPropertyExist(namespace, cropWidthName)) { + return false; + } + if (!meta.doesPropertyExist(namespace, fullWidthName)) { + return false; + } + + Integer cropValue = meta.getPropertyInteger(namespace, cropWidthName); + Integer fullValue = meta.getPropertyInteger(namespace, fullWidthName); + + // Definition of a 360: + // GFullPanoWidthPixels == CroppedAreaImageWidthPixels + if (cropValue != null && fullValue != null) { + return cropValue.equals(fullValue); + } + + return false; + } catch (FileNotFoundException e) { + return false; + } catch (XMPException e) { + return false; + } finally { + Utils.closeSilently(is); + } + } +} diff --git a/src/com/android/gallery3d/filtershow/category/Action.java b/src/com/android/gallery3d/filtershow/category/Action.java new file mode 100644 index 000000000..332ca18b0 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/category/Action.java @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2013 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.filtershow.category; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; +import android.widget.ArrayAdapter; +import android.widget.ListAdapter; +import com.android.gallery3d.filtershow.filters.FilterUserPresetRepresentation; +import com.android.gallery3d.filtershow.pipeline.RenderingRequest; +import com.android.gallery3d.filtershow.pipeline.RenderingRequestCaller; +import com.android.gallery3d.filtershow.filters.FilterRepresentation; +import com.android.gallery3d.filtershow.imageshow.MasterImage; +import com.android.gallery3d.filtershow.pipeline.ImagePreset; + +public class Action implements RenderingRequestCaller { + + private static final String LOGTAG = "Action"; + private FilterRepresentation mRepresentation; + private String mName; + private Rect mImageFrame; + private Bitmap mImage; + private ArrayAdapter mAdapter; + public static final int FULL_VIEW = 0; + public static final int CROP_VIEW = 1; + private int mType = CROP_VIEW; + private Bitmap mPortraitImage; + private Bitmap mOverlayBitmap; + private Context mContext; + + public Action(Context context, FilterRepresentation representation, int type) { + mContext = context; + setRepresentation(representation); + setType(type); + } + + public Action(Context context, FilterRepresentation representation) { + this(context, representation, CROP_VIEW); + } + + public FilterRepresentation getRepresentation() { + return mRepresentation; + } + + public void setRepresentation(FilterRepresentation representation) { + mRepresentation = representation; + mName = representation.getName(); + } + + public String getName() { + return mName; + } + + public void setName(String name) { + mName = name; + } + + public void setImageFrame(Rect imageFrame, int orientation) { + if (mImageFrame != null && mImageFrame.equals(imageFrame)) { + return; + } + Bitmap bitmap = MasterImage.getImage().getLargeThumbnailBitmap(); + if (bitmap != null) { + mImageFrame = imageFrame; + int w = mImageFrame.width(); + int h = mImageFrame.height(); + if (orientation == CategoryView.VERTICAL + && mType == CROP_VIEW) { + w /= 2; + } + Bitmap bitmapCrop = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); + drawCenteredImage(bitmap, bitmapCrop, true); + + postNewIconRenderRequest(bitmapCrop); + } + } + + public Bitmap getImage() { + return mImage; + } + + public void setImage(Bitmap image) { + mImage = image; + } + + public void setAdapter(ArrayAdapter adapter) { + mAdapter = adapter; + } + + public void setType(int type) { + mType = type; + } + + private void postNewIconRenderRequest(Bitmap bitmap) { + if (bitmap != null && mRepresentation != null) { + ImagePreset preset = new ImagePreset(); + preset.addFilter(mRepresentation); + RenderingRequest.post(mContext, bitmap, + preset, RenderingRequest.ICON_RENDERING, this); + } + } + + private void drawCenteredImage(Bitmap source, Bitmap destination, boolean scale) { + RectF image = new RectF(0, 0, source.getWidth(), source.getHeight()); + int border = 0; + if (!scale) { + border = destination.getWidth() - destination.getHeight(); + if (border < 0) { + border = 0; + } + } + RectF frame = new RectF(border, 0, + destination.getWidth() - border, + destination.getHeight()); + Matrix m = new Matrix(); + m.setRectToRect(frame, image, Matrix.ScaleToFit.CENTER); + image.set(frame); + m.mapRect(image); + m.setRectToRect(image, frame, Matrix.ScaleToFit.FILL); + Canvas canvas = new Canvas(destination); + canvas.drawBitmap(source, m, new Paint(Paint.FILTER_BITMAP_FLAG)); + } + + @Override + public void available(RenderingRequest request) { + mImage = request.getBitmap(); + if (mImage == null) { + return; + } + if (mRepresentation.getOverlayId() != 0 && mOverlayBitmap == null) { + mOverlayBitmap = BitmapFactory.decodeResource( + mContext.getResources(), + mRepresentation.getOverlayId()); + } + if (mOverlayBitmap != null) { + if (getRepresentation().getFilterType() == FilterRepresentation.TYPE_BORDER) { + Canvas canvas = new Canvas(mImage); + canvas.drawBitmap(mOverlayBitmap, new Rect(0, 0, mOverlayBitmap.getWidth(), mOverlayBitmap.getHeight()), + new Rect(0, 0, mImage.getWidth(), mImage.getHeight()), new Paint()); + } else { + Canvas canvas = new Canvas(mImage); + canvas.drawARGB(128, 0, 0, 0); + drawCenteredImage(mOverlayBitmap, mImage, false); + } + } + if (mAdapter != null) { + mAdapter.notifyDataSetChanged(); + } + } + + public void setPortraitImage(Bitmap portraitImage) { + mPortraitImage = portraitImage; + } + + public Bitmap getPortraitImage() { + return mPortraitImage; + } + + public Bitmap getOverlayBitmap() { + return mOverlayBitmap; + } + + public void setOverlayBitmap(Bitmap overlayBitmap) { + mOverlayBitmap = overlayBitmap; + } +} diff --git a/src/com/android/gallery3d/filtershow/category/CategoryAdapter.java b/src/com/android/gallery3d/filtershow/category/CategoryAdapter.java new file mode 100644 index 000000000..6451c39df --- /dev/null +++ b/src/com/android/gallery3d/filtershow/category/CategoryAdapter.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2013 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.filtershow.category; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ListView; + +import com.android.gallery3d.filtershow.filters.FilterRepresentation; +import com.android.gallery3d.filtershow.filters.FilterTinyPlanetRepresentation; +import com.android.gallery3d.filtershow.pipeline.ImagePreset; + +public class CategoryAdapter extends ArrayAdapter<Action> { + + private static final String LOGTAG = "CategoryAdapter"; + private int mItemHeight; + private View mContainer; + private int mItemWidth = ListView.LayoutParams.MATCH_PARENT; + private int mSelectedPosition; + int mCategory; + private int mOrientation; + + public CategoryAdapter(Context context, int textViewResourceId) { + super(context, textViewResourceId); + mItemHeight = (int) (context.getResources().getDisplayMetrics().density * 100); + } + + public CategoryAdapter(Context context) { + this(context, 0); + } + + public void setItemHeight(int height) { + mItemHeight = height; + } + + public void setItemWidth(int width) { + mItemWidth = width; + } + + @Override + public void add(Action action) { + super.add(action); + action.setAdapter(this); + } + + public void initializeSelection(int category) { + mCategory = category; + mSelectedPosition = -1; + if (category == MainPanel.LOOKS) { + mSelectedPosition = 0; + } + if (category == MainPanel.BORDERS) { + mSelectedPosition = 0; + } + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = new CategoryView(getContext()); + } + CategoryView view = (CategoryView) convertView; + view.setOrientation(mOrientation); + view.setAction(getItem(position), this); + view.setLayoutParams( + new ListView.LayoutParams(mItemWidth, mItemHeight)); + view.setTag(position); + view.invalidate(); + return view; + } + + public void setSelected(View v) { + int old = mSelectedPosition; + mSelectedPosition = (Integer) v.getTag(); + if (old != -1) { + invalidateView(old); + } + invalidateView(mSelectedPosition); + } + + public boolean isSelected(View v) { + return (Integer) v.getTag() == mSelectedPosition; + } + + private void invalidateView(int position) { + View child = null; + if (mContainer instanceof ListView) { + ListView lv = (ListView) mContainer; + child = lv.getChildAt(position - lv.getFirstVisiblePosition()); + } else { + CategoryTrack ct = (CategoryTrack) mContainer; + child = ct.getChildAt(position); + } + if (child != null) { + child.invalidate(); + } + } + + public void setContainer(View container) { + mContainer = container; + } + + public void imageLoaded() { + notifyDataSetChanged(); + } + + public FilterRepresentation getTinyPlanet() { + for (int i = 0; i < getCount(); i++) { + Action action = getItem(i); + if (action.getRepresentation() != null + && action.getRepresentation() + instanceof FilterTinyPlanetRepresentation) { + return action.getRepresentation(); + } + } + return null; + } + + public void removeTinyPlanet() { + for (int i = 0; i < getCount(); i++) { + Action action = getItem(i); + if (action.getRepresentation() != null + && action.getRepresentation() + instanceof FilterTinyPlanetRepresentation) { + remove(action); + return; + } + } + } + + public void setOrientation(int orientation) { + mOrientation = orientation; + } + + public void reflectImagePreset(ImagePreset preset) { + if (preset == null) { + return; + } + int selected = 0; // if nothing found, select "none" (first element) + FilterRepresentation rep = null; + if (mCategory == MainPanel.LOOKS) { + int pos = preset.getPositionForType(FilterRepresentation.TYPE_FX); + if (pos != -1) { + rep = preset.getFilterRepresentation(pos); + } + } else if (mCategory == MainPanel.BORDERS) { + int pos = preset.getPositionForType(FilterRepresentation.TYPE_BORDER); + if (pos != -1) { + rep = preset.getFilterRepresentation(pos); + } + } + if (rep != null) { + for (int i = 0; i < getCount(); i++) { + if (rep.getName().equalsIgnoreCase( + getItem(i).getRepresentation().getName())) { + selected = i; + break; + } + } + } + if (mSelectedPosition != selected) { + mSelectedPosition = selected; + this.notifyDataSetChanged(); + } + } +} diff --git a/src/com/android/gallery3d/filtershow/category/CategoryPanel.java b/src/com/android/gallery3d/filtershow/category/CategoryPanel.java new file mode 100644 index 000000000..de2481f3f --- /dev/null +++ b/src/com/android/gallery3d/filtershow/category/CategoryPanel.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2013 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.filtershow.category; + +import android.app.Activity; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.ListView; +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.FilterShowActivity; + +public class CategoryPanel extends Fragment { + + public static final String FRAGMENT_TAG = "CategoryPanel"; + private static final String PARAMETER_TAG = "currentPanel"; + + private int mCurrentAdapter = MainPanel.LOOKS; + private CategoryAdapter mAdapter; + + public void setAdapter(int value) { + mCurrentAdapter = value; + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + loadAdapter(mCurrentAdapter); + } + + private void loadAdapter(int adapter) { + FilterShowActivity activity = (FilterShowActivity) getActivity(); + switch (adapter) { + case MainPanel.LOOKS: { + mAdapter = activity.getCategoryLooksAdapter(); + mAdapter.initializeSelection(MainPanel.LOOKS); + activity.updateCategories(); + break; + } + case MainPanel.BORDERS: { + mAdapter = activity.getCategoryBordersAdapter(); + mAdapter.initializeSelection(MainPanel.BORDERS); + activity.updateCategories(); + break; + } + case MainPanel.GEOMETRY: { + mAdapter = activity.getCategoryGeometryAdapter(); + mAdapter.initializeSelection(MainPanel.GEOMETRY); + break; + } + case MainPanel.FILTERS: { + mAdapter = activity.getCategoryFiltersAdapter(); + mAdapter.initializeSelection(MainPanel.FILTERS); + break; + } + } + } + + @Override + public void onSaveInstanceState(Bundle state) { + super.onSaveInstanceState(state); + state.putInt(PARAMETER_TAG, mCurrentAdapter); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + LinearLayout main = (LinearLayout) inflater.inflate( + R.layout.filtershow_category_panel_new, container, + false); + + if (savedInstanceState != null) { + int selectedPanel = savedInstanceState.getInt(PARAMETER_TAG); + loadAdapter(selectedPanel); + } + + View panelView = main.findViewById(R.id.listItems); + if (panelView instanceof CategoryTrack) { + CategoryTrack panel = (CategoryTrack) panelView; + mAdapter.setOrientation(CategoryView.HORIZONTAL); + panel.setAdapter(mAdapter); + mAdapter.setContainer(panel); + } else { + ListView panel = (ListView) main.findViewById(R.id.listItems); + panel.setAdapter(mAdapter); + mAdapter.setContainer(panel); + } + return main; + } + +} diff --git a/src/com/android/gallery3d/filtershow/category/CategoryTrack.java b/src/com/android/gallery3d/filtershow/category/CategoryTrack.java new file mode 100644 index 000000000..ac8245a3b --- /dev/null +++ b/src/com/android/gallery3d/filtershow/category/CategoryTrack.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2013 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.filtershow.category; + +import android.content.Context; +import android.content.res.TypedArray; +import android.database.DataSetObserver; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.LinearLayout; +import com.android.gallery3d.R; + +public class CategoryTrack extends LinearLayout { + + private CategoryAdapter mAdapter; + private int mElemSize; + private DataSetObserver mDataSetObserver = new DataSetObserver() { + @Override + public void onChanged() { + super.onChanged(); + invalidate(); + } + @Override + public void onInvalidated() { + super.onInvalidated(); + fillContent(); + } + }; + + public CategoryTrack(Context context, AttributeSet attrs) { + super(context, attrs); + TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.CategoryTrack); + mElemSize = a.getDimensionPixelSize(R.styleable.CategoryTrack_iconSize, 0); + } + + public void setAdapter(CategoryAdapter adapter) { + mAdapter = adapter; + mAdapter.registerDataSetObserver(mDataSetObserver); + fillContent(); + } + + public void fillContent() { + removeAllViews(); + mAdapter.setItemWidth(mElemSize); + mAdapter.setItemHeight(LayoutParams.MATCH_PARENT); + int n = mAdapter.getCount(); + for (int i = 0; i < n; i++) { + View view = mAdapter.getView(i, null, this); + addView(view, i); + } + requestLayout(); + } + + @Override + public void invalidate() { + for (int i = 0; i < this.getChildCount(); i++) { + View child = getChildAt(i); + child.invalidate(); + } + } + +} diff --git a/src/com/android/gallery3d/filtershow/category/CategoryView.java b/src/com/android/gallery3d/filtershow/category/CategoryView.java new file mode 100644 index 000000000..c456dc207 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/category/CategoryView.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2013 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.filtershow.category; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.view.View; +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.FilterShowActivity; +import com.android.gallery3d.filtershow.filters.FilterRepresentation; +import com.android.gallery3d.filtershow.ui.SelectionRenderer; + +public class CategoryView extends View implements View.OnClickListener { + + private static final String LOGTAG = "CategoryView"; + public static final int VERTICAL = 0; + public static final int HORIZONTAL = 1; + private Paint mPaint = new Paint(); + private Action mAction; + private Rect mTextBounds = new Rect(); + private int mMargin = 16; + private int mTextSize = 32; + private int mTextColor; + private int mBackgroundColor; + private Paint mSelectPaint; + CategoryAdapter mAdapter; + private int mSelectionStroke; + private Paint mBorderPaint; + private int mBorderStroke; + private int mOrientation = VERTICAL; + + public CategoryView(Context context) { + super(context); + setOnClickListener(this); + Resources res = getResources(); + mBackgroundColor = res.getColor(R.color.filtershow_categoryview_background); + mTextColor = res.getColor(R.color.filtershow_categoryview_text); + mSelectionStroke = res.getDimensionPixelSize(R.dimen.thumbnail_margin); + mTextSize = res.getDimensionPixelSize(R.dimen.category_panel_text_size); + mMargin = res.getDimensionPixelOffset(R.dimen.category_panel_margin); + mSelectPaint = new Paint(); + mSelectPaint.setStyle(Paint.Style.FILL); + mSelectPaint.setColor(res.getColor(R.color.filtershow_category_selection)); + mBorderPaint = new Paint(mSelectPaint); + mBorderPaint.setColor(Color.BLACK); + mBorderStroke = mSelectionStroke / 3; + } + + private void computeTextPosition(String text) { + if (text == null) { + return; + } + mPaint.setTextSize(mTextSize); + if (mOrientation == VERTICAL) { + text = text.toUpperCase(); + // TODO: set this in xml + mPaint.setTypeface(Typeface.DEFAULT_BOLD); + } + mPaint.getTextBounds(text, 0, text.length(), mTextBounds); + } + + public void drawText(Canvas canvas, String text) { + if (text == null) { + return; + } + float textWidth = mPaint.measureText(text); + int x = (int) (canvas.getWidth() - textWidth - mMargin); + if (mOrientation == HORIZONTAL) { + x = (int) ((canvas.getWidth() - textWidth) / 2.0f); + } + if (x < 0) { + // If the text takes more than the view width, + // justify to the left. + x = mMargin; + } + int y = canvas.getHeight() - mMargin; + canvas.drawText(text, x, y, mPaint); + } + + @Override + public CharSequence getContentDescription () { + if (mAction != null) { + return mAction.getName(); + } + return null; + } + + @Override + public void onDraw(Canvas canvas) { + canvas.drawColor(mBackgroundColor); + if (mAction != null) { + mPaint.reset(); + mPaint.setAntiAlias(true); + computeTextPosition(mAction.getName()); + if (mAction.getImage() == null) { + mAction.setImageFrame(new Rect(0, 0, getWidth(), getHeight()), mOrientation); + } else { + Bitmap bitmap = mAction.getImage(); + canvas.save(); + Rect clipRect = new Rect(mSelectionStroke, mSelectionStroke, + getWidth() - mSelectionStroke, + getHeight() - 2* mMargin - mTextSize); + int offsetx = 0; + int offsety = 0; + if (mOrientation == HORIZONTAL) { + canvas.clipRect(clipRect); + offsetx = - (bitmap.getWidth() - clipRect.width()) / 2; + offsety = - (bitmap.getHeight() - clipRect.height()) / 2; + } + canvas.drawBitmap(bitmap, offsetx, offsety, mPaint); + canvas.restore(); + if (mAdapter.isSelected(this)) { + if (mOrientation == HORIZONTAL) { + SelectionRenderer.drawSelection(canvas, 0, 0, + getWidth(), getHeight() - mMargin - mTextSize, + mSelectionStroke, mSelectPaint, mBorderStroke, mBorderPaint); + } else { + SelectionRenderer.drawSelection(canvas, 0, 0, + Math.min(bitmap.getWidth(), getWidth()), + Math.min(bitmap.getHeight(), getHeight()), + mSelectionStroke, mSelectPaint, mBorderStroke, mBorderPaint); + } + } + } + mPaint.setColor(mBackgroundColor); + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setStrokeWidth(3); + drawText(canvas, mAction.getName()); + mPaint.setColor(mTextColor); + mPaint.setStyle(Paint.Style.FILL); + mPaint.setStrokeWidth(1); + drawText(canvas, mAction.getName()); + } + } + + public void setAction(Action action, CategoryAdapter adapter) { + mAction = action; + mAdapter = adapter; + invalidate(); + } + + public FilterRepresentation getRepresentation() { + return mAction.getRepresentation(); + } + + @Override + public void onClick(View view) { + FilterShowActivity activity = (FilterShowActivity) getContext(); + activity.showRepresentation(mAction.getRepresentation()); + mAdapter.setSelected(this); + } + + public void setOrientation(int orientation) { + mOrientation = orientation; + } +} diff --git a/src/com/android/gallery3d/filtershow/category/MainPanel.java b/src/com/android/gallery3d/filtershow/category/MainPanel.java new file mode 100644 index 000000000..9a64ffbf3 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/category/MainPanel.java @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2013 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.filtershow.category; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentTransaction; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.LinearLayout; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.FilterShowActivity; +import com.android.gallery3d.filtershow.state.StatePanel; + +public class MainPanel extends Fragment { + + private static final String LOGTAG = "MainPanel"; + + private LinearLayout mMainView; + private ImageButton looksButton; + private ImageButton bordersButton; + private ImageButton geometryButton; + private ImageButton filtersButton; + + public static final String FRAGMENT_TAG = "MainPanel"; + public static final int LOOKS = 0; + public static final int BORDERS = 1; + public static final int GEOMETRY = 2; + public static final int FILTERS = 3; + + private int mCurrentSelected = -1; + + private void selection(int position, boolean value) { + if (value) { + FilterShowActivity activity = (FilterShowActivity) getActivity(); + activity.setCurrentPanel(position); + } + switch (position) { + case LOOKS: { + looksButton.setSelected(value); + break; + } + case BORDERS: { + bordersButton.setSelected(value); + break; + } + case GEOMETRY: { + geometryButton.setSelected(value); + break; + } + case FILTERS: { + filtersButton.setSelected(value); + break; + } + } + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + if (mMainView != null) { + if (mMainView.getParent() != null) { + ViewGroup parent = (ViewGroup) mMainView.getParent(); + parent.removeView(mMainView); + } + } + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + mMainView = (LinearLayout) inflater.inflate( + R.layout.filtershow_main_panel, null, false); + + looksButton = (ImageButton) mMainView.findViewById(R.id.fxButton); + bordersButton = (ImageButton) mMainView.findViewById(R.id.borderButton); + geometryButton = (ImageButton) mMainView.findViewById(R.id.geometryButton); + filtersButton = (ImageButton) mMainView.findViewById(R.id.colorsButton); + + looksButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + showPanel(LOOKS); + } + }); + bordersButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + showPanel(BORDERS); + } + }); + geometryButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + showPanel(GEOMETRY); + } + }); + filtersButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + showPanel(FILTERS); + } + }); + + FilterShowActivity activity = (FilterShowActivity) getActivity(); + showImageStatePanel(activity.isShowingImageStatePanel()); + showPanel(activity.getCurrentPanel()); + return mMainView; + } + + private boolean isRightAnimation(int newPos) { + if (newPos < mCurrentSelected) { + return false; + } + return true; + } + + private void setCategoryFragment(CategoryPanel category, boolean fromRight) { + FragmentTransaction transaction = getChildFragmentManager().beginTransaction(); + if (fromRight) { + transaction.setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_right); + } else { + transaction.setCustomAnimations(R.anim.slide_in_left, R.anim.slide_out_left); + } + transaction.replace(R.id.category_panel_container, category, CategoryPanel.FRAGMENT_TAG); + transaction.commit(); + } + + public void loadCategoryLookPanel() { + if (mCurrentSelected == LOOKS) { + return; + } + boolean fromRight = isRightAnimation(LOOKS); + selection(mCurrentSelected, false); + CategoryPanel categoryPanel = new CategoryPanel(); + categoryPanel.setAdapter(LOOKS); + setCategoryFragment(categoryPanel, fromRight); + mCurrentSelected = LOOKS; + selection(mCurrentSelected, true); + } + + public void loadCategoryBorderPanel() { + if (mCurrentSelected == BORDERS) { + return; + } + boolean fromRight = isRightAnimation(BORDERS); + selection(mCurrentSelected, false); + CategoryPanel categoryPanel = new CategoryPanel(); + categoryPanel.setAdapter(BORDERS); + setCategoryFragment(categoryPanel, fromRight); + mCurrentSelected = BORDERS; + selection(mCurrentSelected, true); + } + + public void loadCategoryGeometryPanel() { + if (mCurrentSelected == GEOMETRY) { + return; + } + boolean fromRight = isRightAnimation(GEOMETRY); + selection(mCurrentSelected, false); + CategoryPanel categoryPanel = new CategoryPanel(); + categoryPanel.setAdapter(GEOMETRY); + setCategoryFragment(categoryPanel, fromRight); + mCurrentSelected = GEOMETRY; + selection(mCurrentSelected, true); + } + + public void loadCategoryFiltersPanel() { + if (mCurrentSelected == FILTERS) { + return; + } + boolean fromRight = isRightAnimation(FILTERS); + selection(mCurrentSelected, false); + CategoryPanel categoryPanel = new CategoryPanel(); + categoryPanel.setAdapter(FILTERS); + setCategoryFragment(categoryPanel, fromRight); + mCurrentSelected = FILTERS; + selection(mCurrentSelected, true); + } + + public void showPanel(int currentPanel) { + switch (currentPanel) { + case LOOKS: { + loadCategoryLookPanel(); + break; + } + case BORDERS: { + loadCategoryBorderPanel(); + break; + } + case GEOMETRY: { + loadCategoryGeometryPanel(); + break; + } + case FILTERS: { + loadCategoryFiltersPanel(); + break; + } + } + } + + public void showImageStatePanel(boolean show) { + if (mMainView.findViewById(R.id.state_panel_container) == null) { + return; + } + FragmentTransaction transaction = getChildFragmentManager().beginTransaction(); + final View container = mMainView.findViewById(R.id.state_panel_container); + if (show) { + container.setVisibility(View.VISIBLE); + StatePanel statePanel = new StatePanel(); + transaction.replace(R.id.state_panel_container, statePanel, StatePanel.FRAGMENT_TAG); + } else { + container.setVisibility(View.GONE); + Fragment statePanel = getChildFragmentManager().findFragmentByTag(StatePanel.FRAGMENT_TAG); + if (statePanel != null) { + transaction.remove(statePanel); + } + } + transaction.commit(); + } +} diff --git a/src/com/android/gallery3d/filtershow/colorpicker/ColorGridDialog.java b/src/com/android/gallery3d/filtershow/colorpicker/ColorGridDialog.java new file mode 100644 index 000000000..dd4df7dc8 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/colorpicker/ColorGridDialog.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2013 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.filtershow.colorpicker; + +import android.app.Dialog; +import android.content.Context; +import android.graphics.Color; +import android.graphics.drawable.GradientDrawable; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; + +import com.android.gallery3d.R; + +import java.util.ArrayList; + +public class ColorGridDialog extends Dialog { + RGBListener mCallback; + private static final String LOGTAG = "ColorGridDialog"; + + public ColorGridDialog(Context context, final RGBListener cl) { + super(context); + mCallback = cl; + setTitle(R.string.color_pick_title); + setContentView(R.layout.filtershow_color_gird); + Button sel = (Button) findViewById(R.id.filtershow_cp_custom); + ArrayList<Button> b = getButtons((ViewGroup) getWindow().getDecorView()); + int k = 0; + float[] hsv = new float[3]; + + for (Button button : b) { + if (!button.equals(sel)){ + hsv[0] = (k % 5) * 360 / 5; + hsv[1] = (k / 5) / 3.0f; + hsv[2] = (k < 5) ? (k / 4f) : 1; + final int c = (Color.HSVToColor(hsv) & 0x00FFFFFF) | 0xAA000000; + GradientDrawable sd = ((GradientDrawable) button.getBackground()); + button.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mCallback.setColor(c); + dismiss(); + } + }); + sd.setColor(c); + k++; + } + + } + sel.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + showColorPicker(); + ColorGridDialog.this.dismiss(); + } + }); + } + + private ArrayList<Button> getButtons(ViewGroup vg) { + ArrayList<Button> list = new ArrayList<Button>(); + for (int i = 0; i < vg.getChildCount(); i++) { + View v = vg.getChildAt(i); + if (v instanceof Button) { + list.add((Button) v); + } else if (v instanceof ViewGroup) { + list.addAll(getButtons((ViewGroup) v)); + } + } + return list; + } + + public void showColorPicker() { + ColorListener cl = new ColorListener() { + @Override + public void setColor(float[] hsvo) { + int c = Color.HSVToColor(hsvo) & 0xFFFFFF; + int alpha = (int) (hsvo[3] * 255); + c |= alpha << 24; + mCallback.setColor(c); + } + }; + ColorPickerDialog cpd = new ColorPickerDialog(this.getContext(), cl); + cpd.show(); + } + +} diff --git a/src/com/android/gallery3d/filtershow/colorpicker/ColorListener.java b/src/com/android/gallery3d/filtershow/colorpicker/ColorListener.java new file mode 100644 index 000000000..5127dad26 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/colorpicker/ColorListener.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2013 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.filtershow.colorpicker; + +public interface ColorListener { + void setColor(float[] hsvo); +} diff --git a/src/com/android/gallery3d/filtershow/colorpicker/ColorOpacityView.java b/src/com/android/gallery3d/filtershow/colorpicker/ColorOpacityView.java new file mode 100644 index 000000000..2bff501f7 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/colorpicker/ColorOpacityView.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2013 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.filtershow.colorpicker; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.LinearGradient; +import android.graphics.Paint; +import android.graphics.RadialGradient; +import android.graphics.Shader; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.view.MotionEvent; +import android.view.View; + +import com.android.gallery3d.R; + +import java.util.ArrayList; + +public class ColorOpacityView extends View implements ColorListener { + + private float mRadius; + private float mWidth; + private Paint mBarPaint1; + private Paint mLinePaint1; + private Paint mLinePaint2; + private Paint mCheckPaint; + + private float mHeight; + private Paint mDotPaint; + private int mBgcolor = 0; + + private float mDotRadius; + private float mBorder; + + private float[] mHSVO = new float[4]; + private int mSliderColor; + private float mDotX = mBorder; + private float mDotY = mBorder; + private final static float DOT_SIZE = ColorRectView.DOT_SIZE; + public final static float BORDER_SIZE = 20;; + + public ColorOpacityView(Context ctx, AttributeSet attrs) { + super(ctx, attrs); + DisplayMetrics metrics = ctx.getResources().getDisplayMetrics(); + float mDpToPix = metrics.density; + mDotRadius = DOT_SIZE * mDpToPix; + mBorder = BORDER_SIZE * mDpToPix; + mBarPaint1 = new Paint(); + + mDotPaint = new Paint(); + + mDotPaint.setStyle(Paint.Style.FILL); + mDotPaint.setColor(ctx.getResources().getColor(R.color.slider_dot_color)); + mSliderColor = ctx.getResources().getColor(R.color.slider_line_color); + + mBarPaint1.setStyle(Paint.Style.FILL); + + mLinePaint1 = new Paint(); + mLinePaint1.setColor(Color.GRAY); + mLinePaint2 = new Paint(); + mLinePaint2.setColor(mSliderColor); + mLinePaint2.setStrokeWidth(4); + + int[] colors = new int[16 * 16]; + for (int i = 0; i < colors.length; i++) { + int y = i / (16 * 8); + int x = (i / 8) % 2; + colors[i] = (x == y) ? 0xFFAAAAAA : 0xFF444444; + } + Bitmap bitmap = Bitmap.createBitmap(colors, 16, 16, Bitmap.Config.ARGB_8888); + BitmapShader bs = new BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT); + mCheckPaint = new Paint(); + mCheckPaint.setShader(bs); + } + + public boolean onDown(MotionEvent e) { + return true; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + float ox = mDotX; + float oy = mDotY; + + float x = event.getX(); + float y = event.getY(); + + mDotX = x; + + if (mDotX < mBorder) { + mDotX = mBorder; + } + + if (mDotX > mWidth - mBorder) { + mDotX = mWidth - mBorder; + } + mHSVO[3] = (mDotX - mBorder) / (mWidth - mBorder * 2); + notifyColorListeners(mHSVO); + setupButton(); + invalidate((int) (ox - mDotRadius), (int) (oy - mDotRadius), (int) (ox + mDotRadius), + (int) (oy + mDotRadius)); + invalidate( + (int) (mDotX - mDotRadius), (int) (mDotY - mDotRadius), (int) (mDotX + mDotRadius), + (int) (mDotY + mDotRadius)); + + return true; + } + + private void setupButton() { + float pos = mHSVO[3] * (mWidth - mBorder * 2); + mDotX = pos + mBorder; + + int[] colors3 = new int[] { + mSliderColor, mSliderColor, 0x66000000, 0 }; + RadialGradient g = new RadialGradient(mDotX, mDotY, mDotRadius, colors3, new float[] { + 0, .3f, .31f, 1 }, Shader.TileMode.CLAMP); + mDotPaint.setShader(g); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + mWidth = w; + mHeight = h; + mDotY = mHeight / 2; + updatePaint(); + setupButton(); + } + + private void updatePaint() { + + int color2 = Color.HSVToColor(mHSVO); + int color1 = color2 & 0xFFFFFF; + + Shader sg = new LinearGradient( + mBorder, mBorder, mWidth - mBorder, mBorder, color1, color2, Shader.TileMode.CLAMP); + mBarPaint1.setShader(sg); + + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + canvas.drawColor(mBgcolor); + canvas.drawRect(mBorder, mBorder, mWidth - mBorder, mHeight - mBorder, mCheckPaint); + canvas.drawRect(mBorder, mBorder, mWidth - mBorder, mHeight - mBorder, mBarPaint1); + canvas.drawLine(mDotX, mDotY, mWidth - mBorder, mDotY, mLinePaint1); + canvas.drawLine(mBorder, mDotY, mDotX, mDotY, mLinePaint2); + if (mDotX != Float.NaN) { + canvas.drawCircle(mDotX, mDotY, mDotRadius, mDotPaint); + } + } + + @Override + public void setColor(float[] hsv) { + System.arraycopy(hsv, 0, mHSVO, 0, mHSVO.length); + + float oy = mDotY; + + updatePaint(); + setupButton(); + invalidate(); + } + + ArrayList<ColorListener> mColorListeners = new ArrayList<ColorListener>(); + + public void notifyColorListeners(float[] hsvo) { + for (ColorListener l : mColorListeners) { + l.setColor(hsvo); + } + } + + public void addColorListener(ColorListener l) { + mColorListeners.add(l); + } + + public void removeColorListener(ColorListener l) { + mColorListeners.remove(l); + } +} diff --git a/src/com/android/gallery3d/filtershow/colorpicker/ColorPickerDialog.java b/src/com/android/gallery3d/filtershow/colorpicker/ColorPickerDialog.java new file mode 100644 index 000000000..73a5c907c --- /dev/null +++ b/src/com/android/gallery3d/filtershow/colorpicker/ColorPickerDialog.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2013 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.filtershow.colorpicker; + +import android.app.Dialog; +import android.content.Context; +import android.graphics.Color; +import android.graphics.drawable.GradientDrawable; +import android.view.View; +import android.widget.Button; +import android.widget.ToggleButton; + +import com.android.gallery3d.R; + +public class ColorPickerDialog extends Dialog implements ColorListener { + ToggleButton mSelectedButton; + GradientDrawable mSelectRect; + + float[] mHSVO = new float[4]; + + public ColorPickerDialog(Context context, final ColorListener cl) { + super(context); + + setContentView(R.layout.filtershow_color_picker); + ColorValueView csv = (ColorValueView) findViewById(R.id.colorValueView); + ColorRectView cwv = (ColorRectView) findViewById(R.id.colorRectView); + ColorOpacityView cvv = (ColorOpacityView) findViewById(R.id.colorOpacityView); + float[] hsvo = new float[] { + 123, .9f, 1, 1 }; + + mSelectRect = (GradientDrawable) getContext() + .getResources().getDrawable(R.drawable.filtershow_color_picker_roundrect); + Button selButton = (Button) findViewById(R.id.btnSelect); + selButton.setCompoundDrawablesWithIntrinsicBounds(null, null, mSelectRect, null); + Button sel = (Button) findViewById(R.id.btnSelect); + + sel.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + ColorPickerDialog.this.dismiss(); + if (cl != null) { + cl.setColor(mHSVO); + } + } + }); + + cwv.setColor(hsvo); + cvv.setColor(hsvo); + csv.setColor(hsvo); + csv.addColorListener(cwv); + cwv.addColorListener(csv); + csv.addColorListener(cvv); + cwv.addColorListener(cvv); + cvv.addColorListener(cwv); + cvv.addColorListener(csv); + cvv.addColorListener(this); + csv.addColorListener(this); + cwv.addColorListener(this); + + } + + void toggleClick(ToggleButton v, int[] buttons, boolean isChecked) { + int id = v.getId(); + if (!isChecked) { + mSelectedButton = null; + return; + } + for (int i = 0; i < buttons.length; i++) { + if (id != buttons[i]) { + ToggleButton b = (ToggleButton) findViewById(buttons[i]); + b.setChecked(false); + } + } + mSelectedButton = v; + + float[] hsv = (float[]) v.getTag(); + + ColorValueView csv = (ColorValueView) findViewById(R.id.colorValueView); + ColorRectView cwv = (ColorRectView) findViewById(R.id.colorRectView); + ColorOpacityView cvv = (ColorOpacityView) findViewById(R.id.colorOpacityView); + cwv.setColor(hsv); + cvv.setColor(hsv); + csv.setColor(hsv); + } + + @Override + public void setColor(float[] hsvo) { + System.arraycopy(hsvo, 0, mHSVO, 0, mHSVO.length); + int color = Color.HSVToColor(hsvo); + mSelectRect.setColor(color); + setButtonColor(mSelectedButton, hsvo); + } + + private void setButtonColor(ToggleButton button, float[] hsv) { + if (button == null) { + return; + } + int color = Color.HSVToColor(hsv); + button.setBackgroundColor(color); + float[] fg = new float[] { + (hsv[0] + 180) % 360, + hsv[1], + (hsv[2] > .5f) ? .1f : .9f + }; + button.setTextColor(Color.HSVToColor(fg)); + button.setTag(hsv); + } + +} diff --git a/src/com/android/gallery3d/filtershow/colorpicker/ColorRectView.java b/src/com/android/gallery3d/filtershow/colorpicker/ColorRectView.java new file mode 100644 index 000000000..07d7c7126 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/colorpicker/ColorRectView.java @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2013 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.filtershow.colorpicker; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.LinearGradient; +import android.graphics.Paint; +import android.graphics.RadialGradient; +import android.graphics.RectF; +import android.graphics.Shader; +import android.graphics.SweepGradient; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.view.MotionEvent; +import android.view.View; + +import com.android.gallery3d.R; + +import java.util.ArrayList; + +public class ColorRectView extends View implements ColorListener { + private float mDpToPix; + private float mRadius = 80; + private float mCtrY = 100; + private Paint mWheelPaint1; + private Paint mWheelPaint2; + private Paint mWheelPaint3; + private float mCtrX = 100; + private Paint mDotPaint; + private float mDotRadus; + private float mBorder; + private int mBgcolor = 0; + private float mDotX = Float.NaN; + private float mDotY; + private int mSliderColor = 0xFF33B5E5; + private float[] mHSVO = new float[4]; + private int[] mColors = new int[] { + 0xFFFF0000,// red + 0xFFFFFF00,// yellow + 0xFF00FF00,// green + 0xFF00FFFF,// cyan + 0xFF0000FF,// blue + 0xFFFF00FF,// magenta + 0xFFFF0000,// red + }; + private int mWidth; + private int mHeight; + public final static float DOT_SIZE = 20; + public final static float BORDER_SIZE = 10; + + public ColorRectView(Context ctx, AttributeSet attrs) { + super(ctx, attrs); + + DisplayMetrics metrics = ctx.getResources().getDisplayMetrics(); + mDpToPix = metrics.density; + mDotRadus = DOT_SIZE * mDpToPix; + mBorder = BORDER_SIZE * mDpToPix; + + mWheelPaint1 = new Paint(); + mWheelPaint2 = new Paint(); + mWheelPaint3 = new Paint(); + mDotPaint = new Paint(); + + mDotPaint.setStyle(Paint.Style.FILL); + mDotPaint.setColor(ctx.getResources().getColor(R.color.slider_dot_color)); + mSliderColor = ctx.getResources().getColor(R.color.slider_line_color); + mWheelPaint1.setStyle(Paint.Style.FILL); + mWheelPaint2.setStyle(Paint.Style.FILL); + mWheelPaint3.setStyle(Paint.Style.FILL); + } + + public boolean onDown(MotionEvent e) { + return true; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + + invalidate((int) (mDotX - mDotRadus), (int) (mDotY - mDotRadus), (int) (mDotX + mDotRadus), + (int) (mDotY + mDotRadus)); + float x = event.getX(); + float y = event.getY(); + + x = Math.max(Math.min(x, mWidth - mBorder), mBorder); + y = Math.max(Math.min(y, mHeight - mBorder), mBorder); + mDotX = x; + mDotY = y; + float sat = 1 - (mDotY - mBorder) / (mHeight - 2 * mBorder); + if (sat > 1) { + sat = 1; + } + + double hue = Math.PI * 2 * (mDotX - mBorder) / (mHeight - 2 * mBorder); + mHSVO[0] = ((float) Math.toDegrees(hue) + 360) % 360; + mHSVO[1] = sat; + notifyColorListeners(mHSVO); + updateDotPaint(); + invalidate((int) (mDotX - mDotRadus), (int) (mDotY - mDotRadus), (int) (mDotX + mDotRadus), + (int) (mDotY + mDotRadus)); + + return true; + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + mWidth = w; + mHeight = h; + mCtrY = h / 2f; + mCtrX = w / 2f; + mRadius = Math.min(mCtrY, mCtrX) - 2 * mBorder; + setUpColorPanel(); + } + + private void setUpColorPanel() { + float val = mHSVO[2]; + int v = 0xFF000000 | 0x10101 * (int) (val * 0xFF); + int[] colors = new int[] { + 0x0000000, v }; + int[] colors2 = new int[] { + 0x0000000, 0xFF000000 }; + int[] wheelColor = new int[mColors.length]; + float[] hsv = new float[3]; + for (int i = 0; i < wheelColor.length; i++) { + Color.colorToHSV(mColors[i], hsv); + hsv[2] = mHSVO[2]; + wheelColor[i] = Color.HSVToColor(hsv); + } + updateDot(); + updateDotPaint(); + SweepGradient sg = new SweepGradient(mCtrX, mCtrY, wheelColor, null); + LinearGradient lg = new LinearGradient( + mBorder, 0, mWidth - mBorder, 0, wheelColor, null, Shader.TileMode.CLAMP); + + mWheelPaint1.setShader(lg); + LinearGradient rg = new LinearGradient( + 0, mBorder, 0, mHeight - mBorder, colors, null, Shader.TileMode.CLAMP); + mWheelPaint2.setShader(rg); + LinearGradient rg2 = new LinearGradient( + 0, mBorder, 0, mHeight - mBorder, colors2, null, Shader.TileMode.CLAMP); + mWheelPaint3.setShader(rg2); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + canvas.drawColor(mBgcolor); + RectF rect = new RectF(); + rect.left = mBorder; + rect.right = mWidth - mBorder; + rect.top = mBorder; + rect.bottom = mHeight - mBorder; + + canvas.drawRect(rect, mWheelPaint1); + canvas.drawRect(rect, mWheelPaint3); + canvas.drawRect(rect, mWheelPaint2); + + if (mDotX != Float.NaN) { + + canvas.drawCircle(mDotX, mDotY, mDotRadus, mDotPaint); + } + } + + private void updateDot() { + + double hue = mHSVO[0]; + double sat = mHSVO[1]; + + mDotX = (float) (mBorder + (mHeight - 2 * mBorder) * Math.toRadians(hue) / (Math.PI * 2)); + mDotY = (float) ((1 - sat) * (mHeight - 2 * mBorder) + mBorder); + + } + + private void updateDotPaint() { + int[] colors3 = new int[] { + mSliderColor, mSliderColor, 0x66000000, 0 }; + RadialGradient g = new RadialGradient(mDotX, mDotY, mDotRadus, colors3, new float[] { + 0, .3f, .31f, 1 }, Shader.TileMode.CLAMP); + mDotPaint.setShader(g); + + } + + @Override + public void setColor(float[] hsvo) { + System.arraycopy(hsvo, 0, mHSVO, 0, mHSVO.length); + + setUpColorPanel(); + invalidate(); + + updateDot(); + updateDotPaint(); + + } + + ArrayList<ColorListener> mColorListeners = new ArrayList<ColorListener>(); + + public void notifyColorListeners(float[] hsv) { + for (ColorListener l : mColorListeners) { + l.setColor(hsv); + } + } + + public void addColorListener(ColorListener l) { + mColorListeners.add(l); + } + + public void removeColorListener(ColorListener l) { + mColorListeners.remove(l); + } +} diff --git a/src/com/android/gallery3d/filtershow/colorpicker/ColorValueView.java b/src/com/android/gallery3d/filtershow/colorpicker/ColorValueView.java new file mode 100644 index 000000000..13cb44bad --- /dev/null +++ b/src/com/android/gallery3d/filtershow/colorpicker/ColorValueView.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2013 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.filtershow.colorpicker; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.LinearGradient; +import android.graphics.Paint; +import android.graphics.RadialGradient; +import android.graphics.Shader; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.view.MotionEvent; +import android.view.View; + +import com.android.gallery3d.R; + +import java.util.ArrayList; + +public class ColorValueView extends View implements ColorListener { + + private float mRadius; + private float mWidth; + private Paint mBarPaint1; + private Paint mLinePaint1; + private Paint mLinePaint2; + private float mHeight; + private int mBgcolor = 0; + private Paint mDotPaint; + private float dotRadus; + private float mBorder; + + private float[] mHSVO = new float[4]; + private int mSliderColor; + private float mDotX; + private float mDotY = mBorder; + private final static float DOT_SIZE = ColorRectView.DOT_SIZE; + private final static float BORDER_SIZE = ColorRectView.DOT_SIZE; + + public ColorValueView(Context ctx, AttributeSet attrs) { + super(ctx, attrs); + DisplayMetrics metrics = ctx.getResources().getDisplayMetrics(); + float mDpToPix = metrics.density; + dotRadus = DOT_SIZE * mDpToPix; + mBorder = BORDER_SIZE * mDpToPix; + + mBarPaint1 = new Paint(); + + mDotPaint = new Paint(); + + mDotPaint.setStyle(Paint.Style.FILL); + mDotPaint.setColor(ctx.getResources().getColor(R.color.slider_dot_color)); + + mBarPaint1.setStyle(Paint.Style.FILL); + + mLinePaint1 = new Paint(); + mLinePaint1.setColor(Color.GRAY); + mLinePaint2 = new Paint(); + mSliderColor = ctx.getResources().getColor(R.color.slider_line_color); + mLinePaint2.setColor(mSliderColor); + mLinePaint2.setStrokeWidth(4); + } + + public boolean onDown(MotionEvent e) { + return true; + } + + public boolean onTouchEvent(MotionEvent event) { + float ox = mDotX; + float oy = mDotY; + + float x = event.getX(); + float y = event.getY(); + + mDotY = y; + + if (mDotY < mBorder) { + mDotY = mBorder; + } + + if (mDotY > mHeight - mBorder) { + mDotY = mHeight - mBorder; + } + mHSVO[2] = (mDotY - mBorder) / (mHeight - mBorder * 2); + notifyColorListeners(mHSVO); + setupButton(); + invalidate((int) (ox - dotRadus), (int) (oy - dotRadus), (int) (ox + dotRadus), + (int) (oy + dotRadus)); + invalidate((int) (mDotX - dotRadus), (int) (mDotY - dotRadus), (int) (mDotX + dotRadus), + (int) (mDotY + dotRadus)); + + return true; + } + + private void setupButton() { + float pos = mHSVO[2] * (mHeight - mBorder * 2); + mDotY = pos + mBorder; + + int[] colors3 = new int[] { + mSliderColor, mSliderColor, 0x66000000, 0 }; + RadialGradient g = new RadialGradient(mDotX, mDotY, dotRadus, colors3, new float[] { + 0, .3f, .31f, 1 }, Shader.TileMode.CLAMP); + mDotPaint.setShader(g); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + mWidth = w; + mHeight = h; + mDotX = mWidth / 2; + updatePaint(); + setupButton(); + } + + private void updatePaint() { + float[] hsv = new float[] { + mHSVO[0], mHSVO[1], 0f }; + int color1 = Color.HSVToColor(hsv); + hsv[2] = 1; + int color2 = Color.HSVToColor(hsv); + + Shader sg = new LinearGradient(mBorder, mBorder, mBorder, mHeight - mBorder, color1, color2, + Shader.TileMode.CLAMP); + mBarPaint1.setShader(sg); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + canvas.drawColor(mBgcolor); + canvas.drawRect(mBorder, mBorder, mWidth - mBorder, mHeight - mBorder, mBarPaint1); + canvas.drawLine(mDotX, mDotY, mDotX, mHeight - mBorder, mLinePaint2); + canvas.drawLine(mDotX, mBorder, mDotX, mDotY, mLinePaint1); + if (mDotX != Float.NaN) { + canvas.drawCircle(mDotX, mDotY, dotRadus, mDotPaint); + } + } + + @Override + public void setColor(float[] hsvo) { + System.arraycopy(hsvo, 0, mHSVO, 0, mHSVO.length); + + float oy = mDotY; + updatePaint(); + setupButton(); + invalidate(); + + } + + ArrayList<ColorListener> mColorListeners = new ArrayList<ColorListener>(); + + public void notifyColorListeners(float[] hsv) { + for (ColorListener l : mColorListeners) { + l.setColor(hsv); + } + } + + public void addColorListener(ColorListener l) { + mColorListeners.add(l); + } + + public void removeColorListener(ColorListener l) { + mColorListeners.remove(l); + } +} diff --git a/src/com/android/gallery3d/filtershow/colorpicker/RGBListener.java b/src/com/android/gallery3d/filtershow/colorpicker/RGBListener.java new file mode 100644 index 000000000..147fb91a4 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/colorpicker/RGBListener.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2013 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.filtershow.colorpicker; + +public interface RGBListener { + void setColor(int hsv); +} diff --git a/src/com/android/gallery3d/filtershow/controller/ActionSlider.java b/src/com/android/gallery3d/filtershow/controller/ActionSlider.java new file mode 100644 index 000000000..f80a1cacb --- /dev/null +++ b/src/com/android/gallery3d/filtershow/controller/ActionSlider.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2013 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.filtershow.controller; + +import android.util.Log; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.ImageButton; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.editors.Editor; + +public class ActionSlider extends TitledSlider { + private static final String LOGTAG = "ActionSlider"; + ImageButton mLeftButton; + ImageButton mRightButton; + public ActionSlider() { + mLayoutID = R.layout.filtershow_control_action_slider; + } + + @Override + public void setUp(ViewGroup container, Parameter parameter, Editor editor) { + super.setUp(container, parameter, editor); + mLeftButton = (ImageButton) mTopView.findViewById(R.id.leftActionButton); + mLeftButton.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + ((ParameterActionAndInt) mParameter).fireLeftAction(); + } + }); + + mRightButton = (ImageButton) mTopView.findViewById(R.id.rightActionButton); + mRightButton.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + ((ParameterActionAndInt) mParameter).fireRightAction(); + } + }); + updateUI(); + } + + @Override + public void updateUI() { + super.updateUI(); + if (mLeftButton != null) { + int iconId = ((ParameterActionAndInt) mParameter).getLeftIcon(); + mLeftButton.setImageResource(iconId); + } + if (mRightButton != null) { + int iconId = ((ParameterActionAndInt) mParameter).getRightIcon(); + mRightButton.setImageResource(iconId); + } + } +} diff --git a/src/com/android/gallery3d/filtershow/controller/BasicParameterInt.java b/src/com/android/gallery3d/filtershow/controller/BasicParameterInt.java new file mode 100644 index 000000000..92145e9be --- /dev/null +++ b/src/com/android/gallery3d/filtershow/controller/BasicParameterInt.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2013 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.filtershow.controller; + +import android.util.Log; + +public class BasicParameterInt implements ParameterInteger { + protected String mParameterName; + protected Control mControl; + protected int mMaximum = 100; + protected int mMinimum = 0; + protected int mDefaultValue; + protected int mValue; + public final int ID; + protected FilterView mEditor; + private final String LOGTAG = "BasicParameterInt"; + + @Override + public void copyFrom(Parameter src) { + if (!(src instanceof BasicParameterInt)) { + throw new IllegalArgumentException(src.getClass().getName()); + } + BasicParameterInt p = (BasicParameterInt) src; + mMaximum = p.mMaximum; + mMinimum = p.mMinimum; + mDefaultValue = p.mDefaultValue; + mValue = p.mValue; + } + + public BasicParameterInt(int id, int value) { + ID = id; + mValue = value; + } + + public BasicParameterInt(int id, int value, int min, int max) { + ID = id; + mValue = value; + mMinimum = min; + mMaximum = max; + } + + @Override + public String getParameterName() { + return mParameterName; + } + + @Override + public String getParameterType() { + return sParameterType; + } + + @Override + public String getValueString() { + return mParameterName + mValue; + } + + @Override + public void setController(Control control) { + mControl = control; + } + + @Override + public int getMaximum() { + return mMaximum; + } + + @Override + public int getMinimum() { + return mMinimum; + } + + @Override + public int getDefaultValue() { + return mDefaultValue; + } + + @Override + public int getValue() { + return mValue; + } + + @Override + public void setValue(int value) { + mValue = value; + if (mEditor != null) { + mEditor.commitLocalRepresentation(); + } + } + + @Override + public String toString() { + return getValueString(); + } + + @Override + public void setFilterView(FilterView editor) { + mEditor = editor; + } +} diff --git a/src/com/android/gallery3d/filtershow/controller/BasicParameterStyle.java b/src/com/android/gallery3d/filtershow/controller/BasicParameterStyle.java new file mode 100644 index 000000000..fb9f95e97 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/controller/BasicParameterStyle.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2013 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.filtershow.controller; + +import android.content.Context; + +import com.android.gallery3d.filtershow.pipeline.RenderingRequestCaller; + +public class BasicParameterStyle implements ParameterStyles { + protected String mParameterName; + protected int mSelectedStyle; + protected int mNumberOfStyles; + protected int mDefaultStyle = 0; + protected Control mControl; + protected FilterView mEditor; + public final int ID; + private final String LOGTAG = "BasicParameterStyle"; + + @Override + public void copyFrom(Parameter src) { + if (!(src instanceof BasicParameterStyle)) { + throw new IllegalArgumentException(src.getClass().getName()); + } + BasicParameterStyle p = (BasicParameterStyle) src; + mNumberOfStyles = p.mNumberOfStyles; + mSelectedStyle = p.mSelectedStyle; + mDefaultStyle = p.mDefaultStyle; + } + + public BasicParameterStyle(int id, int numberOfStyles) { + ID = id; + mNumberOfStyles = numberOfStyles; + } + + @Override + public String getParameterName() { + return mParameterName; + } + + @Override + public String getParameterType() { + return sParameterType; + } + + @Override + public String getValueString() { + return mParameterName + mSelectedStyle; + } + + @Override + public void setController(Control control) { + mControl = control; + } + + @Override + public int getNumberOfStyles() { + return mNumberOfStyles; + } + + @Override + public int getDefaultSelected() { + return mDefaultStyle; + } + + @Override + public int getSelected() { + return mSelectedStyle; + } + + @Override + public void setSelected(int selectedStyle) { + mSelectedStyle = selectedStyle; + if (mEditor != null) { + mEditor.commitLocalRepresentation(); + } + } + + @Override + public void getIcon(int index, RenderingRequestCaller caller) { + mEditor.computeIcon(index, caller); + } + + @Override + public String getStyleTitle(int index, Context context) { + return ""; + } + + @Override + public String toString() { + return getValueString(); + } + + @Override + public void setFilterView(FilterView editor) { + mEditor = editor; + } +} diff --git a/src/com/android/gallery3d/filtershow/controller/BasicSlider.java b/src/com/android/gallery3d/filtershow/controller/BasicSlider.java new file mode 100644 index 000000000..9d8278d52 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/controller/BasicSlider.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2013 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.filtershow.controller; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.SeekBar; +import android.widget.SeekBar.OnSeekBarChangeListener; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.editors.Editor; + +public class BasicSlider implements Control { + private SeekBar mSeekBar; + private ParameterInteger mParameter; + Editor mEditor; + + @Override + public void setUp(ViewGroup container, Parameter parameter, Editor editor) { + container.removeAllViews(); + mEditor = editor; + Context context = container.getContext(); + mParameter = (ParameterInteger) parameter; + LayoutInflater inflater = + (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + LinearLayout lp = (LinearLayout) inflater.inflate( + R.layout.filtershow_seekbar, container, true); + mSeekBar = (SeekBar) lp.findViewById(R.id.primarySeekBar); + + updateUI(); + mSeekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() { + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (mParameter != null) { + mParameter.setValue(progress + mParameter.getMinimum()); + mEditor.commitLocalRepresentation(); + + } + } + }); + } + + @Override + public View getTopView() { + return mSeekBar; + } + + @Override + public void setPrameter(Parameter parameter) { + mParameter = (ParameterInteger) parameter; + if (mSeekBar != null) { + updateUI(); + } + } + + @Override + public void updateUI() { + mSeekBar.setMax(mParameter.getMaximum() - mParameter.getMinimum()); + mSeekBar.setProgress(mParameter.getValue() - mParameter.getMinimum()); + } +} diff --git a/src/com/android/gallery3d/filtershow/controller/Control.java b/src/com/android/gallery3d/filtershow/controller/Control.java new file mode 100644 index 000000000..43422904c --- /dev/null +++ b/src/com/android/gallery3d/filtershow/controller/Control.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2013 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.filtershow.controller; + +import android.view.View; +import android.view.ViewGroup; + +import com.android.gallery3d.filtershow.editors.Editor; + +public interface Control { + public void setUp(ViewGroup container, Parameter parameter, Editor editor); + + public View getTopView(); + + public void setPrameter(Parameter parameter); + + public void updateUI(); +} diff --git a/src/com/android/gallery3d/filtershow/controller/FilterView.java b/src/com/android/gallery3d/filtershow/controller/FilterView.java new file mode 100644 index 000000000..9ca81dc35 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/controller/FilterView.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2013 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.filtershow.controller; + +import com.android.gallery3d.filtershow.pipeline.RenderingRequestCaller; + +public interface FilterView { + public void computeIcon(int index, RenderingRequestCaller caller); + + public void commitLocalRepresentation(); +} diff --git a/src/com/android/gallery3d/filtershow/controller/Parameter.java b/src/com/android/gallery3d/filtershow/controller/Parameter.java new file mode 100644 index 000000000..8f4d5c0a5 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/controller/Parameter.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2013 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.filtershow.controller; + +import com.android.gallery3d.filtershow.editors.Editor; + +public interface Parameter { + String getParameterName(); + + String getParameterType(); + + String getValueString(); + + public void setController(Control c); + + public void setFilterView(FilterView editor); + + public void copyFrom(Parameter src); +} diff --git a/src/com/android/gallery3d/filtershow/controller/ParameterActionAndInt.java b/src/com/android/gallery3d/filtershow/controller/ParameterActionAndInt.java new file mode 100644 index 000000000..8a05c3aa6 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/controller/ParameterActionAndInt.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2013 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.filtershow.controller; + +public interface ParameterActionAndInt extends ParameterInteger { + static String sParameterType = "ParameterActionAndInt"; + + public void fireLeftAction(); + + public int getLeftIcon(); + + public void fireRightAction(); + + public int getRightIcon(); +} diff --git a/src/com/android/gallery3d/filtershow/controller/ParameterInteger.java b/src/com/android/gallery3d/filtershow/controller/ParameterInteger.java new file mode 100644 index 000000000..0bfd20135 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/controller/ParameterInteger.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2013 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.filtershow.controller; + +public interface ParameterInteger extends Parameter { + static String sParameterType = "ParameterInteger"; + + int getMaximum(); + + int getMinimum(); + + int getDefaultValue(); + + int getValue(); + + void setValue(int value); +} diff --git a/src/com/android/gallery3d/filtershow/controller/ParameterSet.java b/src/com/android/gallery3d/filtershow/controller/ParameterSet.java new file mode 100644 index 000000000..6b50a4d0b --- /dev/null +++ b/src/com/android/gallery3d/filtershow/controller/ParameterSet.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2013 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.filtershow.controller; + +public interface ParameterSet { + int getNumberOfParameters(); + + Parameter getFilterParameter(int index); +} diff --git a/src/com/android/gallery3d/filtershow/controller/ParameterStyles.java b/src/com/android/gallery3d/filtershow/controller/ParameterStyles.java new file mode 100644 index 000000000..7d250a0bf --- /dev/null +++ b/src/com/android/gallery3d/filtershow/controller/ParameterStyles.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2013 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.filtershow.controller; + +import android.content.Context; + +import com.android.gallery3d.filtershow.pipeline.RenderingRequestCaller; + +public interface ParameterStyles extends Parameter { + public static String sParameterType = "ParameterStyles"; + + int getNumberOfStyles(); + + int getDefaultSelected(); + + int getSelected(); + + void setSelected(int value); + + void getIcon(int index, RenderingRequestCaller caller); + + String getStyleTitle(int index, Context context); +} diff --git a/src/com/android/gallery3d/filtershow/controller/StyleChooser.java b/src/com/android/gallery3d/filtershow/controller/StyleChooser.java new file mode 100644 index 000000000..fb613abc7 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/controller/StyleChooser.java @@ -0,0 +1,88 @@ +package com.android.gallery3d.filtershow.controller; + +import android.app.ActionBar.LayoutParams; +import android.content.Context; +import android.graphics.Bitmap; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.ImageView.ScaleType; +import android.widget.LinearLayout; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.pipeline.RenderingRequest; +import com.android.gallery3d.filtershow.pipeline.RenderingRequestCaller; +import com.android.gallery3d.filtershow.editors.Editor; + +import java.util.Vector; + +public class StyleChooser implements Control { + private final String LOGTAG = "StyleChooser"; + protected ParameterStyles mParameter; + protected LinearLayout mLinearLayout; + protected Editor mEditor; + private View mTopView; + private Vector<ImageButton> mIconButton = new Vector<ImageButton>(); + protected int mLayoutID = R.layout.filtershow_control_style_chooser; + + @Override + public void setUp(ViewGroup container, Parameter parameter, Editor editor) { + container.removeAllViews(); + mEditor = editor; + Context context = container.getContext(); + mParameter = (ParameterStyles) parameter; + LayoutInflater inflater = + (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mTopView = inflater.inflate(mLayoutID, container, true); + mLinearLayout = (LinearLayout) mTopView.findViewById(R.id.listStyles); + mTopView.setVisibility(View.VISIBLE); + int n = mParameter.getNumberOfStyles(); + mIconButton.clear(); + LayoutParams lp = new LayoutParams(120, 120); + for (int i = 0; i < n; i++) { + final ImageButton button = new ImageButton(context); + button.setScaleType(ScaleType.CENTER_CROP); + button.setLayoutParams(lp); + button.setBackgroundResource(android.R.color.transparent); + mIconButton.add(button); + final int buttonNo = i; + button.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View arg0) { + mParameter.setSelected(buttonNo); + } + }); + mLinearLayout.addView(button); + mParameter.getIcon(i, new RenderingRequestCaller() { + @Override + public void available(RenderingRequest request) { + Bitmap bmap = request.getBitmap(); + if (bmap == null) { + return; + } + button.setImageBitmap(bmap); + } + }); + } + } + + @Override + public View getTopView() { + return mTopView; + } + + @Override + public void setPrameter(Parameter parameter) { + mParameter = (ParameterStyles) parameter; + updateUI(); + } + + @Override + public void updateUI() { + if (mParameter == null) { + return; + } + } + +} diff --git a/src/com/android/gallery3d/filtershow/controller/TitledSlider.java b/src/com/android/gallery3d/filtershow/controller/TitledSlider.java new file mode 100644 index 000000000..f29442bb9 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/controller/TitledSlider.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2013 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.filtershow.controller; + +import android.content.Context; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.SeekBar; +import android.widget.SeekBar.OnSeekBarChangeListener; +import android.widget.TextView; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.editors.Editor; + +public class TitledSlider implements Control { + private final String LOGTAG = "ParametricEditor"; + private SeekBar mSeekBar; + private TextView mControlName; + private TextView mControlValue; + protected ParameterInteger mParameter; + Editor mEditor; + View mTopView; + protected int mLayoutID = R.layout.filtershow_control_title_slider; + + @Override + public void setUp(ViewGroup container, Parameter parameter, Editor editor) { + container.removeAllViews(); + mEditor = editor; + Context context = container.getContext(); + mParameter = (ParameterInteger) parameter; + LayoutInflater inflater = + (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mTopView = inflater.inflate(mLayoutID, container, true); + mTopView.setVisibility(View.VISIBLE); + mSeekBar = (SeekBar) mTopView.findViewById(R.id.controlValueSeekBar); + mControlName = (TextView) mTopView.findViewById(R.id.controlName); + mControlValue = (TextView) mTopView.findViewById(R.id.controlValue); + updateUI(); + mSeekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() { + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (mParameter != null) { + mParameter.setValue(progress + mParameter.getMinimum()); + if (mControlName != null) { + mControlName.setText(mParameter.getParameterName()); + } + if (mControlValue != null) { + mControlValue.setText(Integer.toString(mParameter.getValue())); + } + mEditor.commitLocalRepresentation(); + } + } + }); + } + + @Override + public void setPrameter(Parameter parameter) { + mParameter = (ParameterInteger) parameter; + if (mSeekBar != null) + updateUI(); + } + + @Override + public void updateUI() { + if (mControlName != null && mParameter.getParameterName() != null) { + mControlName.setText(mParameter.getParameterName().toUpperCase()); + } + if (mControlValue != null) { + mControlValue.setText( + Integer.toString(mParameter.getValue())); + } + mSeekBar.setMax(mParameter.getMaximum() - mParameter.getMinimum()); + mSeekBar.setProgress(mParameter.getValue() - mParameter.getMinimum()); + mEditor.commitLocalRepresentation(); + } + + @Override + public View getTopView() { + return mTopView; + } +} diff --git a/src/com/android/gallery3d/filtershow/crop/BoundedRect.java b/src/com/android/gallery3d/filtershow/crop/BoundedRect.java new file mode 100644 index 000000000..13b8d6de1 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/crop/BoundedRect.java @@ -0,0 +1,368 @@ +/* + * Copyright (C) 2012 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.filtershow.crop; + +import android.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.RectF; + +import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils; + +import java.util.Arrays; + +/** + * Maintains invariant that inner rectangle is constrained to be within the + * outer, rotated rectangle. + */ +public class BoundedRect { + private float rot; + private RectF outer; + private RectF inner; + private float[] innerRotated; + + public BoundedRect(float rotation, Rect outerRect, Rect innerRect) { + rot = rotation; + outer = new RectF(outerRect); + inner = new RectF(innerRect); + innerRotated = CropMath.getCornersFromRect(inner); + rotateInner(); + if (!isConstrained()) + reconstrain(); + } + + public BoundedRect(float rotation, RectF outerRect, RectF innerRect) { + rot = rotation; + outer = new RectF(outerRect); + inner = new RectF(innerRect); + innerRotated = CropMath.getCornersFromRect(inner); + rotateInner(); + if (!isConstrained()) + reconstrain(); + } + + public void resetTo(float rotation, RectF outerRect, RectF innerRect) { + rot = rotation; + outer.set(outerRect); + inner.set(innerRect); + innerRotated = CropMath.getCornersFromRect(inner); + rotateInner(); + if (!isConstrained()) + reconstrain(); + } + + /** + * Sets inner, and re-constrains it to fit within the rotated bounding rect. + */ + public void setInner(RectF newInner) { + if (inner.equals(newInner)) + return; + inner = newInner; + innerRotated = CropMath.getCornersFromRect(inner); + rotateInner(); + if (!isConstrained()) + reconstrain(); + } + + /** + * Sets rotation, and re-constrains inner to fit within the rotated bounding rect. + */ + public void setRotation(float rotation) { + if (rotation == rot) + return; + rot = rotation; + innerRotated = CropMath.getCornersFromRect(inner); + rotateInner(); + if (!isConstrained()) + reconstrain(); + } + + public void setToInner(RectF r) { + r.set(inner); + } + + public void setToOuter(RectF r) { + r.set(outer); + } + + public RectF getInner() { + return new RectF(inner); + } + + public RectF getOuter() { + return new RectF(outer); + } + + /** + * Tries to move the inner rectangle by (dx, dy). If this would cause it to leave + * the bounding rectangle, snaps the inner rectangle to the edge of the bounding + * rectangle. + */ + public void moveInner(float dx, float dy) { + Matrix m0 = getInverseRotMatrix(); + + RectF translatedInner = new RectF(inner); + translatedInner.offset(dx, dy); + + float[] translatedInnerCorners = CropMath.getCornersFromRect(translatedInner); + float[] outerCorners = CropMath.getCornersFromRect(outer); + + m0.mapPoints(translatedInnerCorners); + float[] correction = { + 0, 0 + }; + + // find correction vectors for corners that have moved out of bounds + for (int i = 0; i < translatedInnerCorners.length; i += 2) { + float correctedInnerX = translatedInnerCorners[i] + correction[0]; + float correctedInnerY = translatedInnerCorners[i + 1] + correction[1]; + if (!CropMath.inclusiveContains(outer, correctedInnerX, correctedInnerY)) { + float[] badCorner = { + correctedInnerX, correctedInnerY + }; + float[] nearestSide = CropMath.closestSide(badCorner, outerCorners); + float[] correctionVec = + GeometryMathUtils.shortestVectorFromPointToLine(badCorner, nearestSide); + correction[0] += correctionVec[0]; + correction[1] += correctionVec[1]; + } + } + + for (int i = 0; i < translatedInnerCorners.length; i += 2) { + float correctedInnerX = translatedInnerCorners[i] + correction[0]; + float correctedInnerY = translatedInnerCorners[i + 1] + correction[1]; + if (!CropMath.inclusiveContains(outer, correctedInnerX, correctedInnerY)) { + float[] correctionVec = { + correctedInnerX, correctedInnerY + }; + CropMath.getEdgePoints(outer, correctionVec); + correctionVec[0] -= correctedInnerX; + correctionVec[1] -= correctedInnerY; + correction[0] += correctionVec[0]; + correction[1] += correctionVec[1]; + } + } + + // Set correction + for (int i = 0; i < translatedInnerCorners.length; i += 2) { + float correctedInnerX = translatedInnerCorners[i] + correction[0]; + float correctedInnerY = translatedInnerCorners[i + 1] + correction[1]; + // update translated corners with correction vectors + translatedInnerCorners[i] = correctedInnerX; + translatedInnerCorners[i + 1] = correctedInnerY; + } + + innerRotated = translatedInnerCorners; + // reconstrain to update inner + reconstrain(); + } + + /** + * Attempts to resize the inner rectangle. If this would cause it to leave + * the bounding rect, clips the inner rectangle to fit. + */ + public void resizeInner(RectF newInner) { + Matrix m = getRotMatrix(); + Matrix m0 = getInverseRotMatrix(); + + float[] outerCorners = CropMath.getCornersFromRect(outer); + m.mapPoints(outerCorners); + float[] oldInnerCorners = CropMath.getCornersFromRect(inner); + float[] newInnerCorners = CropMath.getCornersFromRect(newInner); + RectF ret = new RectF(newInner); + + for (int i = 0; i < newInnerCorners.length; i += 2) { + float[] c = { + newInnerCorners[i], newInnerCorners[i + 1] + }; + float[] c0 = Arrays.copyOf(c, 2); + m0.mapPoints(c0); + if (!CropMath.inclusiveContains(outer, c0[0], c0[1])) { + float[] outerSide = CropMath.closestSide(c, outerCorners); + float[] pathOfCorner = { + newInnerCorners[i], newInnerCorners[i + 1], + oldInnerCorners[i], oldInnerCorners[i + 1] + }; + float[] p = GeometryMathUtils.lineIntersect(pathOfCorner, outerSide); + if (p == null) { + // lines are parallel or not well defined, so don't resize + p = new float[2]; + p[0] = oldInnerCorners[i]; + p[1] = oldInnerCorners[i + 1]; + } + // relies on corners being in same order as method + // getCornersFromRect + switch (i) { + case 0: + case 1: + ret.left = (p[0] > ret.left) ? p[0] : ret.left; + ret.top = (p[1] > ret.top) ? p[1] : ret.top; + break; + case 2: + case 3: + ret.right = (p[0] < ret.right) ? p[0] : ret.right; + ret.top = (p[1] > ret.top) ? p[1] : ret.top; + break; + case 4: + case 5: + ret.right = (p[0] < ret.right) ? p[0] : ret.right; + ret.bottom = (p[1] < ret.bottom) ? p[1] : ret.bottom; + break; + case 6: + case 7: + ret.left = (p[0] > ret.left) ? p[0] : ret.left; + ret.bottom = (p[1] < ret.bottom) ? p[1] : ret.bottom; + break; + default: + break; + } + } + } + float[] retCorners = CropMath.getCornersFromRect(ret); + m0.mapPoints(retCorners); + innerRotated = retCorners; + // reconstrain to update inner + reconstrain(); + } + + /** + * Attempts to resize the inner rectangle. If this would cause it to leave + * the bounding rect, clips the inner rectangle to fit while maintaining + * aspect ratio. + */ + public void fixedAspectResizeInner(RectF newInner) { + Matrix m = getRotMatrix(); + Matrix m0 = getInverseRotMatrix(); + + float aspectW = inner.width(); + float aspectH = inner.height(); + float aspRatio = aspectW / aspectH; + float[] corners = CropMath.getCornersFromRect(outer); + + m.mapPoints(corners); + float[] oldInnerCorners = CropMath.getCornersFromRect(inner); + float[] newInnerCorners = CropMath.getCornersFromRect(newInner); + + // find fixed corner + int fixed = -1; + if (inner.top == newInner.top) { + if (inner.left == newInner.left) + fixed = 0; // top left + else if (inner.right == newInner.right) + fixed = 2; // top right + } else if (inner.bottom == newInner.bottom) { + if (inner.right == newInner.right) + fixed = 4; // bottom right + else if (inner.left == newInner.left) + fixed = 6; // bottom left + } + // no fixed corner, return without update + if (fixed == -1) + return; + float widthSoFar = newInner.width(); + int moved = -1; + for (int i = 0; i < newInnerCorners.length; i += 2) { + float[] c = { + newInnerCorners[i], newInnerCorners[i + 1] + }; + float[] c0 = Arrays.copyOf(c, 2); + m0.mapPoints(c0); + if (!CropMath.inclusiveContains(outer, c0[0], c0[1])) { + moved = i; + if (moved == fixed) + continue; + float[] l2 = CropMath.closestSide(c, corners); + float[] l1 = { + newInnerCorners[i], newInnerCorners[i + 1], + oldInnerCorners[i], oldInnerCorners[i + 1] + }; + float[] p = GeometryMathUtils.lineIntersect(l1, l2); + if (p == null) { + // lines are parallel or not well defined, so set to old + // corner + p = new float[2]; + p[0] = oldInnerCorners[i]; + p[1] = oldInnerCorners[i + 1]; + } + // relies on corners being in same order as method + // getCornersFromRect + float fixed_x = oldInnerCorners[fixed]; + float fixed_y = oldInnerCorners[fixed + 1]; + float newWidth = Math.abs(fixed_x - p[0]); + float newHeight = Math.abs(fixed_y - p[1]); + newWidth = Math.max(newWidth, aspRatio * newHeight); + if (newWidth < widthSoFar) + widthSoFar = newWidth; + } + } + + float heightSoFar = widthSoFar / aspRatio; + RectF ret = new RectF(inner); + if (fixed == 0) { + ret.right = ret.left + widthSoFar; + ret.bottom = ret.top + heightSoFar; + } else if (fixed == 2) { + ret.left = ret.right - widthSoFar; + ret.bottom = ret.top + heightSoFar; + } else if (fixed == 4) { + ret.left = ret.right - widthSoFar; + ret.top = ret.bottom - heightSoFar; + } else if (fixed == 6) { + ret.right = ret.left + widthSoFar; + ret.top = ret.bottom - heightSoFar; + } + float[] retCorners = CropMath.getCornersFromRect(ret); + m0.mapPoints(retCorners); + innerRotated = retCorners; + // reconstrain to update inner + reconstrain(); + } + + // internal methods + + private boolean isConstrained() { + for (int i = 0; i < 8; i += 2) { + if (!CropMath.inclusiveContains(outer, innerRotated[i], innerRotated[i + 1])) + return false; + } + return true; + } + + private void reconstrain() { + // innerRotated has been changed to have incorrect values + CropMath.getEdgePoints(outer, innerRotated); + Matrix m = getRotMatrix(); + float[] unrotated = Arrays.copyOf(innerRotated, 8); + m.mapPoints(unrotated); + inner = CropMath.trapToRect(unrotated); + } + + private void rotateInner() { + Matrix m = getInverseRotMatrix(); + m.mapPoints(innerRotated); + } + + private Matrix getRotMatrix() { + Matrix m = new Matrix(); + m.setRotate(rot, outer.centerX(), outer.centerY()); + return m; + } + + private Matrix getInverseRotMatrix() { + Matrix m = new Matrix(); + m.setRotate(-rot, outer.centerX(), outer.centerY()); + return m; + } +} diff --git a/src/com/android/gallery3d/filtershow/crop/CropActivity.java b/src/com/android/gallery3d/filtershow/crop/CropActivity.java new file mode 100644 index 000000000..0a0c36703 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/crop/CropActivity.java @@ -0,0 +1,697 @@ +/* + * Copyright (C) 2013 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.filtershow.crop; + +import android.app.ActionBar; +import android.app.Activity; +import android.app.WallpaperManager; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.graphics.Bitmap.CompressFormat; +import android.graphics.BitmapFactory; +import android.graphics.BitmapRegionDecoder; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.provider.MediaStore; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.WindowManager; +import android.widget.Toast; + +import com.android.gallery3d.R; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.filtershow.cache.ImageLoader; +import com.android.gallery3d.filtershow.tools.SaveImage; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Activity for cropping an image. + */ +public class CropActivity extends Activity { + private static final String LOGTAG = "CropActivity"; + public static final String CROP_ACTION = "com.android.camera.action.CROP"; + private CropExtras mCropExtras = null; + private LoadBitmapTask mLoadBitmapTask = null; + + private int mOutputX = 0; + private int mOutputY = 0; + private Bitmap mOriginalBitmap = null; + private RectF mOriginalBounds = null; + private int mOriginalRotation = 0; + private Uri mSourceUri = null; + private CropView mCropView = null; + private View mSaveButton = null; + private boolean finalIOGuard = false; + + private static final int SELECT_PICTURE = 1; // request code for picker + + private static final int DEFAULT_COMPRESS_QUALITY = 90; + /** + * The maximum bitmap size we allow to be returned through the intent. + * Intents have a maximum of 1MB in total size. However, the Bitmap seems to + * have some overhead to hit so that we go way below the limit here to make + * sure the intent stays below 1MB.We should consider just returning a byte + * array instead of a Bitmap instance to avoid overhead. + */ + public static final int MAX_BMAP_IN_INTENT = 750000; + + // Flags + private static final int DO_SET_WALLPAPER = 1; + private static final int DO_RETURN_DATA = 1 << 1; + private static final int DO_EXTRA_OUTPUT = 1 << 2; + + private static final int FLAG_CHECK = DO_SET_WALLPAPER | DO_RETURN_DATA | DO_EXTRA_OUTPUT; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Intent intent = getIntent(); + setResult(RESULT_CANCELED, new Intent()); + mCropExtras = getExtrasFromIntent(intent); + if (mCropExtras != null && mCropExtras.getShowWhenLocked()) { + getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); + } + + setContentView(R.layout.crop_activity); + mCropView = (CropView) findViewById(R.id.cropView); + + ActionBar actionBar = getActionBar(); + actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM); + actionBar.setCustomView(R.layout.filtershow_actionbar); + + View mSaveButton = actionBar.getCustomView(); + mSaveButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + startFinishOutput(); + } + }); + + if (intent.getData() != null) { + mSourceUri = intent.getData(); + startLoadBitmap(mSourceUri); + } else { + pickImage(); + } + } + + private void enableSave(boolean enable) { + if (mSaveButton != null) { + mSaveButton.setEnabled(enable); + } + } + + @Override + protected void onDestroy() { + if (mLoadBitmapTask != null) { + mLoadBitmapTask.cancel(false); + } + super.onDestroy(); + } + + @Override + public void onConfigurationChanged (Configuration newConfig) { + super.onConfigurationChanged(newConfig); + mCropView.configChanged(); + } + + /** + * Opens a selector in Gallery to chose an image for use when none was given + * in the CROP intent. + */ + private void pickImage() { + Intent intent = new Intent(); + intent.setType("image/*"); + intent.setAction(Intent.ACTION_GET_CONTENT); + startActivityForResult(Intent.createChooser(intent, getString(R.string.select_image)), + SELECT_PICTURE); + } + + /** + * Callback for pickImage(). + */ + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode == RESULT_OK && requestCode == SELECT_PICTURE) { + mSourceUri = data.getData(); + startLoadBitmap(mSourceUri); + } + } + + /** + * Gets screen size metric. + */ + private int getScreenImageSize() { + DisplayMetrics outMetrics = new DisplayMetrics(); + getWindowManager().getDefaultDisplay().getMetrics(outMetrics); + return (int) Math.max(outMetrics.heightPixels, outMetrics.widthPixels); + } + + /** + * Method that loads a bitmap in an async task. + */ + private void startLoadBitmap(Uri uri) { + if (uri != null) { + enableSave(false); + final View loading = findViewById(R.id.loading); + loading.setVisibility(View.VISIBLE); + mLoadBitmapTask = new LoadBitmapTask(); + mLoadBitmapTask.execute(uri); + } else { + cannotLoadImage(); + done(); + } + } + + /** + * Method called on UI thread with loaded bitmap. + */ + private void doneLoadBitmap(Bitmap bitmap, RectF bounds, int orientation) { + final View loading = findViewById(R.id.loading); + loading.setVisibility(View.GONE); + mOriginalBitmap = bitmap; + mOriginalBounds = bounds; + mOriginalRotation = orientation; + if (bitmap != null && bitmap.getWidth() != 0 && bitmap.getHeight() != 0) { + RectF imgBounds = new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight()); + mCropView.initialize(bitmap, imgBounds, imgBounds, orientation); + if (mCropExtras != null) { + int aspectX = mCropExtras.getAspectX(); + int aspectY = mCropExtras.getAspectY(); + mOutputX = mCropExtras.getOutputX(); + mOutputY = mCropExtras.getOutputY(); + if (mOutputX > 0 && mOutputY > 0) { + mCropView.applyAspect(mOutputX, mOutputY); + + } + float spotX = mCropExtras.getSpotlightX(); + float spotY = mCropExtras.getSpotlightY(); + if (spotX > 0 && spotY > 0) { + mCropView.setWallpaperSpotlight(spotX, spotY); + } + if (aspectX > 0 && aspectY > 0) { + mCropView.applyAspect(aspectX, aspectY); + } + } + enableSave(true); + } else { + Log.w(LOGTAG, "could not load image for cropping"); + cannotLoadImage(); + setResult(RESULT_CANCELED, new Intent()); + done(); + } + } + + /** + * Display toast for image loading failure. + */ + private void cannotLoadImage() { + CharSequence text = getString(R.string.cannot_load_image); + Toast toast = Toast.makeText(this, text, Toast.LENGTH_SHORT); + toast.show(); + } + + /** + * AsyncTask for loading a bitmap into memory. + * + * @see #startLoadBitmap(Uri) + * @see #doneLoadBitmap(Bitmap) + */ + private class LoadBitmapTask extends AsyncTask<Uri, Void, Bitmap> { + int mBitmapSize; + Context mContext; + Rect mOriginalBounds; + int mOrientation; + + public LoadBitmapTask() { + mBitmapSize = getScreenImageSize(); + mContext = getApplicationContext(); + mOriginalBounds = new Rect(); + mOrientation = 0; + } + + @Override + protected Bitmap doInBackground(Uri... params) { + Uri uri = params[0]; + Bitmap bmap = ImageLoader.loadConstrainedBitmap(uri, mContext, mBitmapSize, + mOriginalBounds, false); + mOrientation = ImageLoader.getMetadataRotation(mContext, uri); + return bmap; + } + + @Override + protected void onPostExecute(Bitmap result) { + doneLoadBitmap(result, new RectF(mOriginalBounds), mOrientation); + } + } + + private void startFinishOutput() { + if (finalIOGuard) { + return; + } else { + finalIOGuard = true; + } + enableSave(false); + Uri destinationUri = null; + int flags = 0; + if (mOriginalBitmap != null && mCropExtras != null) { + if (mCropExtras.getExtraOutput() != null) { + destinationUri = mCropExtras.getExtraOutput(); + if (destinationUri != null) { + flags |= DO_EXTRA_OUTPUT; + } + } + if (mCropExtras.getSetAsWallpaper()) { + flags |= DO_SET_WALLPAPER; + } + if (mCropExtras.getReturnData()) { + flags |= DO_RETURN_DATA; + } + } + if (flags == 0) { + destinationUri = SaveImage.makeAndInsertUri(this, mSourceUri); + if (destinationUri != null) { + flags |= DO_EXTRA_OUTPUT; + } + } + if ((flags & FLAG_CHECK) != 0 && mOriginalBitmap != null) { + RectF photo = new RectF(0, 0, mOriginalBitmap.getWidth(), mOriginalBitmap.getHeight()); + RectF crop = getBitmapCrop(photo); + startBitmapIO(flags, mOriginalBitmap, mSourceUri, destinationUri, crop, + photo, mOriginalBounds, + (mCropExtras == null) ? null : mCropExtras.getOutputFormat(), mOriginalRotation); + return; + } + setResult(RESULT_CANCELED, new Intent()); + done(); + return; + } + + private void startBitmapIO(int flags, Bitmap currentBitmap, Uri sourceUri, Uri destUri, + RectF cropBounds, RectF photoBounds, RectF currentBitmapBounds, String format, + int rotation) { + if (cropBounds == null || photoBounds == null || currentBitmap == null + || currentBitmap.getWidth() == 0 || currentBitmap.getHeight() == 0 + || cropBounds.width() == 0 || cropBounds.height() == 0 || photoBounds.width() == 0 + || photoBounds.height() == 0) { + return; // fail fast + } + if ((flags & FLAG_CHECK) == 0) { + return; // no output options + } + if ((flags & DO_SET_WALLPAPER) != 0) { + Toast.makeText(this, R.string.setting_wallpaper, Toast.LENGTH_LONG).show(); + } + + final View loading = findViewById(R.id.loading); + loading.setVisibility(View.VISIBLE); + BitmapIOTask ioTask = new BitmapIOTask(sourceUri, destUri, format, flags, cropBounds, + photoBounds, currentBitmapBounds, rotation, mOutputX, mOutputY); + ioTask.execute(currentBitmap); + } + + private void doneBitmapIO(boolean success, Intent intent) { + final View loading = findViewById(R.id.loading); + loading.setVisibility(View.GONE); + if (success) { + setResult(RESULT_OK, intent); + } else { + setResult(RESULT_CANCELED, intent); + } + done(); + } + + private class BitmapIOTask extends AsyncTask<Bitmap, Void, Boolean> { + + private final WallpaperManager mWPManager; + InputStream mInStream = null; + OutputStream mOutStream = null; + String mOutputFormat = null; + Uri mOutUri = null; + Uri mInUri = null; + int mFlags = 0; + RectF mCrop = null; + RectF mPhoto = null; + RectF mOrig = null; + Intent mResultIntent = null; + int mRotation = 0; + + // Helper to setup input stream + private void regenerateInputStream() { + if (mInUri == null) { + Log.w(LOGTAG, "cannot read original file, no input URI given"); + } else { + Utils.closeSilently(mInStream); + try { + mInStream = getContentResolver().openInputStream(mInUri); + } catch (FileNotFoundException e) { + Log.w(LOGTAG, "cannot read file: " + mInUri.toString(), e); + } + } + } + + public BitmapIOTask(Uri sourceUri, Uri destUri, String outputFormat, int flags, + RectF cropBounds, RectF photoBounds, RectF originalBitmapBounds, int rotation, + int outputX, int outputY) { + mOutputFormat = outputFormat; + mOutStream = null; + mOutUri = destUri; + mInUri = sourceUri; + mFlags = flags; + mCrop = cropBounds; + mPhoto = photoBounds; + mOrig = originalBitmapBounds; + mWPManager = WallpaperManager.getInstance(getApplicationContext()); + mResultIntent = new Intent(); + mRotation = (rotation < 0) ? -rotation : rotation; + mRotation %= 360; + mRotation = 90 * (int) (mRotation / 90); // now mRotation is a multiple of 90 + mOutputX = outputX; + mOutputY = outputY; + + if ((flags & DO_EXTRA_OUTPUT) != 0) { + if (mOutUri == null) { + Log.w(LOGTAG, "cannot write file, no output URI given"); + } else { + try { + mOutStream = getContentResolver().openOutputStream(mOutUri); + } catch (FileNotFoundException e) { + Log.w(LOGTAG, "cannot write file: " + mOutUri.toString(), e); + } + } + } + + if ((flags & (DO_EXTRA_OUTPUT | DO_SET_WALLPAPER)) != 0) { + regenerateInputStream(); + } + } + + @Override + protected Boolean doInBackground(Bitmap... params) { + boolean failure = false; + Bitmap img = params[0]; + + // Set extra for crop bounds + if (mCrop != null && mPhoto != null && mOrig != null) { + RectF trueCrop = CropMath.getScaledCropBounds(mCrop, mPhoto, mOrig); + Matrix m = new Matrix(); + m.setRotate(mRotation); + m.mapRect(trueCrop); + if (trueCrop != null) { + Rect rounded = new Rect(); + trueCrop.roundOut(rounded); + mResultIntent.putExtra(CropExtras.KEY_CROPPED_RECT, rounded); + } + } + + // Find the small cropped bitmap that is returned in the intent + if ((mFlags & DO_RETURN_DATA) != 0) { + assert (img != null); + Bitmap ret = getCroppedImage(img, mCrop, mPhoto); + if (ret != null) { + ret = getDownsampledBitmap(ret, MAX_BMAP_IN_INTENT); + } + if (ret == null) { + Log.w(LOGTAG, "could not downsample bitmap to return in data"); + failure = true; + } else { + if (mRotation > 0) { + Matrix m = new Matrix(); + m.setRotate(mRotation); + Bitmap tmp = Bitmap.createBitmap(ret, 0, 0, ret.getWidth(), + ret.getHeight(), m, true); + if (tmp != null) { + ret = tmp; + } + } + mResultIntent.putExtra(CropExtras.KEY_DATA, ret); + } + } + + // Do the large cropped bitmap and/or set the wallpaper + if ((mFlags & (DO_EXTRA_OUTPUT | DO_SET_WALLPAPER)) != 0 && mInStream != null) { + // Find crop bounds (scaled to original image size) + RectF trueCrop = CropMath.getScaledCropBounds(mCrop, mPhoto, mOrig); + if (trueCrop == null) { + Log.w(LOGTAG, "cannot find crop for full size image"); + failure = true; + return false; + } + Rect roundedTrueCrop = new Rect(); + trueCrop.roundOut(roundedTrueCrop); + + if (roundedTrueCrop.width() <= 0 || roundedTrueCrop.height() <= 0) { + Log.w(LOGTAG, "crop has bad values for full size image"); + failure = true; + return false; + } + + // Attempt to open a region decoder + BitmapRegionDecoder decoder = null; + try { + decoder = BitmapRegionDecoder.newInstance(mInStream, true); + } catch (IOException e) { + Log.w(LOGTAG, "cannot open region decoder for file: " + mInUri.toString(), e); + } + + Bitmap crop = null; + if (decoder != null) { + // Do region decoding to get crop bitmap + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inMutable = true; + crop = decoder.decodeRegion(roundedTrueCrop, options); + decoder.recycle(); + } + + if (crop == null) { + // BitmapRegionDecoder has failed, try to crop in-memory + regenerateInputStream(); + Bitmap fullSize = null; + if (mInStream != null) { + fullSize = BitmapFactory.decodeStream(mInStream); + } + if (fullSize != null) { + crop = Bitmap.createBitmap(fullSize, roundedTrueCrop.left, + roundedTrueCrop.top, roundedTrueCrop.width(), + roundedTrueCrop.height()); + } + } + + if (crop == null) { + Log.w(LOGTAG, "cannot decode file: " + mInUri.toString()); + failure = true; + return false; + } + if (mOutputX > 0 && mOutputY > 0) { + Matrix m = new Matrix(); + RectF cropRect = new RectF(0, 0, crop.getWidth(), crop.getHeight()); + if (mRotation > 0) { + m.setRotate(mRotation); + m.mapRect(cropRect); + } + RectF returnRect = new RectF(0, 0, mOutputX, mOutputY); + m.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL); + m.preRotate(mRotation); + Bitmap tmp = Bitmap.createBitmap((int) returnRect.width(), + (int) returnRect.height(), Bitmap.Config.ARGB_8888); + if (tmp != null) { + Canvas c = new Canvas(tmp); + c.drawBitmap(crop, m, new Paint()); + crop = tmp; + } + } else if (mRotation > 0) { + Matrix m = new Matrix(); + m.setRotate(mRotation); + Bitmap tmp = Bitmap.createBitmap(crop, 0, 0, crop.getWidth(), + crop.getHeight(), m, true); + if (tmp != null) { + crop = tmp; + } + } + // Get output compression format + CompressFormat cf = + convertExtensionToCompressFormat(getFileExtension(mOutputFormat)); + + // If we only need to output to a URI, compress straight to file + if (mFlags == DO_EXTRA_OUTPUT) { + if (mOutStream == null + || !crop.compress(cf, DEFAULT_COMPRESS_QUALITY, mOutStream)) { + Log.w(LOGTAG, "failed to compress bitmap to file: " + mOutUri.toString()); + failure = true; + } else { + mResultIntent.setData(mOutUri); + } + } else { + // Compress to byte array + ByteArrayOutputStream tmpOut = new ByteArrayOutputStream(2048); + if (crop.compress(cf, DEFAULT_COMPRESS_QUALITY, tmpOut)) { + + // If we need to output to a Uri, write compressed + // bitmap out + if ((mFlags & DO_EXTRA_OUTPUT) != 0) { + if (mOutStream == null) { + Log.w(LOGTAG, + "failed to compress bitmap to file: " + mOutUri.toString()); + failure = true; + } else { + try { + mOutStream.write(tmpOut.toByteArray()); + mResultIntent.setData(mOutUri); + } catch (IOException e) { + Log.w(LOGTAG, + "failed to compress bitmap to file: " + + mOutUri.toString(), e); + failure = true; + } + } + } + + // If we need to set to the wallpaper, set it + if ((mFlags & DO_SET_WALLPAPER) != 0 && mWPManager != null) { + if (mWPManager == null) { + Log.w(LOGTAG, "no wallpaper manager"); + failure = true; + } else { + try { + mWPManager.setStream(new ByteArrayInputStream(tmpOut + .toByteArray())); + } catch (IOException e) { + Log.w(LOGTAG, "cannot write stream to wallpaper", e); + failure = true; + } + } + } + } else { + Log.w(LOGTAG, "cannot compress bitmap"); + failure = true; + } + } + } + return !failure; // True if any of the operations failed + } + + @Override + protected void onPostExecute(Boolean result) { + Utils.closeSilently(mOutStream); + Utils.closeSilently(mInStream); + doneBitmapIO(result.booleanValue(), mResultIntent); + } + + } + + private void done() { + finish(); + } + + protected static Bitmap getCroppedImage(Bitmap image, RectF cropBounds, RectF photoBounds) { + RectF imageBounds = new RectF(0, 0, image.getWidth(), image.getHeight()); + RectF crop = CropMath.getScaledCropBounds(cropBounds, photoBounds, imageBounds); + if (crop == null) { + return null; + } + Rect intCrop = new Rect(); + crop.roundOut(intCrop); + return Bitmap.createBitmap(image, intCrop.left, intCrop.top, intCrop.width(), + intCrop.height()); + } + + protected static Bitmap getDownsampledBitmap(Bitmap image, int max_size) { + if (image == null || image.getWidth() == 0 || image.getHeight() == 0 || max_size < 16) { + throw new IllegalArgumentException("Bad argument to getDownsampledBitmap()"); + } + int shifts = 0; + int size = CropMath.getBitmapSize(image); + while (size > max_size) { + shifts++; + size /= 4; + } + Bitmap ret = Bitmap.createScaledBitmap(image, image.getWidth() >> shifts, + image.getHeight() >> shifts, true); + if (ret == null) { + return null; + } + // Handle edge case for rounding. + if (CropMath.getBitmapSize(ret) > max_size) { + return Bitmap.createScaledBitmap(ret, ret.getWidth() >> 1, ret.getHeight() >> 1, true); + } + return ret; + } + + /** + * Gets the crop extras from the intent, or null if none exist. + */ + protected static CropExtras getExtrasFromIntent(Intent intent) { + Bundle extras = intent.getExtras(); + if (extras != null) { + return new CropExtras(extras.getInt(CropExtras.KEY_OUTPUT_X, 0), + extras.getInt(CropExtras.KEY_OUTPUT_Y, 0), + extras.getBoolean(CropExtras.KEY_SCALE, true) && + extras.getBoolean(CropExtras.KEY_SCALE_UP_IF_NEEDED, false), + extras.getInt(CropExtras.KEY_ASPECT_X, 0), + extras.getInt(CropExtras.KEY_ASPECT_Y, 0), + extras.getBoolean(CropExtras.KEY_SET_AS_WALLPAPER, false), + extras.getBoolean(CropExtras.KEY_RETURN_DATA, false), + (Uri) extras.getParcelable(MediaStore.EXTRA_OUTPUT), + extras.getString(CropExtras.KEY_OUTPUT_FORMAT), + extras.getBoolean(CropExtras.KEY_SHOW_WHEN_LOCKED, false), + extras.getFloat(CropExtras.KEY_SPOTLIGHT_X), + extras.getFloat(CropExtras.KEY_SPOTLIGHT_Y)); + } + return null; + } + + protected static CompressFormat convertExtensionToCompressFormat(String extension) { + return extension.equals("png") ? CompressFormat.PNG : CompressFormat.JPEG; + } + + protected static String getFileExtension(String requestFormat) { + String outputFormat = (requestFormat == null) + ? "jpg" + : requestFormat; + outputFormat = outputFormat.toLowerCase(); + return (outputFormat.equals("png") || outputFormat.equals("gif")) + ? "png" // We don't support gif compression. + : "jpg"; + } + + private RectF getBitmapCrop(RectF imageBounds) { + RectF crop = mCropView.getCrop(); + RectF photo = mCropView.getPhoto(); + if (crop == null || photo == null) { + Log.w(LOGTAG, "could not get crop"); + return null; + } + RectF scaledCrop = CropMath.getScaledCropBounds(crop, photo, imageBounds); + return scaledCrop; + } +} diff --git a/src/com/android/gallery3d/filtershow/crop/CropDrawingUtils.java b/src/com/android/gallery3d/filtershow/crop/CropDrawingUtils.java new file mode 100644 index 000000000..b0d324cbb --- /dev/null +++ b/src/com/android/gallery3d/filtershow/crop/CropDrawingUtils.java @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2013 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.filtershow.crop; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.RectF; +import android.graphics.Region; +import android.graphics.drawable.Drawable; + +public abstract class CropDrawingUtils { + + public static void drawRuleOfThird(Canvas canvas, RectF bounds) { + Paint p = new Paint(); + p.setStyle(Paint.Style.STROKE); + p.setColor(Color.argb(128, 255, 255, 255)); + p.setStrokeWidth(2); + float stepX = bounds.width() / 3.0f; + float stepY = bounds.height() / 3.0f; + float x = bounds.left + stepX; + float y = bounds.top + stepY; + for (int i = 0; i < 2; i++) { + canvas.drawLine(x, bounds.top, x, bounds.bottom, p); + x += stepX; + } + for (int j = 0; j < 2; j++) { + canvas.drawLine(bounds.left, y, bounds.right, y, p); + y += stepY; + } + } + + public static void drawCropRect(Canvas canvas, RectF bounds) { + Paint p = new Paint(); + p.setStyle(Paint.Style.STROKE); + p.setColor(Color.WHITE); + p.setStrokeWidth(3); + canvas.drawRect(bounds, p); + } + + public static void drawIndicator(Canvas canvas, Drawable indicator, int indicatorSize, + float centerX, float centerY) { + int left = (int) centerX - indicatorSize / 2; + int top = (int) centerY - indicatorSize / 2; + indicator.setBounds(left, top, left + indicatorSize, top + indicatorSize); + indicator.draw(canvas); + } + + public static void drawIndicators(Canvas canvas, Drawable cropIndicator, int indicatorSize, + RectF bounds, boolean fixedAspect, int selection) { + boolean notMoving = (selection == CropObject.MOVE_NONE); + if (fixedAspect) { + if ((selection == CropObject.TOP_LEFT) || notMoving) { + drawIndicator(canvas, cropIndicator, indicatorSize, bounds.left, bounds.top); + } + if ((selection == CropObject.TOP_RIGHT) || notMoving) { + drawIndicator(canvas, cropIndicator, indicatorSize, bounds.right, bounds.top); + } + if ((selection == CropObject.BOTTOM_LEFT) || notMoving) { + drawIndicator(canvas, cropIndicator, indicatorSize, bounds.left, bounds.bottom); + } + if ((selection == CropObject.BOTTOM_RIGHT) || notMoving) { + drawIndicator(canvas, cropIndicator, indicatorSize, bounds.right, bounds.bottom); + } + } else { + if (((selection & CropObject.MOVE_TOP) != 0) || notMoving) { + drawIndicator(canvas, cropIndicator, indicatorSize, bounds.centerX(), bounds.top); + } + if (((selection & CropObject.MOVE_BOTTOM) != 0) || notMoving) { + drawIndicator(canvas, cropIndicator, indicatorSize, bounds.centerX(), bounds.bottom); + } + if (((selection & CropObject.MOVE_LEFT) != 0) || notMoving) { + drawIndicator(canvas, cropIndicator, indicatorSize, bounds.left, bounds.centerY()); + } + if (((selection & CropObject.MOVE_RIGHT) != 0) || notMoving) { + drawIndicator(canvas, cropIndicator, indicatorSize, bounds.right, bounds.centerY()); + } + } + } + + public static void drawWallpaperSelectionFrame(Canvas canvas, RectF cropBounds, float spotX, + float spotY, Paint p, Paint shadowPaint) { + float sx = cropBounds.width() * spotX; + float sy = cropBounds.height() * spotY; + float cx = cropBounds.centerX(); + float cy = cropBounds.centerY(); + RectF r1 = new RectF(cx - sx / 2, cy - sy / 2, cx + sx / 2, cy + sy / 2); + float temp = sx; + sx = sy; + sy = temp; + RectF r2 = new RectF(cx - sx / 2, cy - sy / 2, cx + sx / 2, cy + sy / 2); + canvas.save(); + canvas.clipRect(cropBounds); + canvas.clipRect(r1, Region.Op.DIFFERENCE); + canvas.clipRect(r2, Region.Op.DIFFERENCE); + canvas.drawPaint(shadowPaint); + canvas.restore(); + Path path = new Path(); + path.moveTo(r1.left, r1.top); + path.lineTo(r1.right, r1.top); + path.moveTo(r1.left, r1.top); + path.lineTo(r1.left, r1.bottom); + path.moveTo(r1.left, r1.bottom); + path.lineTo(r1.right, r1.bottom); + path.moveTo(r1.right, r1.top); + path.lineTo(r1.right, r1.bottom); + path.moveTo(r2.left, r2.top); + path.lineTo(r2.right, r2.top); + path.moveTo(r2.right, r2.top); + path.lineTo(r2.right, r2.bottom); + path.moveTo(r2.left, r2.bottom); + path.lineTo(r2.right, r2.bottom); + path.moveTo(r2.left, r2.top); + path.lineTo(r2.left, r2.bottom); + canvas.drawPath(path, p); + } + + public static void drawShadows(Canvas canvas, Paint p, RectF innerBounds, RectF outerBounds) { + canvas.drawRect(outerBounds.left, outerBounds.top, innerBounds.right, innerBounds.top, p); + canvas.drawRect(innerBounds.right, outerBounds.top, outerBounds.right, innerBounds.bottom, + p); + canvas.drawRect(innerBounds.left, innerBounds.bottom, outerBounds.right, + outerBounds.bottom, p); + canvas.drawRect(outerBounds.left, innerBounds.top, innerBounds.left, outerBounds.bottom, p); + } + + public static Matrix getBitmapToDisplayMatrix(RectF imageBounds, RectF displayBounds) { + Matrix m = new Matrix(); + CropDrawingUtils.setBitmapToDisplayMatrix(m, imageBounds, displayBounds); + return m; + } + + public static boolean setBitmapToDisplayMatrix(Matrix m, RectF imageBounds, + RectF displayBounds) { + m.reset(); + return m.setRectToRect(imageBounds, displayBounds, Matrix.ScaleToFit.CENTER); + } + + public static boolean setImageToScreenMatrix(Matrix dst, RectF image, + RectF screen, int rotation) { + RectF rotatedImage = new RectF(); + dst.setRotate(rotation, image.centerX(), image.centerY()); + if (!dst.mapRect(rotatedImage, image)) { + return false; // fails for rotations that are not multiples of 90 + // degrees + } + boolean rToR = dst.setRectToRect(rotatedImage, screen, Matrix.ScaleToFit.CENTER); + boolean rot = dst.preRotate(rotation, image.centerX(), image.centerY()); + return rToR && rot; + } + +} diff --git a/src/com/android/gallery3d/filtershow/crop/CropExtras.java b/src/com/android/gallery3d/filtershow/crop/CropExtras.java new file mode 100644 index 000000000..60fe9af53 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/crop/CropExtras.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2012 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.filtershow.crop; + +import android.net.Uri; + +public class CropExtras { + + public static final String KEY_CROPPED_RECT = "cropped-rect"; + public static final String KEY_OUTPUT_X = "outputX"; + public static final String KEY_OUTPUT_Y = "outputY"; + public static final String KEY_SCALE = "scale"; + public static final String KEY_SCALE_UP_IF_NEEDED = "scaleUpIfNeeded"; + public static final String KEY_ASPECT_X = "aspectX"; + public static final String KEY_ASPECT_Y = "aspectY"; + public static final String KEY_SET_AS_WALLPAPER = "set-as-wallpaper"; + public static final String KEY_RETURN_DATA = "return-data"; + public static final String KEY_DATA = "data"; + public static final String KEY_SPOTLIGHT_X = "spotlightX"; + public static final String KEY_SPOTLIGHT_Y = "spotlightY"; + public static final String KEY_SHOW_WHEN_LOCKED = "showWhenLocked"; + public static final String KEY_OUTPUT_FORMAT = "outputFormat"; + + private int mOutputX = 0; + private int mOutputY = 0; + private boolean mScaleUp = true; + private int mAspectX = 0; + private int mAspectY = 0; + private boolean mSetAsWallpaper = false; + private boolean mReturnData = false; + private Uri mExtraOutput = null; + private String mOutputFormat = null; + private boolean mShowWhenLocked = false; + private float mSpotlightX = 0; + private float mSpotlightY = 0; + + public CropExtras(int outputX, int outputY, boolean scaleUp, int aspectX, int aspectY, + boolean setAsWallpaper, boolean returnData, Uri extraOutput, String outputFormat, + boolean showWhenLocked, float spotlightX, float spotlightY) { + mOutputX = outputX; + mOutputY = outputY; + mScaleUp = scaleUp; + mAspectX = aspectX; + mAspectY = aspectY; + mSetAsWallpaper = setAsWallpaper; + mReturnData = returnData; + mExtraOutput = extraOutput; + mOutputFormat = outputFormat; + mShowWhenLocked = showWhenLocked; + mSpotlightX = spotlightX; + mSpotlightY = spotlightY; + } + + public CropExtras(CropExtras c) { + this(c.mOutputX, c.mOutputY, c.mScaleUp, c.mAspectX, c.mAspectY, c.mSetAsWallpaper, + c.mReturnData, c.mExtraOutput, c.mOutputFormat, c.mShowWhenLocked, + c.mSpotlightX, c.mSpotlightY); + } + + public int getOutputX() { + return mOutputX; + } + + public int getOutputY() { + return mOutputY; + } + + public boolean getScaleUp() { + return mScaleUp; + } + + public int getAspectX() { + return mAspectX; + } + + public int getAspectY() { + return mAspectY; + } + + public boolean getSetAsWallpaper() { + return mSetAsWallpaper; + } + + public boolean getReturnData() { + return mReturnData; + } + + public Uri getExtraOutput() { + return mExtraOutput; + } + + public String getOutputFormat() { + return mOutputFormat; + } + + public boolean getShowWhenLocked() { + return mShowWhenLocked; + } + + public float getSpotlightX() { + return mSpotlightX; + } + + public float getSpotlightY() { + return mSpotlightY; + } +} diff --git a/src/com/android/gallery3d/filtershow/crop/CropMath.java b/src/com/android/gallery3d/filtershow/crop/CropMath.java new file mode 100644 index 000000000..02c65310e --- /dev/null +++ b/src/com/android/gallery3d/filtershow/crop/CropMath.java @@ -0,0 +1,260 @@ +/* + * Copyright (C) 2012 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.filtershow.crop; + +import android.graphics.Bitmap; +import android.graphics.Matrix; +import android.graphics.RectF; + +import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils; + +import java.util.Arrays; + +public class CropMath { + + /** + * Gets a float array of the 2D coordinates representing a rectangles + * corners. + * The order of the corners in the float array is: + * 0------->1 + * ^ | + * | v + * 3<-------2 + * + * @param r the rectangle to get the corners of + * @return the float array of corners (8 floats) + */ + + public static float[] getCornersFromRect(RectF r) { + float[] corners = { + r.left, r.top, + r.right, r.top, + r.right, r.bottom, + r.left, r.bottom + }; + return corners; + } + + /** + * Returns true iff point (x, y) is within or on the rectangle's bounds. + * RectF's "contains" function treats points on the bottom and right bound + * as not being contained. + * + * @param r the rectangle + * @param x the x value of the point + * @param y the y value of the point + * @return + */ + public static boolean inclusiveContains(RectF r, float x, float y) { + return !(x > r.right || x < r.left || y > r.bottom || y < r.top); + } + + /** + * Takes an array of 2D coordinates representing corners and returns the + * smallest rectangle containing those coordinates. + * + * @param array array of 2D coordinates + * @return smallest rectangle containing coordinates + */ + public static RectF trapToRect(float[] array) { + RectF r = new RectF(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY, + Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY); + for (int i = 1; i < array.length; i += 2) { + float x = array[i - 1]; + float y = array[i]; + r.left = (x < r.left) ? x : r.left; + r.top = (y < r.top) ? y : r.top; + r.right = (x > r.right) ? x : r.right; + r.bottom = (y > r.bottom) ? y : r.bottom; + } + r.sort(); + return r; + } + + /** + * If edge point [x, y] in array [x0, y0, x1, y1, ...] is outside of the + * image bound rectangle, clamps it to the edge of the rectangle. + * + * @param imageBound the rectangle to clamp edge points to. + * @param array an array of points to clamp to the rectangle, gets set to + * the clamped values. + */ + public static void getEdgePoints(RectF imageBound, float[] array) { + if (array.length < 2) + return; + for (int x = 0; x < array.length; x += 2) { + array[x] = GeometryMathUtils.clamp(array[x], imageBound.left, imageBound.right); + array[x + 1] = GeometryMathUtils.clamp(array[x + 1], imageBound.top, imageBound.bottom); + } + } + + /** + * Takes a point and the corners of a rectangle and returns the two corners + * representing the side of the rectangle closest to the point. + * + * @param point the point which is being checked + * @param corners the corners of the rectangle + * @return two corners representing the side of the rectangle + */ + public static float[] closestSide(float[] point, float[] corners) { + int len = corners.length; + float oldMag = Float.POSITIVE_INFINITY; + float[] bestLine = null; + for (int i = 0; i < len; i += 2) { + float[] line = { + corners[i], corners[(i + 1) % len], + corners[(i + 2) % len], corners[(i + 3) % len] + }; + float mag = GeometryMathUtils.vectorLength( + GeometryMathUtils.shortestVectorFromPointToLine(point, line)); + if (mag < oldMag) { + oldMag = mag; + bestLine = line; + } + } + return bestLine; + } + + /** + * Checks if a given point is within a rotated rectangle. + * + * @param point 2D point to check + * @param bound rectangle to rotate + * @param rot angle of rotation about rectangle center + * @return true if point is within rotated rectangle + */ + public static boolean pointInRotatedRect(float[] point, RectF bound, float rot) { + Matrix m = new Matrix(); + float[] p = Arrays.copyOf(point, 2); + m.setRotate(rot, bound.centerX(), bound.centerY()); + Matrix m0 = new Matrix(); + if (!m.invert(m0)) + return false; + m0.mapPoints(p); + return inclusiveContains(bound, p[0], p[1]); + } + + /** + * Checks if a given point is within a rotated rectangle. + * + * @param point 2D point to check + * @param rotatedRect corners of a rotated rectangle + * @param center center of the rotated rectangle + * @return true if point is within rotated rectangle + */ + public static boolean pointInRotatedRect(float[] point, float[] rotatedRect, float[] center) { + RectF unrotated = new RectF(); + float angle = getUnrotated(rotatedRect, center, unrotated); + return pointInRotatedRect(point, unrotated, angle); + } + + /** + * Resizes rectangle to have a certain aspect ratio (center remains + * stationary). + * + * @param r rectangle to resize + * @param w new width aspect + * @param h new height aspect + */ + public static void fixAspectRatio(RectF r, float w, float h) { + float scale = Math.min(r.width() / w, r.height() / h); + float centX = r.centerX(); + float centY = r.centerY(); + float hw = scale * w / 2; + float hh = scale * h / 2; + r.set(centX - hw, centY - hh, centX + hw, centY + hh); + } + + /** + * Resizes rectangle to have a certain aspect ratio (center remains + * stationary) while constraining it to remain within the original rect. + * + * @param r rectangle to resize + * @param w new width aspect + * @param h new height aspect + */ + public static void fixAspectRatioContained(RectF r, float w, float h) { + float origW = r.width(); + float origH = r.height(); + float origA = origW / origH; + float a = w / h; + float finalW = origW; + float finalH = origH; + if (origA < a) { + finalH = origW / a; + r.top = r.centerY() - finalH / 2; + r.bottom = r.top + finalH; + } else { + finalW = origH * a; + r.left = r.centerX() - finalW / 2; + r.right = r.left + finalW; + } + } + + /** + * Stretches/Scales/Translates photoBounds to match displayBounds, and + * and returns an equivalent stretched/scaled/translated cropBounds or null + * if the mapping is invalid. + * @param cropBounds cropBounds to transform + * @param photoBounds original bounds containing crop bounds + * @param displayBounds final bounds for crop + * @return the stretched/scaled/translated crop bounds that fit within displayBounds + */ + public static RectF getScaledCropBounds(RectF cropBounds, RectF photoBounds, + RectF displayBounds) { + Matrix m = new Matrix(); + m.setRectToRect(photoBounds, displayBounds, Matrix.ScaleToFit.FILL); + RectF trueCrop = new RectF(cropBounds); + if (!m.mapRect(trueCrop)) { + return null; + } + return trueCrop; + } + + /** + * Returns the size of a bitmap in bytes. + * @param bmap bitmap whose size to check + * @return bitmap size in bytes + */ + public static int getBitmapSize(Bitmap bmap) { + return bmap.getRowBytes() * bmap.getHeight(); + } + + /** + * Constrains rotation to be in [0, 90, 180, 270] rounding down. + * @param rotation any rotation value, in degrees + * @return integer rotation in [0, 90, 180, 270] + */ + public static int constrainedRotation(float rotation) { + int r = (int) ((rotation % 360) / 90); + r = (r < 0) ? (r + 4) : r; + return r * 90; + } + + private static float getUnrotated(float[] rotatedRect, float[] center, RectF unrotated) { + float dy = rotatedRect[1] - rotatedRect[3]; + float dx = rotatedRect[0] - rotatedRect[2]; + float angle = (float) (Math.atan(dy / dx) * 180 / Math.PI); + Matrix m = new Matrix(); + m.setRotate(-angle, center[0], center[1]); + float[] unrotatedRect = new float[rotatedRect.length]; + m.mapPoints(unrotatedRect, rotatedRect); + unrotated.set(trapToRect(unrotatedRect)); + return angle; + } + +} diff --git a/src/com/android/gallery3d/filtershow/crop/CropObject.java b/src/com/android/gallery3d/filtershow/crop/CropObject.java new file mode 100644 index 000000000..b98ed1bfd --- /dev/null +++ b/src/com/android/gallery3d/filtershow/crop/CropObject.java @@ -0,0 +1,330 @@ +/* + * Copyright (C) 2013 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.filtershow.crop; + +import android.graphics.Rect; +import android.graphics.RectF; + +import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils; + +public class CropObject { + private BoundedRect mBoundedRect; + private float mAspectWidth = 1; + private float mAspectHeight = 1; + private boolean mFixAspectRatio = false; + private float mRotation = 0; + private float mTouchTolerance = 45; + private float mMinSideSize = 20; + + public static final int MOVE_NONE = 0; + // Sides + public static final int MOVE_LEFT = 1; + public static final int MOVE_TOP = 2; + public static final int MOVE_RIGHT = 4; + public static final int MOVE_BOTTOM = 8; + public static final int MOVE_BLOCK = 16; + + // Corners + public static final int TOP_LEFT = MOVE_TOP | MOVE_LEFT; + public static final int TOP_RIGHT = MOVE_TOP | MOVE_RIGHT; + public static final int BOTTOM_RIGHT = MOVE_BOTTOM | MOVE_RIGHT; + public static final int BOTTOM_LEFT = MOVE_BOTTOM | MOVE_LEFT; + + private int mMovingEdges = MOVE_NONE; + + public CropObject(Rect outerBound, Rect innerBound, int outerAngle) { + mBoundedRect = new BoundedRect(outerAngle % 360, outerBound, innerBound); + } + + public CropObject(RectF outerBound, RectF innerBound, int outerAngle) { + mBoundedRect = new BoundedRect(outerAngle % 360, outerBound, innerBound); + } + + public void resetBoundsTo(RectF inner, RectF outer) { + mBoundedRect.resetTo(0, outer, inner); + } + + public void getInnerBounds(RectF r) { + mBoundedRect.setToInner(r); + } + + public void getOuterBounds(RectF r) { + mBoundedRect.setToOuter(r); + } + + public RectF getInnerBounds() { + return mBoundedRect.getInner(); + } + + public RectF getOuterBounds() { + return mBoundedRect.getOuter(); + } + + public int getSelectState() { + return mMovingEdges; + } + + public boolean isFixedAspect() { + return mFixAspectRatio; + } + + public void rotateOuter(int angle) { + mRotation = angle % 360; + mBoundedRect.setRotation(mRotation); + clearSelectState(); + } + + public boolean setInnerAspectRatio(float width, float height) { + if (width <= 0 || height <= 0) { + throw new IllegalArgumentException("Width and Height must be greater than zero"); + } + RectF inner = mBoundedRect.getInner(); + CropMath.fixAspectRatioContained(inner, width, height); + if (inner.width() < mMinSideSize || inner.height() < mMinSideSize) { + return false; + } + mAspectWidth = width; + mAspectHeight = height; + mFixAspectRatio = true; + mBoundedRect.setInner(inner); + clearSelectState(); + return true; + } + + public void setTouchTolerance(float tolerance) { + if (tolerance <= 0) { + throw new IllegalArgumentException("Tolerance must be greater than zero"); + } + mTouchTolerance = tolerance; + } + + public void setMinInnerSideSize(float minSide) { + if (minSide <= 0) { + throw new IllegalArgumentException("Min dide must be greater than zero"); + } + mMinSideSize = minSide; + } + + public void unsetAspectRatio() { + mFixAspectRatio = false; + clearSelectState(); + } + + public boolean hasSelectedEdge() { + return mMovingEdges != MOVE_NONE; + } + + public static boolean checkCorner(int selected) { + return selected == TOP_LEFT || selected == TOP_RIGHT || selected == BOTTOM_RIGHT + || selected == BOTTOM_LEFT; + } + + public static boolean checkEdge(int selected) { + return selected == MOVE_LEFT || selected == MOVE_TOP || selected == MOVE_RIGHT + || selected == MOVE_BOTTOM; + } + + public static boolean checkBlock(int selected) { + return selected == MOVE_BLOCK; + } + + public static boolean checkValid(int selected) { + return selected == MOVE_NONE || checkBlock(selected) || checkEdge(selected) + || checkCorner(selected); + } + + public void clearSelectState() { + mMovingEdges = MOVE_NONE; + } + + public int wouldSelectEdge(float x, float y) { + int edgeSelected = calculateSelectedEdge(x, y); + if (edgeSelected != MOVE_NONE && edgeSelected != MOVE_BLOCK) { + return edgeSelected; + } + return MOVE_NONE; + } + + public boolean selectEdge(int edge) { + if (!checkValid(edge)) { + // temporary + throw new IllegalArgumentException("bad edge selected"); + // return false; + } + if ((mFixAspectRatio && !checkCorner(edge)) && !checkBlock(edge) && edge != MOVE_NONE) { + // temporary + throw new IllegalArgumentException("bad corner selected"); + // return false; + } + mMovingEdges = edge; + return true; + } + + public boolean selectEdge(float x, float y) { + int edgeSelected = calculateSelectedEdge(x, y); + if (mFixAspectRatio) { + edgeSelected = fixEdgeToCorner(edgeSelected); + } + if (edgeSelected == MOVE_NONE) { + return false; + } + return selectEdge(edgeSelected); + } + + public boolean moveCurrentSelection(float dX, float dY) { + if (mMovingEdges == MOVE_NONE) { + return false; + } + RectF crop = mBoundedRect.getInner(); + + float minWidthHeight = mMinSideSize; + + int movingEdges = mMovingEdges; + if (movingEdges == MOVE_BLOCK) { + mBoundedRect.moveInner(dX, dY); + return true; + } else { + float dx = 0; + float dy = 0; + + if ((movingEdges & MOVE_LEFT) != 0) { + dx = Math.min(crop.left + dX, crop.right - minWidthHeight) - crop.left; + } + if ((movingEdges & MOVE_TOP) != 0) { + dy = Math.min(crop.top + dY, crop.bottom - minWidthHeight) - crop.top; + } + if ((movingEdges & MOVE_RIGHT) != 0) { + dx = Math.max(crop.right + dX, crop.left + minWidthHeight) + - crop.right; + } + if ((movingEdges & MOVE_BOTTOM) != 0) { + dy = Math.max(crop.bottom + dY, crop.top + minWidthHeight) + - crop.bottom; + } + + if (mFixAspectRatio) { + float[] l1 = { + crop.left, crop.bottom + }; + float[] l2 = { + crop.right, crop.top + }; + if (movingEdges == TOP_LEFT || movingEdges == BOTTOM_RIGHT) { + l1[1] = crop.top; + l2[1] = crop.bottom; + } + float[] b = { + l1[0] - l2[0], l1[1] - l2[1] + }; + float[] disp = { + dx, dy + }; + float[] bUnit = GeometryMathUtils.normalize(b); + float sp = GeometryMathUtils.scalarProjection(disp, bUnit); + dx = sp * bUnit[0]; + dy = sp * bUnit[1]; + RectF newCrop = fixedCornerResize(crop, movingEdges, dx, dy); + + mBoundedRect.fixedAspectResizeInner(newCrop); + } else { + if ((movingEdges & MOVE_LEFT) != 0) { + crop.left += dx; + } + if ((movingEdges & MOVE_TOP) != 0) { + crop.top += dy; + } + if ((movingEdges & MOVE_RIGHT) != 0) { + crop.right += dx; + } + if ((movingEdges & MOVE_BOTTOM) != 0) { + crop.bottom += dy; + } + mBoundedRect.resizeInner(crop); + } + } + return true; + } + + // Helper methods + + private int calculateSelectedEdge(float x, float y) { + RectF cropped = mBoundedRect.getInner(); + + float left = Math.abs(x - cropped.left); + float right = Math.abs(x - cropped.right); + float top = Math.abs(y - cropped.top); + float bottom = Math.abs(y - cropped.bottom); + + int edgeSelected = MOVE_NONE; + // Check left or right. + if ((left <= mTouchTolerance) && ((y + mTouchTolerance) >= cropped.top) + && ((y - mTouchTolerance) <= cropped.bottom) && (left < right)) { + edgeSelected |= MOVE_LEFT; + } + else if ((right <= mTouchTolerance) && ((y + mTouchTolerance) >= cropped.top) + && ((y - mTouchTolerance) <= cropped.bottom)) { + edgeSelected |= MOVE_RIGHT; + } + + // Check top or bottom. + if ((top <= mTouchTolerance) && ((x + mTouchTolerance) >= cropped.left) + && ((x - mTouchTolerance) <= cropped.right) && (top < bottom)) { + edgeSelected |= MOVE_TOP; + } + else if ((bottom <= mTouchTolerance) && ((x + mTouchTolerance) >= cropped.left) + && ((x - mTouchTolerance) <= cropped.right)) { + edgeSelected |= MOVE_BOTTOM; + } + return edgeSelected; + } + + private static RectF fixedCornerResize(RectF r, int moving_corner, float dx, float dy) { + RectF newCrop = null; + // Fix opposite corner in place and move sides + if (moving_corner == BOTTOM_RIGHT) { + newCrop = new RectF(r.left, r.top, r.left + r.width() + dx, r.top + r.height() + + dy); + } else if (moving_corner == BOTTOM_LEFT) { + newCrop = new RectF(r.right - r.width() + dx, r.top, r.right, r.top + r.height() + + dy); + } else if (moving_corner == TOP_LEFT) { + newCrop = new RectF(r.right - r.width() + dx, r.bottom - r.height() + dy, + r.right, r.bottom); + } else if (moving_corner == TOP_RIGHT) { + newCrop = new RectF(r.left, r.bottom - r.height() + dy, r.left + + r.width() + dx, r.bottom); + } + return newCrop; + } + + private static int fixEdgeToCorner(int moving_edges) { + if (moving_edges == MOVE_LEFT) { + moving_edges |= MOVE_TOP; + } + if (moving_edges == MOVE_TOP) { + moving_edges |= MOVE_LEFT; + } + if (moving_edges == MOVE_RIGHT) { + moving_edges |= MOVE_BOTTOM; + } + if (moving_edges == MOVE_BOTTOM) { + moving_edges |= MOVE_RIGHT; + } + return moving_edges; + } + +} diff --git a/src/com/android/gallery3d/filtershow/crop/CropView.java b/src/com/android/gallery3d/filtershow/crop/CropView.java new file mode 100644 index 000000000..bbb7cfd4c --- /dev/null +++ b/src/com/android/gallery3d/filtershow/crop/CropView.java @@ -0,0 +1,378 @@ +/* + * Copyright (C) 2013 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.filtershow.crop; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.DashPathEffect; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.NinePatchDrawable; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; + +import com.android.gallery3d.R; + + +public class CropView extends View { + private static final String LOGTAG = "CropView"; + + private RectF mImageBounds = new RectF(); + private RectF mScreenBounds = new RectF(); + private RectF mScreenImageBounds = new RectF(); + private RectF mScreenCropBounds = new RectF(); + private Rect mShadowBounds = new Rect(); + + private Bitmap mBitmap; + private Paint mPaint = new Paint(); + + private NinePatchDrawable mShadow; + private CropObject mCropObj = null; + private Drawable mCropIndicator; + private int mIndicatorSize; + private int mRotation = 0; + private boolean mMovingBlock = false; + private Matrix mDisplayMatrix = null; + private Matrix mDisplayMatrixInverse = null; + private boolean mDirty = false; + + private float mPrevX = 0; + private float mPrevY = 0; + private float mSpotX = 0; + private float mSpotY = 0; + private boolean mDoSpot = false; + + private int mShadowMargin = 15; + private int mMargin = 32; + private int mOverlayShadowColor = 0xCF000000; + private int mOverlayWPShadowColor = 0x5F000000; + private int mWPMarkerColor = 0x7FFFFFFF; + private int mMinSideSize = 90; + private int mTouchTolerance = 40; + private float mDashOnLength = 20; + private float mDashOffLength = 10; + + private enum Mode { + NONE, MOVE + } + + private Mode mState = Mode.NONE; + + public CropView(Context context) { + super(context); + setup(context); + } + + public CropView(Context context, AttributeSet attrs) { + super(context, attrs); + setup(context); + } + + public CropView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + setup(context); + } + + private void setup(Context context) { + Resources rsc = context.getResources(); + mShadow = (NinePatchDrawable) rsc.getDrawable(R.drawable.geometry_shadow); + mCropIndicator = rsc.getDrawable(R.drawable.camera_crop); + mIndicatorSize = (int) rsc.getDimension(R.dimen.crop_indicator_size); + mShadowMargin = (int) rsc.getDimension(R.dimen.shadow_margin); + mMargin = (int) rsc.getDimension(R.dimen.preview_margin); + mMinSideSize = (int) rsc.getDimension(R.dimen.crop_min_side); + mTouchTolerance = (int) rsc.getDimension(R.dimen.crop_touch_tolerance); + mOverlayShadowColor = (int) rsc.getColor(R.color.crop_shadow_color); + mOverlayWPShadowColor = (int) rsc.getColor(R.color.crop_shadow_wp_color); + mWPMarkerColor = (int) rsc.getColor(R.color.crop_wp_markers); + mDashOnLength = rsc.getDimension(R.dimen.wp_selector_dash_length); + mDashOffLength = rsc.getDimension(R.dimen.wp_selector_off_length); + } + + public void initialize(Bitmap image, RectF newCropBounds, RectF newPhotoBounds, int rotation) { + mBitmap = image; + if (mCropObj != null) { + RectF crop = mCropObj.getInnerBounds(); + RectF containing = mCropObj.getOuterBounds(); + if (crop != newCropBounds || containing != newPhotoBounds + || mRotation != rotation) { + mRotation = rotation; + mCropObj.resetBoundsTo(newCropBounds, newPhotoBounds); + clearDisplay(); + } + } else { + mRotation = rotation; + mCropObj = new CropObject(newPhotoBounds, newCropBounds, 0); + clearDisplay(); + } + } + + public RectF getCrop() { + return mCropObj.getInnerBounds(); + } + + public RectF getPhoto() { + return mCropObj.getOuterBounds(); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + float x = event.getX(); + float y = event.getY(); + if (mDisplayMatrix == null || mDisplayMatrixInverse == null) { + return true; + } + float[] touchPoint = { + x, y + }; + mDisplayMatrixInverse.mapPoints(touchPoint); + x = touchPoint[0]; + y = touchPoint[1]; + switch (event.getActionMasked()) { + case (MotionEvent.ACTION_DOWN): + if (mState == Mode.NONE) { + if (!mCropObj.selectEdge(x, y)) { + mMovingBlock = mCropObj.selectEdge(CropObject.MOVE_BLOCK); + } + mPrevX = x; + mPrevY = y; + mState = Mode.MOVE; + } + break; + case (MotionEvent.ACTION_UP): + if (mState == Mode.MOVE) { + mCropObj.selectEdge(CropObject.MOVE_NONE); + mMovingBlock = false; + mPrevX = x; + mPrevY = y; + mState = Mode.NONE; + } + break; + case (MotionEvent.ACTION_MOVE): + if (mState == Mode.MOVE) { + float dx = x - mPrevX; + float dy = y - mPrevY; + mCropObj.moveCurrentSelection(dx, dy); + mPrevX = x; + mPrevY = y; + } + break; + default: + break; + } + invalidate(); + return true; + } + + private void reset() { + Log.w(LOGTAG, "crop reset called"); + mState = Mode.NONE; + mCropObj = null; + mRotation = 0; + mMovingBlock = false; + clearDisplay(); + } + + private void clearDisplay() { + mDisplayMatrix = null; + mDisplayMatrixInverse = null; + invalidate(); + } + + protected void configChanged() { + mDirty = true; + } + + public void applyFreeAspect() { + mCropObj.unsetAspectRatio(); + invalidate(); + } + + public void applyOriginalAspect() { + RectF outer = mCropObj.getOuterBounds(); + float w = outer.width(); + float h = outer.height(); + if (w > 0 && h > 0) { + applyAspect(w, h); + mCropObj.resetBoundsTo(outer, outer); + } else { + Log.w(LOGTAG, "failed to set aspect ratio original"); + } + } + + public void applySquareAspect() { + applyAspect(1, 1); + } + + public void applyAspect(float x, float y) { + if (x <= 0 || y <= 0) { + throw new IllegalArgumentException("Bad arguments to applyAspect"); + } + // If we are rotated by 90 degrees from horizontal, swap x and y + if (((mRotation < 0) ? -mRotation : mRotation) % 180 == 90) { + float tmp = x; + x = y; + y = tmp; + } + if (!mCropObj.setInnerAspectRatio(x, y)) { + Log.w(LOGTAG, "failed to set aspect ratio"); + } + invalidate(); + } + + public void setWallpaperSpotlight(float spotlightX, float spotlightY) { + mSpotX = spotlightX; + mSpotY = spotlightY; + if (mSpotX > 0 && mSpotY > 0) { + mDoSpot = true; + } + } + + public void unsetWallpaperSpotlight() { + mDoSpot = false; + } + + /** + * Rotates first d bits in integer x to the left some number of times. + */ + private int bitCycleLeft(int x, int times, int d) { + int mask = (1 << d) - 1; + int mout = x & mask; + times %= d; + int hi = mout >> (d - times); + int low = (mout << times) & mask; + int ret = x & ~mask; + ret |= low; + ret |= hi; + return ret; + } + + /** + * Find the selected edge or corner in screen coordinates. + */ + private int decode(int movingEdges, float rotation) { + int rot = CropMath.constrainedRotation(rotation); + switch (rot) { + case 90: + return bitCycleLeft(movingEdges, 1, 4); + case 180: + return bitCycleLeft(movingEdges, 2, 4); + case 270: + return bitCycleLeft(movingEdges, 3, 4); + default: + return movingEdges; + } + } + + @Override + public void onDraw(Canvas canvas) { + if (mBitmap == null) { + return; + } + if (mDirty) { + mDirty = false; + clearDisplay(); + } + + mImageBounds = new RectF(0, 0, mBitmap.getWidth(), mBitmap.getHeight()); + mScreenBounds = new RectF(0, 0, canvas.getWidth(), canvas.getHeight()); + mScreenBounds.inset(mMargin, mMargin); + + // If crop object doesn't exist, create it and update it from master + // state + if (mCropObj == null) { + reset(); + mCropObj = new CropObject(mImageBounds, mImageBounds, 0); + } + + // If display matrix doesn't exist, create it and its dependencies + if (mDisplayMatrix == null || mDisplayMatrixInverse == null) { + mDisplayMatrix = new Matrix(); + mDisplayMatrix.reset(); + if (!CropDrawingUtils.setImageToScreenMatrix(mDisplayMatrix, mImageBounds, mScreenBounds, + mRotation)) { + Log.w(LOGTAG, "failed to get screen matrix"); + mDisplayMatrix = null; + return; + } + mDisplayMatrixInverse = new Matrix(); + mDisplayMatrixInverse.reset(); + if (!mDisplayMatrix.invert(mDisplayMatrixInverse)) { + Log.w(LOGTAG, "could not invert display matrix"); + mDisplayMatrixInverse = null; + return; + } + // Scale min side and tolerance by display matrix scale factor + mCropObj.setMinInnerSideSize(mDisplayMatrixInverse.mapRadius(mMinSideSize)); + mCropObj.setTouchTolerance(mDisplayMatrixInverse.mapRadius(mTouchTolerance)); + } + + mScreenImageBounds.set(mImageBounds); + + // Draw background shadow + if (mDisplayMatrix.mapRect(mScreenImageBounds)) { + int margin = (int) mDisplayMatrix.mapRadius(mShadowMargin); + mScreenImageBounds.roundOut(mShadowBounds); + mShadowBounds.set(mShadowBounds.left - margin, mShadowBounds.top - + margin, mShadowBounds.right + margin, mShadowBounds.bottom + margin); + mShadow.setBounds(mShadowBounds); + mShadow.draw(canvas); + } + + mPaint.setAntiAlias(true); + mPaint.setFilterBitmap(true); + // Draw actual bitmap + canvas.drawBitmap(mBitmap, mDisplayMatrix, mPaint); + + mCropObj.getInnerBounds(mScreenCropBounds); + + if (mDisplayMatrix.mapRect(mScreenCropBounds)) { + + // Draw overlay shadows + Paint p = new Paint(); + p.setColor(mOverlayShadowColor); + p.setStyle(Paint.Style.FILL); + CropDrawingUtils.drawShadows(canvas, p, mScreenCropBounds, mScreenImageBounds); + + // Draw crop rect and markers + CropDrawingUtils.drawCropRect(canvas, mScreenCropBounds); + if (!mDoSpot) { + CropDrawingUtils.drawRuleOfThird(canvas, mScreenCropBounds); + } else { + Paint wpPaint = new Paint(); + wpPaint.setColor(mWPMarkerColor); + wpPaint.setStrokeWidth(3); + wpPaint.setStyle(Paint.Style.STROKE); + wpPaint.setPathEffect(new DashPathEffect(new float[] + {mDashOnLength, mDashOnLength + mDashOffLength}, 0)); + p.setColor(mOverlayWPShadowColor); + CropDrawingUtils.drawWallpaperSelectionFrame(canvas, mScreenCropBounds, + mSpotX, mSpotY, wpPaint, p); + } + CropDrawingUtils.drawIndicators(canvas, mCropIndicator, mIndicatorSize, + mScreenCropBounds, mCropObj.isFixedAspect(), decode(mCropObj.getSelectState(), mRotation)); + } + + } +} diff --git a/src/com/android/gallery3d/filtershow/data/FilterStackDBHelper.java b/src/com/android/gallery3d/filtershow/data/FilterStackDBHelper.java new file mode 100644 index 000000000..e18d3104f --- /dev/null +++ b/src/com/android/gallery3d/filtershow/data/FilterStackDBHelper.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2013 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.filtershow.data; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +public class FilterStackDBHelper extends SQLiteOpenHelper { + + public static final int DATABASE_VERSION = 1; + public static final String DATABASE_NAME = "filterstacks.db"; + private static final String SQL_CREATE_TABLE = "CREATE TABLE "; + + public static interface FilterStack { + /** The row uid */ + public static final String _ID = "_id"; + /** The table name */ + public static final String TABLE = "filterstack"; + /** The stack name */ + public static final String STACK_ID = "stack_id"; + /** A serialized stack of filters. */ + public static final String FILTER_STACK= "stack"; + } + + private static final String[][] CREATE_FILTER_STACK = { + { FilterStack._ID, "INTEGER PRIMARY KEY AUTOINCREMENT" }, + { FilterStack.STACK_ID, "TEXT" }, + { FilterStack.FILTER_STACK, "BLOB" }, + }; + + public FilterStackDBHelper(Context context, String name, int version) { + super(context, name, null, version); + } + + public FilterStackDBHelper(Context context, String name) { + this(context, name, DATABASE_VERSION); + } + + public FilterStackDBHelper(Context context) { + this(context, DATABASE_NAME); + } + + @Override + public void onCreate(SQLiteDatabase db) { + createTable(db, FilterStack.TABLE, CREATE_FILTER_STACK); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + dropTable(db, FilterStack.TABLE); + onCreate(db); + } + + protected static void createTable(SQLiteDatabase db, String table, String[][] columns) { + StringBuilder create = new StringBuilder(SQL_CREATE_TABLE); + create.append(table).append('('); + boolean first = true; + for (String[] column : columns) { + if (!first) { + create.append(','); + } + first = false; + for (String val : column) { + create.append(val).append(' '); + } + } + create.append(')'); + db.beginTransaction(); + try { + db.execSQL(create.toString()); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + + protected static void dropTable(SQLiteDatabase db, String table) { + db.beginTransaction(); + try { + db.execSQL("drop table if exists " + table); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } +} diff --git a/src/com/android/gallery3d/filtershow/data/FilterStackSource.java b/src/com/android/gallery3d/filtershow/data/FilterStackSource.java new file mode 100644 index 000000000..d283771b4 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/data/FilterStackSource.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2013 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.filtershow.data; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import android.util.Log; +import android.util.Pair; + +import com.android.gallery3d.filtershow.data.FilterStackDBHelper.FilterStack; +import com.android.gallery3d.filtershow.filters.FilterUserPresetRepresentation; +import com.android.gallery3d.filtershow.pipeline.ImagePreset; + +import java.util.ArrayList; +import java.util.List; + +public class FilterStackSource { + private static final String LOGTAG = "FilterStackSource"; + + private SQLiteDatabase database = null; + private final FilterStackDBHelper dbHelper; + + public FilterStackSource(Context context) { + dbHelper = new FilterStackDBHelper(context); + } + + public void open() { + try { + database = dbHelper.getWritableDatabase(); + } catch (SQLiteException e) { + Log.w(LOGTAG, "could not open database", e); + } + } + + public void close() { + database = null; + dbHelper.close(); + } + + public boolean insertStack(String stackName, byte[] stackBlob) { + boolean ret = true; + ContentValues val = new ContentValues(); + val.put(FilterStack.STACK_ID, stackName); + val.put(FilterStack.FILTER_STACK, stackBlob); + database.beginTransaction(); + try { + ret = (-1 != database.insert(FilterStack.TABLE, null, val)); + database.setTransactionSuccessful(); + } finally { + database.endTransaction(); + } + return ret; + } + + public void updateStackName(int id, String stackName) { + ContentValues val = new ContentValues(); + val.put(FilterStack.STACK_ID, stackName); + database.beginTransaction(); + try { + database.update(FilterStack.TABLE, val, FilterStack._ID + " = ?", + new String[] { "" + id}); + database.setTransactionSuccessful(); + } finally { + database.endTransaction(); + } + } + + public boolean removeStack(int id) { + boolean ret = true; + database.beginTransaction(); + try { + ret = (0 != database.delete(FilterStack.TABLE, FilterStack._ID + " = ?", + new String[] { "" + id })); + database.setTransactionSuccessful(); + } finally { + database.endTransaction(); + } + return ret; + } + + public void removeAllStacks() { + database.beginTransaction(); + try { + database.delete(FilterStack.TABLE, null, null); + database.setTransactionSuccessful(); + } finally { + database.endTransaction(); + } + } + + public byte[] getStack(String stackName) { + byte[] ret = null; + Cursor c = null; + database.beginTransaction(); + try { + c = database.query(FilterStack.TABLE, + new String[] { FilterStack.FILTER_STACK }, + FilterStack.STACK_ID + " = ?", + new String[] { stackName }, null, null, null, null); + if (c != null && c.moveToFirst() && !c.isNull(0)) { + ret = c.getBlob(0); + } + database.setTransactionSuccessful(); + } finally { + if (c != null) { + c.close(); + } + database.endTransaction(); + } + return ret; + } + + public ArrayList<FilterUserPresetRepresentation> getAllUserPresets() { + ArrayList<FilterUserPresetRepresentation> ret = + new ArrayList<FilterUserPresetRepresentation>(); + + Cursor c = null; + database.beginTransaction(); + try { + c = database.query(FilterStack.TABLE, + new String[] { FilterStack._ID, + FilterStack.STACK_ID, + FilterStack.FILTER_STACK }, + null, null, null, null, null, null); + if (c != null) { + boolean loopCheck = c.moveToFirst(); + while (loopCheck) { + int id = c.getInt(0); + String name = (c.isNull(1)) ? null : c.getString(1); + byte[] b = (c.isNull(2)) ? null : c.getBlob(2); + String json = new String(b); + + ImagePreset preset = new ImagePreset(); + preset.readJsonFromString(json); + FilterUserPresetRepresentation representation = + new FilterUserPresetRepresentation(name, preset, id); + ret.add(representation); + loopCheck = c.moveToNext(); + } + } + database.setTransactionSuccessful(); + } finally { + if (c != null) { + c.close(); + } + database.endTransaction(); + } + + return ret; + } + + public List<Pair<String, byte[]>> getAllStacks() { + List<Pair<String, byte[]>> ret = new ArrayList<Pair<String, byte[]>>(); + Cursor c = null; + database.beginTransaction(); + try { + c = database.query(FilterStack.TABLE, + new String[] { FilterStack.STACK_ID, FilterStack.FILTER_STACK }, + null, null, null, null, null, null); + if (c != null) { + boolean loopCheck = c.moveToFirst(); + while (loopCheck) { + String name = (c.isNull(0)) ? null : c.getString(0); + byte[] b = (c.isNull(1)) ? null : c.getBlob(1); + ret.add(new Pair<String, byte[]>(name, b)); + loopCheck = c.moveToNext(); + } + } + database.setTransactionSuccessful(); + } finally { + if (c != null) { + c.close(); + } + database.endTransaction(); + } + if (ret.size() <= 0) { + return null; + } + return ret; + } +} diff --git a/src/com/android/gallery3d/filtershow/data/UserPresetsManager.java b/src/com/android/gallery3d/filtershow/data/UserPresetsManager.java new file mode 100644 index 000000000..114cd3ebc --- /dev/null +++ b/src/com/android/gallery3d/filtershow/data/UserPresetsManager.java @@ -0,0 +1,149 @@ +package com.android.gallery3d.filtershow.data; + +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Message; +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.FilterShowActivity; +import com.android.gallery3d.filtershow.filters.FilterUserPresetRepresentation; +import com.android.gallery3d.filtershow.pipeline.ImagePreset; + +import java.util.ArrayList; + +public class UserPresetsManager implements Handler.Callback { + + private static final String LOGTAG = "UserPresetsManager"; + + private FilterShowActivity mActivity; + private HandlerThread mHandlerThread = null; + private Handler mProcessingHandler = null; + private FilterStackSource mUserPresets; + + private static final int LOAD = 1; + private static final int LOAD_RESULT = 2; + private static final int SAVE = 3; + private static final int DELETE = 4; + private static final int UPDATE = 5; + + private ArrayList<FilterUserPresetRepresentation> mRepresentations; + + private final Handler mResultHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case LOAD_RESULT: + resultLoad(msg); + break; + } + } + }; + + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + case LOAD: + processLoad(); + return true; + case SAVE: + processSave(msg); + return true; + case DELETE: + processDelete(msg); + return true; + case UPDATE: + processUpdate(msg); + return true; + } + return false; + } + + public UserPresetsManager(FilterShowActivity context) { + mActivity = context; + mHandlerThread = new HandlerThread(LOGTAG, + android.os.Process.THREAD_PRIORITY_BACKGROUND); + mHandlerThread.start(); + mProcessingHandler = new Handler(mHandlerThread.getLooper(), this); + mUserPresets = new FilterStackSource(mActivity); + mUserPresets.open(); + } + + public ArrayList<FilterUserPresetRepresentation> getRepresentations() { + return mRepresentations; + } + + public void load() { + Message msg = mProcessingHandler.obtainMessage(LOAD); + mProcessingHandler.sendMessage(msg); + } + + public void close() { + mUserPresets.close(); + mHandlerThread.quit(); + } + + static class SaveOperation { + String json; + String name; + } + + public void save(ImagePreset preset) { + Message msg = mProcessingHandler.obtainMessage(SAVE); + SaveOperation op = new SaveOperation(); + op.json = preset.getJsonString(mActivity.getString(R.string.saved)); + op.name= mActivity.getString(R.string.filtershow_new_preset); + msg.obj = op; + mProcessingHandler.sendMessage(msg); + } + + public void delete(int id) { + Message msg = mProcessingHandler.obtainMessage(DELETE); + msg.arg1 = id; + mProcessingHandler.sendMessage(msg); + } + + static class UpdateOperation { + int id; + String name; + } + + public void update(FilterUserPresetRepresentation representation) { + Message msg = mProcessingHandler.obtainMessage(UPDATE); + UpdateOperation op = new UpdateOperation(); + op.id = representation.getId(); + op.name = representation.getName(); + msg.obj = op; + mProcessingHandler.sendMessage(msg); + } + + private void processLoad() { + ArrayList<FilterUserPresetRepresentation> list = mUserPresets.getAllUserPresets(); + Message msg = mResultHandler.obtainMessage(LOAD_RESULT); + msg.obj = list; + mResultHandler.sendMessage(msg); + } + + private void resultLoad(Message msg) { + mRepresentations = + (ArrayList<FilterUserPresetRepresentation>) msg.obj; + mActivity.updateUserPresetsFromManager(); + } + + private void processSave(Message msg) { + SaveOperation op = (SaveOperation) msg.obj; + mUserPresets.insertStack(op.name, op.json.getBytes()); + processLoad(); + } + + private void processDelete(Message msg) { + int id = msg.arg1; + mUserPresets.removeStack(id); + processLoad(); + } + + private void processUpdate(Message msg) { + UpdateOperation op = (UpdateOperation) msg.obj; + mUserPresets.updateStackName(op.id, op.name); + processLoad(); + } + +} diff --git a/src/com/android/gallery3d/filtershow/editors/BasicEditor.java b/src/com/android/gallery3d/filtershow/editors/BasicEditor.java new file mode 100644 index 000000000..af694d811 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/editors/BasicEditor.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2013 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.filtershow.editors; + +import android.content.Context; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.controller.Control; +import com.android.gallery3d.filtershow.controller.FilterView; +import com.android.gallery3d.filtershow.controller.Parameter; +import com.android.gallery3d.filtershow.controller.ParameterInteger; +import com.android.gallery3d.filtershow.filters.FilterBasicRepresentation; +import com.android.gallery3d.filtershow.filters.FilterRepresentation; + + +/** + * The basic editor that all the one parameter filters + */ +public class BasicEditor extends ParametricEditor implements ParameterInteger { + public static int ID = R.id.basicEditor; + private final String LOGTAG = "BasicEditor"; + + public BasicEditor() { + super(ID, R.layout.filtershow_default_editor, R.id.basicEditor); + } + + protected BasicEditor(int id) { + super(id, R.layout.filtershow_default_editor, R.id.basicEditor); + } + + protected BasicEditor(int id, int layoutID, int viewID) { + super(id, layoutID, viewID); + } + + @Override + public void reflectCurrentFilter() { + super.reflectCurrentFilter(); + if (getLocalRepresentation() != null && getLocalRepresentation() instanceof FilterBasicRepresentation) { + FilterBasicRepresentation interval = (FilterBasicRepresentation) getLocalRepresentation(); + updateText(); + } + } + + private FilterBasicRepresentation getBasicRepresentation() { + FilterRepresentation tmpRep = getLocalRepresentation(); + if (tmpRep != null && tmpRep instanceof FilterBasicRepresentation) { + return (FilterBasicRepresentation) tmpRep; + + } + return null; + } + + @Override + public int getMaximum() { + FilterBasicRepresentation rep = getBasicRepresentation(); + if (rep == null) { + return 0; + } + return rep.getMaximum(); + } + + @Override + public int getMinimum() { + FilterBasicRepresentation rep = getBasicRepresentation(); + if (rep == null) { + return 0; + } + return rep.getMinimum(); + } + + @Override + public int getDefaultValue() { + return 0; + } + + @Override + public int getValue() { + FilterBasicRepresentation rep = getBasicRepresentation(); + if (rep == null) { + return 0; + } + return rep.getValue(); + } + + @Override + public String getValueString() { + return null; + } + + @Override + public void setValue(int value) { + FilterBasicRepresentation rep = getBasicRepresentation(); + if (rep == null) { + return; + } + rep.setValue(value); + commitLocalRepresentation(); + } + + @Override + public String getParameterName() { + FilterBasicRepresentation rep = getBasicRepresentation(); + return mContext.getString(rep.getTextId()); + } + + @Override + public String getParameterType() { + return sParameterType; + } + + @Override + public void setController(Control c) { + } + + @Override + public void setFilterView(FilterView editor) { + + } + + @Override + public void copyFrom(Parameter src) { + + } +} diff --git a/src/com/android/gallery3d/filtershow/editors/Editor.java b/src/com/android/gallery3d/filtershow/editors/Editor.java new file mode 100644 index 000000000..a9e56e0c1 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/editors/Editor.java @@ -0,0 +1,330 @@ +/* + * Copyright (C) 2012 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.filtershow.editors; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.PopupMenu; +import android.widget.SeekBar; +import android.widget.SeekBar.OnSeekBarChangeListener; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.controller.Control; +import com.android.gallery3d.filtershow.filters.FilterRepresentation; +import com.android.gallery3d.filtershow.imageshow.ImageShow; +import com.android.gallery3d.filtershow.imageshow.MasterImage; +import com.android.gallery3d.filtershow.pipeline.ImagePreset; + +import java.util.ArrayList; +import java.util.Collection; + +/** + * Base class for Editors Must contain a mImageShow and a top level view + */ +public class Editor implements OnSeekBarChangeListener, SwapButton.SwapButtonListener { + protected Context mContext; + protected View mView; + protected ImageShow mImageShow; + protected FrameLayout mFrameLayout; + protected SeekBar mSeekBar; + Button mEditTitle; + protected Button mFilterTitle; + protected int mID; + private final String LOGTAG = "Editor"; + protected boolean mChangesGeometry = false; + protected FilterRepresentation mLocalRepresentation = null; + protected byte mShowParameter = SHOW_VALUE_UNDEFINED; + private Button mButton; + public static byte SHOW_VALUE_UNDEFINED = -1; + public static byte SHOW_VALUE_OFF = 0; + public static byte SHOW_VALUE_INT = 1; + + public static void hackFixStrings(Menu menu) { + int count = menu.size(); + for (int i = 0; i < count; i++) { + MenuItem item = menu.getItem(i); + item.setTitle(item.getTitle().toString().toUpperCase()); + } + } + + public String calculateUserMessage(Context context, String effectName, Object parameterValue) { + return effectName.toUpperCase() + " " + parameterValue; + } + + protected Editor(int id) { + mID = id; + } + + public int getID() { + return mID; + } + + public byte showParameterValue() { + return mShowParameter; + } + + public boolean showsSeekBar() { + return true; + } + + public void setUpEditorUI(View actionButton, View editControl, + Button editTitle, Button stateButton) { + mEditTitle = editTitle; + mFilterTitle = stateButton; + mButton = editTitle; + setMenuIcon(true); + setUtilityPanelUI(actionButton, editControl); + } + + public boolean showsPopupIndicator() { + return true; + } + + /** + * @param actionButton the would be the area for menu etc + * @param editControl this is the black area for sliders etc + */ + public void setUtilityPanelUI(View actionButton, View editControl) { + + AttributeSet aset; + Context context = editControl.getContext(); + LayoutInflater inflater = + (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + LinearLayout lp = (LinearLayout) inflater.inflate( + R.layout.filtershow_seekbar, (ViewGroup) editControl, true); + mSeekBar = (SeekBar) lp.findViewById(R.id.primarySeekBar); + mSeekBar.setOnSeekBarChangeListener(this); + + if (showsSeekBar()) { + mSeekBar.setOnSeekBarChangeListener(this); + mSeekBar.setVisibility(View.VISIBLE); + } else { + mSeekBar.setVisibility(View.INVISIBLE); + } + + if (mButton != null) { + if (showsPopupIndicator()) { + mButton.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, + R.drawable.filtershow_menu_marker, 0); + } else { + mButton.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0); + } + } + } + + @Override + public void onProgressChanged(SeekBar sbar, int progress, boolean arg2) { + + } + + public void setPanel() { + + } + + public void createEditor(Context context,FrameLayout frameLayout) { + mContext = context; + mFrameLayout = frameLayout; + mLocalRepresentation = null; + } + + protected void unpack(int viewid, int layoutid) { + + if (mView == null) { + mView = mFrameLayout.findViewById(viewid); + if (mView == null) { + LayoutInflater inflater = (LayoutInflater) mContext.getSystemService + (Context.LAYOUT_INFLATER_SERVICE); + mView = inflater.inflate(layoutid, mFrameLayout, false); + mFrameLayout.addView(mView, mView.getLayoutParams()); + } + } + mImageShow = findImageShow(mView); + } + + private ImageShow findImageShow(View view) { + if (view instanceof ImageShow) { + return (ImageShow) view; + } + if (!(view instanceof ViewGroup)) { + return null; + } + ViewGroup vg = (ViewGroup) view; + int n = vg.getChildCount(); + for (int i = 0; i < n; i++) { + View v = vg.getChildAt(i); + if (v instanceof ImageShow) { + return (ImageShow) v; + } else if (v instanceof ViewGroup) { + return findImageShow(v); + } + } + return null; + } + + public View getTopLevelView() { + return mView; + } + + public ImageShow getImageShow() { + return mImageShow; + } + + public void setVisibility(int visible) { + mView.setVisibility(visible); + } + + public FilterRepresentation getLocalRepresentation() { + if (mLocalRepresentation == null) { + ImagePreset preset = MasterImage.getImage().getPreset(); + FilterRepresentation filterRepresentation = MasterImage.getImage().getCurrentFilterRepresentation(); + mLocalRepresentation = preset.getFilterRepresentationCopyFrom(filterRepresentation); + if (mShowParameter == SHOW_VALUE_UNDEFINED && filterRepresentation != null) { + boolean show = filterRepresentation.showParameterValue(); + mShowParameter = show ? SHOW_VALUE_INT : SHOW_VALUE_OFF; + } + + } + return mLocalRepresentation; + } + + /** + * Call this to update the preset in MasterImage with the current representation + * returned by getLocalRepresentation. This causes the preview bitmap to be + * regenerated. + */ + public void commitLocalRepresentation() { + commitLocalRepresentation(getLocalRepresentation()); + } + + /** + * Call this to update the preset in MasterImage with a given representation. + * This causes the preview bitmap to be regenerated. + */ + public void commitLocalRepresentation(FilterRepresentation rep) { + ArrayList<FilterRepresentation> filter = new ArrayList<FilterRepresentation>(1); + filter.add(rep); + commitLocalRepresentation(filter); + } + + /** + * Call this to update the preset in MasterImage with a collection of FilterRepresnations. + * This causes the preview bitmap to be regenerated. + */ + public void commitLocalRepresentation(Collection<FilterRepresentation> reps) { + ImagePreset preset = MasterImage.getImage().getPreset(); + preset.updateFilterRepresentations(reps); + if (mButton != null) { + updateText(); + } + if (mChangesGeometry) { + // Regenerate both the filtered and the geometry-only bitmaps + MasterImage.getImage().updatePresets(true); + } else { + // Regenerate only the filtered bitmap. + MasterImage.getImage().invalidateFiltersOnly(); + } + preset.fillImageStateAdapter(MasterImage.getImage().getState()); + } + + /** + * This is called in response to a click to apply and leave the editor. + */ + public void finalApplyCalled() { + commitLocalRepresentation(); + } + + protected void updateText() { + String s = ""; + if (mLocalRepresentation != null) { + s = mContext.getString(mLocalRepresentation.getTextId()); + } + mButton.setText(calculateUserMessage(mContext, s, "")); + } + + /** + * called after the filter is set and the select is called + */ + public void reflectCurrentFilter() { + mLocalRepresentation = null; + FilterRepresentation representation = getLocalRepresentation(); + if (representation != null && mFilterTitle != null && representation.getTextId() != 0) { + String text = mContext.getString(representation.getTextId()).toUpperCase(); + mFilterTitle.setText(text); + updateText(); + } + } + + public boolean useUtilityPanel() { + return true; + } + + public void openUtilityPanel(LinearLayout mAccessoryViewList) { + setMenuIcon(false); + if (mImageShow != null) { + mImageShow.openUtilityPanel(mAccessoryViewList); + } + } + + protected void setMenuIcon(boolean on) { + mEditTitle.setCompoundDrawablesRelativeWithIntrinsicBounds( + 0, 0, on ? R.drawable.filtershow_menu_marker : 0, 0); + } + + protected void createMenu(int[] strId, View button) { + PopupMenu pmenu = new PopupMenu(mContext, button); + Menu menu = pmenu.getMenu(); + for (int i = 0; i < strId.length; i++) { + menu.add(Menu.NONE, Menu.FIRST + i, 0, mContext.getString(strId[i])); + } + setMenuIcon(true); + + } + + public Control[] getControls() { + return null; + } + @Override + public void onStartTrackingTouch(SeekBar arg0) { + + } + + @Override + public void onStopTrackingTouch(SeekBar arg0) { + + } + + @Override + public void swapLeft(MenuItem item) { + + } + + @Override + public void swapRight(MenuItem item) { + + } + + public void detach() { + } +} diff --git a/src/com/android/gallery3d/filtershow/editors/EditorChanSat.java b/src/com/android/gallery3d/filtershow/editors/EditorChanSat.java new file mode 100644 index 000000000..7e31f09ae --- /dev/null +++ b/src/com/android/gallery3d/filtershow/editors/EditorChanSat.java @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2013 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.filtershow.editors; + +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Handler; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.LinearLayout; +import android.widget.PopupMenu; +import android.widget.SeekBar.OnSeekBarChangeListener; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.controller.BasicParameterStyle; +import com.android.gallery3d.filtershow.controller.FilterView; +import com.android.gallery3d.filtershow.controller.Parameter; +import com.android.gallery3d.filtershow.filters.FilterChanSatRepresentation; +import com.android.gallery3d.filtershow.filters.FilterRepresentation; +import com.android.gallery3d.filtershow.imageshow.MasterImage; +import com.android.gallery3d.filtershow.pipeline.ImagePreset; +import com.android.gallery3d.filtershow.pipeline.RenderingRequest; +import com.android.gallery3d.filtershow.pipeline.RenderingRequestCaller; + +public class EditorChanSat extends ParametricEditor implements OnSeekBarChangeListener, FilterView { + public static final int ID = R.id.editorChanSat; + private final String LOGTAG = "EditorGrunge"; + private SwapButton mButton; + private final Handler mHandler = new Handler(); + + int[] mMenuStrings = { + R.string.editor_chan_sat_main, + R.string.editor_chan_sat_red, + R.string.editor_chan_sat_yellow, + R.string.editor_chan_sat_green, + R.string.editor_chan_sat_cyan, + R.string.editor_chan_sat_blue, + R.string.editor_chan_sat_magenta + }; + + String mCurrentlyEditing = null; + + public EditorChanSat() { + super(ID, R.layout.filtershow_default_editor, R.id.basicEditor); + } + + @Override + public String calculateUserMessage(Context context, String effectName, Object parameterValue) { + FilterRepresentation rep = getLocalRepresentation(); + if (rep == null || !(rep instanceof FilterChanSatRepresentation)) { + return ""; + } + FilterChanSatRepresentation csrep = (FilterChanSatRepresentation) rep; + int mode = csrep.getParameterMode(); + String paramString; + + paramString = mContext.getString(mMenuStrings[mode]); + + int val = csrep.getCurrentParameter(); + return paramString + ((val > 0) ? " +" : " ") + val; + } + + @Override + public void openUtilityPanel(final LinearLayout accessoryViewList) { + mButton = (SwapButton) accessoryViewList.findViewById(R.id.applyEffect); + mButton.setText(mContext.getString(R.string.editor_chan_sat_main)); + + final PopupMenu popupMenu = new PopupMenu(mImageShow.getActivity(), mButton); + + popupMenu.getMenuInflater().inflate(R.menu.filtershow_menu_chan_sat, popupMenu.getMenu()); + + popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + selectMenuItem(item); + return true; + } + }); + mButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View arg0) { + popupMenu.show(); + } + }); + mButton.setListener(this); + + FilterChanSatRepresentation csrep = getChanSatRep(); + String menuString = mContext.getString(mMenuStrings[0]); + switchToMode(csrep, FilterChanSatRepresentation.MODE_MASTER, menuString); + + } + + public int getParameterIndex(int id) { + switch (id) { + case R.id.editor_chan_sat_main: + return FilterChanSatRepresentation.MODE_MASTER; + case R.id.editor_chan_sat_red: + return FilterChanSatRepresentation.MODE_RED; + case R.id.editor_chan_sat_yellow: + return FilterChanSatRepresentation.MODE_YELLOW; + case R.id.editor_chan_sat_green: + return FilterChanSatRepresentation.MODE_GREEN; + case R.id.editor_chan_sat_cyan: + return FilterChanSatRepresentation.MODE_CYAN; + case R.id.editor_chan_sat_blue: + return FilterChanSatRepresentation.MODE_BLUE; + case R.id.editor_chan_sat_magenta: + return FilterChanSatRepresentation.MODE_MAGENTA; + } + return -1; + } + + @Override + public void detach() { + mButton.setListener(null); + mButton.setOnClickListener(null); + } + + private void updateSeekBar(FilterChanSatRepresentation rep) { + mControl.updateUI(); + } + + @Override + protected Parameter getParameterToEdit(FilterRepresentation rep) { + if (rep instanceof FilterChanSatRepresentation) { + FilterChanSatRepresentation csrep = (FilterChanSatRepresentation) rep; + Parameter param = csrep.getFilterParameter(csrep.getParameterMode()); + if (param instanceof BasicParameterStyle) { + param.setFilterView(EditorChanSat.this); + } + return param; + } + return null; + } + + private FilterChanSatRepresentation getChanSatRep() { + FilterRepresentation rep = getLocalRepresentation(); + if (rep != null + && rep instanceof FilterChanSatRepresentation) { + FilterChanSatRepresentation csrep = (FilterChanSatRepresentation) rep; + return csrep; + } + return null; + } + + @Override + public void computeIcon(int n, RenderingRequestCaller caller) { + FilterChanSatRepresentation rep = getChanSatRep(); + if (rep == null) return; + rep = (FilterChanSatRepresentation) rep.copy(); + ImagePreset preset = new ImagePreset(); + preset.addFilter(rep); + Bitmap src = MasterImage.getImage().getThumbnailBitmap(); + RenderingRequest.post(null, src, preset, RenderingRequest.STYLE_ICON_RENDERING, + caller); + } + + protected void selectMenuItem(MenuItem item) { + if (getLocalRepresentation() != null + && getLocalRepresentation() instanceof FilterChanSatRepresentation) { + FilterChanSatRepresentation csrep = + (FilterChanSatRepresentation) getLocalRepresentation(); + + switchToMode(csrep, getParameterIndex(item.getItemId()), item.getTitle().toString()); + + } + } + + protected void switchToMode(FilterChanSatRepresentation csrep, int mode, String title) { + csrep.setParameterMode(mode); + mCurrentlyEditing = title; + mButton.setText(mCurrentlyEditing); + { + Parameter param = getParameterToEdit(csrep); + + control(param, mEditControl); + } + updateSeekBar(csrep); + mView.invalidate(); + } + + @Override + public void swapLeft(MenuItem item) { + super.swapLeft(item); + mButton.setTranslationX(0); + mButton.animate().translationX(mButton.getWidth()).setDuration(SwapButton.ANIM_DURATION); + Runnable updateButton = new Runnable() { + @Override + public void run() { + mButton.animate().cancel(); + mButton.setTranslationX(0); + } + }; + mHandler.postDelayed(updateButton, SwapButton.ANIM_DURATION); + selectMenuItem(item); + } + + @Override + public void swapRight(MenuItem item) { + super.swapRight(item); + mButton.setTranslationX(0); + mButton.animate().translationX(-mButton.getWidth()).setDuration(SwapButton.ANIM_DURATION); + Runnable updateButton = new Runnable() { + @Override + public void run() { + mButton.animate().cancel(); + mButton.setTranslationX(0); + } + }; + mHandler.postDelayed(updateButton, SwapButton.ANIM_DURATION); + selectMenuItem(item); + } +} diff --git a/src/com/android/gallery3d/filtershow/editors/EditorCrop.java b/src/com/android/gallery3d/filtershow/editors/EditorCrop.java new file mode 100644 index 000000000..511d4ff87 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/editors/EditorCrop.java @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2013 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.filtershow.editors; + +import android.content.Context; +import android.util.Log; +import android.util.SparseArray; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.PopupMenu; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.filters.FilterCropRepresentation; +import com.android.gallery3d.filtershow.filters.FilterRepresentation; +import com.android.gallery3d.filtershow.imageshow.ImageCrop; +import com.android.gallery3d.filtershow.imageshow.MasterImage; + +public class EditorCrop extends Editor implements EditorInfo { + public static final String TAG = EditorCrop.class.getSimpleName(); + public static final int ID = R.id.editorCrop; + + // Holder for an aspect ratio it's string id + protected static final class AspectInfo { + int mAspectX; + int mAspectY; + int mStringId; + AspectInfo(int stringID, int x, int y) { + mStringId = stringID; + mAspectX = x; + mAspectY = y; + } + }; + + // Mapping from menu id to aspect ratio + protected static final SparseArray<AspectInfo> sAspects; + static { + sAspects = new SparseArray<AspectInfo>(); + sAspects.put(R.id.crop_menu_1to1, new AspectInfo(R.string.aspect1to1_effect, 1, 1)); + sAspects.put(R.id.crop_menu_4to3, new AspectInfo(R.string.aspect4to3_effect, 4, 3)); + sAspects.put(R.id.crop_menu_3to4, new AspectInfo(R.string.aspect3to4_effect, 3, 4)); + sAspects.put(R.id.crop_menu_5to7, new AspectInfo(R.string.aspect5to7_effect, 5, 7)); + sAspects.put(R.id.crop_menu_7to5, new AspectInfo(R.string.aspect7to5_effect, 7, 5)); + sAspects.put(R.id.crop_menu_none, new AspectInfo(R.string.aspectNone_effect, 0, 0)); + sAspects.put(R.id.crop_menu_original, new AspectInfo(R.string.aspectOriginal_effect, 0, 0)); + } + + protected ImageCrop mImageCrop; + private String mAspectString = ""; + + public EditorCrop() { + super(ID); + mChangesGeometry = true; + } + + @Override + public void createEditor(Context context, FrameLayout frameLayout) { + super.createEditor(context, frameLayout); + if (mImageCrop == null) { + mImageCrop = new ImageCrop(context); + } + mView = mImageShow = mImageCrop; + mImageCrop.setEditor(this); + } + + @Override + public void reflectCurrentFilter() { + MasterImage master = MasterImage.getImage(); + master.setCurrentFilterRepresentation(master.getPreset() + .getFilterWithSerializationName(FilterCropRepresentation.SERIALIZATION_NAME)); + super.reflectCurrentFilter(); + FilterRepresentation rep = getLocalRepresentation(); + if (rep == null || rep instanceof FilterCropRepresentation) { + mImageCrop.setFilterCropRepresentation((FilterCropRepresentation) rep); + } else { + Log.w(TAG, "Could not reflect current filter, not of type: " + + FilterCropRepresentation.class.getSimpleName()); + } + mImageCrop.invalidate(); + } + + @Override + public void finalApplyCalled() { + commitLocalRepresentation(mImageCrop.getFinalRepresentation()); + } + + @Override + public void openUtilityPanel(final LinearLayout accessoryViewList) { + Button view = (Button) accessoryViewList.findViewById(R.id.applyEffect); + view.setText(mContext.getString(R.string.crop)); + view.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View arg0) { + showPopupMenu(accessoryViewList); + } + }); + } + + private void changeCropAspect(int itemId) { + AspectInfo info = sAspects.get(itemId); + if (info == null) { + throw new IllegalArgumentException("Invalid resource ID: " + itemId); + } + if (itemId == R.id.crop_menu_original) { + mImageCrop.applyOriginalAspect(); + } else if (itemId == R.id.crop_menu_none) { + mImageCrop.applyFreeAspect(); + } else { + mImageCrop.applyAspect(info.mAspectX, info.mAspectY); + } + setAspectString(mContext.getString(info.mStringId)); + } + + private void showPopupMenu(LinearLayout accessoryViewList) { + final Button button = (Button) accessoryViewList.findViewById(R.id.applyEffect); + final PopupMenu popupMenu = new PopupMenu(mImageShow.getActivity(), button); + popupMenu.getMenuInflater().inflate(R.menu.filtershow_menu_crop, popupMenu.getMenu()); + popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + changeCropAspect(item.getItemId()); + return true; + } + }); + popupMenu.show(); + } + + @Override + public boolean showsSeekBar() { + return false; + } + + @Override + public int getTextId() { + return R.string.crop; + } + + @Override + public int getOverlayId() { + return R.drawable.filtershow_button_geometry_crop; + } + + @Override + public boolean getOverlayOnly() { + return true; + } + + private void setAspectString(String s) { + mAspectString = s; + } +} diff --git a/src/com/android/gallery3d/filtershow/editors/EditorCurves.java b/src/com/android/gallery3d/filtershow/editors/EditorCurves.java new file mode 100644 index 000000000..83fbced79 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/editors/EditorCurves.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2012 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.filtershow.editors; + +import android.content.Context; +import android.widget.FrameLayout; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.filters.FilterCurvesRepresentation; +import com.android.gallery3d.filtershow.filters.FilterRepresentation; +import com.android.gallery3d.filtershow.imageshow.ImageCurves; + +public class EditorCurves extends Editor { + public static final int ID = R.id.imageCurves; + ImageCurves mImageCurves; + + public EditorCurves() { + super(ID); + } + + @Override + public void createEditor(Context context, FrameLayout frameLayout) { + super.createEditor(context, frameLayout); + mView = mImageShow = mImageCurves = new ImageCurves(context); + mImageCurves.setEditor(this); + } + + @Override + public void reflectCurrentFilter() { + super.reflectCurrentFilter(); + FilterRepresentation rep = getLocalRepresentation(); + if (rep != null && getLocalRepresentation() instanceof FilterCurvesRepresentation) { + FilterCurvesRepresentation drawRep = (FilterCurvesRepresentation) rep; + mImageCurves.setFilterDrawRepresentation(drawRep); + } + } + + @Override + public boolean showsSeekBar() { + return false; + } +} diff --git a/src/com/android/gallery3d/filtershow/editors/EditorDraw.java b/src/com/android/gallery3d/filtershow/editors/EditorDraw.java new file mode 100644 index 000000000..4b09051e2 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/editors/EditorDraw.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2013 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.filtershow.editors; + +import android.app.Dialog; +import android.content.Context; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.WindowManager.LayoutParams; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.PopupMenu; +import android.widget.SeekBar; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.FilterShowActivity; +import com.android.gallery3d.filtershow.colorpicker.ColorGridDialog; +import com.android.gallery3d.filtershow.colorpicker.RGBListener; +import com.android.gallery3d.filtershow.filters.FilterDrawRepresentation; +import com.android.gallery3d.filtershow.filters.FilterRepresentation; +import com.android.gallery3d.filtershow.filters.ImageFilterDraw; +import com.android.gallery3d.filtershow.imageshow.ImageDraw; + +public class EditorDraw extends Editor { + private static final String LOGTAG = "EditorDraw"; + public static final int ID = R.id.editorDraw; + public ImageDraw mImageDraw; + + public EditorDraw() { + super(ID); + } + + @Override + public void createEditor(Context context, FrameLayout frameLayout) { + super.createEditor(context, frameLayout); + mView = mImageShow = mImageDraw = new ImageDraw(context); + mImageDraw.setEditor(this); + + } + + @Override + public void reflectCurrentFilter() { + super.reflectCurrentFilter(); + FilterRepresentation rep = getLocalRepresentation(); + + if (rep != null && getLocalRepresentation() instanceof FilterDrawRepresentation) { + FilterDrawRepresentation drawRep = (FilterDrawRepresentation) getLocalRepresentation(); + mImageDraw.setFilterDrawRepresentation(drawRep); + } + } + + @Override + public void openUtilityPanel(final LinearLayout accessoryViewList) { + Button view = (Button) accessoryViewList.findViewById(R.id.applyEffect); + view.setText(mContext.getString(R.string.draw_style)); + view.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View arg0) { + showPopupMenu(accessoryViewList); + } + }); + } + + @Override + public boolean showsSeekBar() { + return false; + } + + private void showPopupMenu(LinearLayout accessoryViewList) { + final Button button = (Button) accessoryViewList.findViewById( + R.id.applyEffect); + if (button == null) { + return; + } + final PopupMenu popupMenu = new PopupMenu(mImageShow.getActivity(), button); + popupMenu.getMenuInflater().inflate(R.menu.filtershow_menu_draw, popupMenu.getMenu()); + popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + + @Override + public boolean onMenuItemClick(MenuItem item) { + ImageFilterDraw filter = (ImageFilterDraw) mImageShow.getCurrentFilter(); + if (item.getItemId() == R.id.draw_menu_color) { + showColorGrid(item); + } else if (item.getItemId() == R.id.draw_menu_size) { + showSizeDialog(item); + } else if (item.getItemId() == R.id.draw_menu_style_brush_marker) { + ImageDraw idraw = (ImageDraw) mImageShow; + idraw.setStyle(ImageFilterDraw.BRUSH_STYLE_MARKER); + } else if (item.getItemId() == R.id.draw_menu_style_brush_spatter) { + ImageDraw idraw = (ImageDraw) mImageShow; + idraw.setStyle(ImageFilterDraw.BRUSH_STYLE_SPATTER); + } else if (item.getItemId() == R.id.draw_menu_style_line) { + ImageDraw idraw = (ImageDraw) mImageShow; + idraw.setStyle(ImageFilterDraw.SIMPLE_STYLE); + } else if (item.getItemId() == R.id.draw_menu_clear) { + ImageDraw idraw = (ImageDraw) mImageShow; + idraw.resetParameter(); + commitLocalRepresentation(); + } + mView.invalidate(); + return true; + } + }); + popupMenu.show(); + } + + public void showSizeDialog(final MenuItem item) { + FilterShowActivity ctx = mImageShow.getActivity(); + final Dialog dialog = new Dialog(ctx); + dialog.setTitle(R.string.draw_size_title); + dialog.setContentView(R.layout.filtershow_draw_size); + final SeekBar bar = (SeekBar) dialog.findViewById(R.id.sizeSeekBar); + ImageDraw idraw = (ImageDraw) mImageShow; + bar.setProgress(idraw.getSize()); + Button button = (Button) dialog.findViewById(R.id.sizeAcceptButton); + button.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View arg0) { + int p = bar.getProgress(); + ImageDraw idraw = (ImageDraw) mImageShow; + idraw.setSize(p + 1); + dialog.dismiss(); + } + }); + dialog.show(); + } + + public void showColorGrid(final MenuItem item) { + RGBListener cl = new RGBListener() { + @Override + public void setColor(int rgb) { + ImageDraw idraw = (ImageDraw) mImageShow; + idraw.setColor(rgb); + } + }; + ColorGridDialog cpd = new ColorGridDialog(mImageShow.getActivity(), cl); + cpd.show(); + LayoutParams params = cpd.getWindow().getAttributes(); + } +} diff --git a/src/com/android/gallery3d/filtershow/editors/EditorGrad.java b/src/com/android/gallery3d/filtershow/editors/EditorGrad.java new file mode 100644 index 000000000..f427ccbd8 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/editors/EditorGrad.java @@ -0,0 +1,315 @@ +/* + * Copyright (C) 2013 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.filtershow.editors; + +import android.content.Context; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.PopupMenu; +import android.widget.SeekBar; +import android.widget.SeekBar.OnSeekBarChangeListener; +import android.widget.ToggleButton; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.controller.Control; +import com.android.gallery3d.filtershow.controller.FilterView; +import com.android.gallery3d.filtershow.controller.Parameter; +import com.android.gallery3d.filtershow.controller.ParameterActionAndInt; +import com.android.gallery3d.filtershow.filters.FilterGradRepresentation; +import com.android.gallery3d.filtershow.filters.FilterRepresentation; +import com.android.gallery3d.filtershow.imageshow.ImageGrad; +import com.android.gallery3d.filtershow.imageshow.MasterImage; + +public class EditorGrad extends ParametricEditor + implements OnSeekBarChangeListener, ParameterActionAndInt { + private static final String LOGTAG = "EditorGrad"; + public static final int ID = R.id.editorGrad; + PopupMenu mPopupMenu; + ToggleButton mAddModeButton; + String mEffectName = ""; + private static final int MODE_BRIGHTNESS = FilterGradRepresentation.PARAM_BRIGHTNESS; + private static final int MODE_SATURATION = FilterGradRepresentation.PARAM_SATURATION; + private static final int MODE_CONTRAST = FilterGradRepresentation.PARAM_CONTRAST; + private static final int ADD_ICON = R.drawable.ic_grad_add; + private static final int DEL_ICON = R.drawable.ic_grad_del; + private int mSliderMode = MODE_BRIGHTNESS; + ImageGrad mImageGrad; + + public EditorGrad() { + super(ID, R.layout.filtershow_grad_editor, R.id.gradEditor); + } + + @Override + public void createEditor(Context context, FrameLayout frameLayout) { + super.createEditor(context, frameLayout); + mImageGrad = (ImageGrad) mImageShow; + mImageGrad.setEditor(this); + + } + + public void clearAddMode() { + mAddModeButton.setChecked(false); + FilterRepresentation tmpRep = getLocalRepresentation(); + if (tmpRep instanceof FilterGradRepresentation) { + updateMenuItems((FilterGradRepresentation) tmpRep); + } + } + + @Override + public void reflectCurrentFilter() { + super.reflectCurrentFilter(); + FilterRepresentation tmpRep = getLocalRepresentation(); + if (tmpRep instanceof FilterGradRepresentation) { + FilterGradRepresentation rep = (FilterGradRepresentation) tmpRep; + boolean f = rep.showParameterValue(); + + mImageGrad.setRepresentation(rep); + } + } + + public void updateSeekBar(FilterGradRepresentation rep) { + mControl.updateUI(); + } + + @Override + public void onProgressChanged(SeekBar sbar, int progress, boolean arg2) { + FilterRepresentation tmpRep = getLocalRepresentation(); + if (tmpRep instanceof FilterGradRepresentation) { + FilterGradRepresentation rep = (FilterGradRepresentation) tmpRep; + int min = rep.getParameterMin(mSliderMode); + int value = progress + min; + rep.setParameter(mSliderMode, value); + mView.invalidate(); + commitLocalRepresentation(); + } + } + + @Override + public void openUtilityPanel(final LinearLayout accessoryViewList) { + Button view = (Button) accessoryViewList.findViewById(R.id.applyEffect); + view.setText(mContext.getString(R.string.editor_grad_brightness)); + view.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View arg0) { + showPopupMenu(accessoryViewList); + } + }); + + setUpPopupMenu(view); + setEffectName(); + } + + private void updateMenuItems(FilterGradRepresentation rep) { + int n = rep.getNumberOfBands(); + } + + public void setEffectName() { + if (mPopupMenu != null) { + MenuItem item = mPopupMenu.getMenu().findItem(R.id.editor_grad_brightness); + mEffectName = item.getTitle().toString(); + } + } + + private void showPopupMenu(LinearLayout accessoryViewList) { + Button button = (Button) accessoryViewList.findViewById(R.id.applyEffect); + if (button == null) { + return; + } + + if (mPopupMenu == null) { + setUpPopupMenu(button); + } + mPopupMenu.show(); + } + + private void setUpPopupMenu(Button button) { + mPopupMenu = new PopupMenu(mImageShow.getActivity(), button); + mPopupMenu.getMenuInflater() + .inflate(R.menu.filtershow_menu_grad, mPopupMenu.getMenu()); + FilterGradRepresentation rep = (FilterGradRepresentation) getLocalRepresentation(); + if (rep == null) { + return; + } + updateMenuItems(rep); + hackFixStrings(mPopupMenu.getMenu()); + setEffectName(); + updateText(); + + mPopupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + FilterRepresentation tmpRep = getLocalRepresentation(); + + if (tmpRep instanceof FilterGradRepresentation) { + FilterGradRepresentation rep = (FilterGradRepresentation) tmpRep; + int cmdID = item.getItemId(); + switch (cmdID) { + case R.id.editor_grad_brightness: + mSliderMode = MODE_BRIGHTNESS; + mEffectName = item.getTitle().toString(); + break; + case R.id.editor_grad_contrast: + mSliderMode = MODE_CONTRAST; + mEffectName = item.getTitle().toString(); + break; + case R.id.editor_grad_saturation: + mSliderMode = MODE_SATURATION; + mEffectName = item.getTitle().toString(); + break; + } + updateMenuItems(rep); + updateSeekBar(rep); + + commitLocalRepresentation(); + mView.invalidate(); + } + return true; + } + }); + } + + @Override + public String calculateUserMessage(Context context, String effectName, Object parameterValue) { + FilterGradRepresentation rep = getGradRepresentation(); + if (rep == null) { + return mEffectName; + } + int val = rep.getParameter(mSliderMode); + return mEffectName.toUpperCase() + ((val > 0) ? " +" : " ") + val; + } + + private FilterGradRepresentation getGradRepresentation() { + FilterRepresentation tmpRep = getLocalRepresentation(); + if (tmpRep instanceof FilterGradRepresentation) { + return (FilterGradRepresentation) tmpRep; + } + return null; + } + + @Override + public int getMaximum() { + FilterGradRepresentation rep = getGradRepresentation(); + if (rep == null) { + return 0; + } + return rep.getParameterMax(mSliderMode); + } + + @Override + public int getMinimum() { + FilterGradRepresentation rep = getGradRepresentation(); + if (rep == null) { + return 0; + } + return rep.getParameterMin(mSliderMode); + } + + @Override + public int getDefaultValue() { + return 0; + } + + @Override + public int getValue() { + FilterGradRepresentation rep = getGradRepresentation(); + if (rep == null) { + return 0; + } + return rep.getParameter(mSliderMode); + } + + @Override + public String getValueString() { + return null; + } + + @Override + public void setValue(int value) { + FilterGradRepresentation rep = getGradRepresentation(); + if (rep == null) { + return; + } + rep.setParameter(mSliderMode, value); + } + + @Override + public String getParameterName() { + return mEffectName; + } + + @Override + public String getParameterType() { + return sParameterType; + } + + @Override + public void setController(Control c) { + + } + + @Override + public void fireLeftAction() { + FilterGradRepresentation rep = getGradRepresentation(); + if (rep == null) { + return; + } + rep.addBand(MasterImage.getImage().getOriginalBounds()); + updateMenuItems(rep); + updateSeekBar(rep); + + commitLocalRepresentation(); + mView.invalidate(); + } + + @Override + public int getLeftIcon() { + return ADD_ICON; + } + + @Override + public void fireRightAction() { + FilterGradRepresentation rep = getGradRepresentation(); + if (rep == null) { + return; + } + rep.deleteCurrentBand(); + + updateMenuItems(rep); + updateSeekBar(rep); + commitLocalRepresentation(); + mView.invalidate(); + } + + @Override + public int getRightIcon() { + return DEL_ICON; + } + + @Override + public void setFilterView(FilterView editor) { + + } + + @Override + public void copyFrom(Parameter src) { + + } + +} diff --git a/src/com/android/gallery3d/filtershow/editors/EditorInfo.java b/src/com/android/gallery3d/filtershow/editors/EditorInfo.java new file mode 100644 index 000000000..75afe49c2 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/editors/EditorInfo.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2013 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.filtershow.editors; + +public interface EditorInfo { + public int getTextId(); + public int getOverlayId(); + public boolean getOverlayOnly(); +} diff --git a/src/com/android/gallery3d/filtershow/editors/EditorMirror.java b/src/com/android/gallery3d/filtershow/editors/EditorMirror.java new file mode 100644 index 000000000..d6d9ee75d --- /dev/null +++ b/src/com/android/gallery3d/filtershow/editors/EditorMirror.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2013 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.filtershow.editors; + +import android.content.Context; +import android.util.Log; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.filters.FilterMirrorRepresentation; +import com.android.gallery3d.filtershow.filters.FilterRepresentation; +import com.android.gallery3d.filtershow.imageshow.ImageMirror; +import com.android.gallery3d.filtershow.imageshow.MasterImage; + +public class EditorMirror extends Editor implements EditorInfo { + public static final String TAG = EditorMirror.class.getSimpleName(); + public static final int ID = R.id.editorFlip; + ImageMirror mImageMirror; + + public EditorMirror() { + super(ID); + mChangesGeometry = true; + } + + @Override + public void createEditor(Context context, FrameLayout frameLayout) { + super.createEditor(context, frameLayout); + if (mImageMirror == null) { + mImageMirror = new ImageMirror(context); + } + mView = mImageShow = mImageMirror; + mImageMirror.setEditor(this); + } + + @Override + public void reflectCurrentFilter() { + MasterImage master = MasterImage.getImage(); + master.setCurrentFilterRepresentation(master.getPreset() + .getFilterWithSerializationName(FilterMirrorRepresentation.SERIALIZATION_NAME)); + super.reflectCurrentFilter(); + FilterRepresentation rep = getLocalRepresentation(); + if (rep == null || rep instanceof FilterMirrorRepresentation) { + mImageMirror.setFilterMirrorRepresentation((FilterMirrorRepresentation) rep); + } else { + Log.w(TAG, "Could not reflect current filter, not of type: " + + FilterMirrorRepresentation.class.getSimpleName()); + } + mImageMirror.invalidate(); + } + + @Override + public void openUtilityPanel(final LinearLayout accessoryViewList) { + final Button button = (Button) accessoryViewList.findViewById(R.id.applyEffect); + button.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View arg0) { + mImageMirror.flip(); + } + }); + } + + @Override + public void finalApplyCalled() { + commitLocalRepresentation(mImageMirror.getFinalRepresentation()); + } + + @Override + public int getTextId() { + return R.string.mirror; + } + + @Override + public int getOverlayId() { + return R.drawable.filtershow_button_geometry_flip; + } + + @Override + public boolean getOverlayOnly() { + return true; + } + + @Override + public boolean showsSeekBar() { + return false; + } + + @Override + public boolean showsPopupIndicator() { + return false; + } +} diff --git a/src/com/android/gallery3d/filtershow/editors/EditorPanel.java b/src/com/android/gallery3d/filtershow/editors/EditorPanel.java new file mode 100644 index 000000000..bc4ca6ab6 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/editors/EditorPanel.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2013 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.filtershow.editors; + +import android.app.Activity; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentTransaction; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.FilterShowActivity; +import com.android.gallery3d.filtershow.history.HistoryManager; +import com.android.gallery3d.filtershow.category.MainPanel; +import com.android.gallery3d.filtershow.imageshow.MasterImage; +import com.android.gallery3d.filtershow.state.StatePanel; + +public class EditorPanel extends Fragment { + + private static final String LOGTAG = "EditorPanel"; + + private LinearLayout mMainView; + private Editor mEditor; + private int mEditorID; + + public void setEditor(int editor) { + mEditorID = editor; + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + FilterShowActivity filterShowActivity = (FilterShowActivity) activity; + mEditor = filterShowActivity.getEditor(mEditorID); + } + + public void cancelCurrentFilter() { + MasterImage masterImage = MasterImage.getImage(); + HistoryManager adapter = masterImage.getHistory(); + + int position = adapter.undo(); + masterImage.onHistoryItemClick(position); + ((FilterShowActivity)getActivity()).invalidateViews(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + FilterShowActivity activity = (FilterShowActivity) getActivity(); + if (mMainView != null) { + if (mMainView.getParent() != null) { + ViewGroup parent = (ViewGroup) mMainView.getParent(); + parent.removeView(mMainView); + } + showImageStatePanel(activity.isShowingImageStatePanel()); + return mMainView; + } + mMainView = (LinearLayout) inflater.inflate(R.layout.filtershow_editor_panel, null); + + View actionControl = mMainView.findViewById(R.id.panelAccessoryViewList); + View editControl = mMainView.findViewById(R.id.controlArea); + ImageButton cancelButton = (ImageButton) mMainView.findViewById(R.id.cancelFilter); + ImageButton applyButton = (ImageButton) mMainView.findViewById(R.id.applyFilter); + Button editTitle = (Button) mMainView.findViewById(R.id.applyEffect); + cancelButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + cancelCurrentFilter(); + FilterShowActivity activity = (FilterShowActivity) getActivity(); + activity.backToMain(); + } + }); + + Button toggleState = (Button) mMainView.findViewById(R.id.toggle_state); + mEditor = activity.getEditor(mEditorID); + if (mEditor != null) { + mEditor.setUpEditorUI(actionControl, editControl, editTitle, toggleState); + mEditor.reflectCurrentFilter(); + if (mEditor.useUtilityPanel()) { + mEditor.openUtilityPanel((LinearLayout) actionControl); + } + } + applyButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + FilterShowActivity activity = (FilterShowActivity) getActivity(); + mEditor.finalApplyCalled(); + activity.backToMain(); + } + }); + + showImageStatePanel(activity.isShowingImageStatePanel()); + return mMainView; + } + + @Override + public void onDetach() { + if (mEditor != null) { + mEditor.detach(); + } + super.onDetach(); + } + + public void showImageStatePanel(boolean show) { + if (mMainView.findViewById(R.id.state_panel_container) == null) { + return; + } + FragmentTransaction transaction = getChildFragmentManager().beginTransaction(); + Fragment panel = getActivity().getSupportFragmentManager().findFragmentByTag( + MainPanel.FRAGMENT_TAG); + if (panel == null || panel instanceof MainPanel) { + transaction.setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out); + } + if (show) { + StatePanel statePanel = new StatePanel(); + transaction.replace(R.id.state_panel_container, statePanel, StatePanel.FRAGMENT_TAG); + } else { + Fragment statePanel = getChildFragmentManager().findFragmentByTag(StatePanel.FRAGMENT_TAG); + if (statePanel != null) { + transaction.remove(statePanel); + } + } + transaction.commit(); + } +} diff --git a/src/com/android/gallery3d/filtershow/editors/EditorRedEye.java b/src/com/android/gallery3d/filtershow/editors/EditorRedEye.java new file mode 100644 index 000000000..b0e88dd44 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/editors/EditorRedEye.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2012 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.filtershow.editors; + +import android.content.Context; +import android.widget.FrameLayout; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.filters.FilterRedEyeRepresentation; +import com.android.gallery3d.filtershow.filters.FilterRepresentation; +import com.android.gallery3d.filtershow.imageshow.ImageRedEye; + +/** + * The editor with no slider for filters without UI + */ +public class EditorRedEye extends Editor { + public static int ID = R.id.editorRedEye; + private final String LOGTAG = "EditorRedEye"; + ImageRedEye mImageRedEyes; + + public EditorRedEye() { + super(ID); + } + + protected EditorRedEye(int id) { + super(id); + } + + @Override + public void createEditor(Context context, FrameLayout frameLayout) { + super.createEditor(context, frameLayout); + mView = mImageShow = mImageRedEyes= new ImageRedEye(context); + mImageRedEyes.setEditor(this); + } + + @Override + public void reflectCurrentFilter() { + super.reflectCurrentFilter(); + FilterRepresentation rep = getLocalRepresentation(); + if (rep != null && getLocalRepresentation() instanceof FilterRedEyeRepresentation) { + FilterRedEyeRepresentation redEyeRep = (FilterRedEyeRepresentation) rep; + + mImageRedEyes.setRepresentation(redEyeRep); + } + } + + @Override + public boolean showsSeekBar() { + return false; + } +} diff --git a/src/com/android/gallery3d/filtershow/editors/EditorRotate.java b/src/com/android/gallery3d/filtershow/editors/EditorRotate.java new file mode 100644 index 000000000..9452bf0c0 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/editors/EditorRotate.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2013 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.filtershow.editors; + +import android.content.Context; +import android.util.Log; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.filters.FilterRepresentation; +import com.android.gallery3d.filtershow.filters.FilterRotateRepresentation; +import com.android.gallery3d.filtershow.imageshow.ImageRotate; +import com.android.gallery3d.filtershow.imageshow.MasterImage; + +public class EditorRotate extends Editor implements EditorInfo { + public static final String TAG = EditorRotate.class.getSimpleName(); + public static final int ID = R.id.editorRotate; + ImageRotate mImageRotate; + + public EditorRotate() { + super(ID); + mChangesGeometry = true; + } + + @Override + public void createEditor(Context context, FrameLayout frameLayout) { + super.createEditor(context, frameLayout); + if (mImageRotate == null) { + mImageRotate = new ImageRotate(context); + } + mView = mImageShow = mImageRotate; + mImageRotate.setEditor(this); + } + + @Override + public void reflectCurrentFilter() { + MasterImage master = MasterImage.getImage(); + master.setCurrentFilterRepresentation(master.getPreset() + .getFilterWithSerializationName(FilterRotateRepresentation.SERIALIZATION_NAME)); + super.reflectCurrentFilter(); + FilterRepresentation rep = getLocalRepresentation(); + if (rep == null || rep instanceof FilterRotateRepresentation) { + mImageRotate.setFilterRotateRepresentation((FilterRotateRepresentation) rep); + } else { + Log.w(TAG, "Could not reflect current filter, not of type: " + + FilterRotateRepresentation.class.getSimpleName()); + } + mImageRotate.invalidate(); + } + + @Override + public void openUtilityPanel(final LinearLayout accessoryViewList) { + final Button button = (Button) accessoryViewList.findViewById(R.id.applyEffect); + button.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View arg0) { + mImageRotate.rotate(); + String displayVal = mContext.getString(getTextId()) + " " + + mImageRotate.getLocalValue(); + button.setText(displayVal); + } + }); + } + + @Override + public void finalApplyCalled() { + commitLocalRepresentation(mImageRotate.getFinalRepresentation()); + } + + @Override + public int getTextId() { + return R.string.rotate; + } + + @Override + public int getOverlayId() { + return R.drawable.filtershow_button_geometry_rotate; + } + + @Override + public boolean getOverlayOnly() { + return true; + } + + @Override + public boolean showsSeekBar() { + return false; + } + + @Override + public boolean showsPopupIndicator() { + return false; + } +} diff --git a/src/com/android/gallery3d/filtershow/editors/EditorStraighten.java b/src/com/android/gallery3d/filtershow/editors/EditorStraighten.java new file mode 100644 index 000000000..ff84ba8f9 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/editors/EditorStraighten.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2013 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.filtershow.editors; + +import android.content.Context; +import android.util.Log; +import android.widget.FrameLayout; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.filters.FilterRepresentation; +import com.android.gallery3d.filtershow.filters.FilterStraightenRepresentation; +import com.android.gallery3d.filtershow.imageshow.ImageStraighten; +import com.android.gallery3d.filtershow.imageshow.MasterImage; + +public class EditorStraighten extends Editor implements EditorInfo { + public static final String TAG = EditorStraighten.class.getSimpleName(); + public static final int ID = R.id.editorStraighten; + ImageStraighten mImageStraighten; + + public EditorStraighten() { + super(ID); + mShowParameter = SHOW_VALUE_INT; + mChangesGeometry = true; + } + + @Override + public String calculateUserMessage(Context context, String effectName, Object parameterValue) { + String apply = context.getString(R.string.apply_effect); + apply += " " + effectName; + return apply.toUpperCase(); + } + + @Override + public void createEditor(Context context, FrameLayout frameLayout) { + super.createEditor(context, frameLayout); + if (mImageStraighten == null) { + mImageStraighten = new ImageStraighten(context); + } + mView = mImageShow = mImageStraighten; + mImageStraighten.setEditor(this); + } + + @Override + public void reflectCurrentFilter() { + MasterImage master = MasterImage.getImage(); + master.setCurrentFilterRepresentation(master.getPreset().getFilterWithSerializationName( + FilterStraightenRepresentation.SERIALIZATION_NAME)); + super.reflectCurrentFilter(); + FilterRepresentation rep = getLocalRepresentation(); + if (rep == null || rep instanceof FilterStraightenRepresentation) { + mImageStraighten + .setFilterStraightenRepresentation((FilterStraightenRepresentation) rep); + } else { + Log.w(TAG, "Could not reflect current filter, not of type: " + + FilterStraightenRepresentation.class.getSimpleName()); + } + mImageStraighten.invalidate(); + } + + @Override + public void finalApplyCalled() { + commitLocalRepresentation(mImageStraighten.getFinalRepresentation()); + } + + @Override + public int getTextId() { + return R.string.straighten; + } + + @Override + public int getOverlayId() { + return R.drawable.filtershow_button_geometry_straighten; + } + + @Override + public boolean getOverlayOnly() { + return true; + } + + @Override + public boolean showsSeekBar() { + return false; + } + + @Override + public boolean showsPopupIndicator() { + return false; + } +} diff --git a/src/com/android/gallery3d/filtershow/editors/EditorTinyPlanet.java b/src/com/android/gallery3d/filtershow/editors/EditorTinyPlanet.java new file mode 100644 index 000000000..9376fbef0 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/editors/EditorTinyPlanet.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2012 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.filtershow.editors; + +import android.content.Context; +import android.widget.FrameLayout; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.filters.FilterRepresentation; +import com.android.gallery3d.filtershow.filters.FilterTinyPlanetRepresentation; +import com.android.gallery3d.filtershow.imageshow.ImageTinyPlanet; + +public class EditorTinyPlanet extends BasicEditor { + public static final int ID = R.id.tinyPlanetEditor; + private static final String LOGTAG = "EditorTinyPlanet"; + ImageTinyPlanet mImageTinyPlanet; + + public EditorTinyPlanet() { + super(ID, R.layout.filtershow_tiny_planet_editor, R.id.imageTinyPlanet); + } + + @Override + public void createEditor(Context context, FrameLayout frameLayout) { + super.createEditor(context, frameLayout); + mImageTinyPlanet = (ImageTinyPlanet) mImageShow; + mImageTinyPlanet.setEditor(this); + } + + @Override + public void reflectCurrentFilter() { + super.reflectCurrentFilter(); + FilterRepresentation rep = getLocalRepresentation(); + if (rep != null && rep instanceof FilterTinyPlanetRepresentation) { + FilterTinyPlanetRepresentation drawRep = (FilterTinyPlanetRepresentation) rep; + mImageTinyPlanet.setRepresentation(drawRep); + } + } + + public void updateUI() { + if (mControl != null) { + mControl.updateUI(); + } + } +} diff --git a/src/com/android/gallery3d/filtershow/editors/EditorVignette.java b/src/com/android/gallery3d/filtershow/editors/EditorVignette.java new file mode 100644 index 000000000..7127b2188 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/editors/EditorVignette.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2012 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.filtershow.editors; + +import android.content.Context; +import android.widget.FrameLayout; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.filters.FilterRepresentation; +import com.android.gallery3d.filtershow.filters.FilterVignetteRepresentation; +import com.android.gallery3d.filtershow.imageshow.ImageVignette; + +public class EditorVignette extends ParametricEditor { + public static final int ID = R.id.vignetteEditor; + private static final String LOGTAG = "EditorVignettePlanet"; + ImageVignette mImageVignette; + + public EditorVignette() { + super(ID, R.layout.filtershow_vignette_editor, R.id.imageVignette); + } + + @Override + public void createEditor(Context context, FrameLayout frameLayout) { + super.createEditor(context, frameLayout); + mImageVignette = (ImageVignette) mImageShow; + mImageVignette.setEditor(this); + } + + @Override + public void reflectCurrentFilter() { + super.reflectCurrentFilter(); + + FilterRepresentation rep = getLocalRepresentation(); + if (rep != null && getLocalRepresentation() instanceof FilterVignetteRepresentation) { + FilterVignetteRepresentation drawRep = (FilterVignetteRepresentation) rep; + mImageVignette.setRepresentation(drawRep); + } + } +} diff --git a/src/com/android/gallery3d/filtershow/editors/EditorZoom.java b/src/com/android/gallery3d/filtershow/editors/EditorZoom.java new file mode 100644 index 000000000..ea8e3d140 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/editors/EditorZoom.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2012 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.filtershow.editors; + +import com.android.gallery3d.R; + +public class EditorZoom extends BasicEditor { + public static final int ID = R.id.imageZoom; + + public EditorZoom() { + super(ID, R.layout.filtershow_zoom_editor,R.id.imageZoom); + } +} diff --git a/src/com/android/gallery3d/filtershow/editors/ImageOnlyEditor.java b/src/com/android/gallery3d/filtershow/editors/ImageOnlyEditor.java new file mode 100644 index 000000000..d4e66edf8 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/editors/ImageOnlyEditor.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2012 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.filtershow.editors; + +import android.content.Context; +import android.widget.FrameLayout; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.imageshow.ImageShow; + +/** + * The editor with no slider for filters without UI + */ +public class ImageOnlyEditor extends Editor { + public final static int ID = R.id.imageOnlyEditor; + private final String LOGTAG = "ImageOnlyEditor"; + + public ImageOnlyEditor() { + super(ID); + } + + protected ImageOnlyEditor(int id) { + super(id); + } + + public boolean useUtilityPanel() { + return false; + } + + @Override + public void createEditor(Context context, FrameLayout frameLayout) { + super.createEditor(context, frameLayout); + mView = mImageShow = new ImageShow(context); + } + +} diff --git a/src/com/android/gallery3d/filtershow/editors/ParametricEditor.java b/src/com/android/gallery3d/filtershow/editors/ParametricEditor.java new file mode 100644 index 000000000..9ec858ca5 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/editors/ParametricEditor.java @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2013 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.filtershow.editors; + +import android.content.Context; +import android.graphics.Point; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import android.view.WindowManager; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.SeekBar; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.controller.ActionSlider; +import com.android.gallery3d.filtershow.controller.BasicSlider; +import com.android.gallery3d.filtershow.controller.Control; +import com.android.gallery3d.filtershow.controller.Parameter; +import com.android.gallery3d.filtershow.controller.ParameterActionAndInt; +import com.android.gallery3d.filtershow.controller.ParameterInteger; +import com.android.gallery3d.filtershow.controller.ParameterStyles; +import com.android.gallery3d.filtershow.controller.StyleChooser; +import com.android.gallery3d.filtershow.controller.TitledSlider; +import com.android.gallery3d.filtershow.filters.FilterBasicRepresentation; +import com.android.gallery3d.filtershow.filters.FilterRepresentation; + +import java.lang.reflect.Constructor; +import java.util.HashMap; + +public class ParametricEditor extends Editor { + private int mLayoutID; + private int mViewID; + public static int ID = R.id.editorParametric; + private final String LOGTAG = "ParametricEditor"; + protected Control mControl; + public static final int MINIMUM_WIDTH = 600; + public static final int MINIMUM_HEIGHT = 800; + View mActionButton; + View mEditControl; + static HashMap<String, Class> portraitMap = new HashMap<String, Class>(); + static HashMap<String, Class> landscapeMap = new HashMap<String, Class>(); + static { + portraitMap.put(ParameterInteger.sParameterType, BasicSlider.class); + landscapeMap.put(ParameterInteger.sParameterType, TitledSlider.class); + portraitMap.put(ParameterActionAndInt.sParameterType, ActionSlider.class); + landscapeMap.put(ParameterActionAndInt.sParameterType, ActionSlider.class); + portraitMap.put(ParameterStyles.sParameterType, StyleChooser.class); + landscapeMap.put(ParameterStyles.sParameterType, StyleChooser.class); + } + + static Constructor getConstructor(Class cl) { + try { + return cl.getConstructor(Context.class, ViewGroup.class); + } catch (Exception e) { + return null; + } + } + + public ParametricEditor() { + super(ID); + } + + protected ParametricEditor(int id) { + super(id); + } + + protected ParametricEditor(int id, int layoutID, int viewID) { + super(id); + mLayoutID = layoutID; + mViewID = viewID; + } + + @Override + public String calculateUserMessage(Context context, String effectName, Object parameterValue) { + String apply = ""; + + if (mShowParameter == SHOW_VALUE_INT & useCompact(context)) { + if (getLocalRepresentation() instanceof FilterBasicRepresentation) { + FilterBasicRepresentation interval = (FilterBasicRepresentation) getLocalRepresentation(); + apply += " " + effectName.toUpperCase() + " " + interval.getStateRepresentation(); + } else { + apply += " " + effectName.toUpperCase() + " " + parameterValue; + } + } else { + apply += " " + effectName.toUpperCase(); + } + return apply; + } + + @Override + public void createEditor(Context context, FrameLayout frameLayout) { + super.createEditor(context, frameLayout); + unpack(mViewID, mLayoutID); + } + + @Override + public void reflectCurrentFilter() { + super.reflectCurrentFilter(); + if (getLocalRepresentation() != null + && getLocalRepresentation() instanceof FilterBasicRepresentation) { + FilterBasicRepresentation interval = (FilterBasicRepresentation) getLocalRepresentation(); + mControl.setPrameter(interval); + } + } + + @Override + public Control[] getControls() { + BasicSlider slider = new BasicSlider(); + return new Control[] { + slider + }; + } + + // TODO: need a better way to decide which representation + static boolean useCompact(Context context) { + WindowManager w = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)); + Point size = new Point(); + w.getDefaultDisplay().getSize(size); + if (size.x < size.y) { // if tall than wider + return true; + } + if (size.x < MINIMUM_WIDTH) { + return true; + } + if (size.y < MINIMUM_HEIGHT) { + return true; + } + return false; + } + + protected Parameter getParameterToEdit(FilterRepresentation rep) { + if (this instanceof Parameter) { + return (Parameter) this; + } else if (rep instanceof Parameter) { + return ((Parameter) rep); + } + return null; + } + + @Override + public void setUtilityPanelUI(View actionButton, View editControl) { + mActionButton = actionButton; + mEditControl = editControl; + FilterRepresentation rep = getLocalRepresentation(); + Parameter param = getParameterToEdit(rep); + if (param != null) { + control(param, editControl); + } else { + mSeekBar = new SeekBar(editControl.getContext()); + LayoutParams lp = new LinearLayout.LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + mSeekBar.setLayoutParams(lp); + ((LinearLayout) editControl).addView(mSeekBar); + mSeekBar.setOnSeekBarChangeListener(this); + } + } + + protected void control(Parameter p, View editControl) { + String pType = p.getParameterType(); + Context context = editControl.getContext(); + Class c = ((useCompact(context)) ? portraitMap : landscapeMap).get(pType); + + if (c != null) { + try { + mControl = (Control) c.newInstance(); + p.setController(mControl); + mControl.setUp((ViewGroup) editControl, p, this); + } catch (Exception e) { + Log.e(LOGTAG, "Error in loading Control ", e); + } + } else { + Log.e(LOGTAG, "Unable to find class for " + pType); + for (String string : portraitMap.keySet()) { + Log.e(LOGTAG, "for " + string + " use " + portraitMap.get(string)); + } + } + } + + @Override + public void onProgressChanged(SeekBar sbar, int progress, boolean arg2) { + } + + @Override + public void onStartTrackingTouch(SeekBar arg0) { + } + + @Override + public void onStopTrackingTouch(SeekBar arg0) { + } +} diff --git a/src/com/android/gallery3d/filtershow/editors/SwapButton.java b/src/com/android/gallery3d/filtershow/editors/SwapButton.java new file mode 100644 index 000000000..bb4432e28 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/editors/SwapButton.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2013 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.filtershow.editors; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.GestureDetector; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.widget.Button; + +public class SwapButton extends Button implements GestureDetector.OnGestureListener { + + public static int ANIM_DURATION = 200; + + public interface SwapButtonListener { + public void swapLeft(MenuItem item); + public void swapRight(MenuItem item); + } + + private GestureDetector mDetector; + private SwapButtonListener mListener; + private Menu mMenu; + private int mCurrentMenuIndex; + + public SwapButton(Context context, AttributeSet attrs) { + super(context, attrs); + mDetector = new GestureDetector(context, this); + } + + public SwapButtonListener getListener() { + return mListener; + } + + public void setListener(SwapButtonListener listener) { + mListener = listener; + } + + public boolean onTouchEvent(MotionEvent me) { + if (!mDetector.onTouchEvent(me)) { + return super.onTouchEvent(me); + } + return true; + } + + @Override + public boolean onDown(MotionEvent e) { + return true; + } + + @Override + public void onShowPress(MotionEvent e) { + } + + @Override + public boolean onSingleTapUp(MotionEvent e) { + callOnClick(); + return true; + } + + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + return false; + } + + @Override + public void onLongPress(MotionEvent e) { + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + if (mMenu == null) { + return false; + } + if (e1.getX() - e2.getX() > 0) { + // right to left + mCurrentMenuIndex++; + if (mCurrentMenuIndex == mMenu.size()) { + mCurrentMenuIndex = 0; + } + if (mListener != null) { + mListener.swapRight(mMenu.getItem(mCurrentMenuIndex)); + } + } else { + // left to right + mCurrentMenuIndex--; + if (mCurrentMenuIndex < 0) { + mCurrentMenuIndex = mMenu.size() - 1; + } + if (mListener != null) { + mListener.swapLeft(mMenu.getItem(mCurrentMenuIndex)); + } + } + return true; + } +} diff --git a/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java b/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java new file mode 100644 index 000000000..3fa91916d --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java @@ -0,0 +1,296 @@ +/* + * Copyright (C) 2013 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.filtershow.filters; + +import android.content.Context; +import android.content.res.Resources; +import android.util.Log; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.editors.EditorCrop; +import com.android.gallery3d.filtershow.editors.EditorMirror; +import com.android.gallery3d.filtershow.editors.EditorRotate; +import com.android.gallery3d.filtershow.editors.EditorStraighten; +import com.android.gallery3d.filtershow.pipeline.ImagePreset; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Vector; + +public abstract class BaseFiltersManager implements FiltersManagerInterface { + protected HashMap<Class, ImageFilter> mFilters = null; + protected HashMap<String, FilterRepresentation> mRepresentationLookup = null; + private static final String LOGTAG = "BaseFiltersManager"; + + protected ArrayList<FilterRepresentation> mLooks = new ArrayList<FilterRepresentation>(); + protected ArrayList<FilterRepresentation> mBorders = new ArrayList<FilterRepresentation>(); + protected ArrayList<FilterRepresentation> mTools = new ArrayList<FilterRepresentation>(); + protected ArrayList<FilterRepresentation> mEffects = new ArrayList<FilterRepresentation>(); + + protected void init() { + mFilters = new HashMap<Class, ImageFilter>(); + mRepresentationLookup = new HashMap<String, FilterRepresentation>(); + Vector<Class> filters = new Vector<Class>(); + addFilterClasses(filters); + for (Class filterClass : filters) { + try { + Object filterInstance = filterClass.newInstance(); + if (filterInstance instanceof ImageFilter) { + mFilters.put(filterClass, (ImageFilter) filterInstance); + + FilterRepresentation rep = + ((ImageFilter) filterInstance).getDefaultRepresentation(); + if (rep != null) { + addRepresentation(rep); + } + } + } catch (InstantiationException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + } + + public void addRepresentation(FilterRepresentation rep) { + mRepresentationLookup.put(rep.getSerializationName(), rep); + } + + public FilterRepresentation createFilterFromName(String name) { + try { + return mRepresentationLookup.get(name).copy(); + } catch (Exception e) { + Log.v(LOGTAG, "unable to generate a filter representation for \"" + name + "\""); + e.printStackTrace(); + } + return null; + } + + public ImageFilter getFilter(Class c) { + return mFilters.get(c); + } + + @Override + public ImageFilter getFilterForRepresentation(FilterRepresentation representation) { + return mFilters.get(representation.getFilterClass()); + } + + public FilterRepresentation getRepresentation(Class c) { + ImageFilter filter = mFilters.get(c); + if (filter != null) { + return filter.getDefaultRepresentation(); + } + return null; + } + + public void freeFilterResources(ImagePreset preset) { + if (preset == null) { + return; + } + Vector<ImageFilter> usedFilters = preset.getUsedFilters(this); + for (Class c : mFilters.keySet()) { + ImageFilter filter = mFilters.get(c); + if (!usedFilters.contains(filter)) { + filter.freeResources(); + } + } + } + + public void freeRSFilterScripts() { + for (Class c : mFilters.keySet()) { + ImageFilter filter = mFilters.get(c); + if (filter != null && filter instanceof ImageFilterRS) { + ((ImageFilterRS) filter).resetScripts(); + } + } + } + + protected void addFilterClasses(Vector<Class> filters) { + filters.add(ImageFilterTinyPlanet.class); + filters.add(ImageFilterRedEye.class); + filters.add(ImageFilterWBalance.class); + filters.add(ImageFilterExposure.class); + filters.add(ImageFilterVignette.class); + filters.add(ImageFilterGrad.class); + filters.add(ImageFilterContrast.class); + filters.add(ImageFilterShadows.class); + filters.add(ImageFilterHighlights.class); + filters.add(ImageFilterVibrance.class); + filters.add(ImageFilterSharpen.class); + filters.add(ImageFilterCurves.class); + filters.add(ImageFilterDraw.class); + filters.add(ImageFilterHue.class); + filters.add(ImageFilterChanSat.class); + filters.add(ImageFilterSaturated.class); + filters.add(ImageFilterBwFilter.class); + filters.add(ImageFilterNegative.class); + filters.add(ImageFilterEdge.class); + filters.add(ImageFilterKMeans.class); + filters.add(ImageFilterFx.class); + filters.add(ImageFilterBorder.class); + filters.add(ImageFilterParametricBorder.class); + } + + public ArrayList<FilterRepresentation> getLooks() { + return mLooks; + } + + public ArrayList<FilterRepresentation> getBorders() { + return mBorders; + } + + public ArrayList<FilterRepresentation> getTools() { + return mTools; + } + + public ArrayList<FilterRepresentation> getEffects() { + return mEffects; + } + + public void addBorders(Context context) { + + } + + public void addLooks(Context context) { + int[] drawid = { + R.drawable.filtershow_fx_0005_punch, + R.drawable.filtershow_fx_0000_vintage, + R.drawable.filtershow_fx_0004_bw_contrast, + R.drawable.filtershow_fx_0002_bleach, + R.drawable.filtershow_fx_0001_instant, + R.drawable.filtershow_fx_0007_washout, + R.drawable.filtershow_fx_0003_blue_crush, + R.drawable.filtershow_fx_0008_washout_color, + R.drawable.filtershow_fx_0006_x_process + }; + + int[] fxNameid = { + R.string.ffx_punch, + R.string.ffx_vintage, + R.string.ffx_bw_contrast, + R.string.ffx_bleach, + R.string.ffx_instant, + R.string.ffx_washout, + R.string.ffx_blue_crush, + R.string.ffx_washout_color, + R.string.ffx_x_process + }; + + // Do not localize. + String[] serializationNames = { + "LUT3D_PUNCH", + "LUT3D_VINTAGE", + "LUT3D_BW", + "LUT3D_BLEACH", + "LUT3D_INSTANT", + "LUT3D_WASHOUT", + "LUT3D_BLUECRUSH", + "LUT3D_WASHOUT", + "LUT3D_XPROCESS" + }; + + FilterFxRepresentation nullFx = + new FilterFxRepresentation(context.getString(R.string.none), + 0, R.string.none); + mLooks.add(nullFx); + + for (int i = 0; i < drawid.length; i++) { + FilterFxRepresentation fx = new FilterFxRepresentation( + context.getString(fxNameid[i]), drawid[i], fxNameid[i]); + fx.setSerializationName(serializationNames[i]); + ImagePreset preset = new ImagePreset(); + preset.addFilter(fx); + FilterUserPresetRepresentation rep = new FilterUserPresetRepresentation( + context.getString(fxNameid[i]), preset, -1); + mLooks.add(rep); + addRepresentation(fx); + } + } + + public void addEffects() { + mEffects.add(getRepresentation(ImageFilterTinyPlanet.class)); + mEffects.add(getRepresentation(ImageFilterWBalance.class)); + mEffects.add(getRepresentation(ImageFilterExposure.class)); + mEffects.add(getRepresentation(ImageFilterVignette.class)); + mEffects.add(getRepresentation(ImageFilterGrad.class)); + mEffects.add(getRepresentation(ImageFilterContrast.class)); + mEffects.add(getRepresentation(ImageFilterShadows.class)); + mEffects.add(getRepresentation(ImageFilterHighlights.class)); + mEffects.add(getRepresentation(ImageFilterVibrance.class)); + mEffects.add(getRepresentation(ImageFilterSharpen.class)); + mEffects.add(getRepresentation(ImageFilterCurves.class)); + mEffects.add(getRepresentation(ImageFilterHue.class)); + mEffects.add(getRepresentation(ImageFilterChanSat.class)); + mEffects.add(getRepresentation(ImageFilterBwFilter.class)); + mEffects.add(getRepresentation(ImageFilterNegative.class)); + mEffects.add(getRepresentation(ImageFilterEdge.class)); + mEffects.add(getRepresentation(ImageFilterKMeans.class)); + } + + public void addTools(Context context) { + + int[] editorsId = { + EditorCrop.ID, + EditorStraighten.ID, + EditorRotate.ID, + EditorMirror.ID + }; + + int[] textId = { + R.string.crop, + R.string.straighten, + R.string.rotate, + R.string.mirror + }; + + int[] overlayId = { + R.drawable.filtershow_button_geometry_crop, + R.drawable.filtershow_button_geometry_straighten, + R.drawable.filtershow_button_geometry_rotate, + R.drawable.filtershow_button_geometry_flip + }; + + FilterRepresentation[] geometryFilters = { + new FilterCropRepresentation(), + new FilterStraightenRepresentation(), + new FilterRotateRepresentation(), + new FilterMirrorRepresentation() + }; + + for (int i = 0; i < editorsId.length; i++) { + int editorId = editorsId[i]; + FilterRepresentation geometry = geometryFilters[i]; + geometry.setEditorId(editorId); + geometry.setTextId(textId[i]); + geometry.setOverlayId(overlayId[i]); + geometry.setOverlayOnly(true); + if (geometry.getTextId() != 0) { + geometry.setName(context.getString(geometry.getTextId())); + } + mTools.add(geometry); + } + + mTools.add(getRepresentation(ImageFilterRedEye.class)); + mTools.add(getRepresentation(ImageFilterDraw.class)); + } + + public void setFilterResources(Resources resources) { + ImageFilterBorder filterBorder = (ImageFilterBorder) getFilter(ImageFilterBorder.class); + filterBorder.setResources(resources); + ImageFilterFx filterFx = (ImageFilterFx) getFilter(ImageFilterFx.class); + filterFx.setResources(resources); + } +} diff --git a/src/com/android/gallery3d/filtershow/filters/ColorSpaceMatrix.java b/src/com/android/gallery3d/filtershow/filters/ColorSpaceMatrix.java new file mode 100644 index 000000000..7c307a9e7 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/ColorSpaceMatrix.java @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2012 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.filtershow.filters; + +import java.util.Arrays; + +public class ColorSpaceMatrix { + private final float[] mMatrix = new float[16]; + private static final float RLUM = 0.3086f; + private static final float GLUM = 0.6094f; + private static final float BLUM = 0.0820f; + + public ColorSpaceMatrix() { + identity(); + } + + /** + * Copy constructor + * + * @param matrix + */ + public ColorSpaceMatrix(ColorSpaceMatrix matrix) { + System.arraycopy(matrix.mMatrix, 0, mMatrix, 0, matrix.mMatrix.length); + } + + /** + * get the matrix + * + * @return the internal matrix + */ + public float[] getMatrix() { + return mMatrix; + } + + /** + * set matrix to identity + */ + public void identity() { + Arrays.fill(mMatrix, 0); + mMatrix[0] = mMatrix[5] = mMatrix[10] = mMatrix[15] = 1; + } + + public void convertToLuminance() { + mMatrix[0] = mMatrix[1] = mMatrix[2] = 0.3086f; + mMatrix[4] = mMatrix[5] = mMatrix[6] = 0.6094f; + mMatrix[8] = mMatrix[9] = mMatrix[10] = 0.0820f; + } + + private void multiply(float[] a) + { + int x, y; + float[] temp = new float[16]; + + for (y = 0; y < 4; y++) { + int y4 = y * 4; + for (x = 0; x < 4; x++) { + temp[y4 + x] = mMatrix[y4 + 0] * a[x] + + mMatrix[y4 + 1] * a[4 + x] + + mMatrix[y4 + 2] * a[8 + x] + + mMatrix[y4 + 3] * a[12 + x]; + } + } + for (int i = 0; i < 16; i++) + mMatrix[i] = temp[i]; + } + + private void xRotateMatrix(float rs, float rc) + { + ColorSpaceMatrix c = new ColorSpaceMatrix(); + float[] tmp = c.mMatrix; + + tmp[5] = rc; + tmp[6] = rs; + tmp[9] = -rs; + tmp[10] = rc; + + multiply(tmp); + } + + private void yRotateMatrix(float rs, float rc) + { + ColorSpaceMatrix c = new ColorSpaceMatrix(); + float[] tmp = c.mMatrix; + + tmp[0] = rc; + tmp[2] = -rs; + tmp[8] = rs; + tmp[10] = rc; + + multiply(tmp); + } + + private void zRotateMatrix(float rs, float rc) + { + ColorSpaceMatrix c = new ColorSpaceMatrix(); + float[] tmp = c.mMatrix; + + tmp[0] = rc; + tmp[1] = rs; + tmp[4] = -rs; + tmp[5] = rc; + multiply(tmp); + } + + private void zShearMatrix(float dx, float dy) + { + ColorSpaceMatrix c = new ColorSpaceMatrix(); + float[] tmp = c.mMatrix; + + tmp[2] = dx; + tmp[6] = dy; + multiply(tmp); + } + + /** + * sets the transform to a shift in Hue + * + * @param rot rotation in degrees + */ + public void setHue(float rot) + { + float mag = (float) Math.sqrt(2.0); + float xrs = 1 / mag; + float xrc = 1 / mag; + xRotateMatrix(xrs, xrc); + mag = (float) Math.sqrt(3.0); + float yrs = -1 / mag; + float yrc = (float) Math.sqrt(2.0) / mag; + yRotateMatrix(yrs, yrc); + + float lx = getRedf(RLUM, GLUM, BLUM); + float ly = getGreenf(RLUM, GLUM, BLUM); + float lz = getBluef(RLUM, GLUM, BLUM); + float zsx = lx / lz; + float zsy = ly / lz; + zShearMatrix(zsx, zsy); + + float zrs = (float) Math.sin(rot * Math.PI / 180.0); + float zrc = (float) Math.cos(rot * Math.PI / 180.0); + zRotateMatrix(zrs, zrc); + zShearMatrix(-zsx, -zsy); + yRotateMatrix(-yrs, yrc); + xRotateMatrix(-xrs, xrc); + } + + /** + * set it to a saturation matrix + * + * @param s + */ + public void changeSaturation(float s) { + mMatrix[0] = (1 - s) * RLUM + s; + mMatrix[1] = (1 - s) * RLUM; + mMatrix[2] = (1 - s) * RLUM; + mMatrix[4] = (1 - s) * GLUM; + mMatrix[5] = (1 - s) * GLUM + s; + mMatrix[6] = (1 - s) * GLUM; + mMatrix[8] = (1 - s) * BLUM; + mMatrix[9] = (1 - s) * BLUM; + mMatrix[10] = (1 - s) * BLUM + s; + } + + /** + * Transform RGB value + * + * @param r red pixel value + * @param g green pixel value + * @param b blue pixel value + * @return computed red pixel value + */ + public float getRed(int r, int g, int b) { + return r * mMatrix[0] + g * mMatrix[4] + b * mMatrix[8] + mMatrix[12]; + } + + /** + * Transform RGB value + * + * @param r red pixel value + * @param g green pixel value + * @param b blue pixel value + * @return computed green pixel value + */ + public float getGreen(int r, int g, int b) { + return r * mMatrix[1] + g * mMatrix[5] + b * mMatrix[9] + mMatrix[13]; + } + + /** + * Transform RGB value + * + * @param r red pixel value + * @param g green pixel value + * @param b blue pixel value + * @return computed blue pixel value + */ + public float getBlue(int r, int g, int b) { + return r * mMatrix[2] + g * mMatrix[6] + b * mMatrix[10] + mMatrix[14]; + } + + private float getRedf(float r, float g, float b) { + return r * mMatrix[0] + g * mMatrix[4] + b * mMatrix[8] + mMatrix[12]; + } + + private float getGreenf(float r, float g, float b) { + return r * mMatrix[1] + g * mMatrix[5] + b * mMatrix[9] + mMatrix[13]; + } + + private float getBluef(float r, float g, float b) { + return r * mMatrix[2] + g * mMatrix[6] + b * mMatrix[10] + mMatrix[14]; + } + +} diff --git a/src/com/android/gallery3d/filtershow/filters/FilterBasicRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterBasicRepresentation.java new file mode 100644 index 000000000..1eebdb571 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/FilterBasicRepresentation.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2013 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.filtershow.filters; + + +import android.util.Log; + +import com.android.gallery3d.filtershow.controller.Control; +import com.android.gallery3d.filtershow.controller.FilterView; +import com.android.gallery3d.filtershow.controller.Parameter; +import com.android.gallery3d.filtershow.controller.ParameterInteger; + +public class FilterBasicRepresentation extends FilterRepresentation implements ParameterInteger { + private static final String LOGTAG = "FilterBasicRep"; + private int mMinimum; + private int mValue; + private int mMaximum; + private int mDefaultValue; + private int mPreviewValue; + public static final String SERIAL_NAME = "Name"; + public static final String SERIAL_VALUE = "Value"; + private boolean mLogVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE); + + public FilterBasicRepresentation(String name, int minimum, int value, int maximum) { + super(name); + mMinimum = minimum; + mMaximum = maximum; + setValue(value); + } + + @Override + public String toString() { + return getName() + " : " + mMinimum + " < " + mValue + " < " + mMaximum; + } + + @Override + public FilterRepresentation copy() { + FilterBasicRepresentation representation = new FilterBasicRepresentation(getName(),0,0,0); + copyAllParameters(representation); + return representation; + } + + @Override + protected void copyAllParameters(FilterRepresentation representation) { + super.copyAllParameters(representation); + representation.useParametersFrom(this); + } + + @Override + public void useParametersFrom(FilterRepresentation a) { + if (a instanceof FilterBasicRepresentation) { + FilterBasicRepresentation representation = (FilterBasicRepresentation) a; + setMinimum(representation.getMinimum()); + setMaximum(representation.getMaximum()); + setValue(representation.getValue()); + setDefaultValue(representation.getDefaultValue()); + setPreviewValue(representation.getPreviewValue()); + } + } + + @Override + public boolean equals(FilterRepresentation representation) { + if (!super.equals(representation)) { + return false; + } + if (representation instanceof FilterBasicRepresentation) { + FilterBasicRepresentation basic = (FilterBasicRepresentation) representation; + if (basic.mMinimum == mMinimum + && basic.mMaximum == mMaximum + && basic.mValue == mValue + && basic.mDefaultValue == mDefaultValue + && basic.mPreviewValue == mPreviewValue) { + return true; + } + } + return false; + } + + @Override + public int getMinimum() { + return mMinimum; + } + + public void setMinimum(int minimum) { + mMinimum = minimum; + } + + @Override + public int getValue() { + return mValue; + } + + @Override + public void setValue(int value) { + mValue = value; + if (mValue < mMinimum) { + mValue = mMinimum; + } + if (mValue > mMaximum) { + mValue = mMaximum; + } + } + + @Override + public int getMaximum() { + return mMaximum; + } + + public void setMaximum(int maximum) { + mMaximum = maximum; + } + + public void setDefaultValue(int defaultValue) { + mDefaultValue = defaultValue; + } + + @Override + public int getDefaultValue() { + return mDefaultValue; + } + + public int getPreviewValue() { + return mPreviewValue; + } + + public void setPreviewValue(int previewValue) { + mPreviewValue = previewValue; + } + + @Override + public String getStateRepresentation() { + int val = getValue(); + return ((val > 0) ? "+" : "") + val; + } + + @Override + public String getParameterType(){ + return sParameterType; + } + + @Override + public void setController(Control control) { + } + + @Override + public String getValueString() { + return getStateRepresentation(); + } + + @Override + public String getParameterName() { + return getName(); + } + + @Override + public void setFilterView(FilterView editor) { + } + + @Override + public void copyFrom(Parameter src) { + useParametersFrom((FilterBasicRepresentation) src); + } + + @Override + public String[][] serializeRepresentation() { + String[][] ret = { + {SERIAL_NAME , getName() }, + {SERIAL_VALUE , Integer.toString(mValue)}}; + return ret; + } + + @Override + public void deSerializeRepresentation(String[][] rep) { + super.deSerializeRepresentation(rep); + for (int i = 0; i < rep.length; i++) { + if (SERIAL_VALUE.equals(rep[i][0])) { + mValue = Integer.parseInt(rep[i][1]); + break; + } + } + } +} diff --git a/src/com/android/gallery3d/filtershow/filters/FilterChanSatRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterChanSatRepresentation.java new file mode 100644 index 000000000..7ce67dd96 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/FilterChanSatRepresentation.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2013 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.filtershow.filters; + +import android.util.JsonReader; +import android.util.JsonWriter; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.controller.BasicParameterInt; +import com.android.gallery3d.filtershow.controller.Parameter; +import com.android.gallery3d.filtershow.controller.ParameterSet; +import com.android.gallery3d.filtershow.editors.EditorChanSat; +import com.android.gallery3d.filtershow.imageshow.ControlPoint; +import com.android.gallery3d.filtershow.imageshow.Spline; + +import java.io.IOException; +import java.util.Vector; + +/** + * Representation for a filter that has per channel & Master saturation + */ +public class FilterChanSatRepresentation extends FilterRepresentation implements ParameterSet { + private static final String LOGTAG = "FilterChanSatRepresentation"; + private static final String ARGS = "ARGS"; + private static final String SERIALIZATION_NAME = "channelsaturation"; + + public static final int MODE_MASTER = 0; + public static final int MODE_RED = 1; + public static final int MODE_YELLOW = 2; + public static final int MODE_GREEN = 3; + public static final int MODE_CYAN = 4; + public static final int MODE_BLUE = 5; + public static final int MODE_MAGENTA = 6; + private int mParameterMode = MODE_MASTER; + + private static int MINSAT = -100; + private static int MAXSAT = 100; + private BasicParameterInt mParamMaster = new BasicParameterInt(MODE_MASTER, 0, MINSAT, MAXSAT); + private BasicParameterInt mParamRed = new BasicParameterInt(MODE_RED, 0, MINSAT, MAXSAT); + private BasicParameterInt mParamYellow = new BasicParameterInt(MODE_YELLOW, 0, MINSAT, MAXSAT); + private BasicParameterInt mParamGreen = new BasicParameterInt(MODE_GREEN, 0, MINSAT, MAXSAT); + private BasicParameterInt mParamCyan = new BasicParameterInt(MODE_CYAN, 0, MINSAT, MAXSAT); + private BasicParameterInt mParamBlue = new BasicParameterInt(MODE_BLUE, 0, MINSAT, MAXSAT); + private BasicParameterInt mParamMagenta = new BasicParameterInt(MODE_MAGENTA, 0, MINSAT, MAXSAT); + + private BasicParameterInt[] mAllParam = { + mParamMaster, + mParamRed, + mParamYellow, + mParamGreen, + mParamCyan, + mParamBlue, + mParamMagenta}; + + public FilterChanSatRepresentation() { + super("ChannelSaturation"); + setTextId(R.string.saturation); + setFilterType(FilterRepresentation.TYPE_NORMAL); + setSerializationName(SERIALIZATION_NAME); + setFilterClass(ImageFilterChanSat.class); + setEditorId(EditorChanSat.ID); + } + + public String toString() { + return getName() + " : " + mParamRed + ", " + mParamCyan + ", " + mParamRed + + ", " + mParamGreen + ", " + mParamMaster + ", " + mParamYellow; + } + + @Override + public FilterRepresentation copy() { + FilterChanSatRepresentation representation = new FilterChanSatRepresentation(); + copyAllParameters(representation); + return representation; + } + + @Override + protected void copyAllParameters(FilterRepresentation representation) { + super.copyAllParameters(representation); + representation.useParametersFrom(this); + } + + public void useParametersFrom(FilterRepresentation a) { + if (a instanceof FilterChanSatRepresentation) { + FilterChanSatRepresentation representation = (FilterChanSatRepresentation) a; + + for (int i = 0; i < mAllParam.length; i++) { + mAllParam[i].copyFrom(representation.mAllParam[i]); + } + } + } + + @Override + public boolean equals(FilterRepresentation representation) { + if (!super.equals(representation)) { + return false; + } + if (representation instanceof FilterChanSatRepresentation) { + FilterChanSatRepresentation rep = (FilterChanSatRepresentation) representation; + for (int i = 0; i < mAllParam.length; i++) { + if (rep.getValue(i) != getValue(i)) + return false; + } + return true; + } + return false; + } + + public int getValue(int mode) { + return mAllParam[mode].getValue(); + } + + public void setValue(int mode, int value) { + mAllParam[mode].setValue(value); + } + + public int getMinimum() { + return mParamMaster.getMinimum(); + } + + public int getMaximum() { + return mParamMaster.getMaximum(); + } + + public int getParameterMode() { + return mParameterMode; + } + + public void setParameterMode(int parameterMode) { + mParameterMode = parameterMode; + } + + public int getCurrentParameter() { + return getValue(mParameterMode); + } + + public void setCurrentParameter(int value) { + setValue(mParameterMode, value); + } + + @Override + public int getNumberOfParameters() { + return 6; + } + + @Override + public Parameter getFilterParameter(int index) { + return mAllParam[index]; + } + + @Override + public void serializeRepresentation(JsonWriter writer) throws IOException { + writer.beginObject(); + + writer.name(ARGS); + writer.beginArray(); + writer.value(getValue(MODE_MASTER)); + writer.value(getValue(MODE_RED)); + writer.value(getValue(MODE_YELLOW)); + writer.value(getValue(MODE_GREEN)); + writer.value(getValue(MODE_CYAN)); + writer.value(getValue(MODE_BLUE)); + writer.value(getValue(MODE_MAGENTA)); + writer.endArray(); + writer.endObject(); + } + + @Override + public void deSerializeRepresentation(JsonReader sreader) throws IOException { + sreader.beginObject(); + + while (sreader.hasNext()) { + String name = sreader.nextName(); + if (name.startsWith(ARGS)) { + sreader.beginArray(); + sreader.hasNext(); + setValue(MODE_MASTER, sreader.nextInt()); + sreader.hasNext(); + setValue(MODE_RED, sreader.nextInt()); + sreader.hasNext(); + setValue(MODE_YELLOW, sreader.nextInt()); + sreader.hasNext(); + setValue(MODE_GREEN, sreader.nextInt()); + sreader.hasNext(); + setValue(MODE_CYAN, sreader.nextInt()); + sreader.hasNext(); + setValue(MODE_BLUE, sreader.nextInt()); + sreader.hasNext(); + setValue(MODE_MAGENTA, sreader.nextInt()); + sreader.hasNext(); + sreader.endArray(); + } else { + sreader.skipValue(); + } + } + sreader.endObject(); + } +}
\ No newline at end of file diff --git a/src/com/android/gallery3d/filtershow/filters/FilterColorBorderRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterColorBorderRepresentation.java new file mode 100644 index 000000000..94eb20631 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/FilterColorBorderRepresentation.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2013 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.filtershow.filters; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.editors.ImageOnlyEditor; + +public class FilterColorBorderRepresentation extends FilterRepresentation { + private int mColor; + private int mBorderSize; + private int mBorderRadius; + + public FilterColorBorderRepresentation(int color, int size, int radius) { + super("ColorBorder"); + mColor = color; + mBorderSize = size; + mBorderRadius = radius; + setFilterType(FilterRepresentation.TYPE_BORDER); + setTextId(R.string.borders); + setEditorId(ImageOnlyEditor.ID); + setShowParameterValue(false); + } + + public String toString() { + return "FilterBorder: " + getName(); + } + + @Override + public FilterRepresentation copy() { + FilterColorBorderRepresentation representation = new FilterColorBorderRepresentation(0,0,0); + copyAllParameters(representation); + return representation; + } + + @Override + protected void copyAllParameters(FilterRepresentation representation) { + super.copyAllParameters(representation); + representation.useParametersFrom(this); + } + + public void useParametersFrom(FilterRepresentation a) { + if (a instanceof FilterColorBorderRepresentation) { + FilterColorBorderRepresentation representation = (FilterColorBorderRepresentation) a; + setName(representation.getName()); + setColor(representation.getColor()); + setBorderSize(representation.getBorderSize()); + setBorderRadius(representation.getBorderRadius()); + } + } + + @Override + public boolean equals(FilterRepresentation representation) { + if (!super.equals(representation)) { + return false; + } + if (representation instanceof FilterColorBorderRepresentation) { + FilterColorBorderRepresentation border = (FilterColorBorderRepresentation) representation; + if (border.mColor == mColor + && border.mBorderSize == mBorderSize + && border.mBorderRadius == mBorderRadius) { + return true; + } + } + return false; + } + + public boolean allowsSingleInstanceOnly() { + return true; + } + + @Override + public int getTextId() { + return R.string.borders; + } + + public int getColor() { + return mColor; + } + + public void setColor(int color) { + mColor = color; + } + + public int getBorderSize() { + return mBorderSize; + } + + public void setBorderSize(int borderSize) { + mBorderSize = borderSize; + } + + public int getBorderRadius() { + return mBorderRadius; + } + + public void setBorderRadius(int borderRadius) { + mBorderRadius = borderRadius; + } +} diff --git a/src/com/android/gallery3d/filtershow/filters/FilterCropRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterCropRepresentation.java new file mode 100644 index 000000000..c1bd7b3bb --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/FilterCropRepresentation.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2013 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.filtershow.filters; + +import android.graphics.RectF; +import android.util.JsonReader; +import android.util.JsonWriter; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.editors.EditorCrop; + +import java.io.IOException; + +public class FilterCropRepresentation extends FilterRepresentation { + public static final String SERIALIZATION_NAME = "CROP"; + public static final String[] BOUNDS = { + "C0", "C1", "C2", "C3" + }; + private static final String TAG = FilterCropRepresentation.class.getSimpleName(); + + RectF mCrop = getNil(); + + public FilterCropRepresentation(RectF crop) { + super(FilterCropRepresentation.class.getSimpleName()); + setSerializationName(SERIALIZATION_NAME); + setShowParameterValue(true); + setFilterClass(FilterCropRepresentation.class); + setFilterType(FilterRepresentation.TYPE_GEOMETRY); + setTextId(R.string.crop); + setEditorId(EditorCrop.ID); + setCrop(crop); + } + + public FilterCropRepresentation(FilterCropRepresentation m) { + this(m.mCrop); + } + + public FilterCropRepresentation() { + this(sNilRect); + } + + public void set(FilterCropRepresentation r) { + mCrop.set(r.mCrop); + } + + @Override + public boolean equals(FilterRepresentation rep) { + if (!(rep instanceof FilterCropRepresentation)) { + return false; + } + FilterCropRepresentation crop = (FilterCropRepresentation) rep; + if (mCrop.bottom != crop.mCrop.bottom + || mCrop.left != crop.mCrop.left + || mCrop.right != crop.mCrop.right + || mCrop.top != crop.mCrop.top) { + return false; + } + return true; + } + + public RectF getCrop() { + return new RectF(mCrop); + } + + public void getCrop(RectF r) { + r.set(mCrop); + } + + public void setCrop(RectF crop) { + if (crop == null) { + throw new IllegalArgumentException("Argument to setCrop is null"); + } + mCrop.set(crop); + } + + /** + * Takes a crop rect contained by [0, 0, 1, 1] and scales it by the height + * and width of the image rect. + */ + public static void findScaledCrop(RectF crop, int bitmapWidth, int bitmapHeight) { + crop.left *= bitmapWidth; + crop.top *= bitmapHeight; + crop.right *= bitmapWidth; + crop.bottom *= bitmapHeight; + } + + /** + * Takes crop rect and normalizes it by scaling down by the height and width + * of the image rect. + */ + public static void findNormalizedCrop(RectF crop, int bitmapWidth, int bitmapHeight) { + crop.left /= bitmapWidth; + crop.top /= bitmapHeight; + crop.right /= bitmapWidth; + crop.bottom /= bitmapHeight; + } + + @Override + public boolean allowsSingleInstanceOnly() { + return true; + } + + @Override + public FilterRepresentation copy() { + return new FilterCropRepresentation(this); + } + + @Override + protected void copyAllParameters(FilterRepresentation representation) { + if (!(representation instanceof FilterCropRepresentation)) { + throw new IllegalArgumentException("calling copyAllParameters with incompatible types!"); + } + super.copyAllParameters(representation); + representation.useParametersFrom(this); + } + + @Override + public void useParametersFrom(FilterRepresentation a) { + if (!(a instanceof FilterCropRepresentation)) { + throw new IllegalArgumentException("calling useParametersFrom with incompatible types!"); + } + setCrop(((FilterCropRepresentation) a).mCrop); + } + + private static final RectF sNilRect = new RectF(0, 0, 1, 1); + + @Override + public boolean isNil() { + return mCrop.equals(sNilRect); + } + + public static RectF getNil() { + return new RectF(sNilRect); + } + + @Override + public void serializeRepresentation(JsonWriter writer) throws IOException { + writer.beginObject(); + writer.name(BOUNDS[0]).value(mCrop.left); + writer.name(BOUNDS[1]).value(mCrop.top); + writer.name(BOUNDS[2]).value(mCrop.right); + writer.name(BOUNDS[3]).value(mCrop.bottom); + writer.endObject(); + } + + @Override + public void deSerializeRepresentation(JsonReader reader) throws IOException { + reader.beginObject(); + while (reader.hasNext()) { + String name = reader.nextName(); + if (BOUNDS[0].equals(name)) { + mCrop.left = (float) reader.nextDouble(); + } else if (BOUNDS[1].equals(name)) { + mCrop.top = (float) reader.nextDouble(); + } else if (BOUNDS[2].equals(name)) { + mCrop.right = (float) reader.nextDouble(); + } else if (BOUNDS[3].equals(name)) { + mCrop.bottom = (float) reader.nextDouble(); + } else { + reader.skipValue(); + } + } + reader.endObject(); + } +} diff --git a/src/com/android/gallery3d/filtershow/filters/FilterCurvesRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterCurvesRepresentation.java new file mode 100644 index 000000000..edab2a08d --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/FilterCurvesRepresentation.java @@ -0,0 +1,170 @@ +package com.android.gallery3d.filtershow.filters; + +import android.util.JsonReader; +import android.util.JsonWriter; +import android.util.Log; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.imageshow.ControlPoint; +import com.android.gallery3d.filtershow.imageshow.Spline; + +import java.io.IOException; + +/** + * TODO: Insert description here. (generated by hoford) + */ +public class FilterCurvesRepresentation extends FilterRepresentation { + private static final String LOGTAG = "FilterCurvesRepresentation"; + public static final String SERIALIZATION_NAME = "Curve"; + private static final int MAX_SPLINE_NUMBER = 4; + + private Spline[] mSplines = new Spline[MAX_SPLINE_NUMBER]; + + public FilterCurvesRepresentation() { + super("Curves"); + setSerializationName("CURVES"); + setFilterClass(ImageFilterCurves.class); + setTextId(R.string.curvesRGB); + setOverlayId(R.drawable.filtershow_button_colors_curve); + setEditorId(R.id.imageCurves); + setShowParameterValue(false); + setSupportsPartialRendering(true); + reset(); + } + + @Override + public FilterRepresentation copy() { + FilterCurvesRepresentation representation = new FilterCurvesRepresentation(); + copyAllParameters(representation); + return representation; + } + + @Override + protected void copyAllParameters(FilterRepresentation representation) { + super.copyAllParameters(representation); + representation.useParametersFrom(this); + } + + @Override + public void useParametersFrom(FilterRepresentation a) { + if (!(a instanceof FilterCurvesRepresentation)) { + Log.v(LOGTAG, "cannot use parameters from " + a); + return; + } + FilterCurvesRepresentation representation = (FilterCurvesRepresentation) a; + Spline[] spline = new Spline[MAX_SPLINE_NUMBER]; + for (int i = 0; i < spline.length; i++) { + Spline sp = representation.mSplines[i]; + if (sp != null) { + spline[i] = new Spline(sp); + } else { + spline[i] = new Spline(); + } + } + mSplines = spline; + } + + @Override + public boolean isNil() { + for (int i = 0; i < MAX_SPLINE_NUMBER; i++) { + if (getSpline(i) != null && !getSpline(i).isOriginal()) { + return false; + } + } + return true; + } + + @Override + public boolean equals(FilterRepresentation representation) { + if (!super.equals(representation)) { + return false; + } + + if (!(representation instanceof FilterCurvesRepresentation)) { + return false; + } else { + FilterCurvesRepresentation curve = + (FilterCurvesRepresentation) representation; + for (int i = 0; i < MAX_SPLINE_NUMBER; i++) { + if (!getSpline(i).sameValues(curve.getSpline(i))) { + return false; + } + } + } + // Every spline matches, therefore they are the same. + return true; + } + + public void reset() { + Spline spline = new Spline(); + + spline.addPoint(0.0f, 1.0f); + spline.addPoint(1.0f, 0.0f); + + for (int i = 0; i < MAX_SPLINE_NUMBER; i++) { + mSplines[i] = new Spline(spline); + } + } + + public void setSpline(int splineIndex, Spline s) { + mSplines[splineIndex] = s; + } + + public Spline getSpline(int splineIndex) { + return mSplines[splineIndex]; + } + + @Override + public void serializeRepresentation(JsonWriter writer) throws IOException { + writer.beginObject(); + { + writer.name(NAME_TAG); + writer.value(getName()); + for (int i = 0; i < mSplines.length; i++) { + writer.name(SERIALIZATION_NAME + i); + writer.beginArray(); + int nop = mSplines[i].getNbPoints(); + for (int j = 0; j < nop; j++) { + ControlPoint p = mSplines[i].getPoint(j); + writer.beginArray(); + writer.value(p.x); + writer.value(p.y); + writer.endArray(); + } + writer.endArray(); + } + + } + writer.endObject(); + } + + @Override + public void deSerializeRepresentation(JsonReader sreader) throws IOException { + sreader.beginObject(); + Spline[] spline = new Spline[MAX_SPLINE_NUMBER]; + while (sreader.hasNext()) { + String name = sreader.nextName(); + if (NAME_TAG.equals(name)) { + setName(sreader.nextString()); + } else if (name.startsWith(SERIALIZATION_NAME)) { + int curveNo = Integer.parseInt(name.substring(SERIALIZATION_NAME.length())); + spline[curveNo] = new Spline(); + sreader.beginArray(); + while (sreader.hasNext()) { + sreader.beginArray(); + sreader.hasNext(); + float x = (float) sreader.nextDouble(); + sreader.hasNext(); + float y = (float) sreader.nextDouble(); + sreader.endArray(); + spline[curveNo].addPoint(x, y); + } + sreader.endArray(); + + } + } + mSplines = spline; + sreader.endObject(); + } + +} diff --git a/src/com/android/gallery3d/filtershow/filters/FilterDirectRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterDirectRepresentation.java new file mode 100644 index 000000000..ac0cb7492 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/FilterDirectRepresentation.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2013 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.filtershow.filters; + +public class FilterDirectRepresentation extends FilterRepresentation { + + @Override + public FilterRepresentation copy() { + FilterDirectRepresentation representation = new FilterDirectRepresentation(getName()); + copyAllParameters(representation); + return representation; + } + + @Override + protected void copyAllParameters(FilterRepresentation representation) { + super.copyAllParameters(representation); + representation.useParametersFrom(this); + } + + public FilterDirectRepresentation(String name) { + super(name); + } + +} diff --git a/src/com/android/gallery3d/filtershow/filters/FilterDrawRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterDrawRepresentation.java new file mode 100644 index 000000000..977dbeac5 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/FilterDrawRepresentation.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2013 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.filtershow.filters; + +import android.graphics.Path; +import android.util.Log; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.editors.EditorDraw; + +import java.util.Vector; + +public class FilterDrawRepresentation extends FilterRepresentation { + private static final String LOGTAG = "FilterDrawRepresentation"; + + public static class StrokeData implements Cloneable { + public byte mType; + public Path mPath; + public float mRadius; + public int mColor; + public int noPoints = 0; + @Override + public String toString() { + return "stroke(" + mType + ", path(" + (mPath) + "), " + mRadius + " , " + + Integer.toHexString(mColor) + ")"; + } + @Override + public StrokeData clone() throws CloneNotSupportedException { + return (StrokeData) super.clone(); + } + } + + private Vector<StrokeData> mDrawing = new Vector<StrokeData>(); + private StrokeData mCurrent; // used in the currently drawing style + + public FilterDrawRepresentation() { + super("Draw"); + setFilterClass(ImageFilterDraw.class); + setSerializationName("DRAW"); + setFilterType(FilterRepresentation.TYPE_VIGNETTE); + setTextId(R.string.imageDraw); + setEditorId(EditorDraw.ID); + setOverlayId(R.drawable.filtershow_drawing); + setOverlayOnly(true); + } + + @Override + public String toString() { + return getName() + " : strokes=" + mDrawing.size() + + ((mCurrent == null) ? " no current " + : ("draw=" + mCurrent.mType + " " + mCurrent.noPoints)); + } + + public Vector<StrokeData> getDrawing() { + return mDrawing; + } + + public StrokeData getCurrentDrawing() { + return mCurrent; + } + + @Override + public FilterRepresentation copy() { + FilterDrawRepresentation representation = new FilterDrawRepresentation(); + copyAllParameters(representation); + return representation; + } + + @Override + protected void copyAllParameters(FilterRepresentation representation) { + super.copyAllParameters(representation); + representation.useParametersFrom(this); + } + + @Override + public boolean isNil() { + return getDrawing().isEmpty(); + } + + @Override + public void useParametersFrom(FilterRepresentation a) { + if (a instanceof FilterDrawRepresentation) { + FilterDrawRepresentation representation = (FilterDrawRepresentation) a; + try { + if (representation.mCurrent != null) { + mCurrent = (StrokeData) representation.mCurrent.clone(); + } else { + mCurrent = null; + } + if (representation.mDrawing != null) { + mDrawing = (Vector<StrokeData>) representation.mDrawing.clone(); + } else { + mDrawing = null; + } + + } catch (CloneNotSupportedException e) { + e.printStackTrace(); + } + } else { + Log.v(LOGTAG, "cannot use parameters from " + a); + } + } + + @Override + public boolean equals(FilterRepresentation representation) { + if (!super.equals(representation)) { + return false; + } + if (representation instanceof FilterDrawRepresentation) { + FilterDrawRepresentation fdRep = (FilterDrawRepresentation) representation; + if (fdRep.mDrawing.size() != mDrawing.size()) + return false; + if (fdRep.mCurrent == null && mCurrent.mPath == null) { + return true; + } + if (fdRep.mCurrent != null && mCurrent.mPath != null) { + if (fdRep.mCurrent.noPoints == mCurrent.noPoints) { + return true; + } + return false; + } + } + return false; + } + + public void startNewSection(byte type, int color, float size, float x, float y) { + mCurrent = new StrokeData(); + mCurrent.mColor = color; + mCurrent.mRadius = size; + mCurrent.mType = type; + mCurrent.mPath = new Path(); + mCurrent.mPath.moveTo(x, y); + mCurrent.noPoints = 0; + } + + public void addPoint(float x, float y) { + mCurrent.noPoints++; + mCurrent.mPath.lineTo(x, y); + } + + public void endSection(float x, float y) { + mCurrent.mPath.lineTo(x, y); + mCurrent.noPoints++; + mDrawing.add(mCurrent); + mCurrent = null; + } + + public void clearCurrentSection() { + mCurrent = null; + } + + public void clear() { + mCurrent = null; + mDrawing.clear(); + } + +} diff --git a/src/com/android/gallery3d/filtershow/filters/FilterFxRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterFxRepresentation.java new file mode 100644 index 000000000..e5a6fdd23 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/FilterFxRepresentation.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2013 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.filtershow.filters; + +import com.android.gallery3d.filtershow.editors.ImageOnlyEditor; + +public class FilterFxRepresentation extends FilterRepresentation { + private static final String LOGTAG = "FilterFxRepresentation"; + // TODO: When implementing serialization, we should find a unique way of + // specifying bitmaps / names (the resource IDs being random) + private int mBitmapResource = 0; + private int mNameResource = 0; + + public FilterFxRepresentation(String name, int bitmapResource, int nameResource) { + super(name); + setFilterClass(ImageFilterFx.class); + mBitmapResource = bitmapResource; + mNameResource = nameResource; + setFilterType(FilterRepresentation.TYPE_FX); + setTextId(nameResource); + setEditorId(ImageOnlyEditor.ID); + setShowParameterValue(false); + setSupportsPartialRendering(true); + } + + @Override + public String toString() { + return "FilterFx: " + hashCode() + " : " + getName() + " bitmap rsc: " + mBitmapResource; + } + + @Override + public FilterRepresentation copy() { + FilterFxRepresentation representation = new FilterFxRepresentation(getName(),0,0); + copyAllParameters(representation); + return representation; + } + + @Override + protected void copyAllParameters(FilterRepresentation representation) { + super.copyAllParameters(representation); + representation.useParametersFrom(this); + } + + @Override + public synchronized void useParametersFrom(FilterRepresentation a) { + if (a instanceof FilterFxRepresentation) { + FilterFxRepresentation representation = (FilterFxRepresentation) a; + setName(representation.getName()); + setSerializationName(representation.getSerializationName()); + setBitmapResource(representation.getBitmapResource()); + setNameResource(representation.getNameResource()); + } + } + + @Override + public boolean equals(FilterRepresentation representation) { + if (!super.equals(representation)) { + return false; + } + if (representation instanceof FilterFxRepresentation) { + FilterFxRepresentation fx = (FilterFxRepresentation) representation; + if (fx.mNameResource == mNameResource + && fx.mBitmapResource == mBitmapResource) { + return true; + } + } + return false; + } + + @Override + public boolean same(FilterRepresentation representation) { + if (!super.same(representation)) { + return false; + } + return equals(representation); + } + + @Override + public boolean allowsSingleInstanceOnly() { + return true; + } + + public int getNameResource() { + return mNameResource; + } + + public void setNameResource(int nameResource) { + mNameResource = nameResource; + } + + public int getBitmapResource() { + return mBitmapResource; + } + + public void setBitmapResource(int bitmapResource) { + mBitmapResource = bitmapResource; + } +} diff --git a/src/com/android/gallery3d/filtershow/filters/FilterGradRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterGradRepresentation.java new file mode 100644 index 000000000..0c272d48a --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/FilterGradRepresentation.java @@ -0,0 +1,497 @@ +/* + * Copyright (C) 2013 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.filtershow.filters; + +import android.graphics.Rect; +import android.util.JsonReader; +import android.util.JsonWriter; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.editors.EditorGrad; +import com.android.gallery3d.filtershow.imageshow.MasterImage; +import com.android.gallery3d.filtershow.imageshow.Line; + +import java.io.IOException; +import java.util.Vector; + +public class FilterGradRepresentation extends FilterRepresentation + implements Line { + private static final String LOGTAG = "FilterGradRepresentation"; + public static final int MAX_POINTS = 16; + public static final int PARAM_BRIGHTNESS = 0; + public static final int PARAM_SATURATION = 1; + public static final int PARAM_CONTRAST = 2; + private static final double ADD_MIN_DIST = .05; + private static String LINE_NAME = "Point"; + private static final String SERIALIZATION_NAME = "grad"; + + public FilterGradRepresentation() { + super("Grad"); + setSerializationName(SERIALIZATION_NAME); + creatExample(); + setOverlayId(R.drawable.filtershow_button_grad); + setFilterClass(ImageFilterGrad.class); + setTextId(R.string.grad); + setEditorId(EditorGrad.ID); + } + + public void trimVector(){ + int n = mBands.size(); + for (int i = n; i < MAX_POINTS; i++) { + mBands.add(new Band()); + } + for (int i = MAX_POINTS; i < n; i++) { + mBands.remove(i); + } + } + + Vector<Band> mBands = new Vector<Band>(); + Band mCurrentBand; + + static class Band { + private boolean mask = true; + + private int xPos1 = -1; + private int yPos1 = 100; + private int xPos2 = -1; + private int yPos2 = 100; + private int brightness = 40; + private int contrast = 0; + private int saturation = 0; + + + public Band() { + } + + public Band(int x, int y) { + xPos1 = x; + yPos1 = y+30; + xPos2 = x; + yPos2 = y-30; + } + + public Band(Band copy) { + mask = copy.mask; + xPos1 = copy.xPos1; + yPos1 = copy.yPos1; + xPos2 = copy.xPos2; + yPos2 = copy.yPos2; + brightness = copy.brightness; + contrast = copy.contrast; + saturation = copy.saturation; + } + + } + + @Override + public String toString() { + int count = 0; + for (Band point : mBands) { + if (!point.mask) { + count++; + } + } + return "c=" + mBands.indexOf(mBands) + "[" + mBands.size() + "]" + count; + } + + private void creatExample() { + Band p = new Band(); + p.mask = false; + p.xPos1 = -1; + p.yPos1 = 100; + p.xPos2 = -1; + p.yPos2 = 100; + p.brightness = 40; + p.contrast = 0; + p.saturation = 0; + mBands.add(0, p); + mCurrentBand = p; + trimVector(); + } + + @Override + public void useParametersFrom(FilterRepresentation a) { + FilterGradRepresentation rep = (FilterGradRepresentation) a; + Vector<Band> tmpBands = new Vector<Band>(); + int n = (rep.mCurrentBand == null) ? 0 : rep.mBands.indexOf(rep.mCurrentBand); + for (Band band : rep.mBands) { + tmpBands.add(new Band(band)); + } + mCurrentBand = null; + mBands = tmpBands; + mCurrentBand = mBands.elementAt(n); + } + + @Override + public FilterRepresentation copy() { + FilterGradRepresentation representation = new FilterGradRepresentation(); + copyAllParameters(representation); + return representation; + } + + @Override + protected void copyAllParameters(FilterRepresentation representation) { + super.copyAllParameters(representation); + representation.useParametersFrom(this); + } + + @Override + public boolean equals(FilterRepresentation representation) { + if (representation instanceof FilterGradRepresentation) { + FilterGradRepresentation rep = (FilterGradRepresentation) representation; + int n = getNumberOfBands(); + if (rep.getNumberOfBands() != n) { + return false; + } + for (int i = 0; i < mBands.size(); i++) { + Band b1 = mBands.get(i); + Band b2 = rep.mBands.get(i); + if (b1.mask != b2.mask + || b1.brightness != b2.brightness + || b1.contrast != b2.contrast + || b1.saturation != b2.saturation + || b1.xPos1 != b2.xPos1 + || b1.xPos2 != b2.xPos2 + || b1.yPos1 != b2.yPos1 + || b1.yPos2 != b2.yPos2) { + return false; + } + } + return true; + } + return false; + } + + public int getNumberOfBands() { + int count = 0; + for (Band point : mBands) { + if (!point.mask) { + count++; + } + } + return count; + } + + public int addBand(Rect rect) { + mBands.add(0, mCurrentBand = new Band(rect.centerX(), rect.centerY())); + mCurrentBand.mask = false; + int x = (mCurrentBand.xPos1 + mCurrentBand.xPos2)/2; + int y = (mCurrentBand.yPos1 + mCurrentBand.yPos2)/2; + double addDelta = ADD_MIN_DIST * Math.max(rect.width(), rect.height()); + boolean moved = true; + int count = 0; + int toMove = mBands.indexOf(mCurrentBand); + + while (moved) { + moved = false; + count++; + if (count > 14) { + break; + } + + for (Band point : mBands) { + if (point.mask) { + break; + } + } + + for (Band point : mBands) { + if (point.mask) { + break; + } + int index = mBands.indexOf(point); + + if (toMove != index) { + double dist = Math.hypot(point.xPos1 - x, point.yPos1 - y); + if (dist < addDelta) { + moved = true; + mCurrentBand.xPos1 += addDelta; + mCurrentBand.yPos1 += addDelta; + mCurrentBand.xPos2 += addDelta; + mCurrentBand.yPos2 += addDelta; + x = (mCurrentBand.xPos1 + mCurrentBand.xPos2)/2; + y = (mCurrentBand.yPos1 + mCurrentBand.yPos2)/2; + + if (mCurrentBand.yPos1 > rect.bottom) { + mCurrentBand.yPos1 = (int) (rect.top + addDelta); + } + if (mCurrentBand.xPos1 > rect.right) { + mCurrentBand.xPos1 = (int) (rect.left + addDelta); + } + } + } + } + } + trimVector(); + return 0; + } + + public void deleteCurrentBand() { + int index = mBands.indexOf(mCurrentBand); + mBands.remove(mCurrentBand); + trimVector(); + if (getNumberOfBands() == 0) { + addBand(MasterImage.getImage().getOriginalBounds()); + } + mCurrentBand = mBands.get(0); + } + + public void nextPoint(){ + int index = mBands.indexOf(mCurrentBand); + int tmp = index; + Band point; + int k = 0; + do { + index = (index+1)% mBands.size(); + point = mBands.get(index); + if (k++ >= mBands.size()) { + break; + } + } + while (point.mask == true); + mCurrentBand = mBands.get(index); + } + + public void setSelectedPoint(int pos) { + mCurrentBand = mBands.get(pos); + } + + public int getSelectedPoint() { + return mBands.indexOf(mCurrentBand); + } + + public boolean[] getMask() { + boolean[] ret = new boolean[mBands.size()]; + int i = 0; + for (Band point : mBands) { + ret[i++] = !point.mask; + } + return ret; + } + + public int[] getXPos1() { + int[] ret = new int[mBands.size()]; + int i = 0; + for (Band point : mBands) { + ret[i++] = point.xPos1; + } + return ret; + } + + public int[] getYPos1() { + int[] ret = new int[mBands.size()]; + int i = 0; + for (Band point : mBands) { + ret[i++] = point.yPos1; + } + return ret; + } + + public int[] getXPos2() { + int[] ret = new int[mBands.size()]; + int i = 0; + for (Band point : mBands) { + ret[i++] = point.xPos2; + } + return ret; + } + + public int[] getYPos2() { + int[] ret = new int[mBands.size()]; + int i = 0; + for (Band point : mBands) { + ret[i++] = point.yPos2; + } + return ret; + } + + public int[] getBrightness() { + int[] ret = new int[mBands.size()]; + int i = 0; + for (Band point : mBands) { + ret[i++] = point.brightness; + } + return ret; + } + + public int[] getContrast() { + int[] ret = new int[mBands.size()]; + int i = 0; + for (Band point : mBands) { + ret[i++] = point.contrast; + } + return ret; + } + + public int[] getSaturation() { + int[] ret = new int[mBands.size()]; + int i = 0; + for (Band point : mBands) { + ret[i++] = point.saturation; + } + return ret; + } + + public int getParameter(int type) { + switch (type){ + case PARAM_BRIGHTNESS: + return mCurrentBand.brightness; + case PARAM_SATURATION: + return mCurrentBand.saturation; + case PARAM_CONTRAST: + return mCurrentBand.contrast; + } + throw new IllegalArgumentException("no such type " + type); + } + + public int getParameterMax(int type) { + switch (type) { + case PARAM_BRIGHTNESS: + return 100; + case PARAM_SATURATION: + return 100; + case PARAM_CONTRAST: + return 100; + } + throw new IllegalArgumentException("no such type " + type); + } + + public int getParameterMin(int type) { + switch (type) { + case PARAM_BRIGHTNESS: + return -100; + case PARAM_SATURATION: + return -100; + case PARAM_CONTRAST: + return -100; + } + throw new IllegalArgumentException("no such type " + type); + } + + public void setParameter(int type, int value) { + mCurrentBand.mask = false; + switch (type) { + case PARAM_BRIGHTNESS: + mCurrentBand.brightness = value; + break; + case PARAM_SATURATION: + mCurrentBand.saturation = value; + break; + case PARAM_CONTRAST: + mCurrentBand.contrast = value; + break; + default: + throw new IllegalArgumentException("no such type " + type); + } + } + + @Override + public void setPoint1(float x, float y) { + mCurrentBand.xPos1 = (int)x; + mCurrentBand.yPos1 = (int)y; + } + + @Override + public void setPoint2(float x, float y) { + mCurrentBand.xPos2 = (int)x; + mCurrentBand.yPos2 = (int)y; + } + + @Override + public float getPoint1X() { + return mCurrentBand.xPos1; + } + + @Override + public float getPoint1Y() { + return mCurrentBand.yPos1; + } + @Override + public float getPoint2X() { + return mCurrentBand.xPos2; + } + + @Override + public float getPoint2Y() { + return mCurrentBand.yPos2; + } + + @Override + public void serializeRepresentation(JsonWriter writer) throws IOException { + writer.beginObject(); + int len = mBands.size(); + int count = 0; + + for (int i = 0; i < len; i++) { + Band point = mBands.get(i); + if (point.mask) { + continue; + } + writer.name(LINE_NAME + count); + count++; + writer.beginArray(); + writer.value(point.xPos1); + writer.value(point.yPos1); + writer.value(point.xPos2); + writer.value(point.yPos2); + writer.value(point.brightness); + writer.value(point.contrast); + writer.value(point.saturation); + writer.endArray(); + } + writer.endObject(); + } + + @Override + public void deSerializeRepresentation(JsonReader sreader) throws IOException { + sreader.beginObject(); + Vector<Band> points = new Vector<Band>(); + + while (sreader.hasNext()) { + String name = sreader.nextName(); + if (name.startsWith(LINE_NAME)) { + int pointNo = Integer.parseInt(name.substring(LINE_NAME.length())); + sreader.beginArray(); + Band p = new Band(); + p.mask = false; + sreader.hasNext(); + p.xPos1 = sreader.nextInt(); + sreader.hasNext(); + p.yPos1 = sreader.nextInt(); + sreader.hasNext(); + p.xPos2 = sreader.nextInt(); + sreader.hasNext(); + p.yPos2 = sreader.nextInt(); + sreader.hasNext(); + p.brightness = sreader.nextInt(); + sreader.hasNext(); + p.contrast = sreader.nextInt(); + sreader.hasNext(); + p.saturation = sreader.nextInt(); + sreader.hasNext(); + sreader.endArray(); + points.add(p); + + } else { + sreader.skipValue(); + } + } + mBands = points; + trimVector(); + mCurrentBand = mBands.get(0); + sreader.endObject(); + } +} diff --git a/src/com/android/gallery3d/filtershow/filters/FilterImageBorderRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterImageBorderRepresentation.java new file mode 100644 index 000000000..f310a2be1 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/FilterImageBorderRepresentation.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2013 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.filtershow.filters; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.editors.ImageOnlyEditor; + +public class FilterImageBorderRepresentation extends FilterRepresentation { + private int mDrawableResource = 0; + + public FilterImageBorderRepresentation(int drawableResource) { + super("ImageBorder"); + setFilterClass(ImageFilterBorder.class); + mDrawableResource = drawableResource; + setFilterType(FilterRepresentation.TYPE_BORDER); + setTextId(R.string.borders); + setEditorId(ImageOnlyEditor.ID); + setShowParameterValue(false); + } + + public String toString() { + return "FilterBorder: " + getName(); + } + + @Override + public FilterRepresentation copy() { + FilterImageBorderRepresentation representation = + new FilterImageBorderRepresentation(mDrawableResource); + copyAllParameters(representation); + return representation; + } + + @Override + protected void copyAllParameters(FilterRepresentation representation) { + super.copyAllParameters(representation); + representation.useParametersFrom(this); + } + + public void useParametersFrom(FilterRepresentation a) { + if (a instanceof FilterImageBorderRepresentation) { + FilterImageBorderRepresentation representation = (FilterImageBorderRepresentation) a; + setName(representation.getName()); + setDrawableResource(representation.getDrawableResource()); + } + } + + @Override + public boolean equals(FilterRepresentation representation) { + if (!super.equals(representation)) { + return false; + } + if (representation instanceof FilterImageBorderRepresentation) { + FilterImageBorderRepresentation border = (FilterImageBorderRepresentation) representation; + if (border.mDrawableResource == mDrawableResource) { + return true; + } + } + return false; + } + + @Override + public int getTextId() { + return R.string.none; + } + + public boolean allowsSingleInstanceOnly() { + return true; + } + + public int getDrawableResource() { + return mDrawableResource; + } + + public void setDrawableResource(int drawableResource) { + mDrawableResource = drawableResource; + } +} diff --git a/src/com/android/gallery3d/filtershow/filters/FilterMirrorRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterMirrorRepresentation.java new file mode 100644 index 000000000..8dcff0d16 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/FilterMirrorRepresentation.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2013 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.filtershow.filters; + +import android.util.JsonReader; +import android.util.JsonWriter; +import android.util.Log; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.editors.EditorMirror; + +import java.io.IOException; + +public class FilterMirrorRepresentation extends FilterRepresentation { + public static final String SERIALIZATION_NAME = "MIRROR"; + private static final String SERIALIZATION_MIRROR_VALUE = "value"; + private static final String TAG = FilterMirrorRepresentation.class.getSimpleName(); + + Mirror mMirror; + + public enum Mirror { + NONE('N'), VERTICAL('V'), HORIZONTAL('H'), BOTH('B'); + char mValue; + + private Mirror(char value) { + mValue = value; + } + + public char value() { + return mValue; + } + + public static Mirror fromValue(char value) { + switch (value) { + case 'N': + return NONE; + case 'V': + return VERTICAL; + case 'H': + return HORIZONTAL; + case 'B': + return BOTH; + default: + return null; + } + } + } + + public FilterMirrorRepresentation(Mirror mirror) { + super(FilterMirrorRepresentation.class.getSimpleName()); + setSerializationName(SERIALIZATION_NAME); + setShowParameterValue(true); + setFilterClass(FilterMirrorRepresentation.class); + setFilterType(FilterRepresentation.TYPE_GEOMETRY); + setTextId(R.string.mirror); + setEditorId(EditorMirror.ID); + setMirror(mirror); + } + + public FilterMirrorRepresentation(FilterMirrorRepresentation m) { + this(m.getMirror()); + } + + public FilterMirrorRepresentation() { + this(getNil()); + } + + @Override + public boolean equals(FilterRepresentation rep) { + if (!(rep instanceof FilterMirrorRepresentation)) { + return false; + } + FilterMirrorRepresentation mirror = (FilterMirrorRepresentation) rep; + if (mMirror != mirror.mMirror) { + return false; + } + return true; + } + + public Mirror getMirror() { + return mMirror; + } + + public void set(FilterMirrorRepresentation r) { + mMirror = r.mMirror; + } + + public void setMirror(Mirror mirror) { + if (mirror == null) { + throw new IllegalArgumentException("Argument to setMirror is null"); + } + mMirror = mirror; + } + + public void cycle() { + switch (mMirror) { + case NONE: + mMirror = Mirror.HORIZONTAL; + break; + case HORIZONTAL: + mMirror = Mirror.VERTICAL; + break; + case VERTICAL: + mMirror = Mirror.BOTH; + break; + case BOTH: + mMirror = Mirror.NONE; + break; + } + } + + @Override + public boolean allowsSingleInstanceOnly() { + return true; + } + + @Override + public FilterRepresentation copy() { + return new FilterMirrorRepresentation(this); + } + + @Override + protected void copyAllParameters(FilterRepresentation representation) { + if (!(representation instanceof FilterMirrorRepresentation)) { + throw new IllegalArgumentException("calling copyAllParameters with incompatible types!"); + } + super.copyAllParameters(representation); + representation.useParametersFrom(this); + } + + @Override + public void useParametersFrom(FilterRepresentation a) { + if (!(a instanceof FilterMirrorRepresentation)) { + throw new IllegalArgumentException("calling useParametersFrom with incompatible types!"); + } + setMirror(((FilterMirrorRepresentation) a).getMirror()); + } + + @Override + public boolean isNil() { + return mMirror == getNil(); + } + + public static Mirror getNil() { + return Mirror.NONE; + } + + @Override + public void serializeRepresentation(JsonWriter writer) throws IOException { + writer.beginObject(); + writer.name(SERIALIZATION_MIRROR_VALUE).value(mMirror.value()); + writer.endObject(); + } + + @Override + public void deSerializeRepresentation(JsonReader reader) throws IOException { + boolean unset = true; + reader.beginObject(); + while (reader.hasNext()) { + String name = reader.nextName(); + if (SERIALIZATION_MIRROR_VALUE.equals(name)) { + Mirror r = Mirror.fromValue((char) reader.nextInt()); + if (r != null) { + setMirror(r); + unset = false; + } + } else { + reader.skipValue(); + } + } + if (unset) { + Log.w(TAG, "WARNING: bad value when deserializing " + SERIALIZATION_NAME); + } + reader.endObject(); + } +} diff --git a/src/com/android/gallery3d/filtershow/filters/FilterPoint.java b/src/com/android/gallery3d/filtershow/filters/FilterPoint.java new file mode 100644 index 000000000..4520717a1 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/FilterPoint.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2013 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.filtershow.filters; + +public interface FilterPoint { + +} diff --git a/src/com/android/gallery3d/filtershow/filters/FilterPointRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterPointRepresentation.java new file mode 100644 index 000000000..9bd1699d9 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/FilterPointRepresentation.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2013 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.filtershow.filters; + +import java.util.Vector; + +public abstract class FilterPointRepresentation extends FilterRepresentation { + private static final String LOGTAG = "FilterPointRepresentation"; + private Vector<FilterPoint> mCandidates = new Vector<FilterPoint>(); + + public FilterPointRepresentation(String type, int textid, int editorID) { + super(type); + setFilterClass(ImageFilterRedEye.class); + setFilterType(FilterRepresentation.TYPE_NORMAL); + setTextId(textid); + setEditorId(editorID); + } + + @Override + public abstract FilterRepresentation copy(); + + @Override + protected void copyAllParameters(FilterRepresentation representation) { + super.copyAllParameters(representation); + representation.useParametersFrom(this); + } + + public boolean hasCandidates() { + return mCandidates != null; + } + + public Vector<FilterPoint> getCandidates() { + return mCandidates; + } + + @Override + public boolean isNil() { + if (getCandidates() != null && getCandidates().size() > 0) { + return false; + } + return true; + } + + public Object getCandidate(int index) { + return this.mCandidates.get(index); + } + + public void addCandidate(FilterPoint c) { + this.mCandidates.add(c); + } + + @Override + public void useParametersFrom(FilterRepresentation a) { + if (a instanceof FilterPointRepresentation) { + FilterPointRepresentation representation = (FilterPointRepresentation) a; + mCandidates.clear(); + for (FilterPoint redEyeCandidate : representation.mCandidates) { + mCandidates.add(redEyeCandidate); + } + } + } + + public void removeCandidate(RedEyeCandidate c) { + this.mCandidates.remove(c); + } + + public void clearCandidates() { + this.mCandidates.clear(); + } + + public int getNumberOfCandidates() { + return mCandidates.size(); + } +} diff --git a/src/com/android/gallery3d/filtershow/filters/FilterRedEyeRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterRedEyeRepresentation.java new file mode 100644 index 000000000..dd06a9760 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/FilterRedEyeRepresentation.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2013 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.filtershow.filters; + +import android.graphics.RectF; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.editors.EditorRedEye; + +import java.util.Vector; + +public class FilterRedEyeRepresentation extends FilterPointRepresentation { + private static final String LOGTAG = "FilterRedEyeRepresentation"; + + public FilterRedEyeRepresentation() { + super("RedEye",R.string.redeye,EditorRedEye.ID); + setSerializationName("REDEYE"); + setFilterClass(ImageFilterRedEye.class); + setOverlayId(R.drawable.photoeditor_effect_redeye); + setOverlayOnly(true); + } + + @Override + public FilterRepresentation copy() { + FilterRedEyeRepresentation representation = new FilterRedEyeRepresentation(); + copyAllParameters(representation); + return representation; + } + + @Override + protected void copyAllParameters(FilterRepresentation representation) { + super.copyAllParameters(representation); + representation.useParametersFrom(this); + } + + public void addRect(RectF rect, RectF bounds) { + Vector<RedEyeCandidate> intersects = new Vector<RedEyeCandidate>(); + for (int i = 0; i < getCandidates().size(); i++) { + RedEyeCandidate r = (RedEyeCandidate) getCandidate(i); + if (r.intersect(rect)) { + intersects.add(r); + } + } + for (int i = 0; i < intersects.size(); i++) { + RedEyeCandidate r = intersects.elementAt(i); + rect.union(r.mRect); + bounds.union(r.mBounds); + removeCandidate(r); + } + addCandidate(new RedEyeCandidate(rect, bounds)); + } + +} diff --git a/src/com/android/gallery3d/filtershow/filters/FilterRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterRepresentation.java new file mode 100644 index 000000000..5b33ffba5 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/FilterRepresentation.java @@ -0,0 +1,262 @@ +/* + * Copyright (C) 2013 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.filtershow.filters; + +import android.util.JsonReader; +import android.util.JsonWriter; +import android.util.Log; + +import com.android.gallery3d.filtershow.editors.BasicEditor; + +import java.io.IOException; +import java.util.ArrayList; + +public class FilterRepresentation { + private static final String LOGTAG = "FilterRepresentation"; + private static final boolean DEBUG = false; + private String mName; + private int mPriority = TYPE_NORMAL; + private Class<?> mFilterClass; + private boolean mSupportsPartialRendering = false; + private int mTextId = 0; + private int mEditorId = BasicEditor.ID; + private int mButtonId = 0; + private int mOverlayId = 0; + private boolean mOverlayOnly = false; + private boolean mShowParameterValue = true; + private String mSerializationName; + public static final byte TYPE_BORDER = 1; + public static final byte TYPE_FX = 2; + public static final byte TYPE_WBALANCE = 3; + public static final byte TYPE_VIGNETTE = 4; + public static final byte TYPE_NORMAL = 5; + public static final byte TYPE_TINYPLANET = 6; + public static final byte TYPE_GEOMETRY = 7; + protected static final String NAME_TAG = "Name"; + + public FilterRepresentation(String name) { + mName = name; + } + + public FilterRepresentation copy(){ + FilterRepresentation representation = new FilterRepresentation(mName); + representation.useParametersFrom(this); + return representation; + } + + protected void copyAllParameters(FilterRepresentation representation) { + representation.setName(getName()); + representation.setFilterClass(getFilterClass()); + representation.setFilterType(getFilterType()); + representation.setSupportsPartialRendering(supportsPartialRendering()); + representation.setTextId(getTextId()); + representation.setEditorId(getEditorId()); + representation.setOverlayId(getOverlayId()); + representation.setOverlayOnly(getOverlayOnly()); + representation.setShowParameterValue(showParameterValue()); + representation.mSerializationName = mSerializationName; + + } + + public boolean equals(FilterRepresentation representation) { + if (representation == null) { + return false; + } + if (representation.mFilterClass == mFilterClass + && representation.mName.equalsIgnoreCase(mName) + && representation.mPriority == mPriority + // TODO: After we enable partial rendering, we can switch back + // to use member variable here. + && representation.supportsPartialRendering() == supportsPartialRendering() + && representation.mTextId == mTextId + && representation.mEditorId == mEditorId + && representation.mButtonId == mButtonId + && representation.mOverlayId == mOverlayId + && representation.mOverlayOnly == mOverlayOnly + && representation.mShowParameterValue == mShowParameterValue) { + return true; + } + return false; + } + + @Override + public String toString() { + return mName; + } + + public void setName(String name) { + mName = name; + } + + public String getName() { + return mName; + } + + public void setSerializationName(String sname) { + mSerializationName = sname; + } + + public String getSerializationName() { + return mSerializationName; + } + + public void setFilterType(int priority) { + mPriority = priority; + } + + public int getFilterType() { + return mPriority; + } + + public boolean isNil() { + return false; + } + + public boolean supportsPartialRendering() { + return false && mSupportsPartialRendering; // disable for now + } + + public void setSupportsPartialRendering(boolean value) { + mSupportsPartialRendering = value; + } + + public void useParametersFrom(FilterRepresentation a) { + } + + public boolean allowsSingleInstanceOnly() { + return false; + } + + public Class<?> getFilterClass() { + return mFilterClass; + } + + public void setFilterClass(Class<?> filterClass) { + mFilterClass = filterClass; + } + + // This same() function is different from equals(), basically it checks + // whether 2 FilterRepresentations are the same type. It doesn't care about + // the values. + public boolean same(FilterRepresentation b) { + if (b == null) { + return false; + } + return getFilterClass() == b.getFilterClass(); + } + + public int getTextId() { + return mTextId; + } + + public void setTextId(int textId) { + mTextId = textId; + } + + public int getOverlayId() { + return mOverlayId; + } + + public void setOverlayId(int overlayId) { + mOverlayId = overlayId; + } + + public boolean getOverlayOnly() { + return mOverlayOnly; + } + + public void setOverlayOnly(boolean value) { + mOverlayOnly = value; + } + + final public int getEditorId() { + return mEditorId; + } + + public int[] getEditorIds() { + return new int[] { + mEditorId }; + } + + public void setEditorId(int editorId) { + mEditorId = editorId; + } + + public boolean showParameterValue() { + return mShowParameterValue; + } + + public void setShowParameterValue(boolean showParameterValue) { + mShowParameterValue = showParameterValue; + } + + public String getStateRepresentation() { + return ""; + } + + /** + * Method must "beginObject()" add its info and "endObject()" + * @param writer + * @throws IOException + */ + public void serializeRepresentation(JsonWriter writer) throws IOException { + writer.beginObject(); + { + String[][] rep = serializeRepresentation(); + for (int k = 0; k < rep.length; k++) { + writer.name(rep[k][0]); + writer.value(rep[k][1]); + } + } + writer.endObject(); + } + + // this is the old way of doing this and will be removed soon + public String[][] serializeRepresentation() { + String[][] ret = {{NAME_TAG, getName()}}; + return ret; + } + + public void deSerializeRepresentation(JsonReader reader) throws IOException { + ArrayList<String[]> al = new ArrayList<String[]>(); + reader.beginObject(); + while (reader.hasNext()) { + String[] kv = {reader.nextName(), reader.nextString()}; + al.add(kv); + + } + reader.endObject(); + String[][] oldFormat = al.toArray(new String[al.size()][]); + + deSerializeRepresentation(oldFormat); + } + + // this is the old way of doing this and will be removed soon + public void deSerializeRepresentation(String[][] rep) { + for (int i = 0; i < rep.length; i++) { + if (NAME_TAG.equals(rep[i][0])) { + mName = rep[i][1]; + break; + } + } + } + + // Override this in subclasses + public int getStyle() { + return -1; + } +} diff --git a/src/com/android/gallery3d/filtershow/filters/FilterRotateRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterRotateRepresentation.java new file mode 100644 index 000000000..eb89de036 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/FilterRotateRepresentation.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2013 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.filtershow.filters; + +import android.util.JsonReader; +import android.util.JsonWriter; +import android.util.Log; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.editors.EditorRotate; + +import java.io.IOException; + +public class FilterRotateRepresentation extends FilterRepresentation { + public static final String SERIALIZATION_NAME = "ROTATION"; + public static final String SERIALIZATION_ROTATE_VALUE = "value"; + private static final String TAG = FilterRotateRepresentation.class.getSimpleName(); + + Rotation mRotation; + + public enum Rotation { + ZERO(0), NINETY(90), ONE_EIGHTY(180), TWO_SEVENTY(270); + private final int mValue; + + private Rotation(int value) { + mValue = value; + } + + public int value() { + return mValue; + } + + public static Rotation fromValue(int value) { + switch (value) { + case 0: + return ZERO; + case 90: + return NINETY; + case 180: + return ONE_EIGHTY; + case 270: + return TWO_SEVENTY; + default: + return null; + } + } + } + + public FilterRotateRepresentation(Rotation rotation) { + super(FilterRotateRepresentation.class.getSimpleName()); + setSerializationName(SERIALIZATION_NAME); + setShowParameterValue(true); + setFilterClass(FilterRotateRepresentation.class); + setFilterType(FilterRepresentation.TYPE_GEOMETRY); + setTextId(R.string.rotate); + setEditorId(EditorRotate.ID); + setRotation(rotation); + } + + public FilterRotateRepresentation(FilterRotateRepresentation r) { + this(r.getRotation()); + } + + public FilterRotateRepresentation() { + this(getNil()); + } + + public Rotation getRotation() { + return mRotation; + } + + public void rotateCW() { + switch(mRotation) { + case ZERO: + mRotation = Rotation.NINETY; + break; + case NINETY: + mRotation = Rotation.ONE_EIGHTY; + break; + case ONE_EIGHTY: + mRotation = Rotation.TWO_SEVENTY; + break; + case TWO_SEVENTY: + mRotation = Rotation.ZERO; + break; + } + } + + public void set(FilterRotateRepresentation r) { + mRotation = r.mRotation; + } + + public void setRotation(Rotation rotation) { + if (rotation == null) { + throw new IllegalArgumentException("Argument to setRotation is null"); + } + mRotation = rotation; + } + + @Override + public boolean allowsSingleInstanceOnly() { + return true; + } + + @Override + public FilterRepresentation copy() { + return new FilterRotateRepresentation(this); + } + + @Override + protected void copyAllParameters(FilterRepresentation representation) { + if (!(representation instanceof FilterRotateRepresentation)) { + throw new IllegalArgumentException("calling copyAllParameters with incompatible types!"); + } + super.copyAllParameters(representation); + representation.useParametersFrom(this); + } + + @Override + public void useParametersFrom(FilterRepresentation a) { + if (!(a instanceof FilterRotateRepresentation)) { + throw new IllegalArgumentException("calling useParametersFrom with incompatible types!"); + } + setRotation(((FilterRotateRepresentation) a).getRotation()); + } + + @Override + public boolean isNil() { + return mRotation == getNil(); + } + + public static Rotation getNil() { + return Rotation.ZERO; + } + + @Override + public void serializeRepresentation(JsonWriter writer) throws IOException { + writer.beginObject(); + writer.name(SERIALIZATION_ROTATE_VALUE).value(mRotation.value()); + writer.endObject(); + } + + @Override + public boolean equals(FilterRepresentation rep) { + if (!(rep instanceof FilterRotateRepresentation)) { + return false; + } + FilterRotateRepresentation rotate = (FilterRotateRepresentation) rep; + if (rotate.mRotation.value() != mRotation.value()) { + return false; + } + return true; + } + + @Override + public void deSerializeRepresentation(JsonReader reader) throws IOException { + boolean unset = true; + reader.beginObject(); + while (reader.hasNext()) { + String name = reader.nextName(); + if (SERIALIZATION_ROTATE_VALUE.equals(name)) { + Rotation r = Rotation.fromValue(reader.nextInt()); + if (r != null) { + setRotation(r); + unset = false; + } + } else { + reader.skipValue(); + } + } + if (unset) { + Log.w(TAG, "WARNING: bad value when deserializing " + SERIALIZATION_NAME); + } + reader.endObject(); + } +} diff --git a/src/com/android/gallery3d/filtershow/filters/FilterStraightenRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterStraightenRepresentation.java new file mode 100644 index 000000000..94c9497fc --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/FilterStraightenRepresentation.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2013 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.filtershow.filters; + +import android.util.JsonReader; +import android.util.JsonWriter; +import android.util.Log; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.editors.EditorStraighten; + +import java.io.IOException; + +public class FilterStraightenRepresentation extends FilterRepresentation { + public static final String SERIALIZATION_NAME = "STRAIGHTEN"; + public static final String SERIALIZATION_STRAIGHTEN_VALUE = "value"; + private static final String TAG = FilterStraightenRepresentation.class.getSimpleName(); + public static final int MAX_STRAIGHTEN_ANGLE = 45; + public static final int MIN_STRAIGHTEN_ANGLE = -45; + + float mStraighten; + + public FilterStraightenRepresentation(float straighten) { + super(FilterStraightenRepresentation.class.getSimpleName()); + setSerializationName(SERIALIZATION_NAME); + setShowParameterValue(true); + setFilterClass(FilterStraightenRepresentation.class); + setFilterType(FilterRepresentation.TYPE_GEOMETRY); + setTextId(R.string.straighten); + setEditorId(EditorStraighten.ID); + setStraighten(straighten); + } + + public FilterStraightenRepresentation(FilterStraightenRepresentation s) { + this(s.getStraighten()); + } + + public FilterStraightenRepresentation() { + this(getNil()); + } + + public void set(FilterStraightenRepresentation r) { + mStraighten = r.mStraighten; + } + + @Override + public boolean equals(FilterRepresentation rep) { + if (!(rep instanceof FilterStraightenRepresentation)) { + return false; + } + FilterStraightenRepresentation straighten = (FilterStraightenRepresentation) rep; + if (straighten.mStraighten != mStraighten) { + return false; + } + return true; + } + + public float getStraighten() { + return mStraighten; + } + + public void setStraighten(float straighten) { + if (!rangeCheck(straighten)) { + straighten = Math.min(Math.max(straighten, MIN_STRAIGHTEN_ANGLE), MAX_STRAIGHTEN_ANGLE); + } + mStraighten = straighten; + } + + @Override + public boolean allowsSingleInstanceOnly() { + return true; + } + + @Override + public FilterRepresentation copy() { + return new FilterStraightenRepresentation(this); + } + + @Override + protected void copyAllParameters(FilterRepresentation representation) { + if (!(representation instanceof FilterStraightenRepresentation)) { + throw new IllegalArgumentException("calling copyAllParameters with incompatible types!"); + } + super.copyAllParameters(representation); + representation.useParametersFrom(this); + } + + @Override + public void useParametersFrom(FilterRepresentation a) { + if (!(a instanceof FilterStraightenRepresentation)) { + throw new IllegalArgumentException("calling useParametersFrom with incompatible types!"); + } + setStraighten(((FilterStraightenRepresentation) a).getStraighten()); + } + + @Override + public boolean isNil() { + return mStraighten == getNil(); + } + + public static float getNil() { + return 0; + } + + @Override + public void serializeRepresentation(JsonWriter writer) throws IOException { + writer.beginObject(); + writer.name(SERIALIZATION_STRAIGHTEN_VALUE).value(mStraighten); + writer.endObject(); + } + + @Override + public void deSerializeRepresentation(JsonReader reader) throws IOException { + boolean unset = true; + reader.beginObject(); + while (reader.hasNext()) { + String name = reader.nextName(); + if (SERIALIZATION_STRAIGHTEN_VALUE.equals(name)) { + float s = (float) reader.nextDouble(); + if (rangeCheck(s)) { + setStraighten(s); + unset = false; + } + } else { + reader.skipValue(); + } + } + if (unset) { + Log.w(TAG, "WARNING: bad value when deserializing " + SERIALIZATION_NAME); + } + reader.endObject(); + } + + private boolean rangeCheck(double s) { + if (s < -45 || s > 45) { + return false; + } + return true; + } +} diff --git a/src/com/android/gallery3d/filtershow/filters/FilterTinyPlanetRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterTinyPlanetRepresentation.java new file mode 100644 index 000000000..be1812957 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/FilterTinyPlanetRepresentation.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2012 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.filtershow.filters; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.editors.EditorTinyPlanet; + +public class FilterTinyPlanetRepresentation extends FilterBasicRepresentation { + private static final String SERIALIZATION_NAME = "TINYPLANET"; + private static final String LOGTAG = "FilterTinyPlanetRepresentation"; + private static final String SERIAL_ANGLE = "Angle"; + private float mAngle = 0; + + public FilterTinyPlanetRepresentation() { + super("TinyPlanet", 0, 50, 100); + setSerializationName(SERIALIZATION_NAME); + setShowParameterValue(true); + setFilterClass(ImageFilterTinyPlanet.class); + setFilterType(FilterRepresentation.TYPE_TINYPLANET); + setTextId(R.string.tinyplanet); + setEditorId(EditorTinyPlanet.ID); + setMinimum(1); + } + + @Override + public FilterRepresentation copy() { + FilterTinyPlanetRepresentation representation = new FilterTinyPlanetRepresentation(); + copyAllParameters(representation); + return representation; + } + + @Override + protected void copyAllParameters(FilterRepresentation representation) { + super.copyAllParameters(representation); + representation.useParametersFrom(this); + } + + @Override + public void useParametersFrom(FilterRepresentation a) { + FilterTinyPlanetRepresentation representation = (FilterTinyPlanetRepresentation) a; + super.useParametersFrom(a); + mAngle = representation.mAngle; + setZoom(representation.getZoom()); + } + + public void setAngle(float angle) { + mAngle = angle; + } + + public float getAngle() { + return mAngle; + } + + public int getZoom() { + return getValue(); + } + + public void setZoom(int zoom) { + setValue(zoom); + } + + public boolean isNil() { + // TinyPlanet always has an effect + return false; + } + + @Override + public String[][] serializeRepresentation() { + String[][] ret = { + {SERIAL_NAME , getName() }, + {SERIAL_VALUE , Integer.toString(getValue())}, + {SERIAL_ANGLE , Float.toString(mAngle)}}; + return ret; + } + + @Override + public void deSerializeRepresentation(String[][] rep) { + super.deSerializeRepresentation(rep); + for (int i = 0; i < rep.length; i++) { + if (SERIAL_VALUE.equals(rep[i][0])) { + setValue(Integer.parseInt(rep[i][1])); + } else if (SERIAL_ANGLE.equals(rep[i][0])) { + setAngle(Float.parseFloat(rep[i][1])); + } + } + } +} diff --git a/src/com/android/gallery3d/filtershow/filters/FilterUserPresetRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterUserPresetRepresentation.java new file mode 100644 index 000000000..dfdb6fcf0 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/FilterUserPresetRepresentation.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2013 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.filtershow.filters; + +import com.android.gallery3d.filtershow.editors.ImageOnlyEditor; +import com.android.gallery3d.filtershow.pipeline.ImagePreset; + +public class FilterUserPresetRepresentation extends FilterRepresentation { + + private ImagePreset mPreset; + private int mId; + + public FilterUserPresetRepresentation(String name, ImagePreset preset, int id) { + super(name); + setEditorId(ImageOnlyEditor.ID); + setFilterType(FilterRepresentation.TYPE_FX); + mPreset = preset; + mId = id; + } + + public ImagePreset getImagePreset() { + return mPreset; + } + + public int getId() { + return mId; + } + + public FilterRepresentation copy(){ + FilterRepresentation representation = new FilterUserPresetRepresentation(getName(), + new ImagePreset(mPreset), mId); + return representation; + } + + @Override + public boolean allowsSingleInstanceOnly() { + return true; + } +} diff --git a/src/com/android/gallery3d/filtershow/filters/FilterVignetteRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterVignetteRepresentation.java new file mode 100644 index 000000000..42a7406bc --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/FilterVignetteRepresentation.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2012 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.filtershow.filters; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.editors.EditorVignette; +import com.android.gallery3d.filtershow.imageshow.Oval; + +public class FilterVignetteRepresentation extends FilterBasicRepresentation implements Oval { + private static final String LOGTAG = "FilterVignetteRepresentation"; + private float mCenterX = Float.NaN; + private float mCenterY; + private float mRadiusX = Float.NaN; + private float mRadiusY; + + public FilterVignetteRepresentation() { + super("Vignette", -100, 50, 100); + setSerializationName("VIGNETTE"); + setShowParameterValue(true); + setFilterType(FilterRepresentation.TYPE_VIGNETTE); + setTextId(R.string.vignette); + setEditorId(EditorVignette.ID); + setName("Vignette"); + setFilterClass(ImageFilterVignette.class); + setMinimum(-100); + setMaximum(100); + setDefaultValue(0); + } + + @Override + public void useParametersFrom(FilterRepresentation a) { + super.useParametersFrom(a); + mCenterX = ((FilterVignetteRepresentation) a).mCenterX; + mCenterY = ((FilterVignetteRepresentation) a).mCenterY; + mRadiusX = ((FilterVignetteRepresentation) a).mRadiusX; + mRadiusY = ((FilterVignetteRepresentation) a).mRadiusY; + } + + @Override + public FilterRepresentation copy() { + FilterVignetteRepresentation representation = new FilterVignetteRepresentation(); + copyAllParameters(representation); + return representation; + } + + @Override + protected void copyAllParameters(FilterRepresentation representation) { + super.copyAllParameters(representation); + representation.useParametersFrom(this); + } + + @Override + public void setCenter(float centerX, float centerY) { + mCenterX = centerX; + mCenterY = centerY; + } + + @Override + public float getCenterX() { + return mCenterX; + } + + @Override + public float getCenterY() { + return mCenterY; + } + + @Override + public void setRadius(float radiusX, float radiusY) { + mRadiusX = radiusX; + mRadiusY = radiusY; + } + + @Override + public void setRadiusX(float radiusX) { + mRadiusX = radiusX; + } + + @Override + public void setRadiusY(float radiusY) { + mRadiusY = radiusY; + } + + @Override + public float getRadiusX() { + return mRadiusX; + } + + @Override + public float getRadiusY() { + return mRadiusY; + } + + public boolean isCenterSet() { + return mCenterX != Float.NaN; + } + + @Override + public boolean isNil() { + return getValue() == 0; + } + + @Override + public boolean equals(FilterRepresentation representation) { + if (!super.equals(representation)) { + return false; + } + if (representation instanceof FilterVignetteRepresentation) { + FilterVignetteRepresentation rep = (FilterVignetteRepresentation) representation; + if (rep.getCenterX() == getCenterX() + && rep.getCenterY() == getCenterY() + && rep.getRadiusX() == getRadiusX() + && rep.getRadiusY() == getRadiusY()) { + return true; + } + } + return false; + } + + private static final String[] sParams = { + "Name", "value", "mCenterX", "mCenterY", "mRadiusX", + "mRadiusY" + }; + + @Override + public String[][] serializeRepresentation() { + String[][] ret = { + { sParams[0], getName() }, + { sParams[1], Integer.toString(getValue()) }, + { sParams[2], Float.toString(mCenterX) }, + { sParams[3], Float.toString(mCenterY) }, + { sParams[4], Float.toString(mRadiusX) }, + { sParams[5], Float.toString(mRadiusY) } + }; + return ret; + } + + @Override + public void deSerializeRepresentation(String[][] rep) { + super.deSerializeRepresentation(rep); + for (int i = 0; i < rep.length; i++) { + String key = rep[i][0]; + String value = rep[i][1]; + if (sParams[0].equals(key)) { + setName(value); + } else if (sParams[1].equals(key)) { + setValue(Integer.parseInt(value)); + } else if (sParams[2].equals(key)) { + mCenterX = Float.parseFloat(value); + } else if (sParams[3].equals(key)) { + mCenterY = Float.parseFloat(value); + } else if (sParams[4].equals(key)) { + mRadiusX = Float.parseFloat(value); + } else if (sParams[5].equals(key)) { + mRadiusY = Float.parseFloat(value); + } + } + } +} diff --git a/src/com/android/gallery3d/filtershow/filters/FiltersManagerInterface.java b/src/com/android/gallery3d/filtershow/filters/FiltersManagerInterface.java new file mode 100644 index 000000000..710128f99 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/FiltersManagerInterface.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2013 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.filtershow.filters; + +public interface FiltersManagerInterface { + ImageFilter getFilterForRepresentation(FilterRepresentation representation); +} diff --git a/src/com/android/gallery3d/filtershow/filters/IconUtilities.java b/src/com/android/gallery3d/filtershow/filters/IconUtilities.java new file mode 100644 index 000000000..e2a01472d --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/IconUtilities.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2013 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.filtershow.filters; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; + +import com.android.gallery3d.R; + +public class IconUtilities { + public static final int PUNCH = R.drawable.filtershow_fx_0005_punch; + public static final int VINTAGE = R.drawable.filtershow_fx_0000_vintage; + public static final int BW_CONTRAST = R.drawable.filtershow_fx_0004_bw_contrast; + public static final int BLEACH = R.drawable.filtershow_fx_0002_bleach; + public static final int INSTANT = R.drawable.filtershow_fx_0001_instant; + public static final int WASHOUT = R.drawable.filtershow_fx_0007_washout; + public static final int BLUECRUSH = R.drawable.filtershow_fx_0003_blue_crush; + public static final int WASHOUT_COLOR = R.drawable.filtershow_fx_0008_washout_color; + public static final int X_PROCESS = R.drawable.filtershow_fx_0006_x_process; + + public static Bitmap getFXBitmap(Resources res, int id) { + Bitmap ret; + BitmapFactory.Options o = new BitmapFactory.Options(); + o.inScaled = false; + + if (id != 0) { + return BitmapFactory.decodeResource(res, id, o); + } + return null; + } + + public static Bitmap loadBitmap(Resources res, int resource) { + + final BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + Bitmap bitmap = BitmapFactory.decodeResource( + res, + resource, options); + + return bitmap; + } + + public static Bitmap applyFX(Bitmap bitmap, final Bitmap fxBitmap) { + ImageFilterFx fx = new ImageFilterFx() { + @Override + public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) { + + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + int fxw = fxBitmap.getWidth(); + int fxh = fxBitmap.getHeight(); + int start = 0; + int end = w * h * 4; + nativeApplyFilter(bitmap, w, h, fxBitmap, fxw, fxh, start, end); + return bitmap; + } + }; + return fx.apply(bitmap, 0, 0); + } +} diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilter.java b/src/com/android/gallery3d/filtershow/filters/ImageFilter.java new file mode 100644 index 000000000..437137416 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilter.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2012 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.filtershow.filters; + +import android.app.Activity; +import android.graphics.Bitmap; +import android.graphics.Matrix; +import android.support.v8.renderscript.Allocation; +import android.widget.Toast; + +import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils; +import com.android.gallery3d.filtershow.imageshow.MasterImage; +import com.android.gallery3d.filtershow.pipeline.FilterEnvironment; + +public abstract class ImageFilter implements Cloneable { + private FilterEnvironment mEnvironment = null; + + protected String mName = "Original"; + private final String LOGTAG = "ImageFilter"; + protected static final boolean SIMPLE_ICONS = true; + // TODO: Temporary, for dogfood note memory issues with toasts for better + // feedback. Remove this when filters actually work in low memory + // situations. + private static Activity sActivity = null; + + public static void setActivityForMemoryToasts(Activity activity) { + sActivity = activity; + } + + public static void resetStatics() { + sActivity = null; + } + + public void freeResources() {} + + public void displayLowMemoryToast() { + if (sActivity != null) { + sActivity.runOnUiThread(new Runnable() { + public void run() { + Toast.makeText(sActivity, "Memory too low for filter " + getName() + + ", please file a bug report", Toast.LENGTH_SHORT).show(); + } + }); + } + } + + public void setName(String name) { + mName = name; + } + + public String getName() { + return mName; + } + + public boolean supportsAllocationInput() { return false; } + + public void apply(Allocation in, Allocation out) { + setGeneralParameters(); + } + + public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) { + // do nothing here, subclasses will implement filtering here + setGeneralParameters(); + return bitmap; + } + + public abstract void useRepresentation(FilterRepresentation representation); + + native protected void nativeApplyGradientFilter(Bitmap bitmap, int w, int h, + int[] redGradient, int[] greenGradient, int[] blueGradient); + + public FilterRepresentation getDefaultRepresentation() { + return null; + } + + protected Matrix getOriginalToScreenMatrix(int w, int h) { + return GeometryMathUtils.getImageToScreenMatrix(getEnvironment().getImagePreset() + .getGeometryFilters(), true, MasterImage.getImage().getOriginalBounds(), w, h); + } + + public void setEnvironment(FilterEnvironment environment) { + mEnvironment = environment; + } + + public FilterEnvironment getEnvironment() { + return mEnvironment; + } + + public void setGeneralParameters() { + // should implement in subclass which like to transport + // some information to other filters. (like the style setting from RetroLux + // and Film to FixedFrame) + mEnvironment.clearGeneralParameters(); + } +} diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterBorder.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterBorder.java new file mode 100644 index 000000000..a7286f0fa --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterBorder.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2012 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.filtershow.filters; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; + +import java.util.HashMap; + +public class ImageFilterBorder extends ImageFilter { + private static final float NINEPATCH_ICON_SCALING = 10; + private static final float BITMAP_ICON_SCALING = 1 / 3.0f; + private FilterImageBorderRepresentation mParameters = null; + private Resources mResources = null; + + private HashMap<Integer, Drawable> mDrawables = new HashMap<Integer, Drawable>(); + + public ImageFilterBorder() { + mName = "Border"; + } + + public void useRepresentation(FilterRepresentation representation) { + FilterImageBorderRepresentation parameters = (FilterImageBorderRepresentation) representation; + mParameters = parameters; + } + + public FilterImageBorderRepresentation getParameters() { + return mParameters; + } + + public void freeResources() { + mDrawables.clear(); + } + + public Bitmap applyHelper(Bitmap bitmap, float scale1, float scale2 ) { + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + Rect bounds = new Rect(0, 0, (int) (w * scale1), (int) (h * scale1)); + Canvas canvas = new Canvas(bitmap); + canvas.scale(scale2, scale2); + Drawable drawable = getDrawable(getParameters().getDrawableResource()); + drawable.setBounds(bounds); + drawable.draw(canvas); + return bitmap; + } + + @Override + public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) { + if (getParameters() == null || getParameters().getDrawableResource() == 0) { + return bitmap; + } + float scale2 = scaleFactor * 2.0f; + float scale1 = 1 / scale2; + return applyHelper(bitmap, scale1, scale2); + } + + public void setResources(Resources resources) { + if (mResources != resources) { + mResources = resources; + mDrawables.clear(); + } + } + + public Drawable getDrawable(int rsc) { + Drawable drawable = mDrawables.get(rsc); + if (drawable == null && mResources != null && rsc != 0) { + drawable = new BitmapDrawable(mResources, BitmapFactory.decodeResource(mResources, rsc)); + mDrawables.put(rsc, drawable); + } + return drawable; + } + +} diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterBwFilter.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterBwFilter.java new file mode 100644 index 000000000..50837ca2f --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterBwFilter.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2012 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.filtershow.filters; + +import com.android.gallery3d.R; + +import android.graphics.Bitmap; +import android.graphics.Color; + + +public class ImageFilterBwFilter extends SimpleImageFilter { + private static final String SERIALIZATION_NAME = "BWFILTER"; + + public ImageFilterBwFilter() { + mName = "BW Filter"; + } + + public FilterRepresentation getDefaultRepresentation() { + FilterBasicRepresentation representation = (FilterBasicRepresentation) super.getDefaultRepresentation(); + representation.setName("BW Filter"); + representation.setSerializationName(SERIALIZATION_NAME); + + representation.setFilterClass(ImageFilterBwFilter.class); + representation.setMaximum(180); + representation.setMinimum(-180); + representation.setTextId(R.string.bwfilter); + representation.setSupportsPartialRendering(true); + return representation; + } + + native protected void nativeApplyFilter(Bitmap bitmap, int w, int h, int r, int g, int b); + + @Override + public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) { + if (getParameters() == null) { + return bitmap; + } + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + float[] hsv = new float[] { + 180 + getParameters().getValue(), 1, 1 + }; + int rgb = Color.HSVToColor(hsv); + int r = 0xFF & (rgb >> 16); + int g = 0xFF & (rgb >> 8); + int b = 0xFF & (rgb >> 0); + nativeApplyFilter(bitmap, w, h, r, g, b); + return bitmap; + } +} diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterChanSat.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterChanSat.java new file mode 100644 index 000000000..1ea8edfb8 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterChanSat.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2013 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.filtershow.filters; + +import android.graphics.Bitmap; +import android.graphics.Matrix; +import android.support.v8.renderscript.Allocation; +import android.support.v8.renderscript.Element; +import android.support.v8.renderscript.RenderScript; +import android.support.v8.renderscript.Script.LaunchOptions; +import android.support.v8.renderscript.Type; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.pipeline.FilterEnvironment; + +public class ImageFilterChanSat extends ImageFilterRS { + private static final String LOGTAG = "ImageFilterChanSat"; + private ScriptC_saturation mScript; + private Bitmap mSourceBitmap; + + private static final int STRIP_SIZE = 64; + + FilterChanSatRepresentation mParameters = new FilterChanSatRepresentation(); + private Bitmap mOverlayBitmap; + + public ImageFilterChanSat() { + mName = "ChannelSat"; + } + + @Override + public FilterRepresentation getDefaultRepresentation() { + return new FilterChanSatRepresentation(); + } + + @Override + public void useRepresentation(FilterRepresentation representation) { + mParameters = (FilterChanSatRepresentation) representation; + } + + @Override + protected void resetAllocations() { + + } + + @Override + public void resetScripts() { + if (mScript != null) { + mScript.destroy(); + mScript = null; + } + } + @Override + protected void createFilter(android.content.res.Resources res, float scaleFactor, + int quality) { + createFilter(res, scaleFactor, quality, getInPixelsAllocation()); + } + + @Override + protected void createFilter(android.content.res.Resources res, float scaleFactor, + int quality, Allocation in) { + RenderScript rsCtx = getRenderScriptContext(); + + Type.Builder tb_float = new Type.Builder(rsCtx, Element.F32_4(rsCtx)); + tb_float.setX(in.getType().getX()); + tb_float.setY(in.getType().getY()); + mScript = new ScriptC_saturation(rsCtx, res, R.raw.saturation); + } + + + private Bitmap getSourceBitmap() { + assert (mSourceBitmap != null); + return mSourceBitmap; + } + + @Override + public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) { + if (SIMPLE_ICONS && FilterEnvironment.QUALITY_ICON == quality) { + return bitmap; + } + + mSourceBitmap = bitmap; + Bitmap ret = super.apply(bitmap, scaleFactor, quality); + mSourceBitmap = null; + + return ret; + } + + @Override + protected void bindScriptValues() { + int width = getInPixelsAllocation().getType().getX(); + int height = getInPixelsAllocation().getType().getY(); + } + + + + @Override + protected void runFilter() { + int []sat = new int[7]; + for(int i = 0;i<sat.length ;i ++){ + sat[i] = mParameters.getValue(i); + } + + + int width = getInPixelsAllocation().getType().getX(); + int height = getInPixelsAllocation().getType().getY(); + Matrix m = getOriginalToScreenMatrix(width, height); + + + mScript.set_saturation(sat); + + mScript.invoke_setupGradParams(); + runSelectiveAdjust( + getInPixelsAllocation(), getOutPixelsAllocation()); + + } + + private void runSelectiveAdjust(Allocation in, Allocation out) { + int width = in.getType().getX(); + int height = in.getType().getY(); + + LaunchOptions options = new LaunchOptions(); + int ty; + options.setX(0, width); + + for (ty = 0; ty < height; ty += STRIP_SIZE) { + int endy = ty + STRIP_SIZE; + if (endy > height) { + endy = height; + } + options.setY(ty, endy); + mScript.forEach_selectiveAdjust(in, out, options); + if (checkStop()) { + return; + } + } + } + + private boolean checkStop() { + RenderScript rsCtx = getRenderScriptContext(); + rsCtx.finish(); + if (getEnvironment().needsStop()) { + return true; + } + return false; + } +} + diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterContrast.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterContrast.java new file mode 100644 index 000000000..27c0e0877 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterContrast.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2012 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.filtershow.filters; + +import com.android.gallery3d.R; + +import android.graphics.Bitmap; + +public class ImageFilterContrast extends SimpleImageFilter { + private static final String SERIALIZATION_NAME = "CONTRAST"; + + public ImageFilterContrast() { + mName = "Contrast"; + } + + public FilterRepresentation getDefaultRepresentation() { + FilterBasicRepresentation representation = + (FilterBasicRepresentation) super.getDefaultRepresentation(); + representation.setName("Contrast"); + representation.setSerializationName(SERIALIZATION_NAME); + + representation.setFilterClass(ImageFilterContrast.class); + representation.setTextId(R.string.contrast); + representation.setMinimum(-100); + representation.setMaximum(100); + representation.setDefaultValue(0); + representation.setSupportsPartialRendering(true); + return representation; + } + + native protected void nativeApplyFilter(Bitmap bitmap, int w, int h, float strength); + + @Override + public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) { + if (getParameters() == null) { + return bitmap; + } + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + float value = getParameters().getValue(); + nativeApplyFilter(bitmap, w, h, value); + return bitmap; + } +} diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterCurves.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterCurves.java new file mode 100644 index 000000000..61b60d2e3 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterCurves.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2012 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.filtershow.filters; + +import android.graphics.Bitmap; + +import com.android.gallery3d.filtershow.imageshow.Spline; + +public class ImageFilterCurves extends ImageFilter { + + private static final String LOGTAG = "ImageFilterCurves"; + FilterCurvesRepresentation mParameters = new FilterCurvesRepresentation(); + + @Override + public FilterRepresentation getDefaultRepresentation() { + return new FilterCurvesRepresentation(); + } + + @Override + public void useRepresentation(FilterRepresentation representation) { + FilterCurvesRepresentation parameters = (FilterCurvesRepresentation) representation; + mParameters = parameters; + } + + public ImageFilterCurves() { + mName = "Curves"; + reset(); + } + + public void populateArray(int[] array, int curveIndex) { + Spline spline = mParameters.getSpline(curveIndex); + if (spline == null) { + return; + } + float[] curve = spline.getAppliedCurve(); + for (int i = 0; i < 256; i++) { + array[i] = (int) (curve[i] * 255); + } + } + + @Override + public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) { + if (!mParameters.getSpline(Spline.RGB).isOriginal()) { + int[] rgbGradient = new int[256]; + populateArray(rgbGradient, Spline.RGB); + nativeApplyGradientFilter(bitmap, bitmap.getWidth(), bitmap.getHeight(), + rgbGradient, rgbGradient, rgbGradient); + } + + int[] redGradient = null; + if (!mParameters.getSpline(Spline.RED).isOriginal()) { + redGradient = new int[256]; + populateArray(redGradient, Spline.RED); + } + int[] greenGradient = null; + if (!mParameters.getSpline(Spline.GREEN).isOriginal()) { + greenGradient = new int[256]; + populateArray(greenGradient, Spline.GREEN); + } + int[] blueGradient = null; + if (!mParameters.getSpline(Spline.BLUE).isOriginal()) { + blueGradient = new int[256]; + populateArray(blueGradient, Spline.BLUE); + } + + nativeApplyGradientFilter(bitmap, bitmap.getWidth(), bitmap.getHeight(), + redGradient, greenGradient, blueGradient); + return bitmap; + } + + public void setSpline(Spline spline, int splineIndex) { + mParameters.setSpline(splineIndex, new Spline(spline)); + } + + public Spline getSpline(int splineIndex) { + return mParameters.getSpline(splineIndex); + } + + public void reset() { + Spline spline = new Spline(); + + spline.addPoint(0.0f, 1.0f); + spline.addPoint(1.0f, 0.0f); + + for (int i = 0; i < 4; i++) { + mParameters.setSpline(i, new Spline(spline)); + } + } + + public void useFilter(ImageFilter a) { + ImageFilterCurves c = (ImageFilterCurves) a; + for (int i = 0; i < 4; i++) { + if (c.mParameters.getSpline(i) != null) { + setSpline(c.mParameters.getSpline(i), i); + } + } + } +} diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterDownsample.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterDownsample.java new file mode 100644 index 000000000..efb9cde71 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterDownsample.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2012 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.filtershow.filters; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Rect; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.cache.ImageLoader; +import com.android.gallery3d.filtershow.imageshow.MasterImage; + +public class ImageFilterDownsample extends SimpleImageFilter { + private static final String SERIALIZATION_NAME = "DOWNSAMPLE"; + private static final int ICON_DOWNSAMPLE_FRACTION = 8; + private ImageLoader mImageLoader; + + public ImageFilterDownsample(ImageLoader loader) { + mName = "Downsample"; + mImageLoader = loader; + } + + public FilterRepresentation getDefaultRepresentation() { + FilterBasicRepresentation representation = (FilterBasicRepresentation) super.getDefaultRepresentation(); + representation.setName("Downsample"); + representation.setSerializationName(SERIALIZATION_NAME); + + representation.setFilterClass(ImageFilterDownsample.class); + representation.setMaximum(100); + representation.setMinimum(1); + representation.setValue(50); + representation.setDefaultValue(50); + representation.setPreviewValue(3); + representation.setTextId(R.string.downsample); + return representation; + } + + @Override + public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) { + if (getParameters() == null) { + return bitmap; + } + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + int p = getParameters().getValue(); + + // size of original precached image + Rect size = MasterImage.getImage().getOriginalBounds(); + int orig_w = size.width(); + int orig_h = size.height(); + + if (p > 0 && p < 100) { + // scale preview to same size as the resulting bitmap from a "save" + int newWidth = orig_w * p / 100; + int newHeight = orig_h * p / 100; + + // only scale preview if preview isn't already scaled enough + if (newWidth <= 0 || newHeight <= 0 || newWidth >= w || newHeight >= h) { + return bitmap; + } + Bitmap ret = Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true); + if (ret != bitmap) { + bitmap.recycle(); + } + return ret; + } + return bitmap; + } +} diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterDraw.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterDraw.java new file mode 100644 index 000000000..7df5ffb64 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterDraw.java @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2012 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.filtershow.filters; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.Path; +import android.graphics.PathMeasure; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.cache.ImageLoader; +import com.android.gallery3d.filtershow.filters.FilterDrawRepresentation.StrokeData; +import com.android.gallery3d.filtershow.imageshow.MasterImage; +import com.android.gallery3d.filtershow.pipeline.FilterEnvironment; + +import java.util.Vector; + +public class ImageFilterDraw extends ImageFilter { + private static final String LOGTAG = "ImageFilterDraw"; + public final static byte SIMPLE_STYLE = 0; + public final static byte BRUSH_STYLE_SPATTER = 1; + public final static byte BRUSH_STYLE_MARKER = 2; + public final static int NUMBER_OF_STYLES = 3; + Bitmap mOverlayBitmap; // this accelerates interaction + int mCachedStrokes = -1; + int mCurrentStyle = 0; + + FilterDrawRepresentation mParameters = new FilterDrawRepresentation(); + + public ImageFilterDraw() { + mName = "Image Draw"; + } + + DrawStyle[] mDrawingsTypes = new DrawStyle[] { + new SimpleDraw(), + new Brush(R.drawable.brush_marker), + new Brush(R.drawable.brush_spatter) + }; + { + for (int i = 0; i < mDrawingsTypes.length; i++) { + mDrawingsTypes[i].setType((byte) i); + } + + } + + @Override + public FilterRepresentation getDefaultRepresentation() { + return new FilterDrawRepresentation(); + } + + @Override + public void useRepresentation(FilterRepresentation representation) { + FilterDrawRepresentation parameters = (FilterDrawRepresentation) representation; + mParameters = parameters; + } + + public void setStyle(byte style) { + mCurrentStyle = style % mDrawingsTypes.length; + } + + public int getStyle() { + return mCurrentStyle; + } + + public static interface DrawStyle { + public void setType(byte type); + public void paint(FilterDrawRepresentation.StrokeData sd, Canvas canvas, Matrix toScrMatrix, + int quality); + } + + class SimpleDraw implements DrawStyle { + byte mType; + + @Override + public void setType(byte type) { + mType = type; + } + + @Override + public void paint(FilterDrawRepresentation.StrokeData sd, Canvas canvas, Matrix toScrMatrix, + int quality) { + if (sd == null) { + return; + } + if (sd.mPath == null) { + return; + } + Paint paint = new Paint(); + + paint.setStyle(Style.STROKE); + paint.setColor(sd.mColor); + paint.setStrokeWidth(toScrMatrix.mapRadius(sd.mRadius)); + + // done this way because of a bug in path.transform(matrix) + Path mCacheTransPath = new Path(); + mCacheTransPath.addPath(sd.mPath, toScrMatrix); + + canvas.drawPath(mCacheTransPath, paint); + } + } + + class Brush implements DrawStyle { + int mBrushID; + Bitmap mBrush; + byte mType; + + public Brush(int brushID) { + mBrushID = brushID; + } + + public Bitmap getBrush() { + if (mBrush == null) { + BitmapFactory.Options opt = new BitmapFactory.Options(); + opt.inPreferredConfig = Bitmap.Config.ALPHA_8; + mBrush = BitmapFactory.decodeResource(MasterImage.getImage().getActivity() + .getResources(), mBrushID, opt); + mBrush = mBrush.extractAlpha(); + } + return mBrush; + } + + @Override + public void paint(FilterDrawRepresentation.StrokeData sd, Canvas canvas, + Matrix toScrMatrix, + int quality) { + if (sd == null || sd.mPath == null) { + return; + } + Paint paint = new Paint(); + paint.setStyle(Style.STROKE); + paint.setAntiAlias(true); + Path mCacheTransPath = new Path(); + mCacheTransPath.addPath(sd.mPath, toScrMatrix); + draw(canvas, paint, sd.mColor, toScrMatrix.mapRadius(sd.mRadius) * 2, + mCacheTransPath); + } + + public Bitmap createScaledBitmap(Bitmap src, int dstWidth, int dstHeight, boolean filter) + { + Matrix m = new Matrix(); + m.setScale(dstWidth / (float) src.getWidth(), dstHeight / (float) src.getHeight()); + Bitmap result = Bitmap.createBitmap(dstWidth, dstHeight, src.getConfig()); + Canvas canvas = new Canvas(result); + + Paint paint = new Paint(); + paint.setFilterBitmap(filter); + canvas.drawBitmap(src, m, paint); + + return result; + + } + void draw(Canvas canvas, Paint paint, int color, float size, Path path) { + PathMeasure mPathMeasure = new PathMeasure(); + float[] mPosition = new float[2]; + float[] mTan = new float[2]; + + mPathMeasure.setPath(path, false); + + paint.setAntiAlias(true); + paint.setColor(color); + + paint.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)); + Bitmap brush; + // done this way because of a bug in + // Bitmap.createScaledBitmap(getBrush(),(int) size,(int) size,true); + brush = createScaledBitmap(getBrush(), (int) size, (int) size, true); + float len = mPathMeasure.getLength(); + float s2 = size / 2; + float step = s2 / 8; + for (float i = 0; i < len; i += step) { + mPathMeasure.getPosTan(i, mPosition, mTan); + // canvas.drawCircle(pos[0], pos[1], size, paint); + canvas.drawBitmap(brush, mPosition[0] - s2, mPosition[1] - s2, paint); + } + } + + @Override + public void setType(byte type) { + mType = type; + } + } + + void paint(FilterDrawRepresentation.StrokeData sd, Canvas canvas, Matrix toScrMatrix, + int quality) { + mDrawingsTypes[sd.mType].paint(sd, canvas, toScrMatrix, quality); + } + + public void drawData(Canvas canvas, Matrix originalRotateToScreen, int quality) { + Paint paint = new Paint(); + if (quality == FilterEnvironment.QUALITY_FINAL) { + paint.setAntiAlias(true); + } + paint.setStyle(Style.STROKE); + paint.setColor(Color.RED); + paint.setStrokeWidth(40); + + if (mParameters.getDrawing().isEmpty() && mParameters.getCurrentDrawing() == null) { + return; + } + if (quality == FilterEnvironment.QUALITY_FINAL) { + for (FilterDrawRepresentation.StrokeData strokeData : mParameters.getDrawing()) { + paint(strokeData, canvas, originalRotateToScreen, quality); + } + return; + } + + if (mOverlayBitmap == null || + mOverlayBitmap.getWidth() != canvas.getWidth() || + mOverlayBitmap.getHeight() != canvas.getHeight() || + mParameters.getDrawing().size() < mCachedStrokes) { + + mOverlayBitmap = Bitmap.createBitmap( + canvas.getWidth(), canvas.getHeight(), Bitmap.Config.ARGB_8888); + mCachedStrokes = 0; + } + + if (mCachedStrokes < mParameters.getDrawing().size()) { + fillBuffer(originalRotateToScreen); + } + canvas.drawBitmap(mOverlayBitmap, 0, 0, paint); + + StrokeData stroke = mParameters.getCurrentDrawing(); + if (stroke != null) { + paint(stroke, canvas, originalRotateToScreen, quality); + } + } + + public void fillBuffer(Matrix originalRotateToScreen) { + Canvas drawCache = new Canvas(mOverlayBitmap); + Vector<FilterDrawRepresentation.StrokeData> v = mParameters.getDrawing(); + int n = v.size(); + + for (int i = mCachedStrokes; i < n; i++) { + paint(v.get(i), drawCache, originalRotateToScreen, FilterEnvironment.QUALITY_PREVIEW); + } + mCachedStrokes = n; + } + + public void draw(Canvas canvas, Matrix originalRotateToScreen) { + for (FilterDrawRepresentation.StrokeData strokeData : mParameters.getDrawing()) { + paint(strokeData, canvas, originalRotateToScreen, FilterEnvironment.QUALITY_PREVIEW); + } + mDrawingsTypes[mCurrentStyle].paint( + null, canvas, originalRotateToScreen, FilterEnvironment.QUALITY_PREVIEW); + } + + @Override + public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) { + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + + Matrix m = getOriginalToScreenMatrix(w, h); + drawData(new Canvas(bitmap), m, quality); + return bitmap; + } + +} diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterEdge.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterEdge.java new file mode 100644 index 000000000..2d0d7653d --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterEdge.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2012 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.filtershow.filters; + +import android.graphics.Bitmap; + +import com.android.gallery3d.R; + +public class ImageFilterEdge extends SimpleImageFilter { + private static final String SERIALIZATION_NAME = "EDGE"; + public ImageFilterEdge() { + mName = "Edge"; + } + + public FilterRepresentation getDefaultRepresentation() { + FilterRepresentation representation = super.getDefaultRepresentation(); + representation.setName("Edge"); + representation.setSerializationName(SERIALIZATION_NAME); + representation.setFilterClass(ImageFilterEdge.class); + representation.setTextId(R.string.edge); + representation.setSupportsPartialRendering(true); + return representation; + } + + native protected void nativeApplyFilter(Bitmap bitmap, int w, int h, float p); + + @Override + public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) { + if (getParameters() == null) { + return bitmap; + } + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + float p = getParameters().getValue() + 101; + p = (float) p / 100; + nativeApplyFilter(bitmap, w, h, p); + return bitmap; + } +} diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterExposure.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterExposure.java new file mode 100644 index 000000000..69eab7330 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterExposure.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2012 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.filtershow.filters; + +import com.android.gallery3d.R; + +import android.graphics.Bitmap; + +public class ImageFilterExposure extends SimpleImageFilter { + private static final String SERIALIZATION_NAME = "EXPOSURE"; + public ImageFilterExposure() { + mName = "Exposure"; + } + + public FilterRepresentation getDefaultRepresentation() { + FilterBasicRepresentation representation = + (FilterBasicRepresentation) super.getDefaultRepresentation(); + representation.setName("Exposure"); + representation.setSerializationName(SERIALIZATION_NAME); + representation.setFilterClass(ImageFilterExposure.class); + representation.setTextId(R.string.exposure); + representation.setMinimum(-100); + representation.setMaximum(100); + representation.setDefaultValue(0); + representation.setSupportsPartialRendering(true); + return representation; + } + + native protected void nativeApplyFilter(Bitmap bitmap, int w, int h, float bright); + + @Override + public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) { + if (getParameters() == null) { + return bitmap; + } + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + float value = getParameters().getValue(); + nativeApplyFilter(bitmap, w, h, value); + return bitmap; + } +} diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterFx.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterFx.java new file mode 100644 index 000000000..19bea593b --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterFx.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2012 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.filtershow.filters; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import com.android.gallery3d.app.Log; + +public class ImageFilterFx extends ImageFilter { + private static final String LOGTAG = "ImageFilterFx"; + private FilterFxRepresentation mParameters = null; + private Bitmap mFxBitmap = null; + private Resources mResources = null; + private int mFxBitmapId = 0; + + public ImageFilterFx() { + } + + @Override + public void freeResources() { + if (mFxBitmap != null) mFxBitmap.recycle(); + mFxBitmap = null; + } + + @Override + public FilterRepresentation getDefaultRepresentation() { + return null; + } + + public void useRepresentation(FilterRepresentation representation) { + FilterFxRepresentation parameters = (FilterFxRepresentation) representation; + mParameters = parameters; + } + + public FilterFxRepresentation getParameters() { + return mParameters; + } + + native protected void nativeApplyFilter(Bitmap bitmap, int w, int h, + Bitmap fxBitmap, int fxw, int fxh, + int start, int end); + + @Override + public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) { + if (getParameters() == null || mResources == null) { + return bitmap; + } + + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + + int bitmapResourceId = getParameters().getBitmapResource(); + if (bitmapResourceId == 0) { // null filter fx + return bitmap; + } + + if (mFxBitmap == null || mFxBitmapId != bitmapResourceId) { + BitmapFactory.Options o = new BitmapFactory.Options(); + o.inScaled = false; + mFxBitmapId = bitmapResourceId; + if (mFxBitmapId != 0) { + mFxBitmap = BitmapFactory.decodeResource(mResources, mFxBitmapId, o); + } else { + Log.w(LOGTAG, "bad resource for filter: " + mName); + } + } + + if (mFxBitmap == null) { + return bitmap; + } + + int fxw = mFxBitmap.getWidth(); + int fxh = mFxBitmap.getHeight(); + + int stride = w * 4; + int max = stride * h; + int increment = stride * 256; // 256 lines + for (int i = 0; i < max; i += increment) { + int start = i; + int end = i + increment; + if (end > max) { + end = max; + } + if (!getEnvironment().needsStop()) { + nativeApplyFilter(bitmap, w, h, mFxBitmap, fxw, fxh, start, end); + } + } + + return bitmap; + } + + public void setResources(Resources resources) { + mResources = resources; + } + +} diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterGrad.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterGrad.java new file mode 100644 index 000000000..cbdfaa623 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterGrad.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2013 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.filtershow.filters; + +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.Matrix; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.pipeline.FilterEnvironment; + +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.Matrix; +import android.support.v8.renderscript.Allocation; +import android.support.v8.renderscript.Element; +import android.support.v8.renderscript.RenderScript; +import android.support.v8.renderscript.Script.LaunchOptions; +import android.support.v8.renderscript.Type; +import android.util.Log; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.pipeline.FilterEnvironment; + +public class ImageFilterGrad extends ImageFilterRS { + private static final String LOGTAG = "ImageFilterGrad"; + private ScriptC_grad mScript; + private Bitmap mSourceBitmap; + private static final int RADIUS_SCALE_FACTOR = 160; + + private static final int STRIP_SIZE = 64; + + FilterGradRepresentation mParameters = new FilterGradRepresentation(); + private Bitmap mOverlayBitmap; + + public ImageFilterGrad() { + mName = "grad"; + } + + @Override + public FilterRepresentation getDefaultRepresentation() { + return new FilterGradRepresentation(); + } + + @Override + public void useRepresentation(FilterRepresentation representation) { + mParameters = (FilterGradRepresentation) representation; + } + + @Override + protected void resetAllocations() { + + } + + @Override + public void resetScripts() { + if (mScript != null) { + mScript.destroy(); + mScript = null; + } + } + @Override + protected void createFilter(android.content.res.Resources res, float scaleFactor, + int quality) { + createFilter(res, scaleFactor, quality, getInPixelsAllocation()); + } + + @Override + protected void createFilter(android.content.res.Resources res, float scaleFactor, + int quality, Allocation in) { + RenderScript rsCtx = getRenderScriptContext(); + + Type.Builder tb_float = new Type.Builder(rsCtx, Element.F32_4(rsCtx)); + tb_float.setX(in.getType().getX()); + tb_float.setY(in.getType().getY()); + mScript = new ScriptC_grad(rsCtx, res, R.raw.grad); + } + + + private Bitmap getSourceBitmap() { + assert (mSourceBitmap != null); + return mSourceBitmap; + } + + @Override + public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) { + if (SIMPLE_ICONS && FilterEnvironment.QUALITY_ICON == quality) { + return bitmap; + } + + mSourceBitmap = bitmap; + Bitmap ret = super.apply(bitmap, scaleFactor, quality); + mSourceBitmap = null; + + return ret; + } + + @Override + protected void bindScriptValues() { + int width = getInPixelsAllocation().getType().getX(); + int height = getInPixelsAllocation().getType().getY(); + mScript.set_inputWidth(width); + mScript.set_inputHeight(height); + } + + @Override + protected void runFilter() { + int[] x1 = mParameters.getXPos1(); + int[] y1 = mParameters.getYPos1(); + int[] x2 = mParameters.getXPos2(); + int[] y2 = mParameters.getYPos2(); + + int width = getInPixelsAllocation().getType().getX(); + int height = getInPixelsAllocation().getType().getY(); + Matrix m = getOriginalToScreenMatrix(width, height); + float[] coord = new float[2]; + for (int i = 0; i < x1.length; i++) { + coord[0] = x1[i]; + coord[1] = y1[i]; + m.mapPoints(coord); + x1[i] = (int) coord[0]; + y1[i] = (int) coord[1]; + coord[0] = x2[i]; + coord[1] = y2[i]; + m.mapPoints(coord); + x2[i] = (int) coord[0]; + y2[i] = (int) coord[1]; + } + + mScript.set_mask(mParameters.getMask()); + mScript.set_xPos1(x1); + mScript.set_yPos1(y1); + mScript.set_xPos2(x2); + mScript.set_yPos2(y2); + + mScript.set_brightness(mParameters.getBrightness()); + mScript.set_contrast(mParameters.getContrast()); + mScript.set_saturation(mParameters.getSaturation()); + + mScript.invoke_setupGradParams(); + runSelectiveAdjust( + getInPixelsAllocation(), getOutPixelsAllocation()); + + } + + private void runSelectiveAdjust(Allocation in, Allocation out) { + int width = in.getType().getX(); + int height = in.getType().getY(); + + LaunchOptions options = new LaunchOptions(); + int ty; + options.setX(0, width); + + for (ty = 0; ty < height; ty += STRIP_SIZE) { + int endy = ty + STRIP_SIZE; + if (endy > height) { + endy = height; + } + options.setY(ty, endy); + mScript.forEach_selectiveAdjust(in, out, options); + if (checkStop()) { + return; + } + } + } + + private boolean checkStop() { + RenderScript rsCtx = getRenderScriptContext(); + rsCtx.finish(); + if (getEnvironment().needsStop()) { + return true; + } + return false; + } +} + diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterHighlights.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterHighlights.java new file mode 100644 index 000000000..4c837e0bf --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterHighlights.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2013 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.filtershow.filters; + +import android.graphics.Bitmap; + +import com.android.gallery3d.R; + +public class ImageFilterHighlights extends SimpleImageFilter { + private static final String SERIALIZATION_NAME = "HIGHLIGHTS"; + private static final String LOGTAG = "ImageFilterVignette"; + + public ImageFilterHighlights() { + mName = "Highlights"; + } + + SplineMath mSpline = new SplineMath(5); + double[] mHighlightCurve = { 0.0, 0.32, 0.418, 0.476, 0.642 }; + + public FilterRepresentation getDefaultRepresentation() { + FilterBasicRepresentation representation = + (FilterBasicRepresentation) super.getDefaultRepresentation(); + representation.setName("Highlights"); + representation.setSerializationName(SERIALIZATION_NAME); + representation.setFilterClass(ImageFilterHighlights.class); + representation.setTextId(R.string.highlight_recovery); + representation.setMinimum(-100); + representation.setMaximum(100); + representation.setDefaultValue(0); + representation.setSupportsPartialRendering(true); + return representation; + } + + native protected void nativeApplyFilter(Bitmap bitmap, int w, int h, float[] luminanceMap); + + @Override + public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) { + if (getParameters() == null) { + return bitmap; + } + float p = getParameters().getValue(); + double t = p/100.; + for (int i = 0; i < 5; i++) { + double x = i / 4.; + double y = mHighlightCurve[i] *t+x*(1-t); + mSpline.setPoint(i, x, y); + } + + float[][] curve = mSpline.calculatetCurve(256); + float[] luminanceMap = new float[curve.length]; + for (int i = 0; i < luminanceMap.length; i++) { + luminanceMap[i] = curve[i][1]; + } + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + + nativeApplyFilter(bitmap, w, h, luminanceMap); + return bitmap; + } +} diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterHue.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterHue.java new file mode 100644 index 000000000..b87c25490 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterHue.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2012 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.filtershow.filters; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.editors.BasicEditor; + +import android.graphics.Bitmap; + +public class ImageFilterHue extends SimpleImageFilter { + private static final String SERIALIZATION_NAME = "HUE"; + private ColorSpaceMatrix cmatrix = null; + + public ImageFilterHue() { + mName = "Hue"; + cmatrix = new ColorSpaceMatrix(); + } + + public FilterRepresentation getDefaultRepresentation() { + FilterBasicRepresentation representation = + (FilterBasicRepresentation) super.getDefaultRepresentation(); + representation.setName("Hue"); + representation.setSerializationName(SERIALIZATION_NAME); + representation.setFilterClass(ImageFilterHue.class); + representation.setMinimum(-180); + representation.setMaximum(180); + representation.setTextId(R.string.hue); + representation.setEditorId(BasicEditor.ID); + representation.setSupportsPartialRendering(true); + return representation; + } + + native protected void nativeApplyFilter(Bitmap bitmap, int w, int h, float []matrix); + + @Override + public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) { + if (getParameters() == null) { + return bitmap; + } + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + float value = getParameters().getValue(); + cmatrix.identity(); + cmatrix.setHue(value); + + nativeApplyFilter(bitmap, w, h, cmatrix.getMatrix()); + + return bitmap; + } +} diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterKMeans.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterKMeans.java new file mode 100644 index 000000000..77cdf47b3 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterKMeans.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2012 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.filtershow.filters; + +import android.graphics.Bitmap; +import android.text.format.Time; + +import com.android.gallery3d.R; + +public class ImageFilterKMeans extends SimpleImageFilter { + private static final String SERIALIZATION_NAME = "KMEANS"; + private int mSeed = 0; + + public ImageFilterKMeans() { + mName = "KMeans"; + + // set random seed for session + Time t = new Time(); + t.setToNow(); + mSeed = (int) t.toMillis(false); + } + + public FilterRepresentation getDefaultRepresentation() { + FilterBasicRepresentation representation = (FilterBasicRepresentation) super.getDefaultRepresentation(); + representation.setName("KMeans"); + representation.setSerializationName(SERIALIZATION_NAME); + representation.setFilterClass(ImageFilterKMeans.class); + representation.setMaximum(20); + representation.setMinimum(2); + representation.setValue(4); + representation.setDefaultValue(4); + representation.setPreviewValue(4); + representation.setTextId(R.string.kmeans); + representation.setSupportsPartialRendering(true); + return representation; + } + + native protected void nativeApplyFilter(Bitmap bitmap, int width, int height, + Bitmap large_ds_bm, int lwidth, int lheight, Bitmap small_ds_bm, + int swidth, int sheight, int p, int seed); + + @Override + public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) { + if (getParameters() == null) { + return bitmap; + } + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + + Bitmap large_bm_ds = bitmap; + Bitmap small_bm_ds = bitmap; + + // find width/height for larger downsampled bitmap + int lw = w; + int lh = h; + while (lw > 256 && lh > 256) { + lw /= 2; + lh /= 2; + } + if (lw != w) { + large_bm_ds = Bitmap.createScaledBitmap(bitmap, lw, lh, true); + } + + // find width/height for smaller downsampled bitmap + int sw = lw; + int sh = lh; + while (sw > 64 && sh > 64) { + sw /= 2; + sh /= 2; + } + if (sw != lw) { + small_bm_ds = Bitmap.createScaledBitmap(large_bm_ds, sw, sh, true); + } + + if (getParameters() != null) { + int p = Math.max(getParameters().getValue(), getParameters().getMinimum()) % (getParameters().getMaximum() + 1); + nativeApplyFilter(bitmap, w, h, large_bm_ds, lw, lh, small_bm_ds, sw, sh, p, mSeed); + } + return bitmap; + } +} diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterNegative.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterNegative.java new file mode 100644 index 000000000..98497596b --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterNegative.java @@ -0,0 +1,39 @@ +package com.android.gallery3d.filtershow.filters; + +import android.graphics.Bitmap; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.editors.ImageOnlyEditor; + +public class ImageFilterNegative extends ImageFilter { + private static final String SERIALIZATION_NAME = "NEGATIVE"; + public ImageFilterNegative() { + mName = "Negative"; + } + + public FilterRepresentation getDefaultRepresentation() { + FilterRepresentation representation = new FilterDirectRepresentation("Negative"); + representation.setSerializationName(SERIALIZATION_NAME); + representation.setFilterClass(ImageFilterNegative.class); + representation.setTextId(R.string.negative); + representation.setShowParameterValue(false); + representation.setEditorId(ImageOnlyEditor.ID); + representation.setSupportsPartialRendering(true); + return representation; + } + + native protected void nativeApplyFilter(Bitmap bitmap, int w, int h); + + @Override + public void useRepresentation(FilterRepresentation representation) { + + } + + @Override + public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) { + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + nativeApplyFilter(bitmap, w, h); + return bitmap; + } +} diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterParametricBorder.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterParametricBorder.java new file mode 100644 index 000000000..25e5d1476 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterParametricBorder.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2012 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.filtershow.filters; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.RectF; + +public class ImageFilterParametricBorder extends ImageFilter { + private FilterColorBorderRepresentation mParameters = null; + + public ImageFilterParametricBorder() { + mName = "Border"; + } + + public void useRepresentation(FilterRepresentation representation) { + FilterColorBorderRepresentation parameters = (FilterColorBorderRepresentation) representation; + mParameters = parameters; + } + + public FilterColorBorderRepresentation getParameters() { + return mParameters; + } + + private void applyHelper(Canvas canvas, int w, int h) { + if (getParameters() == null) { + return; + } + Path border = new Path(); + border.moveTo(0, 0); + float bs = getParameters().getBorderSize() / 100.0f * w; + float r = getParameters().getBorderRadius() / 100.0f * w; + border.lineTo(0, h); + border.lineTo(w, h); + border.lineTo(w, 0); + border.lineTo(0, 0); + border.addRoundRect(new RectF(bs, bs, w - bs, h - bs), + r, r, Path.Direction.CW); + + Paint paint = new Paint(); + paint.setAntiAlias(true); + paint.setColor(getParameters().getColor()); + canvas.drawPath(border, paint); + } + + @Override + public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) { + Canvas canvas = new Canvas(bitmap); + applyHelper(canvas, bitmap.getWidth(), bitmap.getHeight()); + return bitmap; + } + +} diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterRS.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterRS.java new file mode 100644 index 000000000..5695ef53e --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterRS.java @@ -0,0 +1,260 @@ +/* + * Copyright (C) 2012 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.filtershow.filters; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.support.v8.renderscript.*; +import android.util.Log; +import android.content.res.Resources; +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.pipeline.PipelineInterface; + +public abstract class ImageFilterRS extends ImageFilter { + private static final String LOGTAG = "ImageFilterRS"; + private boolean DEBUG = false; + private int mLastInputWidth = 0; + private int mLastInputHeight = 0; + private long mLastTimeCalled; + + public static boolean PERF_LOGGING = false; + + private static ScriptC_grey mGreyConvert = null; + private static RenderScript mRScache = null; + + private volatile boolean mResourcesLoaded = false; + + protected abstract void createFilter(android.content.res.Resources res, + float scaleFactor, int quality); + + protected void createFilter(android.content.res.Resources res, + float scaleFactor, int quality, Allocation in) {} + protected void bindScriptValues(Allocation in) {} + + protected abstract void runFilter(); + + protected void update(Bitmap bitmap) { + getOutPixelsAllocation().copyTo(bitmap); + } + + protected RenderScript getRenderScriptContext() { + PipelineInterface pipeline = getEnvironment().getPipeline(); + return pipeline.getRSContext(); + } + + protected Allocation getInPixelsAllocation() { + PipelineInterface pipeline = getEnvironment().getPipeline(); + return pipeline.getInPixelsAllocation(); + } + + protected Allocation getOutPixelsAllocation() { + PipelineInterface pipeline = getEnvironment().getPipeline(); + return pipeline.getOutPixelsAllocation(); + } + + @Override + public void apply(Allocation in, Allocation out) { + long startOverAll = System.nanoTime(); + if (PERF_LOGGING) { + long delay = (startOverAll - mLastTimeCalled) / 1000; + String msg = String.format("%s; image size %dx%d; ", getName(), + in.getType().getX(), in.getType().getY()); + msg += String.format("called after %.2f ms (%.2f FPS); ", + delay / 1000.f, 1000000.f / delay); + Log.i(LOGTAG, msg); + } + mLastTimeCalled = startOverAll; + long startFilter = 0; + long endFilter = 0; + if (!mResourcesLoaded) { + PipelineInterface pipeline = getEnvironment().getPipeline(); + createFilter(pipeline.getResources(), getEnvironment().getScaleFactor(), + getEnvironment().getQuality(), in); + mResourcesLoaded = true; + } + startFilter = System.nanoTime(); + bindScriptValues(in); + run(in, out); + if (PERF_LOGGING) { + getRenderScriptContext().finish(); + endFilter = System.nanoTime(); + long endOverAll = System.nanoTime(); + String msg = String.format("%s; image size %dx%d; ", getName(), + in.getType().getX(), in.getType().getY()); + long timeOverAll = (endOverAll - startOverAll) / 1000; + long timeFilter = (endFilter - startFilter) / 1000; + msg += String.format("over all %.2f ms (%.2f FPS); ", + timeOverAll / 1000.f, 1000000.f / timeOverAll); + msg += String.format("run filter %.2f ms (%.2f FPS)", + timeFilter / 1000.f, 1000000.f / timeFilter); + Log.i(LOGTAG, msg); + } + } + + protected void run(Allocation in, Allocation out) {} + + @Override + public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) { + if (bitmap == null || bitmap.getWidth() == 0 || bitmap.getHeight() == 0) { + return bitmap; + } + try { + PipelineInterface pipeline = getEnvironment().getPipeline(); + if (DEBUG) { + Log.v(LOGTAG, "apply filter " + getName() + " in pipeline " + pipeline.getName()); + } + Resources rsc = pipeline.getResources(); + boolean sizeChanged = false; + if (getInPixelsAllocation() != null + && ((getInPixelsAllocation().getType().getX() != mLastInputWidth) + || (getInPixelsAllocation().getType().getY() != mLastInputHeight))) { + sizeChanged = true; + } + if (pipeline.prepareRenderscriptAllocations(bitmap) + || !isResourcesLoaded() || sizeChanged) { + freeResources(); + createFilter(rsc, scaleFactor, quality); + setResourcesLoaded(true); + mLastInputWidth = getInPixelsAllocation().getType().getX(); + mLastInputHeight = getInPixelsAllocation().getType().getY(); + } + bindScriptValues(); + runFilter(); + update(bitmap); + if (DEBUG) { + Log.v(LOGTAG, "DONE apply filter " + getName() + " in pipeline " + pipeline.getName()); + } + } catch (android.renderscript.RSIllegalArgumentException e) { + Log.e(LOGTAG, "Illegal argument? " + e); + } catch (android.renderscript.RSRuntimeException e) { + Log.e(LOGTAG, "RS runtime exception ? " + e); + } catch (java.lang.OutOfMemoryError e) { + // Many of the renderscript filters allocated large (>16Mb resources) in order to apply. + System.gc(); + displayLowMemoryToast(); + Log.e(LOGTAG, "not enough memory for filter " + getName(), e); + } + return bitmap; + } + + protected static Allocation convertBitmap(RenderScript RS, Bitmap bitmap) { + return Allocation.createFromBitmap(RS, bitmap, + Allocation.MipmapControl.MIPMAP_NONE, + Allocation.USAGE_SCRIPT | Allocation.USAGE_GRAPHICS_TEXTURE); + } + + private static Allocation convertRGBAtoA(RenderScript RS, Bitmap bitmap) { + if (RS != mRScache || mGreyConvert == null) { + mGreyConvert = new ScriptC_grey(RS, RS.getApplicationContext().getResources(), + R.raw.grey); + mRScache = RS; + } + + Type.Builder tb_a8 = new Type.Builder(RS, Element.A_8(RS)); + + Allocation bitmapTemp = convertBitmap(RS, bitmap); + if (bitmapTemp.getType().getElement().isCompatible(Element.A_8(RS))) { + return bitmapTemp; + } + + tb_a8.setX(bitmapTemp.getType().getX()); + tb_a8.setY(bitmapTemp.getType().getY()); + Allocation bitmapAlloc = Allocation.createTyped(RS, tb_a8.create(), + Allocation.MipmapControl.MIPMAP_NONE, + Allocation.USAGE_SCRIPT | Allocation.USAGE_GRAPHICS_TEXTURE); + mGreyConvert.forEach_RGBAtoA(bitmapTemp, bitmapAlloc); + bitmapTemp.destroy(); + return bitmapAlloc; + } + + public Allocation loadScaledResourceAlpha(int resource, int inSampleSize) { + Resources res = getEnvironment().getPipeline().getResources(); + final BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Bitmap.Config.ALPHA_8; + options.inSampleSize = inSampleSize; + Bitmap bitmap = BitmapFactory.decodeResource( + res, + resource, options); + Allocation ret = convertRGBAtoA(getRenderScriptContext(), bitmap); + bitmap.recycle(); + return ret; + } + + public Allocation loadScaledResourceAlpha(int resource, int w, int h, int inSampleSize) { + Resources res = getEnvironment().getPipeline().getResources(); + final BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Bitmap.Config.ALPHA_8; + options.inSampleSize = inSampleSize; + Bitmap bitmap = BitmapFactory.decodeResource( + res, + resource, options); + Bitmap resizeBitmap = Bitmap.createScaledBitmap(bitmap, w, h, true); + Allocation ret = convertRGBAtoA(getRenderScriptContext(), resizeBitmap); + resizeBitmap.recycle(); + bitmap.recycle(); + return ret; + } + + public Allocation loadResourceAlpha(int resource) { + return loadScaledResourceAlpha(resource, 1); + } + + public Allocation loadResource(int resource) { + Resources res = getEnvironment().getPipeline().getResources(); + final BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + Bitmap bitmap = BitmapFactory.decodeResource( + res, + resource, options); + Allocation ret = convertBitmap(getRenderScriptContext(), bitmap); + bitmap.recycle(); + return ret; + } + + private boolean isResourcesLoaded() { + return mResourcesLoaded; + } + + private void setResourcesLoaded(boolean resourcesLoaded) { + mResourcesLoaded = resourcesLoaded; + } + + /** + * Bitmaps and RS Allocations should be cleared here + */ + abstract protected void resetAllocations(); + + /** + * RS Script objects (and all other RS objects) should be cleared here + */ + public abstract void resetScripts(); + + /** + * Scripts values should be bound here + */ + abstract protected void bindScriptValues(); + + public void freeResources() { + if (!isResourcesLoaded()) { + return; + } + resetAllocations(); + mLastInputWidth = 0; + mLastInputHeight = 0; + setResourcesLoaded(false); + } +} diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterRedEye.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterRedEye.java new file mode 100644 index 000000000..511f9e90f --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterRedEye.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2013 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.filtershow.filters; + +import android.graphics.Bitmap; +import android.graphics.Matrix; +import android.graphics.RectF; + +import java.util.Vector; + +public class ImageFilterRedEye extends ImageFilter { + private static final String LOGTAG = "ImageFilterRedEye"; + FilterRedEyeRepresentation mParameters = new FilterRedEyeRepresentation(); + + public ImageFilterRedEye() { + mName = "Red Eye"; + } + + @Override + public FilterRepresentation getDefaultRepresentation() { + return new FilterRedEyeRepresentation(); + } + + public boolean isNil() { + return mParameters.isNil(); + } + + public Vector<FilterPoint> getCandidates() { + return mParameters.getCandidates(); + } + + public void clear() { + mParameters.clearCandidates(); + } + + native protected void nativeApplyFilter(Bitmap bitmap, int w, int h, short[] matrix); + + @Override + public void useRepresentation(FilterRepresentation representation) { + FilterRedEyeRepresentation parameters = (FilterRedEyeRepresentation) representation; + mParameters = parameters; + } + + @Override + public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) { + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + short[] rect = new short[4]; + + int size = mParameters.getNumberOfCandidates(); + Matrix originalToScreen = getOriginalToScreenMatrix(w, h); + for (int i = 0; i < size; i++) { + RectF r = new RectF(((RedEyeCandidate) (mParameters.getCandidate(i))).mRect); + originalToScreen.mapRect(r); + if (r.intersect(0, 0, w, h)) { + rect[0] = (short) r.left; + rect[1] = (short) r.top; + rect[2] = (short) r.width(); + rect[3] = (short) r.height(); + nativeApplyFilter(bitmap, w, h, rect); + } + } + return bitmap; + } +} diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterSaturated.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterSaturated.java new file mode 100644 index 000000000..c3124ff77 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterSaturated.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2012 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.filtershow.filters; + +import com.android.gallery3d.R; + +import android.graphics.Bitmap; + +public class ImageFilterSaturated extends SimpleImageFilter { + private static final String SERIALIZATION_NAME = "SATURATED"; + public ImageFilterSaturated() { + mName = "Saturated"; + } + + @Override + public FilterRepresentation getDefaultRepresentation() { + FilterBasicRepresentation representation = + (FilterBasicRepresentation) super.getDefaultRepresentation(); + representation.setName("Saturated"); + representation.setSerializationName(SERIALIZATION_NAME); + representation.setFilterClass(ImageFilterSaturated.class); + representation.setTextId(R.string.saturation); + representation.setMinimum(-100); + representation.setMaximum(100); + representation.setDefaultValue(0); + representation.setSupportsPartialRendering(true); + return representation; + } + + native protected void nativeApplyFilter(Bitmap bitmap, int w, int h, float saturation); + + @Override + public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) { + if (getParameters() == null) { + return bitmap; + } + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + int p = getParameters().getValue(); + float value = 1 + p / 100.0f; + nativeApplyFilter(bitmap, w, h, value); + return bitmap; + } +} diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterShadows.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterShadows.java new file mode 100644 index 000000000..bd119bbc9 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterShadows.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2012 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.filtershow.filters; + +import com.android.gallery3d.R; + +import android.graphics.Bitmap; + +public class ImageFilterShadows extends SimpleImageFilter { + private static final String SERIALIZATION_NAME = "SHADOWS"; + public ImageFilterShadows() { + mName = "Shadows"; + + } + + public FilterRepresentation getDefaultRepresentation() { + FilterBasicRepresentation representation = + (FilterBasicRepresentation) super.getDefaultRepresentation(); + representation.setName("Shadows"); + representation.setSerializationName(SERIALIZATION_NAME); + representation.setFilterClass(ImageFilterShadows.class); + representation.setTextId(R.string.shadow_recovery); + representation.setMinimum(-100); + representation.setMaximum(100); + representation.setDefaultValue(0); + representation.setSupportsPartialRendering(true); + return representation; + } + + native protected void nativeApplyFilter(Bitmap bitmap, int w, int h, float factor); + + @Override + public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) { + if (getParameters() == null) { + return bitmap; + } + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + float p = getParameters().getValue(); + + nativeApplyFilter(bitmap, w, h, p); + return bitmap; + } +} diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterSharpen.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterSharpen.java new file mode 100644 index 000000000..3bd794464 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterSharpen.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2012 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.filtershow.filters; + +import com.android.gallery3d.R; + +public class ImageFilterSharpen extends ImageFilterRS { + private static final String SERIALIZATION_NAME = "SHARPEN"; + private static final String LOGTAG = "ImageFilterSharpen"; + private ScriptC_convolve3x3 mScript; + + private FilterBasicRepresentation mParameters; + + public ImageFilterSharpen() { + mName = "Sharpen"; + } + + public FilterRepresentation getDefaultRepresentation() { + FilterRepresentation representation = new FilterBasicRepresentation("Sharpen", 0, 0, 100); + representation.setSerializationName(SERIALIZATION_NAME); + representation.setShowParameterValue(true); + representation.setFilterClass(ImageFilterSharpen.class); + representation.setTextId(R.string.sharpness); + representation.setOverlayId(R.drawable.filtershow_button_colors_sharpen); + representation.setEditorId(R.id.imageShow); + representation.setSupportsPartialRendering(true); + return representation; + } + + public void useRepresentation(FilterRepresentation representation) { + FilterBasicRepresentation parameters = (FilterBasicRepresentation) representation; + mParameters = parameters; + } + + @Override + protected void resetAllocations() { + // nothing to do + } + + @Override + public void resetScripts() { + if (mScript != null) { + mScript.destroy(); + mScript = null; + } + } + + @Override + protected void createFilter(android.content.res.Resources res, float scaleFactor, + int quality) { + if (mScript == null) { + mScript = new ScriptC_convolve3x3(getRenderScriptContext(), res, R.raw.convolve3x3); + } + } + + private void computeKernel() { + float scaleFactor = getEnvironment().getScaleFactor(); + float p1 = mParameters.getValue() * scaleFactor; + float value = p1 / 100.0f; + float f[] = new float[9]; + float p = value; + f[0] = -p; + f[1] = -p; + f[2] = -p; + f[3] = -p; + f[4] = 8 * p + 1; + f[5] = -p; + f[6] = -p; + f[7] = -p; + f[8] = -p; + mScript.set_gCoeffs(f); + } + + @Override + protected void bindScriptValues() { + int w = getInPixelsAllocation().getType().getX(); + int h = getInPixelsAllocation().getType().getY(); + mScript.set_gWidth(w); + mScript.set_gHeight(h); + } + + @Override + protected void runFilter() { + if (mParameters == null) { + return; + } + computeKernel(); + mScript.set_gIn(getInPixelsAllocation()); + mScript.bind_gPixels(getInPixelsAllocation()); + mScript.forEach_root(getInPixelsAllocation(), getOutPixelsAllocation()); + } + +} diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterTinyPlanet.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterTinyPlanet.java new file mode 100644 index 000000000..77250bd7a --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterTinyPlanet.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2012 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.filtershow.filters; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.RectF; + +import com.adobe.xmp.XMPException; +import com.adobe.xmp.XMPMeta; +import com.android.gallery3d.app.Log; +import com.android.gallery3d.filtershow.cache.ImageLoader; +import com.android.gallery3d.filtershow.imageshow.MasterImage; +import com.android.gallery3d.filtershow.pipeline.ImagePreset; + +/** + * An image filter which creates a tiny planet projection. + */ +public class ImageFilterTinyPlanet extends SimpleImageFilter { + + + private static final String LOGTAG = ImageFilterTinyPlanet.class.getSimpleName(); + public static final String GOOGLE_PANO_NAMESPACE = "http://ns.google.com/photos/1.0/panorama/"; + FilterTinyPlanetRepresentation mParameters = new FilterTinyPlanetRepresentation(); + + public static final String CROPPED_AREA_IMAGE_WIDTH_PIXELS = + "CroppedAreaImageWidthPixels"; + public static final String CROPPED_AREA_IMAGE_HEIGHT_PIXELS = + "CroppedAreaImageHeightPixels"; + public static final String CROPPED_AREA_FULL_PANO_WIDTH_PIXELS = + "FullPanoWidthPixels"; + public static final String CROPPED_AREA_FULL_PANO_HEIGHT_PIXELS = + "FullPanoHeightPixels"; + public static final String CROPPED_AREA_LEFT = + "CroppedAreaLeftPixels"; + public static final String CROPPED_AREA_TOP = + "CroppedAreaTopPixels"; + + public ImageFilterTinyPlanet() { + mName = "TinyPlanet"; + } + + @Override + public void useRepresentation(FilterRepresentation representation) { + FilterTinyPlanetRepresentation parameters = (FilterTinyPlanetRepresentation) representation; + mParameters = parameters; + } + + @Override + public FilterRepresentation getDefaultRepresentation() { + return new FilterTinyPlanetRepresentation(); + } + + + native protected void nativeApplyFilter( + Bitmap bitmapIn, int width, int height, Bitmap bitmapOut, int outSize, float scale, + float angle); + + + @Override + public Bitmap apply(Bitmap bitmapIn, float scaleFactor, int quality) { + int w = bitmapIn.getWidth(); + int h = bitmapIn.getHeight(); + int outputSize = (int) (w / 2f); + ImagePreset preset = getEnvironment().getImagePreset(); + Bitmap mBitmapOut = null; + if (preset != null) { + XMPMeta xmp = ImageLoader.getXmpObject(MasterImage.getImage().getActivity()); + // Do nothing, just use bitmapIn as is if we don't have XMP. + if(xmp != null) { + bitmapIn = applyXmp(bitmapIn, xmp, w); + } + } + if (mBitmapOut != null) { + if (outputSize != mBitmapOut.getHeight()) { + mBitmapOut = null; + } + } + while (mBitmapOut == null) { + try { + mBitmapOut = getEnvironment().getBitmap(outputSize, outputSize); + } catch (java.lang.OutOfMemoryError e) { + System.gc(); + outputSize /= 2; + Log.v(LOGTAG, "No memory to create Full Tiny Planet create half"); + } + } + nativeApplyFilter(bitmapIn, bitmapIn.getWidth(), bitmapIn.getHeight(), mBitmapOut, + outputSize, mParameters.getZoom() / 100f, mParameters.getAngle()); + + return mBitmapOut; + } + + private Bitmap applyXmp(Bitmap bitmapIn, XMPMeta xmp, int intermediateWidth) { + try { + int croppedAreaWidth = + getInt(xmp, CROPPED_AREA_IMAGE_WIDTH_PIXELS); + int croppedAreaHeight = + getInt(xmp, CROPPED_AREA_IMAGE_HEIGHT_PIXELS); + int fullPanoWidth = + getInt(xmp, CROPPED_AREA_FULL_PANO_WIDTH_PIXELS); + int fullPanoHeight = + getInt(xmp, CROPPED_AREA_FULL_PANO_HEIGHT_PIXELS); + int left = getInt(xmp, CROPPED_AREA_LEFT); + int top = getInt(xmp, CROPPED_AREA_TOP); + + if (fullPanoWidth == 0 || fullPanoHeight == 0) { + return bitmapIn; + } + // Make sure the intermediate image has the similar size to the + // input. + Bitmap paddedBitmap = null; + float scale = intermediateWidth / (float) fullPanoWidth; + while (paddedBitmap == null) { + try { + paddedBitmap = Bitmap.createBitmap( + (int) (fullPanoWidth * scale), (int) (fullPanoHeight * scale), + Bitmap.Config.ARGB_8888); + } catch (java.lang.OutOfMemoryError e) { + System.gc(); + scale /= 2; + } + } + Canvas paddedCanvas = new Canvas(paddedBitmap); + + int right = left + croppedAreaWidth; + int bottom = top + croppedAreaHeight; + RectF destRect = new RectF(left * scale, top * scale, right * scale, bottom * scale); + paddedCanvas.drawBitmap(bitmapIn, null, destRect, null); + bitmapIn = paddedBitmap; + } catch (XMPException ex) { + // Do nothing, just use bitmapIn as is. + } + return bitmapIn; + } + + private static int getInt(XMPMeta xmp, String key) throws XMPException { + if (xmp.doesPropertyExist(GOOGLE_PANO_NAMESPACE, key)) { + return xmp.getPropertyInteger(GOOGLE_PANO_NAMESPACE, key); + } else { + return 0; + } + } +} diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterVibrance.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterVibrance.java new file mode 100644 index 000000000..86be9a155 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterVibrance.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2012 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.filtershow.filters; + +import com.android.gallery3d.R; + +import android.graphics.Bitmap; + +public class ImageFilterVibrance extends SimpleImageFilter { + private static final String SERIALIZATION_NAME = "VIBRANCE"; + public ImageFilterVibrance() { + mName = "Vibrance"; + } + + public FilterRepresentation getDefaultRepresentation() { + FilterBasicRepresentation representation = + (FilterBasicRepresentation) super.getDefaultRepresentation(); + representation.setName("Vibrance"); + representation.setSerializationName(SERIALIZATION_NAME); + representation.setFilterClass(ImageFilterVibrance.class); + representation.setTextId(R.string.vibrance); + representation.setMinimum(-100); + representation.setMaximum(100); + representation.setDefaultValue(0); + representation.setSupportsPartialRendering(true); + return representation; + } + + native protected void nativeApplyFilter(Bitmap bitmap, int w, int h, float bright); + + @Override + public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) { + if (getParameters() == null) { + return bitmap; + } + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + float value = getParameters().getValue(); + nativeApplyFilter(bitmap, w, h, value); + + return bitmap; + } +} diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterVignette.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterVignette.java new file mode 100644 index 000000000..7e0a452bf --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterVignette.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2012 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.filtershow.filters; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Rect; +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.pipeline.FilterEnvironment; + +public class ImageFilterVignette extends SimpleImageFilter { + private static final String LOGTAG = "ImageFilterVignette"; + private Bitmap mOverlayBitmap; + + public ImageFilterVignette() { + mName = "Vignette"; + } + + @Override + public FilterRepresentation getDefaultRepresentation() { + FilterVignetteRepresentation representation = new FilterVignetteRepresentation(); + return representation; + } + + native protected void nativeApplyFilter( + Bitmap bitmap, int w, int h, int cx, int cy, float radx, float rady, float strength); + + private float calcRadius(float cx, float cy, int w, int h) { + float d = cx; + if (d < (w - cx)) { + d = w - cx; + } + if (d < cy) { + d = cy; + } + if (d < (h - cy)) { + d = h - cy; + } + return d * d * 2.0f; + } + + @Override + public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) { + if (SIMPLE_ICONS && FilterEnvironment.QUALITY_ICON == quality) { + if (mOverlayBitmap == null) { + Resources res = getEnvironment().getPipeline().getResources(); + mOverlayBitmap = IconUtilities.getFXBitmap(res, + R.drawable.filtershow_icon_vignette); + } + Canvas c = new Canvas(bitmap); + int dim = Math.max(bitmap.getWidth(), bitmap.getHeight()); + Rect r = new Rect(0, 0, dim, dim); + c.drawBitmap(mOverlayBitmap, null, r, null); + return bitmap; + } + FilterVignetteRepresentation rep = (FilterVignetteRepresentation) getParameters(); + if (rep == null) { + return bitmap; + } + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + float value = rep.getValue() / 100.0f; + float cx = w / 2; + float cy = h / 2; + float r = calcRadius(cx, cy, w, h); + float rx = r; + float ry = r; + if (rep.isCenterSet()) { + Matrix m = getOriginalToScreenMatrix(w, h); + cx = rep.getCenterX(); + cy = rep.getCenterY(); + float[] center = new float[] { cx, cy }; + m.mapPoints(center); + cx = center[0]; + cy = center[1]; + rx = m.mapRadius(rep.getRadiusX()); + ry = m.mapRadius(rep.getRadiusY()); + } + nativeApplyFilter(bitmap, w, h, (int) cx, (int) cy, rx, ry, value); + return bitmap; + } +} diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterWBalance.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterWBalance.java new file mode 100644 index 000000000..6bb88ec21 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterWBalance.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2012 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.filtershow.filters; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.editors.ImageOnlyEditor; + +import android.graphics.Bitmap; + +public class ImageFilterWBalance extends ImageFilter { + private static final String SERIALIZATION_NAME = "WBALANCE"; + private static final String TAG = "ImageFilterWBalance"; + + public ImageFilterWBalance() { + mName = "WBalance"; + } + + public FilterRepresentation getDefaultRepresentation() { + FilterRepresentation representation = new FilterDirectRepresentation("WBalance"); + representation.setSerializationName(SERIALIZATION_NAME); + representation.setFilterClass(ImageFilterWBalance.class); + representation.setFilterType(FilterRepresentation.TYPE_WBALANCE); + representation.setTextId(R.string.wbalance); + representation.setShowParameterValue(false); + representation.setEditorId(ImageOnlyEditor.ID); + representation.setSupportsPartialRendering(true); + return representation; + } + + @Override + public void useRepresentation(FilterRepresentation representation) { + + } + + native protected void nativeApplyFilter(Bitmap bitmap, int w, int h, int locX, int locY); + + @Override + public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) { + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + nativeApplyFilter(bitmap, w, h, -1, -1); + return bitmap; + } + +} diff --git a/src/com/android/gallery3d/filtershow/filters/RedEyeCandidate.java b/src/com/android/gallery3d/filtershow/filters/RedEyeCandidate.java new file mode 100644 index 000000000..a40d4fa3b --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/RedEyeCandidate.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2013 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.filtershow.filters; + +import android.graphics.RectF; + +public class RedEyeCandidate implements FilterPoint { + RectF mRect = new RectF(); + RectF mBounds = new RectF(); + + public RedEyeCandidate(RedEyeCandidate candidate) { + mRect.set(candidate.mRect); + mBounds.set(candidate.mBounds); + } + + public RedEyeCandidate(RectF rect, RectF bounds) { + mRect.set(rect); + mBounds.set(bounds); + } + + public boolean equals(RedEyeCandidate candidate) { + if (candidate.mRect.equals(mRect) + && candidate.mBounds.equals(mBounds)) { + return true; + } + return false; + } + + public boolean intersect(RectF rect) { + return mRect.intersect(rect); + } + + public RectF getRect() { + return mRect; + } +} diff --git a/src/com/android/gallery3d/filtershow/filters/SimpleImageFilter.java b/src/com/android/gallery3d/filtershow/filters/SimpleImageFilter.java new file mode 100644 index 000000000..c891d20f3 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/SimpleImageFilter.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2013 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.filtershow.filters; + +public class SimpleImageFilter extends ImageFilter { + + private FilterBasicRepresentation mParameters; + + public FilterRepresentation getDefaultRepresentation() { + FilterRepresentation representation = new FilterBasicRepresentation("Default", 0, 50, 100); + representation.setShowParameterValue(true); + return representation; + } + + public void useRepresentation(FilterRepresentation representation) { + FilterBasicRepresentation parameters = (FilterBasicRepresentation) representation; + mParameters = parameters; + } + + public FilterBasicRepresentation getParameters() { + return mParameters; + } +} diff --git a/src/com/android/gallery3d/filtershow/filters/SplineMath.java b/src/com/android/gallery3d/filtershow/filters/SplineMath.java new file mode 100644 index 000000000..5b12d0a61 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/SplineMath.java @@ -0,0 +1,166 @@ +package com.android.gallery3d.filtershow.filters; + + +public class SplineMath { + double[][] mPoints = new double[6][2]; + double[] mDerivatives; + SplineMath(int n) { + mPoints = new double[n][2]; + } + + public void setPoint(int index, double x, double y) { + mPoints[index][0] = x; + mPoints[index][1] = y; + mDerivatives = null; + } + + public float[][] calculatetCurve(int n) { + float[][] curve = new float[n][2]; + double[][] points = new double[mPoints.length][2]; + for (int i = 0; i < mPoints.length; i++) { + + points[i][0] = mPoints[i][0]; + points[i][1] = mPoints[i][1]; + + } + double[] derivatives = solveSystem(points); + float start = (float) points[0][0]; + float end = (float) (points[points.length - 1][0]); + + curve[0][0] = (float) (points[0][0]); + curve[0][1] = (float) (points[0][1]); + int last = curve.length - 1; + curve[last][0] = (float) (points[points.length - 1][0]); + curve[last][1] = (float) (points[points.length - 1][1]); + + for (int i = 0; i < curve.length; i++) { + + double[] cur = null; + double[] next = null; + double x = start + i * (end - start) / (curve.length - 1); + int pivot = 0; + for (int j = 0; j < points.length - 1; j++) { + if (x >= points[j][0] && x <= points[j + 1][0]) { + pivot = j; + } + } + cur = points[pivot]; + next = points[pivot + 1]; + if (x <= next[0]) { + double x1 = cur[0]; + double x2 = next[0]; + double y1 = cur[1]; + double y2 = next[1]; + + // Use the second derivatives to apply the cubic spline + // equation: + double delta = (x2 - x1); + double delta2 = delta * delta; + double b = (x - x1) / delta; + double a = 1 - b; + double ta = a * y1; + double tb = b * y2; + double tc = (a * a * a - a) * derivatives[pivot]; + double td = (b * b * b - b) * derivatives[pivot + 1]; + double y = ta + tb + (delta2 / 6) * (tc + td); + + curve[i][0] = (float) (x); + curve[i][1] = (float) (y); + } else { + curve[i][0] = (float) (next[0]); + curve[i][1] = (float) (next[1]); + } + } + return curve; + } + + public double getValue(double x) { + double[] cur = null; + double[] next = null; + if (mDerivatives == null) + mDerivatives = solveSystem(mPoints); + int pivot = 0; + for (int j = 0; j < mPoints.length - 1; j++) { + pivot = j; + if (x <= mPoints[j][0]) { + break; + } + } + cur = mPoints[pivot]; + next = mPoints[pivot + 1]; + double x1 = cur[0]; + double x2 = next[0]; + double y1 = cur[1]; + double y2 = next[1]; + + // Use the second derivatives to apply the cubic spline + // equation: + double delta = (x2 - x1); + double delta2 = delta * delta; + double b = (x - x1) / delta; + double a = 1 - b; + double ta = a * y1; + double tb = b * y2; + double tc = (a * a * a - a) * mDerivatives[pivot]; + double td = (b * b * b - b) * mDerivatives[pivot + 1]; + double y = ta + tb + (delta2 / 6) * (tc + td); + + return y; + + } + + double[] solveSystem(double[][] points) { + int n = points.length; + double[][] system = new double[n][3]; + double[] result = new double[n]; // d + double[] solution = new double[n]; // returned coefficients + system[0][1] = 1; + system[n - 1][1] = 1; + double d6 = 1.0 / 6.0; + double d3 = 1.0 / 3.0; + + // let's create a tridiagonal matrix representing the + // system, and apply the TDMA algorithm to solve it + // (see http://en.wikipedia.org/wiki/Tridiagonal_matrix_algorithm) + for (int i = 1; i < n - 1; i++) { + double deltaPrevX = points[i][0] - points[i - 1][0]; + double deltaX = points[i + 1][0] - points[i - 1][0]; + double deltaNextX = points[i + 1][0] - points[i][0]; + double deltaNextY = points[i + 1][1] - points[i][1]; + double deltaPrevY = points[i][1] - points[i - 1][1]; + system[i][0] = d6 * deltaPrevX; // a_i + system[i][1] = d3 * deltaX; // b_i + system[i][2] = d6 * deltaNextX; // c_i + result[i] = (deltaNextY / deltaNextX) - (deltaPrevY / deltaPrevX); // d_i + } + + // Forward sweep + for (int i = 1; i < n; i++) { + // m = a_i/b_i-1 + double m = system[i][0] / system[i - 1][1]; + // b_i = b_i - m(c_i-1) + system[i][1] = system[i][1] - m * system[i - 1][2]; + // d_i = d_i - m(d_i-1) + result[i] = result[i] - m * result[i - 1]; + } + + // Back substitution + solution[n - 1] = result[n - 1] / system[n - 1][1]; + for (int i = n - 2; i >= 0; --i) { + solution[i] = (result[i] - system[i][2] * solution[i + 1]) / system[i][1]; + } + return solution; + } + + public static void main(String[] args) { + SplineMath s = new SplineMath(10); + for (int i = 0; i < 10; i++) { + s.setPoint(i, i, i); + } + float[][] curve = s.calculatetCurve(40); + + for (int j = 0; j < curve.length; j++) { + System.out.println(curve[j][0] + "," + curve[j][1]); + } + } +} diff --git a/src/com/android/gallery3d/filtershow/filters/convolve3x3.rs b/src/com/android/gallery3d/filtershow/filters/convolve3x3.rs new file mode 100644 index 000000000..2acffab06 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/convolve3x3.rs @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2012 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. + */ + +#pragma version(1) +#pragma rs java_package_name(com.android.gallery3d.filtershow.filters) +#pragma rs_fp_relaxed + +int32_t gWidth; +int32_t gHeight; +const uchar4 *gPixels; +rs_allocation gIn; + +float gCoeffs[9]; + +void root(const uchar4 *in, uchar4 *out, const void *usrData, uint32_t x, uint32_t y) { + uint32_t x1 = min((int32_t)x+1, gWidth-1); + uint32_t x2 = max((int32_t)x-1, 0); + uint32_t y1 = min((int32_t)y+1, gHeight-1); + uint32_t y2 = max((int32_t)y-1, 0); + + float4 p00 = rsUnpackColor8888(gPixels[x1 + gWidth * y1]); + float4 p01 = rsUnpackColor8888(gPixels[x + gWidth * y1]); + float4 p02 = rsUnpackColor8888(gPixels[x2 + gWidth * y1]); + float4 p10 = rsUnpackColor8888(gPixels[x1 + gWidth * y]); + float4 p11 = rsUnpackColor8888(gPixels[x + gWidth * y]); + float4 p12 = rsUnpackColor8888(gPixels[x2 + gWidth * y]); + float4 p20 = rsUnpackColor8888(gPixels[x1 + gWidth * y2]); + float4 p21 = rsUnpackColor8888(gPixels[x + gWidth * y2]); + float4 p22 = rsUnpackColor8888(gPixels[x2 + gWidth * y2]); + + p00 *= gCoeffs[0]; + p01 *= gCoeffs[1]; + p02 *= gCoeffs[2]; + p10 *= gCoeffs[3]; + p11 *= gCoeffs[4]; + p12 *= gCoeffs[5]; + p20 *= gCoeffs[6]; + p21 *= gCoeffs[7]; + p22 *= gCoeffs[8]; + + p00 += p01; + p02 += p10; + p11 += p12; + p20 += p21; + + p22 += p00; + p02 += p11; + + p20 += p22; + p20 += p02; + + p20 = clamp(p20, 0.f, 1.f); + *out = rsPackColorTo8888(p20.r, p20.g, p20.b); +} diff --git a/src/com/android/gallery3d/filtershow/filters/grad.rs b/src/com/android/gallery3d/filtershow/filters/grad.rs new file mode 100644 index 000000000..ddbafd349 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/grad.rs @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2012 Unknown + * + * 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. + */ + +#pragma version(1) +#pragma rs java_package_name(com.android.gallery3d.filtershow.filters) + +#define MAX_POINTS 16 + +uint32_t inputWidth; +uint32_t inputHeight; +static const float Rf = 0.2999f; +static const float Gf = 0.587f; +static const float Bf = 0.114f; +//static const float size_scale = 0.01f; + +typedef struct { + rs_matrix3x3 colorMatrix; + float rgbOff; + float dx; + float dy; + float off; +} UPointData; +int mNumberOfLines; +// input data +bool mask[MAX_POINTS]; +int xPos1[MAX_POINTS]; +int yPos1[MAX_POINTS]; +int xPos2[MAX_POINTS]; +int yPos2[MAX_POINTS]; +int size[MAX_POINTS]; +int brightness[MAX_POINTS]; +int contrast[MAX_POINTS]; +int saturation[MAX_POINTS]; + +// generated data +static UPointData grads[MAX_POINTS]; + +void setupGradParams() { + int k = 0; + for (int i = 0; i < MAX_POINTS; i++) { + if (!mask[i]) { + continue; + } + float x1 = xPos1[i]; + float y1 = yPos1[i]; + float x2 = xPos2[i]; + float y2 = yPos2[i]; + + float denom = (y2 * y2 - 2 * y1 * y2 + x2 * x2 - 2 * x1 * x2 + y1 * y1 + x1 * x1); + if (denom == 0) { + continue; + } + grads[k].dy = (y1 - y2) / denom; + grads[k].dx = (x1 - x2) / denom; + grads[k].off = (y2 * y2 + x2 * x2 - x1 * x2 - y1 * y2) / denom; + + float S = 1+saturation[i]/100.f; + float MS = 1-S; + float Rt = Rf * MS; + float Gt = Gf * MS; + float Bt = Bf * MS; + + float b = 1+brightness[i]/100.f; + float c = 1+contrast[i]/100.f; + b *= c; + grads[k].rgbOff = .5f - c/2.f; + rsMatrixSet(&grads[i].colorMatrix, 0, 0, b * (Rt + S)); + rsMatrixSet(&grads[i].colorMatrix, 1, 0, b * Gt); + rsMatrixSet(&grads[i].colorMatrix, 2, 0, b * Bt); + rsMatrixSet(&grads[i].colorMatrix, 0, 1, b * Rt); + rsMatrixSet(&grads[i].colorMatrix, 1, 1, b * (Gt + S)); + rsMatrixSet(&grads[i].colorMatrix, 2, 1, b * Bt); + rsMatrixSet(&grads[i].colorMatrix, 0, 2, b * Rt); + rsMatrixSet(&grads[i].colorMatrix, 1, 2, b * Gt); + rsMatrixSet(&grads[i].colorMatrix, 2, 2, b * (Bt + S)); + + k++; + } + mNumberOfLines = k; +} + +void init() { + +} + +uchar4 __attribute__((kernel)) selectiveAdjust(const uchar4 in, uint32_t x, + uint32_t y) { + float4 pixel = rsUnpackColor8888(in); + + float4 wsum = pixel; + wsum.a = 0.f; + for (int i = 0; i < mNumberOfLines; i++) { + UPointData* grad = &grads[i]; + float t = clamp(x*grad->dx+y*grad->dy+grad->off,0.f,1.0f); + wsum.xyz = wsum.xyz*(1-t)+ + t*(rsMatrixMultiply(&grad->colorMatrix ,wsum.xyz)+grad->rgbOff); + + } + + pixel.rgb = wsum.rgb; + pixel.a = 1.0f; + + uchar4 out = rsPackColorTo8888(clamp(pixel, 0.f, 1.0f)); + return out; +} + + + diff --git a/src/com/android/gallery3d/filtershow/filters/grey.rs b/src/com/android/gallery3d/filtershow/filters/grey.rs new file mode 100644 index 000000000..e01880360 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/grey.rs @@ -0,0 +1,22 @@ + /* + * Copyright (C) 2013 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. + */ + +#pragma version(1) +#pragma rs java_package_name(com.android.gallery3d.filtershow.filters) + +uchar __attribute__((kernel)) RGBAtoA(uchar4 in) { + return in.r; +} diff --git a/src/com/android/gallery3d/filtershow/filters/saturation.rs b/src/com/android/gallery3d/filtershow/filters/saturation.rs new file mode 100644 index 000000000..5210e34a3 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/filters/saturation.rs @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2012 Unknown + * + * 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. + */ + +#pragma version(1) +#pragma rs java_package_name(com.android.gallery3d.filtershow.filters) + +#define MAX_CHANELS 7 +#define MAX_HUE 4096 +static const int ABITS = 4; +static const int HSCALE = 256; +static const int k1=255 << ABITS; +static const int k2=HSCALE << ABITS; + +static const float Rf = 0.2999f; +static const float Gf = 0.587f; +static const float Bf = 0.114f; + +rs_matrix3x3 colorMatrix_min; +rs_matrix3x3 colorMatrix_max; + +int mNumberOfLines; +// input data +int saturation[MAX_CHANELS]; +float sat[MAX_CHANELS]; + +float satLut[MAX_HUE]; +// generated data + + +void setupGradParams() { + + int master = saturation[0]; + int max = master+saturation[1]; + int min = max; + + // calculate the minimum and maximum saturation + for (int i = 1; i < MAX_CHANELS; i++) { + int v = master+saturation[i]; + if (max < v) { + max = v; + } + else if (min > v) { + min = v; + } + } + // generate a lookup table for all hue 0 to 4K which goes from 0 to 1 0=min sat 1 = max sat + min = min - 1; + for(int i = 0; i < MAX_HUE ; i++) { + float p = i * 6 / (float)MAX_HUE; + int ip = ((int)(p + .5f)) % 6; + int v = master + saturation[ip + 1]; + satLut[i] = (v - min)/(float)(max - min); + } + + float S = 1 + max / 100.f; + float MS = 1 - S; + float Rt = Rf * MS; + float Gt = Gf * MS; + float Bt = Bf * MS; + float b = 1.f; + + // Generate 2 color matrix one at min sat and one at max + rsMatrixSet(&colorMatrix_max, 0, 0, b * (Rt + S)); + rsMatrixSet(&colorMatrix_max, 1, 0, b * Gt); + rsMatrixSet(&colorMatrix_max, 2, 0, b * Bt); + rsMatrixSet(&colorMatrix_max, 0, 1, b * Rt); + rsMatrixSet(&colorMatrix_max, 1, 1, b * (Gt + S)); + rsMatrixSet(&colorMatrix_max, 2, 1, b * Bt); + rsMatrixSet(&colorMatrix_max, 0, 2, b * Rt); + rsMatrixSet(&colorMatrix_max, 1, 2, b * Gt); + rsMatrixSet(&colorMatrix_max, 2, 2, b * (Bt + S)); + + S = 1 + min / 100.f; + MS = 1-S; + Rt = Rf * MS; + Gt = Gf * MS; + Bt = Bf * MS; + b = 1; + + rsMatrixSet(&colorMatrix_min, 0, 0, b * (Rt + S)); + rsMatrixSet(&colorMatrix_min, 1, 0, b * Gt); + rsMatrixSet(&colorMatrix_min, 2, 0, b * Bt); + rsMatrixSet(&colorMatrix_min, 0, 1, b * Rt); + rsMatrixSet(&colorMatrix_min, 1, 1, b * (Gt + S)); + rsMatrixSet(&colorMatrix_min, 2, 1, b * Bt); + rsMatrixSet(&colorMatrix_min, 0, 2, b * Rt); + rsMatrixSet(&colorMatrix_min, 1, 2, b * Gt); + rsMatrixSet(&colorMatrix_min, 2, 2, b * (Bt + S)); +} + +static ushort rgb2hue( uchar4 rgb) +{ + int iMin,iMax,chroma; + + int ri = rgb.r; + int gi = rgb.g; + int bi = rgb.b; + short rv,rs,rh; + + if (ri > gi) { + iMax = max (ri, bi); + iMin = min (gi, bi); + } else { + iMax = max (gi, bi); + iMin = min (ri, bi); + } + + rv = (short) (iMax << ABITS); + + if (rv == 0) { + return 0; + } + + chroma = iMax - iMin; + rs = (short) ((k1 * chroma) / iMax); + if (rs == 0) { + return 0; + } + + if ( ri == iMax ) { + rh = (short) ((k2 * (6 * chroma + gi - bi))/(6 * chroma)); + if (rh >= k2) { + rh -= k2; + } + return rh; + } + + if (gi == iMax) { + return(short) ((k2 * (2 * chroma + bi - ri)) / (6 * chroma)); + } + + return (short) ((k2 * (4 * chroma + ri - gi)) / (6 * chroma)); +} + +uchar4 __attribute__((kernel)) selectiveAdjust(const uchar4 in, uint32_t x, + uint32_t y) { + float4 pixel = rsUnpackColor8888(in); + + float4 wsum = pixel; + int hue = rgb2hue(in); + + float t = satLut[hue]; + pixel.xyz = rsMatrixMultiply(&colorMatrix_min ,pixel.xyz) * (1 - t) + + t * (rsMatrixMultiply(&colorMatrix_max ,pixel.xyz)); + + pixel.a = 1.0f; + return rsPackColorTo8888(clamp(pixel, 0.f, 1.0f)); +}
\ No newline at end of file diff --git a/src/com/android/gallery3d/filtershow/history/HistoryItem.java b/src/com/android/gallery3d/filtershow/history/HistoryItem.java new file mode 100644 index 000000000..2baaac327 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/history/HistoryItem.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2013 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.filtershow.history; + +import android.graphics.Bitmap; +import android.util.Log; +import com.android.gallery3d.filtershow.filters.FilterRepresentation; +import com.android.gallery3d.filtershow.pipeline.ImagePreset; + +public class HistoryItem { + private static final String LOGTAG = "HistoryItem"; + private ImagePreset mImagePreset; + private FilterRepresentation mFilterRepresentation; + private Bitmap mPreviewImage; + + public HistoryItem(ImagePreset preset, FilterRepresentation representation) { + mImagePreset = new ImagePreset(preset); + if (representation != null) { + mFilterRepresentation = representation.copy(); + } + } + + public ImagePreset getImagePreset() { + return mImagePreset; + } + + public FilterRepresentation getFilterRepresentation() { + return mFilterRepresentation; + } + + public Bitmap getPreviewImage() { + return mPreviewImage; + } + + public void setPreviewImage(Bitmap previewImage) { + mPreviewImage = previewImage; + } + +} diff --git a/src/com/android/gallery3d/filtershow/history/HistoryManager.java b/src/com/android/gallery3d/filtershow/history/HistoryManager.java new file mode 100644 index 000000000..755e2ea58 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/history/HistoryManager.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2013 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.filtershow.history; + +import android.graphics.drawable.Drawable; +import android.view.MenuItem; + +import java.util.Vector; + +public class HistoryManager { + private static final String LOGTAG = "HistoryManager"; + + private Vector<HistoryItem> mHistoryItems = new Vector<HistoryItem>(); + private int mCurrentPresetPosition = 0; + private MenuItem mUndoMenuItem = null; + private MenuItem mRedoMenuItem = null; + private MenuItem mResetMenuItem = null; + + public void setMenuItems(MenuItem undoItem, MenuItem redoItem, MenuItem resetItem) { + mUndoMenuItem = undoItem; + mRedoMenuItem = redoItem; + mResetMenuItem = resetItem; + updateMenuItems(); + } + + private int getCount() { + return mHistoryItems.size(); + } + + public HistoryItem getItem(int position) { + return mHistoryItems.elementAt(position); + } + + private void clear() { + mHistoryItems.clear(); + } + + private void add(HistoryItem item) { + mHistoryItems.add(item); + } + + private void notifyDataSetChanged() { + // TODO + } + + public boolean canReset() { + if (getCount() <= 1) { + return false; + } + return true; + } + + public boolean canUndo() { + if (mCurrentPresetPosition == getCount() - 1) { + return false; + } + return true; + } + + public boolean canRedo() { + if (mCurrentPresetPosition == 0) { + return false; + } + return true; + } + + public void updateMenuItems() { + if (mUndoMenuItem != null) { + setEnabled(mUndoMenuItem, canUndo()); + } + if (mRedoMenuItem != null) { + setEnabled(mRedoMenuItem, canRedo()); + } + if (mResetMenuItem != null) { + setEnabled(mResetMenuItem, canReset()); + } + } + + private void setEnabled(MenuItem item, boolean enabled) { + item.setEnabled(enabled); + Drawable drawable = item.getIcon(); + if (drawable != null) { + drawable.setAlpha(enabled ? 255 : 80); + } + } + + public void setCurrentPreset(int n) { + mCurrentPresetPosition = n; + updateMenuItems(); + notifyDataSetChanged(); + } + + public void reset() { + if (getCount() == 0) { + return; + } + HistoryItem first = getItem(getCount() - 1); + clear(); + addHistoryItem(first); + updateMenuItems(); + } + + public HistoryItem getLast() { + if (getCount() == 0) { + return null; + } + return getItem(0); + } + + public HistoryItem getCurrent() { + return getItem(mCurrentPresetPosition); + } + + public void addHistoryItem(HistoryItem preset) { + insert(preset, 0); + updateMenuItems(); + } + + private void insert(HistoryItem preset, int position) { + if (mCurrentPresetPosition != 0) { + // in this case, let's discount the presets before the current one + Vector<HistoryItem> oldItems = new Vector<HistoryItem>(); + for (int i = mCurrentPresetPosition; i < getCount(); i++) { + oldItems.add(getItem(i)); + } + clear(); + for (int i = 0; i < oldItems.size(); i++) { + add(oldItems.elementAt(i)); + } + mCurrentPresetPosition = position; + notifyDataSetChanged(); + } + mHistoryItems.insertElementAt(preset, position); + mCurrentPresetPosition = position; + notifyDataSetChanged(); + } + + public int redo() { + mCurrentPresetPosition--; + if (mCurrentPresetPosition < 0) { + mCurrentPresetPosition = 0; + } + notifyDataSetChanged(); + updateMenuItems(); + return mCurrentPresetPosition; + } + + public int undo() { + mCurrentPresetPosition++; + if (mCurrentPresetPosition >= getCount()) { + mCurrentPresetPosition = getCount() - 1; + } + notifyDataSetChanged(); + updateMenuItems(); + return mCurrentPresetPosition; + } + +} diff --git a/src/com/android/gallery3d/filtershow/imageshow/ControlPoint.java b/src/com/android/gallery3d/filtershow/imageshow/ControlPoint.java new file mode 100644 index 000000000..aaec728a6 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/imageshow/ControlPoint.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2012 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.filtershow.imageshow; + +public class ControlPoint implements Comparable { + public float x; + public float y; + + public ControlPoint(float px, float py) { + x = px; + y = py; + } + + public ControlPoint(ControlPoint point) { + x = point.x; + y = point.y; + } + + public boolean sameValues(ControlPoint other) { + if (this == other) { + return true; + } + if (other == null) { + return false; + } + + if (Float.floatToIntBits(x) != Float.floatToIntBits(other.x)) { + return false; + } + if (Float.floatToIntBits(y) != Float.floatToIntBits(other.y)) { + return false; + } + return true; + } + + public ControlPoint copy() { + return new ControlPoint(x, y); + } + + @Override + public int compareTo(Object another) { + ControlPoint p = (ControlPoint) another; + if (p.x < x) { + return 1; + } else if (p.x > x) { + return -1; + } + return 0; + } +} diff --git a/src/com/android/gallery3d/filtershow/imageshow/EclipseControl.java b/src/com/android/gallery3d/filtershow/imageshow/EclipseControl.java new file mode 100644 index 000000000..8ceb37599 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/imageshow/EclipseControl.java @@ -0,0 +1,302 @@ +/* + * Copyright (C) 2012 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.filtershow.imageshow; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.RadialGradient; +import android.graphics.RectF; +import android.graphics.Shader; + +import com.android.gallery3d.R; + +public class EclipseControl { + private float mCenterX = Float.NaN; + private float mCenterY = 0; + private float mRadiusX = 200; + private float mRadiusY = 300; + private static int MIN_TOUCH_DIST = 80;// should be a resource & in dips + + private float[] handlex = new float[9]; + private float[] handley = new float[9]; + private int mSliderColor; + private int mCenterDotSize = 40; + private float mDownX; + private float mDownY; + private float mDownCenterX; + private float mDownCenterY; + private float mDownRadiusX; + private float mDownRadiusY; + private Matrix mScrToImg; + + private boolean mShowReshapeHandles = true; + public final static int HAN_CENTER = 0; + public final static int HAN_NORTH = 7; + public final static int HAN_NE = 8; + public final static int HAN_EAST = 1; + public final static int HAN_SE = 2; + public final static int HAN_SOUTH = 3; + public final static int HAN_SW = 4; + public final static int HAN_WEST = 5; + public final static int HAN_NW = 6; + + public EclipseControl(Context context) { + mSliderColor = Color.WHITE; + } + + public void setRadius(float x, float y) { + mRadiusX = x; + mRadiusY = y; + } + + public void setCenter(float x, float y) { + mCenterX = x; + mCenterY = y; + } + + public int getCloseHandle(float x, float y) { + float min = Float.MAX_VALUE; + int handle = -1; + for (int i = 0; i < handlex.length; i++) { + float dx = handlex[i] - x; + float dy = handley[i] - y; + float dist = dx * dx + dy * dy; + if (dist < min) { + min = dist; + handle = i; + } + } + + if (min < MIN_TOUCH_DIST * MIN_TOUCH_DIST) { + return handle; + } + for (int i = 0; i < handlex.length; i++) { + float dx = handlex[i] - x; + float dy = handley[i] - y; + float dist = (float) Math.sqrt(dx * dx + dy * dy); + } + + return -1; + } + + public void setScrToImageMatrix(Matrix scrToImg) { + mScrToImg = scrToImg; + } + + public void actionDown(float x, float y, Oval oval) { + float[] point = new float[] { + x, y }; + mScrToImg.mapPoints(point); + mDownX = point[0]; + mDownY = point[1]; + mDownCenterX = oval.getCenterX(); + mDownCenterY = oval.getCenterY(); + mDownRadiusX = oval.getRadiusX(); + mDownRadiusY = oval.getRadiusY(); + } + + public void actionMove(int handle, float x, float y, Oval oval) { + float[] point = new float[] { + x, y }; + mScrToImg.mapPoints(point); + x = point[0]; + y = point[1]; + + // Test if the matrix is swapping x and y + point[0] = 0; + point[1] = 1; + mScrToImg.mapVectors(point); + boolean swapxy = (point[0] > 0.0f); + + int sign = 1; + switch (handle) { + case HAN_CENTER: + float ctrdx = mDownX - mDownCenterX; + float ctrdy = mDownY - mDownCenterY; + oval.setCenter(x - ctrdx, y - ctrdy); + // setRepresentation(mVignetteRep); + break; + case HAN_NORTH: + sign = -1; + case HAN_SOUTH: + if (swapxy) { + float raddx = mDownRadiusY - Math.abs(mDownX - mDownCenterY); + oval.setRadiusY(Math.abs(x - oval.getCenterY() + sign * raddx)); + } else { + float raddy = mDownRadiusY - Math.abs(mDownY - mDownCenterY); + oval.setRadiusY(Math.abs(y - oval.getCenterY() + sign * raddy)); + } + break; + case HAN_EAST: + sign = -1; + case HAN_WEST: + if (swapxy) { + float raddy = mDownRadiusX - Math.abs(mDownY - mDownCenterX); + oval.setRadiusX(Math.abs(y - oval.getCenterX() + sign * raddy)); + } else { + float raddx = mDownRadiusX - Math.abs(mDownX - mDownCenterX); + oval.setRadiusX(Math.abs(x - oval.getCenterX() - sign * raddx)); + } + break; + case HAN_SE: + case HAN_NE: + case HAN_SW: + case HAN_NW: + float sin45 = (float) Math.sin(45); + float dr = (mDownRadiusX + mDownRadiusY) * sin45; + float ctr_dx = mDownX - mDownCenterX; + float ctr_dy = mDownY - mDownCenterY; + float downRad = Math.abs(ctr_dx) + Math.abs(ctr_dy) - dr; + float rx = oval.getRadiusX(); + float ry = oval.getRadiusY(); + float r = (Math.abs(rx) + Math.abs(ry)) * sin45; + float dx = x - oval.getCenterX(); + float dy = y - oval.getCenterY(); + float nr = Math.abs(Math.abs(dx) + Math.abs(dy) - downRad); + oval.setRadius(rx * nr / r, ry * nr / r); + + break; + } + } + + public void paintGrayPoint(Canvas canvas, float x, float y) { + if (x == Float.NaN) { + return; + } + + Paint paint = new Paint(); + + paint.setStyle(Paint.Style.FILL); + paint.setColor(Color.BLUE); + int[] colors3 = new int[] { + Color.GRAY, Color.LTGRAY, 0x66000000, 0 }; + RadialGradient g = new RadialGradient(x, y, mCenterDotSize, colors3, new float[] { + 0, .3f, .31f, 1 }, Shader.TileMode.CLAMP); + paint.setShader(g); + canvas.drawCircle(x, y, mCenterDotSize, paint); + } + + public void paintPoint(Canvas canvas, float x, float y) { + if (x == Float.NaN) { + return; + } + + Paint paint = new Paint(); + + paint.setStyle(Paint.Style.FILL); + paint.setColor(Color.BLUE); + int[] colors3 = new int[] { + mSliderColor, mSliderColor, 0x66000000, 0 }; + RadialGradient g = new RadialGradient(x, y, mCenterDotSize, colors3, new float[] { + 0, .3f, .31f, 1 }, Shader.TileMode.CLAMP); + paint.setShader(g); + canvas.drawCircle(x, y, mCenterDotSize, paint); + } + + void paintRadius(Canvas canvas, float cx, float cy, float rx, float ry) { + if (cx == Float.NaN) { + return; + } + int mSliderColor = 0xFF33B5E5; + Paint paint = new Paint(); + RectF rect = new RectF(cx - rx, cy - ry, cx + rx, cy + ry); + paint.setAntiAlias(true); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(6); + paint.setColor(Color.BLACK); + paintOvallines(canvas, rect, paint, cx, cy, rx, ry); + + paint.setStrokeWidth(3); + paint.setColor(Color.WHITE); + paintOvallines(canvas, rect, paint, cx, cy, rx, ry); + } + + public void paintOvallines( + Canvas canvas, RectF rect, Paint paint, float cx, float cy, float rx, float ry) { + canvas.drawOval(rect, paint); + float da = 4; + float arclen = da + da; + if (mShowReshapeHandles) { + paint.setStyle(Paint.Style.STROKE); + + for (int i = 0; i < 361; i += 90) { + float dx = rx + 10; + float dy = ry + 10; + rect.left = cx - dx; + rect.top = cy - dy; + rect.right = cx + dx; + rect.bottom = cy + dy; + canvas.drawArc(rect, i - da, arclen, false, paint); + dx = rx - 10; + dy = ry - 10; + rect.left = cx - dx; + rect.top = cy - dy; + rect.right = cx + dx; + rect.bottom = cy + dy; + canvas.drawArc(rect, i - da, arclen, false, paint); + } + } + da *= 2; + paint.setStyle(Paint.Style.FILL); + + for (int i = 45; i < 361; i += 90) { + double angle = Math.PI * i / 180.; + float x = cx + (float) (rx * Math.cos(angle)); + float y = cy + (float) (ry * Math.sin(angle)); + canvas.drawRect(x - da, y - da, x + da, y + da, paint); + } + paint.setStyle(Paint.Style.STROKE); + rect.left = cx - rx; + rect.top = cy - ry; + rect.right = cx + rx; + rect.bottom = cy + ry; + } + + public void fillHandles(Canvas canvas, float cx, float cy, float rx, float ry) { + handlex[0] = cx; + handley[0] = cy; + int k = 1; + + for (int i = 0; i < 360; i += 45) { + double angle = Math.PI * i / 180.; + + float x = cx + (float) (rx * Math.cos(angle)); + float y = cy + (float) (ry * Math.sin(angle)); + handlex[k] = x; + handley[k] = y; + + k++; + } + } + + public void draw(Canvas canvas) { + paintRadius(canvas, mCenterX, mCenterY, mRadiusX, mRadiusY); + fillHandles(canvas, mCenterX, mCenterY, mRadiusX, mRadiusY); + paintPoint(canvas, mCenterX, mCenterY); + } + + public boolean isUndefined() { + return Float.isNaN(mCenterX); + } + + public void setShowReshapeHandles(boolean showReshapeHandles) { + this.mShowReshapeHandles = showReshapeHandles; + } +} diff --git a/src/com/android/gallery3d/filtershow/imageshow/GeometryMathUtils.java b/src/com/android/gallery3d/filtershow/imageshow/GeometryMathUtils.java new file mode 100644 index 000000000..81394f142 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/imageshow/GeometryMathUtils.java @@ -0,0 +1,416 @@ +/* + * Copyright (C) 2012 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.filtershow.imageshow; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; + +import com.android.gallery3d.filtershow.cache.ImageLoader; +import com.android.gallery3d.filtershow.filters.FilterCropRepresentation; +import com.android.gallery3d.filtershow.filters.FilterMirrorRepresentation; +import com.android.gallery3d.filtershow.filters.FilterMirrorRepresentation.Mirror; +import com.android.gallery3d.filtershow.filters.FilterRepresentation; +import com.android.gallery3d.filtershow.filters.FilterRotateRepresentation; +import com.android.gallery3d.filtershow.filters.FilterRotateRepresentation.Rotation; +import com.android.gallery3d.filtershow.filters.FilterStraightenRepresentation; +import com.android.gallery3d.filtershow.pipeline.ImagePreset; + +import java.util.Collection; +import java.util.Iterator; + +public final class GeometryMathUtils { + private GeometryMathUtils() {}; + + // Holder class for Geometry data. + public static final class GeometryHolder { + public Rotation rotation = FilterRotateRepresentation.getNil(); + public float straighten = FilterStraightenRepresentation.getNil(); + public RectF crop = FilterCropRepresentation.getNil(); + public Mirror mirror = FilterMirrorRepresentation.getNil(); + + public void set(GeometryHolder h) { + rotation = h.rotation; + straighten = h.straighten; + crop.set(h.crop); + mirror = h.mirror; + } + + public void wipe() { + rotation = FilterRotateRepresentation.getNil(); + straighten = FilterStraightenRepresentation.getNil(); + crop = FilterCropRepresentation.getNil(); + mirror = FilterMirrorRepresentation.getNil(); + } + + public boolean isNil() { + return rotation == FilterRotateRepresentation.getNil() && + straighten == FilterStraightenRepresentation.getNil() && + crop.equals(FilterCropRepresentation.getNil()) && + mirror == FilterMirrorRepresentation.getNil(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof GeometryHolder)) { + return false; + } + GeometryHolder h = (GeometryHolder) o; + return rotation == h.rotation && straighten == h.straighten && + ((crop == null && h.crop == null) || (crop != null && crop.equals(h.crop))) && + mirror == h.mirror; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "[" + "rotation:" + rotation.value() + + ",straighten:" + straighten + ",crop:" + crop.toString() + + ",mirror:" + mirror.value() + "]"; + } + } + + // Math operations for 2d vectors + public static float clamp(float i, float low, float high) { + return Math.max(Math.min(i, high), low); + } + + public static float[] lineIntersect(float[] line1, float[] line2) { + float a0 = line1[0]; + float a1 = line1[1]; + float b0 = line1[2]; + float b1 = line1[3]; + float c0 = line2[0]; + float c1 = line2[1]; + float d0 = line2[2]; + float d1 = line2[3]; + float t0 = a0 - b0; + float t1 = a1 - b1; + float t2 = b0 - d0; + float t3 = d1 - b1; + float t4 = c0 - d0; + float t5 = c1 - d1; + + float denom = t1 * t4 - t0 * t5; + if (denom == 0) + return null; + float u = (t3 * t4 + t5 * t2) / denom; + float[] intersect = { + b0 + u * t0, b1 + u * t1 + }; + return intersect; + } + + public static float[] shortestVectorFromPointToLine(float[] point, float[] line) { + float x1 = line[0]; + float x2 = line[2]; + float y1 = line[1]; + float y2 = line[3]; + float xdelt = x2 - x1; + float ydelt = y2 - y1; + if (xdelt == 0 && ydelt == 0) + return null; + float u = ((point[0] - x1) * xdelt + (point[1] - y1) * ydelt) + / (xdelt * xdelt + ydelt * ydelt); + float[] ret = { + (x1 + u * (x2 - x1)), (y1 + u * (y2 - y1)) + }; + float[] vec = { + ret[0] - point[0], ret[1] - point[1] + }; + return vec; + } + + // A . B + public static float dotProduct(float[] a, float[] b) { + return a[0] * b[0] + a[1] * b[1]; + } + + public static float[] normalize(float[] a) { + float length = (float) Math.sqrt(a[0] * a[0] + a[1] * a[1]); + float[] b = { + a[0] / length, a[1] / length + }; + return b; + } + + // A onto B + public static float scalarProjection(float[] a, float[] b) { + float length = (float) Math.sqrt(b[0] * b[0] + b[1] * b[1]); + return dotProduct(a, b) / length; + } + + public static float[] getVectorFromPoints(float[] point1, float[] point2) { + float[] p = { + point2[0] - point1[0], point2[1] - point1[1] + }; + return p; + } + + public static float[] getUnitVectorFromPoints(float[] point1, float[] point2) { + float[] p = { + point2[0] - point1[0], point2[1] - point1[1] + }; + float length = (float) Math.sqrt(p[0] * p[0] + p[1] * p[1]); + p[0] = p[0] / length; + p[1] = p[1] / length; + return p; + } + + public static void scaleRect(RectF r, float scale) { + r.set(r.left * scale, r.top * scale, r.right * scale, r.bottom * scale); + } + + // A - B + public static float[] vectorSubtract(float[] a, float[] b) { + int len = a.length; + if (len != b.length) + return null; + float[] ret = new float[len]; + for (int i = 0; i < len; i++) { + ret[i] = a[i] - b[i]; + } + return ret; + } + + public static float vectorLength(float[] a) { + return (float) Math.sqrt(a[0] * a[0] + a[1] * a[1]); + } + + public static float scale(float oldWidth, float oldHeight, float newWidth, float newHeight) { + if (oldHeight == 0 || oldWidth == 0 || (oldWidth == newWidth && oldHeight == newHeight)) { + return 1; + } + return Math.min(newWidth / oldWidth, newHeight / oldHeight); + } + + public static Rect roundNearest(RectF r) { + Rect q = new Rect(Math.round(r.left), Math.round(r.top), Math.round(r.right), + Math.round(r.bottom)); + return q; + } + + private static void concatMirrorMatrix(Matrix m, Mirror type) { + if (type == Mirror.HORIZONTAL) { + m.postScale(-1, 1); + } else if (type == Mirror.VERTICAL) { + m.postScale(1, -1); + } else if (type == Mirror.BOTH) { + m.postScale(1, -1); + m.postScale(-1, 1); + } + } + + private static int getRotationForOrientation(int orientation) { + switch (orientation) { + case ImageLoader.ORI_ROTATE_90: + return 90; + case ImageLoader.ORI_ROTATE_180: + return 180; + case ImageLoader.ORI_ROTATE_270: + return 270; + default: + return 0; + } + } + + public static GeometryHolder unpackGeometry(Collection<FilterRepresentation> geometry) { + GeometryHolder holder = new GeometryHolder(); + unpackGeometry(holder, geometry); + return holder; + } + + public static void unpackGeometry(GeometryHolder out, + Collection<FilterRepresentation> geometry) { + out.wipe(); + // Get geometry data from filters + for (FilterRepresentation r : geometry) { + if (r.isNil()) { + continue; + } + if (r.getSerializationName() == FilterRotateRepresentation.SERIALIZATION_NAME) { + out.rotation = ((FilterRotateRepresentation) r).getRotation(); + } else if (r.getSerializationName() == + FilterStraightenRepresentation.SERIALIZATION_NAME) { + out.straighten = ((FilterStraightenRepresentation) r).getStraighten(); + } else if (r.getSerializationName() == FilterCropRepresentation.SERIALIZATION_NAME) { + ((FilterCropRepresentation) r).getCrop(out.crop); + } else if (r.getSerializationName() == FilterMirrorRepresentation.SERIALIZATION_NAME) { + out.mirror = ((FilterMirrorRepresentation) r).getMirror(); + } + } + } + + public static void replaceInstances(Collection<FilterRepresentation> geometry, + FilterRepresentation rep) { + Iterator<FilterRepresentation> iter = geometry.iterator(); + while (iter.hasNext()) { + FilterRepresentation r = iter.next(); + if (ImagePreset.sameSerializationName(rep, r)) { + iter.remove(); + } + } + if (!rep.isNil()) { + geometry.add(rep); + } + } + + public static void initializeHolder(GeometryHolder outHolder, + FilterRepresentation currentLocal) { + Collection<FilterRepresentation> geometry = MasterImage.getImage().getPreset() + .getGeometryFilters(); + replaceInstances(geometry, currentLocal); + unpackGeometry(outHolder, geometry); + } + + private static Bitmap applyFullGeometryMatrix(Bitmap image, GeometryHolder holder) { + int width = image.getWidth(); + int height = image.getHeight(); + RectF crop = getTrueCropRect(holder, width, height); + Rect frame = new Rect(); + crop.roundOut(frame); + Matrix m = getCropSelectionToScreenMatrix(null, holder, width, height, frame.width(), + frame.height()); + Bitmap temp = Bitmap.createBitmap(frame.width(), frame.height(), Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(temp); + Paint paint = new Paint(); + paint.setAntiAlias(true); + paint.setFilterBitmap(true); + paint.setDither(true); + canvas.drawBitmap(image, m, paint); + return temp; + } + + public static Matrix getImageToScreenMatrix(Collection<FilterRepresentation> geometry, + boolean reflectRotation, Rect bmapDimens, float viewWidth, float viewHeight) { + GeometryHolder h = unpackGeometry(geometry); + return GeometryMathUtils.getOriginalToScreen(h, reflectRotation, bmapDimens.width(), + bmapDimens.height(), viewWidth, viewHeight); + } + + public static Matrix getOriginalToScreen(GeometryHolder holder, boolean rotate, + float originalWidth, + float originalHeight, float viewWidth, float viewHeight) { + int orientation = MasterImage.getImage().getZoomOrientation(); + int rotation = getRotationForOrientation(orientation); + Rotation prev = holder.rotation; + rotation = (rotation + prev.value()) % 360; + holder.rotation = Rotation.fromValue(rotation); + Matrix m = getCropSelectionToScreenMatrix(null, holder, (int) originalWidth, + (int) originalHeight, (int) viewWidth, (int) viewHeight); + holder.rotation = prev; + return m; + } + + public static Bitmap applyGeometryRepresentations(Collection<FilterRepresentation> res, + Bitmap image) { + GeometryHolder holder = unpackGeometry(res); + Bitmap bmap = image; + // If there are geometry changes, apply them to the image + if (!holder.isNil()) { + bmap = applyFullGeometryMatrix(bmap, holder); + } + return bmap; + } + + public static RectF drawTransformedCropped(GeometryHolder holder, Canvas canvas, + Bitmap photo, int viewWidth, int viewHeight) { + if (photo == null) { + return null; + } + RectF crop = new RectF(); + Matrix m = getCropSelectionToScreenMatrix(crop, holder, photo.getWidth(), photo.getHeight(), + viewWidth, viewHeight); + canvas.save(); + canvas.clipRect(crop); + Paint p = new Paint(); + p.setAntiAlias(true); + canvas.drawBitmap(photo, m, p); + canvas.restore(); + return crop; + } + + public static boolean needsDimensionSwap(Rotation rotation) { + switch (rotation) { + case NINETY: + case TWO_SEVENTY: + return true; + default: + return false; + } + } + + // Gives matrix for rotated, straightened, mirrored bitmap centered at 0,0. + private static Matrix getFullGeometryMatrix(GeometryHolder holder, int bitmapWidth, + int bitmapHeight) { + float centerX = bitmapWidth / 2f; + float centerY = bitmapHeight / 2f; + Matrix m = new Matrix(); + m.setTranslate(-centerX, -centerY); + m.postRotate(holder.straighten + holder.rotation.value()); + concatMirrorMatrix(m, holder.mirror); + return m; + } + + public static Matrix getFullGeometryToScreenMatrix(GeometryHolder holder, int bitmapWidth, + int bitmapHeight, int viewWidth, int viewHeight) { + float scale = GeometryMathUtils.scale(bitmapWidth, bitmapHeight, viewWidth, viewHeight); + Matrix m = getFullGeometryMatrix(holder, bitmapWidth, bitmapHeight); + m.postScale(scale, scale); + m.postTranslate(viewWidth / 2f, viewHeight / 2f); + return m; + } + + public static RectF getTrueCropRect(GeometryHolder holder, int bitmapWidth, int bitmapHeight) { + RectF r = new RectF(holder.crop); + FilterCropRepresentation.findScaledCrop(r, bitmapWidth, bitmapHeight); + float s = holder.straighten; + holder.straighten = 0; + Matrix m1 = getFullGeometryMatrix(holder, bitmapWidth, bitmapHeight); + holder.straighten = s; + m1.mapRect(r); + return r; + } + + public static Matrix getCropSelectionToScreenMatrix(RectF outCrop, GeometryHolder holder, + int bitmapWidth, int bitmapHeight, int viewWidth, int viewHeight) { + Matrix m = getFullGeometryMatrix(holder, bitmapWidth, bitmapHeight); + RectF crop = getTrueCropRect(holder, bitmapWidth, bitmapHeight); + float scale = GeometryMathUtils.scale(crop.width(), crop.height(), viewWidth, viewHeight); + m.postScale(scale, scale); + GeometryMathUtils.scaleRect(crop, scale); + m.postTranslate(viewWidth / 2f - crop.centerX(), viewHeight / 2f - crop.centerY()); + if (outCrop != null) { + crop.offset(viewWidth / 2f - crop.centerX(), viewHeight / 2f - crop.centerY()); + outCrop.set(crop); + } + return m; + } + + public static Matrix getCropSelectionToScreenMatrix(RectF outCrop, + Collection<FilterRepresentation> res, int bitmapWidth, int bitmapHeight, int viewWidth, + int viewHeight) { + GeometryHolder holder = unpackGeometry(res); + return getCropSelectionToScreenMatrix(outCrop, holder, bitmapWidth, bitmapHeight, + viewWidth, viewHeight); + } +} diff --git a/src/com/android/gallery3d/filtershow/imageshow/GradControl.java b/src/com/android/gallery3d/filtershow/imageshow/GradControl.java new file mode 100644 index 000000000..964da99e9 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/imageshow/GradControl.java @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2012 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.filtershow.imageshow; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.DashPathEffect; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.RadialGradient; +import android.graphics.Rect; +import android.graphics.Shader; + +import com.android.gallery3d.R; + +public class GradControl { + private float mPoint1X = Float.NaN; // used to flag parameters have not been set + private float mPoint1Y = 0; + private float mPoint2X = 200; + private float mPoint2Y = 300; + private int mMinTouchDist = 80;// should be a resource & in dips + + private float[] handlex = new float[3]; + private float[] handley = new float[3]; + private int mSliderColor; + private int mCenterDotSize; + private float mDownX; + private float mDownY; + private float mDownPoint1X; + private float mDownPoint1Y; + private float mDownPoint2X; + private float mDownPoint2Y; + Rect mImageBounds; + int mImageHeight; + private Matrix mScrToImg; + Paint mPaint = new Paint(); + DashPathEffect mDash = new DashPathEffect(new float[]{30, 30}, 0); + private boolean mShowReshapeHandles = true; + public final static int HAN_CENTER = 0; + public final static int HAN_NORTH = 2; + public final static int HAN_SOUTH = 1; + private int[] mPointColorPatern; + private int[] mGrayPointColorPatern; + private float[] mPointRadialPos = new float[]{0, .3f, .31f, 1}; + private int mLineColor; + private int mlineShadowColor; + + public GradControl(Context context) { + + Resources res = context.getResources(); + mCenterDotSize = (int) res.getDimension(R.dimen.gradcontrol_dot_size); + mMinTouchDist = (int) res.getDimension(R.dimen.gradcontrol_min_touch_dist); + int grayPointCenterColor = res.getColor(R.color.gradcontrol_graypoint_center); + int grayPointEdgeColor = res.getColor(R.color.gradcontrol_graypoint_edge); + int pointCenterColor = res.getColor(R.color.gradcontrol_point_center); + int pointEdgeColor = res.getColor(R.color.gradcontrol_point_edge); + int pointShadowStartColor = res.getColor(R.color.gradcontrol_point_shadow_start); + int pointShadowEndColor = res.getColor(R.color.gradcontrol_point_shadow_end); + mPointColorPatern = new int[]{ + pointCenterColor, pointEdgeColor, pointShadowStartColor, pointShadowEndColor}; + mGrayPointColorPatern = new int[]{ + grayPointCenterColor, grayPointEdgeColor, pointShadowStartColor, pointShadowEndColor}; + mSliderColor = Color.WHITE; + mLineColor = res.getColor(R.color.gradcontrol_line_color); + mlineShadowColor = res.getColor(R.color.gradcontrol_line_shadow); + } + + public void setPoint2(float x, float y) { + mPoint2X = x; + mPoint2Y = y; + } + + public void setPoint1(float x, float y) { + mPoint1X = x; + mPoint1Y = y; + } + + public int getCloseHandle(float x, float y) { + float min = Float.MAX_VALUE; + int handle = -1; + for (int i = 0; i < handlex.length; i++) { + float dx = handlex[i] - x; + float dy = handley[i] - y; + float dist = dx * dx + dy * dy; + if (dist < min) { + min = dist; + handle = i; + } + } + + if (min < mMinTouchDist * mMinTouchDist) { + return handle; + } + for (int i = 0; i < handlex.length; i++) { + float dx = handlex[i] - x; + float dy = handley[i] - y; + float dist = (float) Math.sqrt(dx * dx + dy * dy); + } + + return -1; + } + + public void setScrImageInfo(Matrix scrToImg, Rect imageBounds) { + mScrToImg = scrToImg; + mImageBounds = new Rect(imageBounds); + } + + private boolean centerIsOutside(float x1, float y1, float x2, float y2) { + return (!mImageBounds.contains((int) ((x1 + x2) / 2), (int) ((y1 + y2) / 2))); + } + + public void actionDown(float x, float y, Line line) { + float[] point = new float[]{ + x, y}; + mScrToImg.mapPoints(point); + mDownX = point[0]; + mDownY = point[1]; + mDownPoint1X = line.getPoint1X(); + mDownPoint1Y = line.getPoint1Y(); + mDownPoint2X = line.getPoint2X(); + mDownPoint2Y = line.getPoint2Y(); + } + + public void actionMove(int handle, float x, float y, Line line) { + float[] point = new float[]{ + x, y}; + mScrToImg.mapPoints(point); + x = point[0]; + y = point[1]; + + // Test if the matrix is swapping x and y + point[0] = 0; + point[1] = 1; + mScrToImg.mapVectors(point); + boolean swapxy = (point[0] > 0.0f); + + int sign = 1; + + float dx = x - mDownX; + float dy = y - mDownY; + switch (handle) { + case HAN_CENTER: + if (centerIsOutside(mDownPoint1X + dx, mDownPoint1Y + dy, + mDownPoint2X + dx, mDownPoint2Y + dy)) { + break; + } + line.setPoint1(mDownPoint1X + dx, mDownPoint1Y + dy); + line.setPoint2(mDownPoint2X + dx, mDownPoint2Y + dy); + break; + case HAN_SOUTH: + if (centerIsOutside(mDownPoint1X + dx, mDownPoint1Y + dy, + mDownPoint2X, mDownPoint2Y)) { + break; + } + line.setPoint1(mDownPoint1X + dx, mDownPoint1Y + dy); + break; + case HAN_NORTH: + if (centerIsOutside(mDownPoint1X, mDownPoint1Y, + mDownPoint2X + dx, mDownPoint2Y + dy)) { + break; + } + line.setPoint2(mDownPoint2X + dx, mDownPoint2Y + dy); + break; + } + } + + public void paintGrayPoint(Canvas canvas, float x, float y) { + if (isUndefined()) { + return; + } + + Paint paint = new Paint(); + paint.setStyle(Paint.Style.FILL); + RadialGradient g = new RadialGradient(x, y, mCenterDotSize, mGrayPointColorPatern, + mPointRadialPos, Shader.TileMode.CLAMP); + paint.setShader(g); + canvas.drawCircle(x, y, mCenterDotSize, paint); + } + + public void paintPoint(Canvas canvas, float x, float y) { + if (isUndefined()) { + return; + } + + Paint paint = new Paint(); + paint.setStyle(Paint.Style.FILL); + RadialGradient g = new RadialGradient(x, y, mCenterDotSize, mPointColorPatern, + mPointRadialPos, Shader.TileMode.CLAMP); + paint.setShader(g); + canvas.drawCircle(x, y, mCenterDotSize, paint); + } + + void paintLines(Canvas canvas, float p1x, float p1y, float p2x, float p2y) { + if (isUndefined()) { + return; + } + + mPaint.setAntiAlias(true); + mPaint.setStyle(Paint.Style.STROKE); + + mPaint.setStrokeWidth(6); + mPaint.setColor(mlineShadowColor); + mPaint.setPathEffect(mDash); + paintOvallines(canvas, mPaint, p1x, p1y, p2x, p2y); + + mPaint.setStrokeWidth(3); + mPaint.setColor(mLineColor); + mPaint.setPathEffect(mDash); + paintOvallines(canvas, mPaint, p1x, p1y, p2x, p2y); + } + + public void paintOvallines( + Canvas canvas, Paint paint, float p1x, float p1y, float p2x, float p2y) { + + + + canvas.drawLine(p1x, p1y, p2x, p2y, paint); + + float cx = (p1x + p2x) / 2; + float cy = (p1y + p2y) / 2; + float dx = p1x - p2x; + float dy = p1y - p2y; + float len = (float) Math.sqrt(dx * dx + dy * dy); + dx *= 2048 / len; + dy *= 2048 / len; + + canvas.drawLine(p1x + dy, p1y - dx, p1x - dy, p1y + dx, paint); + canvas.drawLine(p2x + dy, p2y - dx, p2x - dy, p2y + dx, paint); + } + + public void fillHandles(Canvas canvas, float p1x, float p1y, float p2x, float p2y) { + float cx = (p1x + p2x) / 2; + float cy = (p1y + p2y) / 2; + handlex[0] = cx; + handley[0] = cy; + handlex[1] = p1x; + handley[1] = p1y; + handlex[2] = p2x; + handley[2] = p2y; + + } + + public void draw(Canvas canvas) { + paintLines(canvas, mPoint1X, mPoint1Y, mPoint2X, mPoint2Y); + fillHandles(canvas, mPoint1X, mPoint1Y, mPoint2X, mPoint2Y); + paintPoint(canvas, mPoint2X, mPoint2Y); + paintPoint(canvas, mPoint1X, mPoint1Y); + paintPoint(canvas, (mPoint1X + mPoint2X) / 2, (mPoint1Y + mPoint2Y) / 2); + } + + public boolean isUndefined() { + return Float.isNaN(mPoint1X); + } + + public void setShowReshapeHandles(boolean showReshapeHandles) { + this.mShowReshapeHandles = showReshapeHandles; + } +} diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageCrop.java b/src/com/android/gallery3d/filtershow/imageshow/ImageCrop.java new file mode 100644 index 000000000..7fee03188 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/imageshow/ImageCrop.java @@ -0,0 +1,307 @@ +/* + * Copyright (C) 2013 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.filtershow.imageshow; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.crop.CropDrawingUtils; +import com.android.gallery3d.filtershow.crop.CropMath; +import com.android.gallery3d.filtershow.crop.CropObject; +import com.android.gallery3d.filtershow.editors.EditorCrop; +import com.android.gallery3d.filtershow.filters.FilterCropRepresentation; +import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils.GeometryHolder; + +public class ImageCrop extends ImageShow { + private static final String TAG = ImageCrop.class.getSimpleName(); + private RectF mImageBounds = new RectF(); + private RectF mScreenCropBounds = new RectF(); + private Paint mPaint = new Paint(); + private CropObject mCropObj = null; + private GeometryHolder mGeometry = new GeometryHolder(); + private GeometryHolder mUpdateHolder = new GeometryHolder(); + private Drawable mCropIndicator; + private int mIndicatorSize; + private boolean mMovingBlock = false; + private Matrix mDisplayMatrix = null; + private Matrix mDisplayCropMatrix = null; + private Matrix mDisplayMatrixInverse = null; + private float mPrevX = 0; + private float mPrevY = 0; + private int mMinSideSize = 90; + private int mTouchTolerance = 40; + private enum Mode { + NONE, MOVE + } + private Mode mState = Mode.NONE; + private boolean mValidDraw = false; + FilterCropRepresentation mLocalRep = new FilterCropRepresentation(); + EditorCrop mEditorCrop; + + public ImageCrop(Context context) { + super(context); + setup(context); + } + + public ImageCrop(Context context, AttributeSet attrs) { + super(context, attrs); + setup(context); + } + + public ImageCrop(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + setup(context); + } + + private void setup(Context context) { + Resources rsc = context.getResources(); + mCropIndicator = rsc.getDrawable(R.drawable.camera_crop); + mIndicatorSize = (int) rsc.getDimension(R.dimen.crop_indicator_size); + mMinSideSize = (int) rsc.getDimension(R.dimen.crop_min_side); + mTouchTolerance = (int) rsc.getDimension(R.dimen.crop_touch_tolerance); + } + + public void setFilterCropRepresentation(FilterCropRepresentation crop) { + mLocalRep = (crop == null) ? new FilterCropRepresentation() : crop; + GeometryMathUtils.initializeHolder(mUpdateHolder, mLocalRep); + mValidDraw = true; + } + + public FilterCropRepresentation getFinalRepresentation() { + return mLocalRep; + } + + private void internallyUpdateLocalRep(RectF crop, RectF image) { + FilterCropRepresentation + .findNormalizedCrop(crop, (int) image.width(), (int) image.height()); + mGeometry.crop.set(crop); + mUpdateHolder.set(mGeometry); + mLocalRep.setCrop(crop); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + float x = event.getX(); + float y = event.getY(); + if (mDisplayMatrix == null || mDisplayMatrixInverse == null) { + return true; + } + float[] touchPoint = { + x, y + }; + mDisplayMatrixInverse.mapPoints(touchPoint); + x = touchPoint[0]; + y = touchPoint[1]; + switch (event.getActionMasked()) { + case (MotionEvent.ACTION_DOWN): + if (mState == Mode.NONE) { + if (!mCropObj.selectEdge(x, y)) { + mMovingBlock = mCropObj.selectEdge(CropObject.MOVE_BLOCK); + } + mPrevX = x; + mPrevY = y; + mState = Mode.MOVE; + } + break; + case (MotionEvent.ACTION_UP): + if (mState == Mode.MOVE) { + mCropObj.selectEdge(CropObject.MOVE_NONE); + mMovingBlock = false; + mPrevX = x; + mPrevY = y; + mState = Mode.NONE; + internallyUpdateLocalRep(mCropObj.getInnerBounds(), mCropObj.getOuterBounds()); + } + break; + case (MotionEvent.ACTION_MOVE): + if (mState == Mode.MOVE) { + float dx = x - mPrevX; + float dy = y - mPrevY; + mCropObj.moveCurrentSelection(dx, dy); + mPrevX = x; + mPrevY = y; + } + break; + default: + break; + } + invalidate(); + return true; + } + + private void clearDisplay() { + mDisplayMatrix = null; + mDisplayMatrixInverse = null; + invalidate(); + } + + public void applyFreeAspect() { + mCropObj.unsetAspectRatio(); + invalidate(); + } + + public void applyOriginalAspect() { + RectF outer = mCropObj.getOuterBounds(); + float w = outer.width(); + float h = outer.height(); + if (w > 0 && h > 0) { + applyAspect(w, h); + mCropObj.resetBoundsTo(outer, outer); + internallyUpdateLocalRep(mCropObj.getInnerBounds(), mCropObj.getOuterBounds()); + } else { + Log.w(TAG, "failed to set aspect ratio original"); + } + invalidate(); + } + + public void applyAspect(float x, float y) { + if (x <= 0 || y <= 0) { + throw new IllegalArgumentException("Bad arguments to applyAspect"); + } + // If we are rotated by 90 degrees from horizontal, swap x and y + if (GeometryMathUtils.needsDimensionSwap(mGeometry.rotation)) { + float tmp = x; + x = y; + y = tmp; + } + if (!mCropObj.setInnerAspectRatio(x, y)) { + Log.w(TAG, "failed to set aspect ratio"); + } + internallyUpdateLocalRep(mCropObj.getInnerBounds(), mCropObj.getOuterBounds()); + invalidate(); + } + + /** + * Rotates first d bits in integer x to the left some number of times. + */ + private int bitCycleLeft(int x, int times, int d) { + int mask = (1 << d) - 1; + int mout = x & mask; + times %= d; + int hi = mout >> (d - times); + int low = (mout << times) & mask; + int ret = x & ~mask; + ret |= low; + ret |= hi; + return ret; + } + + /** + * Find the selected edge or corner in screen coordinates. + */ + private int decode(int movingEdges, float rotation) { + int rot = CropMath.constrainedRotation(rotation); + switch (rot) { + case 90: + return bitCycleLeft(movingEdges, 1, 4); + case 180: + return bitCycleLeft(movingEdges, 2, 4); + case 270: + return bitCycleLeft(movingEdges, 3, 4); + default: + return movingEdges; + } + } + + private void forceStateConsistency() { + MasterImage master = MasterImage.getImage(); + Bitmap image = master.getFiltersOnlyImage(); + int width = image.getWidth(); + int height = image.getHeight(); + if (mCropObj == null || !mUpdateHolder.equals(mGeometry) + || mImageBounds.width() != width || mImageBounds.height() != height + || !mLocalRep.getCrop().equals(mUpdateHolder.crop)) { + mImageBounds.set(0, 0, width, height); + mGeometry.set(mUpdateHolder); + mLocalRep.setCrop(mUpdateHolder.crop); + RectF scaledCrop = new RectF(mUpdateHolder.crop); + FilterCropRepresentation.findScaledCrop(scaledCrop, width, height); + mCropObj = new CropObject(mImageBounds, scaledCrop, (int) mUpdateHolder.straighten); + mState = Mode.NONE; + clearDisplay(); + } + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + clearDisplay(); + } + + @Override + public void onDraw(Canvas canvas) { + Bitmap bitmap = MasterImage.getImage().getFiltersOnlyImage(); + if (!mValidDraw || bitmap == null) { + return; + } + forceStateConsistency(); + mImageBounds.set(0, 0, bitmap.getWidth(), bitmap.getHeight()); + // If display matrix doesn't exist, create it and its dependencies + if (mDisplayCropMatrix == null || mDisplayMatrix == null || mDisplayMatrixInverse == null) { + mDisplayMatrix = GeometryMathUtils.getFullGeometryToScreenMatrix(mGeometry, + bitmap.getWidth(), bitmap.getHeight(), canvas.getWidth(), canvas.getHeight()); + float straighten = mGeometry.straighten; + mGeometry.straighten = 0; + mDisplayCropMatrix = GeometryMathUtils.getFullGeometryToScreenMatrix(mGeometry, + bitmap.getWidth(), bitmap.getHeight(), canvas.getWidth(), canvas.getHeight()); + mGeometry.straighten = straighten; + mDisplayMatrixInverse = new Matrix(); + mDisplayMatrixInverse.reset(); + if (!mDisplayCropMatrix.invert(mDisplayMatrixInverse)) { + Log.w(TAG, "could not invert display matrix"); + mDisplayMatrixInverse = null; + return; + } + // Scale min side and tolerance by display matrix scale factor + mCropObj.setMinInnerSideSize(mDisplayMatrixInverse.mapRadius(mMinSideSize)); + mCropObj.setTouchTolerance(mDisplayMatrixInverse.mapRadius(mTouchTolerance)); + } + // Draw actual bitmap + mPaint.reset(); + mPaint.setAntiAlias(true); + mPaint.setFilterBitmap(true); + canvas.drawBitmap(bitmap, mDisplayMatrix, mPaint); + mCropObj.getInnerBounds(mScreenCropBounds); + RectF outer = mCropObj.getOuterBounds(); + FilterCropRepresentation.findNormalizedCrop(mScreenCropBounds, (int) outer.width(), + (int) outer.height()); + FilterCropRepresentation.findScaledCrop(mScreenCropBounds, bitmap.getWidth(), + bitmap.getHeight()); + if (mDisplayCropMatrix.mapRect(mScreenCropBounds)) { + // Draw crop rect and markers + CropDrawingUtils.drawCropRect(canvas, mScreenCropBounds); + CropDrawingUtils.drawRuleOfThird(canvas, mScreenCropBounds); + CropDrawingUtils.drawIndicators(canvas, mCropIndicator, mIndicatorSize, + mScreenCropBounds, mCropObj.isFixedAspect(), + decode(mCropObj.getSelectState(), mGeometry.rotation.value())); + } + } + + public void setEditor(EditorCrop editorCrop) { + mEditorCrop = editorCrop; + } +} diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageCurves.java b/src/com/android/gallery3d/filtershow/imageshow/ImageCurves.java new file mode 100644 index 000000000..82c4b2fc7 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/imageshow/ImageCurves.java @@ -0,0 +1,445 @@ +/* + * Copyright (C) 2012 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.filtershow.imageshow; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.os.AsyncTask; +import android.util.AttributeSet; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.PopupMenu; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.editors.Editor; +import com.android.gallery3d.filtershow.editors.EditorCurves; +import com.android.gallery3d.filtershow.filters.FilterCurvesRepresentation; +import com.android.gallery3d.filtershow.filters.FiltersManager; +import com.android.gallery3d.filtershow.filters.ImageFilterCurves; +import com.android.gallery3d.filtershow.pipeline.ImagePreset; + +import java.util.HashMap; + +public class ImageCurves extends ImageShow { + + private static final String LOGTAG = "ImageCurves"; + Paint gPaint = new Paint(); + Path gPathSpline = new Path(); + HashMap<Integer, String> mIdStrLut; + + private int mCurrentCurveIndex = Spline.RGB; + private boolean mDidAddPoint = false; + private boolean mDidDelete = false; + private ControlPoint mCurrentControlPoint = null; + private int mCurrentPick = -1; + private ImagePreset mLastPreset = null; + int[] redHistogram = new int[256]; + int[] greenHistogram = new int[256]; + int[] blueHistogram = new int[256]; + Path gHistoPath = new Path(); + + boolean mDoingTouchMove = false; + private EditorCurves mEditorCurves; + private FilterCurvesRepresentation mFilterCurvesRepresentation; + + public ImageCurves(Context context) { + super(context); + setLayerType(LAYER_TYPE_SOFTWARE, gPaint); + resetCurve(); + } + + public ImageCurves(Context context, AttributeSet attrs) { + super(context, attrs); + setLayerType(LAYER_TYPE_SOFTWARE, gPaint); + resetCurve(); + } + + @Override + protected boolean enableComparison() { + return false; + } + + @Override + public boolean useUtilityPanel() { + return true; + } + + private void showPopupMenu(LinearLayout accessoryViewList) { + final Button button = (Button) accessoryViewList.findViewById( + R.id.applyEffect); + if (button == null) { + return; + } + if (mIdStrLut == null){ + mIdStrLut = new HashMap<Integer, String>(); + mIdStrLut.put(R.id.curve_menu_rgb, + getContext().getString(R.string.curves_channel_rgb)); + mIdStrLut.put(R.id.curve_menu_red, + getContext().getString(R.string.curves_channel_red)); + mIdStrLut.put(R.id.curve_menu_green, + getContext().getString(R.string.curves_channel_green)); + mIdStrLut.put(R.id.curve_menu_blue, + getContext().getString(R.string.curves_channel_blue)); + } + PopupMenu popupMenu = new PopupMenu(getActivity(), button); + popupMenu.getMenuInflater().inflate(R.menu.filtershow_menu_curves, popupMenu.getMenu()); + popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + setChannel(item.getItemId()); + button.setText(mIdStrLut.get(item.getItemId())); + return true; + } + }); + Editor.hackFixStrings(popupMenu.getMenu()); + popupMenu.show(); + } + + @Override + public void openUtilityPanel(final LinearLayout accessoryViewList) { + Context context = accessoryViewList.getContext(); + Button view = (Button) accessoryViewList.findViewById(R.id.applyEffect); + view.setText(context.getString(R.string.curves_channel_rgb)); + view.setVisibility(View.VISIBLE); + + view.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View arg0) { + showPopupMenu(accessoryViewList); + } + }); + + if (view != null) { + view.setVisibility(View.VISIBLE); + } + } + + public void nextChannel() { + mCurrentCurveIndex = ((mCurrentCurveIndex + 1) % 4); + invalidate(); + } + + private ImageFilterCurves curves() { + String filterName = getFilterName(); + ImagePreset p = getImagePreset(); + if (p != null) { + return (ImageFilterCurves) FiltersManager.getManager().getFilter(ImageFilterCurves.class); + } + return null; + } + + private Spline getSpline(int index) { + return mFilterCurvesRepresentation.getSpline(index); + } + + @Override + public void resetParameter() { + super.resetParameter(); + resetCurve(); + mLastPreset = null; + invalidate(); + } + + public void resetCurve() { + if (mFilterCurvesRepresentation != null) { + mFilterCurvesRepresentation.reset(); + updateCachedImage(); + } + } + + @Override + public void onDraw(Canvas canvas) { + super.onDraw(canvas); + if (mFilterCurvesRepresentation == null) { + return; + } + + gPaint.setAntiAlias(true); + + if (getImagePreset() != mLastPreset && getFilteredImage() != null) { + new ComputeHistogramTask().execute(getFilteredImage()); + mLastPreset = getImagePreset(); + } + + if (curves() == null) { + return; + } + + if (mCurrentCurveIndex == Spline.RGB || mCurrentCurveIndex == Spline.RED) { + drawHistogram(canvas, redHistogram, Color.RED, PorterDuff.Mode.SCREEN); + } + if (mCurrentCurveIndex == Spline.RGB || mCurrentCurveIndex == Spline.GREEN) { + drawHistogram(canvas, greenHistogram, Color.GREEN, PorterDuff.Mode.SCREEN); + } + if (mCurrentCurveIndex == Spline.RGB || mCurrentCurveIndex == Spline.BLUE) { + drawHistogram(canvas, blueHistogram, Color.BLUE, PorterDuff.Mode.SCREEN); + } + // We only display the other channels curves when showing the RGB curve + if (mCurrentCurveIndex == Spline.RGB) { + for (int i = 0; i < 4; i++) { + Spline spline = getSpline(i); + if (i != mCurrentCurveIndex && !spline.isOriginal()) { + // And we only display a curve if it has more than two + // points + spline.draw(canvas, Spline.colorForCurve(i), getWidth(), + getHeight(), false, mDoingTouchMove); + } + } + } + // ...but we always display the current curve. + getSpline(mCurrentCurveIndex) + .draw(canvas, Spline.colorForCurve(mCurrentCurveIndex), getWidth(), getHeight(), + true, mDoingTouchMove); + + } + + private int pickControlPoint(float x, float y) { + int pick = 0; + Spline spline = getSpline(mCurrentCurveIndex); + float px = spline.getPoint(0).x; + float py = spline.getPoint(0).y; + double delta = Math.sqrt((px - x) * (px - x) + (py - y) * (py - y)); + for (int i = 1; i < spline.getNbPoints(); i++) { + px = spline.getPoint(i).x; + py = spline.getPoint(i).y; + double currentDelta = Math.sqrt((px - x) * (px - x) + (py - y) + * (py - y)); + if (currentDelta < delta) { + delta = currentDelta; + pick = i; + } + } + + if (!mDidAddPoint && (delta * getWidth() > 100) + && (spline.getNbPoints() < 10)) { + return -1; + } + + return pick; + } + + private String getFilterName() { + return "Curves"; + } + + @Override + public synchronized boolean onTouchEvent(MotionEvent e) { + if (e.getPointerCount() != 1) { + return true; + } + + if (didFinishScalingOperation()) { + return true; + } + + float margin = Spline.curveHandleSize() / 2; + float posX = e.getX(); + if (posX < margin) { + posX = margin; + } + float posY = e.getY(); + if (posY < margin) { + posY = margin; + } + if (posX > getWidth() - margin) { + posX = getWidth() - margin; + } + if (posY > getHeight() - margin) { + posY = getHeight() - margin; + } + posX = (posX - margin) / (getWidth() - 2 * margin); + posY = (posY - margin) / (getHeight() - 2 * margin); + + if (e.getActionMasked() == MotionEvent.ACTION_UP) { + mCurrentControlPoint = null; + mCurrentPick = -1; + updateCachedImage(); + mDidAddPoint = false; + if (mDidDelete) { + mDidDelete = false; + } + mDoingTouchMove = false; + return true; + } + + if (mDidDelete) { + return true; + } + + if (curves() == null) { + return true; + } + + if (e.getActionMasked() == MotionEvent.ACTION_MOVE) { + mDoingTouchMove = true; + Spline spline = getSpline(mCurrentCurveIndex); + int pick = mCurrentPick; + if (mCurrentControlPoint == null) { + pick = pickControlPoint(posX, posY); + if (pick == -1) { + mCurrentControlPoint = new ControlPoint(posX, posY); + pick = spline.addPoint(mCurrentControlPoint); + mDidAddPoint = true; + } else { + mCurrentControlPoint = spline.getPoint(pick); + } + mCurrentPick = pick; + } + + if (spline.isPointContained(posX, pick)) { + spline.movePoint(pick, posX, posY); + } else if (pick != -1 && spline.getNbPoints() > 2) { + spline.deletePoint(pick); + mDidDelete = true; + } + updateCachedImage(); + invalidate(); + } + return true; + } + + public synchronized void updateCachedImage() { + if (getImagePreset() != null) { + resetImageCaches(this); + if (mEditorCurves != null) { + mEditorCurves.commitLocalRepresentation(); + } + invalidate(); + } + } + + class ComputeHistogramTask extends AsyncTask<Bitmap, Void, int[]> { + @Override + protected int[] doInBackground(Bitmap... params) { + int[] histo = new int[256 * 3]; + Bitmap bitmap = params[0]; + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + int[] pixels = new int[w * h]; + bitmap.getPixels(pixels, 0, w, 0, 0, w, h); + for (int i = 0; i < w; i++) { + for (int j = 0; j < h; j++) { + int index = j * w + i; + int r = Color.red(pixels[index]); + int g = Color.green(pixels[index]); + int b = Color.blue(pixels[index]); + histo[r]++; + histo[256 + g]++; + histo[512 + b]++; + } + } + return histo; + } + + @Override + protected void onPostExecute(int[] result) { + System.arraycopy(result, 0, redHistogram, 0, 256); + System.arraycopy(result, 256, greenHistogram, 0, 256); + System.arraycopy(result, 512, blueHistogram, 0, 256); + invalidate(); + } + } + + private void drawHistogram(Canvas canvas, int[] histogram, int color, PorterDuff.Mode mode) { + int max = 0; + for (int i = 0; i < histogram.length; i++) { + if (histogram[i] > max) { + max = histogram[i]; + } + } + float w = getWidth() - Spline.curveHandleSize(); + float h = getHeight() - Spline.curveHandleSize() / 2.0f; + float dx = Spline.curveHandleSize() / 2.0f; + float wl = w / histogram.length; + float wh = (0.3f * h) / max; + Paint paint = new Paint(); + paint.setARGB(100, 255, 255, 255); + paint.setStrokeWidth((int) Math.ceil(wl)); + + Paint paint2 = new Paint(); + paint2.setColor(color); + paint2.setStrokeWidth(6); + paint2.setXfermode(new PorterDuffXfermode(mode)); + gHistoPath.reset(); + gHistoPath.moveTo(dx, h); + boolean firstPointEncountered = false; + float prev = 0; + float last = 0; + for (int i = 0; i < histogram.length; i++) { + float x = i * wl + dx; + float l = histogram[i] * wh; + if (l != 0) { + float v = h - (l + prev) / 2.0f; + if (!firstPointEncountered) { + gHistoPath.lineTo(x, h); + firstPointEncountered = true; + } + gHistoPath.lineTo(x, v); + prev = l; + last = x; + } + } + gHistoPath.lineTo(last, h); + gHistoPath.lineTo(w, h); + gHistoPath.close(); + canvas.drawPath(gHistoPath, paint2); + paint2.setStrokeWidth(2); + paint2.setStyle(Paint.Style.STROKE); + paint2.setARGB(255, 200, 200, 200); + canvas.drawPath(gHistoPath, paint2); + } + + public void setChannel(int itemId) { + switch (itemId) { + case R.id.curve_menu_rgb: { + mCurrentCurveIndex = Spline.RGB; + break; + } + case R.id.curve_menu_red: { + mCurrentCurveIndex = Spline.RED; + break; + } + case R.id.curve_menu_green: { + mCurrentCurveIndex = Spline.GREEN; + break; + } + case R.id.curve_menu_blue: { + mCurrentCurveIndex = Spline.BLUE; + break; + } + } + mEditorCurves.commitLocalRepresentation(); + invalidate(); + } + + public void setEditor(EditorCurves editorCurves) { + mEditorCurves = editorCurves; + } + + public void setFilterDrawRepresentation(FilterCurvesRepresentation drawRep) { + mFilterCurvesRepresentation = drawRep; + } +} diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageDraw.java b/src/com/android/gallery3d/filtershow/imageshow/ImageDraw.java new file mode 100644 index 000000000..9722034e0 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/imageshow/ImageDraw.java @@ -0,0 +1,139 @@ + +package com.android.gallery3d.filtershow.imageshow; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.MotionEvent; + +import com.android.gallery3d.filtershow.editors.EditorDraw; +import com.android.gallery3d.filtershow.filters.FilterDrawRepresentation; +import com.android.gallery3d.filtershow.filters.ImageFilterDraw; + +public class ImageDraw extends ImageShow { + + private static final String LOGTAG = "ImageDraw"; + private int mCurrentColor = Color.RED; + final static float INITAL_STROKE_RADIUS = 40; + private float mCurrentSize = INITAL_STROKE_RADIUS; + private byte mType = 0; + private FilterDrawRepresentation mFRep; + private EditorDraw mEditorDraw; + + public ImageDraw(Context context, AttributeSet attrs) { + super(context, attrs); + resetParameter(); + } + + public ImageDraw(Context context) { + super(context); + resetParameter(); + } + + public void setEditor(EditorDraw editorDraw) { + mEditorDraw = editorDraw; + } + public void setFilterDrawRepresentation(FilterDrawRepresentation fr) { + mFRep = fr; + } + + public Drawable getIcon(Context context) { + + return null; + } + + @Override + public void resetParameter() { + if (mFRep != null) { + mFRep.clear(); + } + } + + public void setColor(int color) { + mCurrentColor = color; + } + + public void setSize(int size) { + mCurrentSize = size; + } + + public void setStyle(byte style) { + mType = (byte) (style % ImageFilterDraw.NUMBER_OF_STYLES); + } + + public int getStyle() { + return mType; + } + + public int getSize() { + return (int) mCurrentSize; + } + + float[] mTmpPoint = new float[2]; // so we do not malloc + @Override + public boolean onTouchEvent(MotionEvent event) { + if (event.getPointerCount() > 1) { + boolean ret = super.onTouchEvent(event); + if (mFRep.getCurrentDrawing() != null) { + mFRep.clearCurrentSection(); + mEditorDraw.commitLocalRepresentation(); + } + return ret; + } + if (event.getAction() != MotionEvent.ACTION_DOWN) { + if (mFRep.getCurrentDrawing() == null) { + return super.onTouchEvent(event); + } + } + + if (event.getAction() == MotionEvent.ACTION_DOWN) { + calcScreenMapping(); + mTmpPoint[0] = event.getX(); + mTmpPoint[1] = event.getY(); + mToOrig.mapPoints(mTmpPoint); + mFRep.startNewSection(mType, mCurrentColor, mCurrentSize, mTmpPoint[0], mTmpPoint[1]); + } + + if (event.getAction() == MotionEvent.ACTION_MOVE) { + + int historySize = event.getHistorySize(); + for (int h = 0; h < historySize; h++) { + int p = 0; + { + mTmpPoint[0] = event.getHistoricalX(p, h); + mTmpPoint[1] = event.getHistoricalY(p, h); + mToOrig.mapPoints(mTmpPoint); + mFRep.addPoint(mTmpPoint[0], mTmpPoint[1]); + } + } + } + + if (event.getAction() == MotionEvent.ACTION_UP) { + mTmpPoint[0] = event.getX(); + mTmpPoint[1] = event.getY(); + mToOrig.mapPoints(mTmpPoint); + mFRep.endSection(mTmpPoint[0], mTmpPoint[1]); + } + mEditorDraw.commitLocalRepresentation(); + invalidate(); + return true; + } + + Matrix mRotateToScreen = new Matrix(); + Matrix mToOrig; + private void calcScreenMapping() { + mToOrig = getScreenToImageMatrix(true); + mToOrig.invert(mRotateToScreen); + } + + @Override + public void onDraw(Canvas canvas) { + super.onDraw(canvas); + calcScreenMapping(); + + } + +} diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageGrad.java b/src/com/android/gallery3d/filtershow/imageshow/ImageGrad.java new file mode 100644 index 000000000..b55cc2bc4 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/imageshow/ImageGrad.java @@ -0,0 +1,215 @@ +package com.android.gallery3d.filtershow.imageshow; +/* + * Copyright (C) 2013 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. + */ + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.util.AttributeSet; +import android.view.MotionEvent; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.editors.EditorGrad; +import com.android.gallery3d.filtershow.filters.FilterGradRepresentation; + +public class ImageGrad extends ImageShow { + private static final String LOGTAG = "ImageGrad"; + private FilterGradRepresentation mGradRep; + private EditorGrad mEditorGrad; + private float mMinTouchDist; + private int mActiveHandle = -1; + private GradControl mEllipse; + + Matrix mToScr = new Matrix(); + float[] mPointsX = new float[FilterGradRepresentation.MAX_POINTS]; + float[] mPointsY = new float[FilterGradRepresentation.MAX_POINTS]; + + public ImageGrad(Context context) { + super(context); + Resources res = context.getResources(); + mMinTouchDist = res.getDimensionPixelSize(R.dimen.gradcontrol_min_touch_dist); + mEllipse = new GradControl(context); + mEllipse.setShowReshapeHandles(false); + } + + public ImageGrad(Context context, AttributeSet attrs) { + super(context, attrs); + Resources res = context.getResources(); + mMinTouchDist = res.getDimensionPixelSize(R.dimen.gradcontrol_min_touch_dist); + mEllipse = new GradControl(context); + mEllipse.setShowReshapeHandles(false); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + int mask = event.getActionMasked(); + + if (mActiveHandle == -1) { + if (MotionEvent.ACTION_DOWN != mask) { + return super.onTouchEvent(event); + } + if (event.getPointerCount() == 1) { + mActiveHandle = mEllipse.getCloseHandle(event.getX(), event.getY()); + if (mActiveHandle == -1) { + float x = event.getX(); + float y = event.getY(); + float min_d = Float.MAX_VALUE; + int pos = -1; + for (int i = 0; i < mPointsX.length; i++) { + if (mPointsX[i] == -1) { + continue; + } + float d = (float) Math.hypot(x - mPointsX[i], y - mPointsY[i]); + if ( min_d > d) { + min_d = d; + pos = i; + } + } + if (min_d > mMinTouchDist){ + pos = -1; + } + + if (pos != -1) { + mGradRep.setSelectedPoint(pos); + resetImageCaches(this); + mEditorGrad.updateSeekBar(mGradRep); + mEditorGrad.commitLocalRepresentation(); + invalidate(); + } + } + } + if (mActiveHandle == -1) { + return super.onTouchEvent(event); + } + } else { + switch (mask) { + case MotionEvent.ACTION_UP: { + + mActiveHandle = -1; + break; + } + case MotionEvent.ACTION_DOWN: { + break; + } + } + } + float x = event.getX(); + float y = event.getY(); + + mEllipse.setScrImageInfo(getScreenToImageMatrix(true), + MasterImage.getImage().getOriginalBounds()); + + switch (mask) { + case (MotionEvent.ACTION_DOWN): { + mEllipse.actionDown(x, y, mGradRep); + break; + } + case (MotionEvent.ACTION_UP): + case (MotionEvent.ACTION_MOVE): { + mEllipse.actionMove(mActiveHandle, x, y, mGradRep); + setRepresentation(mGradRep); + break; + } + } + invalidate(); + mEditorGrad.commitLocalRepresentation(); + return true; + } + + public void setRepresentation(FilterGradRepresentation pointRep) { + mGradRep = pointRep; + Matrix toImg = getScreenToImageMatrix(false); + + toImg.invert(mToScr); + + float[] c1 = new float[] { mGradRep.getPoint1X(), mGradRep.getPoint1Y() }; + float[] c2 = new float[] { mGradRep.getPoint2X(), mGradRep.getPoint2Y() }; + + if (c1[0] == -1) { + float cx = MasterImage.getImage().getOriginalBounds().width() / 2; + float cy = MasterImage.getImage().getOriginalBounds().height() / 2; + float rx = Math.min(cx, cy) * .4f; + + mGradRep.setPoint1(cx, cy-rx); + mGradRep.setPoint2(cx, cy+rx); + c1[0] = cx; + c1[1] = cy-rx; + mToScr.mapPoints(c1); + if (getWidth() != 0) { + mEllipse.setPoint1(c1[0], c1[1]); + c2[0] = cx; + c2[1] = cy+rx; + mToScr.mapPoints(c2); + mEllipse.setPoint2(c2[0], c2[1]); + } + mEditorGrad.commitLocalRepresentation(); + } else { + mToScr.mapPoints(c1); + mToScr.mapPoints(c2); + mEllipse.setPoint1(c1[0], c1[1]); + mEllipse.setPoint2(c2[0], c2[1]); + } + } + + public void drawOtherPoints(Canvas canvas) { + computCenterLocations(); + for (int i = 0; i < mPointsX.length; i++) { + if (mPointsX[i] != -1) { + mEllipse.paintGrayPoint(canvas, mPointsX[i], mPointsY[i]); + } + } + } + + public void computCenterLocations() { + int x1[] = mGradRep.getXPos1(); + int y1[] = mGradRep.getYPos1(); + int x2[] = mGradRep.getXPos2(); + int y2[] = mGradRep.getYPos2(); + int selected = mGradRep.getSelectedPoint(); + boolean m[] = mGradRep.getMask(); + float[] c = new float[2]; + for (int i = 0; i < m.length; i++) { + if (selected == i || !m[i]) { + mPointsX[i] = -1; + continue; + } + + c[0] = (x1[i]+x2[i])/2; + c[1] = (y1[i]+y2[i])/2; + mToScr.mapPoints(c); + + mPointsX[i] = c[0]; + mPointsY[i] = c[1]; + } + } + + public void setEditor(EditorGrad editorGrad) { + mEditorGrad = editorGrad; + } + + @Override + public void onDraw(Canvas canvas) { + super.onDraw(canvas); + if (mGradRep == null) { + return; + } + setRepresentation(mGradRep); + mEllipse.draw(canvas); + drawOtherPoints(canvas); + } + +} diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageMirror.java b/src/com/android/gallery3d/filtershow/imageshow/ImageMirror.java new file mode 100644 index 000000000..26c49b1a8 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/imageshow/ImageMirror.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2013 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.filtershow.imageshow; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.view.MotionEvent; + +import com.android.gallery3d.filtershow.editors.EditorMirror; +import com.android.gallery3d.filtershow.filters.FilterMirrorRepresentation; +import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils.GeometryHolder; + +public class ImageMirror extends ImageShow { + private static final String TAG = ImageMirror.class.getSimpleName(); + private EditorMirror mEditorMirror; + private FilterMirrorRepresentation mLocalRep = new FilterMirrorRepresentation(); + private GeometryHolder mDrawHolder = new GeometryHolder(); + + public ImageMirror(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public ImageMirror(Context context) { + super(context); + } + + public void setFilterMirrorRepresentation(FilterMirrorRepresentation rep) { + mLocalRep = (rep == null) ? new FilterMirrorRepresentation() : rep; + } + + public void flip() { + mLocalRep.cycle(); + invalidate(); + } + + public FilterMirrorRepresentation getFinalRepresentation() { + return mLocalRep; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + // Treat event as handled. + return true; + } + + @Override + public void onDraw(Canvas canvas) { + MasterImage master = MasterImage.getImage(); + Bitmap image = master.getFiltersOnlyImage(); + if (image == null) { + return; + } + GeometryMathUtils.initializeHolder(mDrawHolder, mLocalRep); + GeometryMathUtils.drawTransformedCropped(mDrawHolder, canvas, image, getWidth(), + getHeight()); + } + + public void setEditor(EditorMirror editorFlip) { + mEditorMirror = editorFlip; + } + +} diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImagePoint.java b/src/com/android/gallery3d/filtershow/imageshow/ImagePoint.java new file mode 100644 index 000000000..fd5714139 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/imageshow/ImagePoint.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2013 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.filtershow.imageshow; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.util.AttributeSet; + +import com.android.gallery3d.filtershow.editors.EditorRedEye; +import com.android.gallery3d.filtershow.filters.FilterPoint; +import com.android.gallery3d.filtershow.filters.FilterRedEyeRepresentation; +import com.android.gallery3d.filtershow.filters.ImageFilterRedEye; + +public abstract class ImagePoint extends ImageShow { + + private static final String LOGTAG = "ImageRedEyes"; + protected EditorRedEye mEditorRedEye; + protected FilterRedEyeRepresentation mRedEyeRep; + protected static float mTouchPadding = 80; + + public static void setTouchPadding(float padding) { + mTouchPadding = padding; + } + + public ImagePoint(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public ImagePoint(Context context) { + super(context); + } + + @Override + public void resetParameter() { + ImageFilterRedEye filter = (ImageFilterRedEye) getCurrentFilter(); + if (filter != null) { + filter.clear(); + } + invalidate(); + } + + @Override + public void onDraw(Canvas canvas) { + super.onDraw(canvas); + Paint paint = new Paint(); + paint.setStyle(Style.STROKE); + paint.setColor(Color.RED); + paint.setStrokeWidth(2); + + Matrix originalToScreen = getImageToScreenMatrix(false); + Matrix originalRotateToScreen = getImageToScreenMatrix(true); + + if (mRedEyeRep != null) { + for (FilterPoint candidate : mRedEyeRep.getCandidates()) { + drawPoint(candidate, canvas, originalToScreen, originalRotateToScreen, paint); + } + } + } + + protected abstract void drawPoint( + FilterPoint candidate, Canvas canvas, Matrix originalToScreen, + Matrix originalRotateToScreen, Paint paint); + + public void setEditor(EditorRedEye editorRedEye) { + mEditorRedEye = editorRedEye; + } + + public void setRepresentation(FilterRedEyeRepresentation redEyeRep) { + mRedEyeRep = redEyeRep; + } +} diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageRedEye.java b/src/com/android/gallery3d/filtershow/imageshow/ImageRedEye.java new file mode 100644 index 000000000..40433a02e --- /dev/null +++ b/src/com/android/gallery3d/filtershow/imageshow/ImageRedEye.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2013 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.filtershow.imageshow; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.RectF; +import android.view.MotionEvent; + +import com.android.gallery3d.filtershow.filters.FilterPoint; +import com.android.gallery3d.filtershow.filters.RedEyeCandidate; + +public class ImageRedEye extends ImagePoint { + private static final String LOGTAG = "ImageRedEyes"; + private RectF mCurrentRect = null; + + public ImageRedEye(Context context) { + super(context); + } + + @Override + public void resetParameter() { + super.resetParameter(); + invalidate(); + } + + @Override + + public boolean onTouchEvent(MotionEvent event) { + super.onTouchEvent(event); + + if (event.getPointerCount() > 1) { + return true; + } + + if (didFinishScalingOperation()) { + return true; + } + + float ex = event.getX(); + float ey = event.getY(); + + // let's transform (ex, ey) to displayed image coordinates + if (event.getAction() == MotionEvent.ACTION_DOWN) { + mCurrentRect = new RectF(); + mCurrentRect.left = ex - mTouchPadding; + mCurrentRect.top = ey - mTouchPadding; + } + if (event.getAction() == MotionEvent.ACTION_MOVE) { + mCurrentRect.right = ex + mTouchPadding; + mCurrentRect.bottom = ey + mTouchPadding; + } + if (event.getAction() == MotionEvent.ACTION_UP) { + if (mCurrentRect != null) { + // transform to original coordinates + Matrix originalNoRotateToScreen = getImageToScreenMatrix(false); + Matrix originalToScreen = getImageToScreenMatrix(true); + Matrix invert = new Matrix(); + originalToScreen.invert(invert); + RectF r = new RectF(mCurrentRect); + invert.mapRect(r); + RectF r2 = new RectF(mCurrentRect); + invert.reset(); + originalNoRotateToScreen.invert(invert); + invert.mapRect(r2); + mRedEyeRep.addRect(r, r2); + this.resetImageCaches(this); + } + mCurrentRect = null; + } + mEditorRedEye.commitLocalRepresentation(); + invalidate(); + return true; + } + + @Override + public void onDraw(Canvas canvas) { + super.onDraw(canvas); + Paint paint = new Paint(); + paint.setStyle(Style.STROKE); + paint.setColor(Color.RED); + paint.setStrokeWidth(2); + if (mCurrentRect != null) { + paint.setColor(Color.RED); + RectF drawRect = new RectF(mCurrentRect); + canvas.drawRect(drawRect, paint); + } + } + + @Override + protected void drawPoint(FilterPoint point, Canvas canvas, Matrix originalToScreen, + Matrix originalRotateToScreen, Paint paint) { + RedEyeCandidate candidate = (RedEyeCandidate) point; + RectF rect = candidate.getRect(); + RectF drawRect = new RectF(); + originalToScreen.mapRect(drawRect, rect); + RectF fullRect = new RectF(); + originalRotateToScreen.mapRect(fullRect, rect); + paint.setColor(Color.BLUE); + canvas.drawRect(fullRect, paint); + canvas.drawLine(fullRect.centerX(), fullRect.top, + fullRect.centerX(), fullRect.bottom, paint); + canvas.drawLine(fullRect.left, fullRect.centerY(), + fullRect.right, fullRect.centerY(), paint); + paint.setColor(Color.GREEN); + float dw = drawRect.width(); + float dh = drawRect.height(); + float dx = fullRect.centerX() - dw / 2; + float dy = fullRect.centerY() - dh / 2; + drawRect.set(dx, dy, dx + dw, dy + dh); + canvas.drawRect(drawRect, paint); + canvas.drawLine(drawRect.centerX(), drawRect.top, + drawRect.centerX(), drawRect.bottom, paint); + canvas.drawLine(drawRect.left, drawRect.centerY(), + drawRect.right, drawRect.centerY(), paint); + canvas.drawCircle(drawRect.centerX(), drawRect.centerY(), + mTouchPadding, paint); + } +} diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageRotate.java b/src/com/android/gallery3d/filtershow/imageshow/ImageRotate.java new file mode 100644 index 000000000..5186c09d7 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/imageshow/ImageRotate.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2012 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.filtershow.imageshow; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.view.MotionEvent; + +import com.android.gallery3d.filtershow.editors.EditorRotate; +import com.android.gallery3d.filtershow.filters.FilterRotateRepresentation; +import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils.GeometryHolder; + +public class ImageRotate extends ImageShow { + private EditorRotate mEditorRotate; + private static final String TAG = ImageRotate.class.getSimpleName(); + private FilterRotateRepresentation mLocalRep = new FilterRotateRepresentation(); + private GeometryHolder mDrawHolder = new GeometryHolder(); + + public ImageRotate(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public ImageRotate(Context context) { + super(context); + } + + public void setFilterRotateRepresentation(FilterRotateRepresentation rep) { + mLocalRep = (rep == null) ? new FilterRotateRepresentation() : rep; + } + + public void rotate() { + mLocalRep.rotateCW(); + invalidate(); + } + + public FilterRotateRepresentation getFinalRepresentation() { + return mLocalRep; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + // Treat event as handled. + return true; + } + + public int getLocalValue() { + return mLocalRep.getRotation().value(); + } + + @Override + public void onDraw(Canvas canvas) { + MasterImage master = MasterImage.getImage(); + Bitmap image = master.getFiltersOnlyImage(); + if (image == null) { + return; + } + GeometryMathUtils.initializeHolder(mDrawHolder, mLocalRep); + GeometryMathUtils.drawTransformedCropped(mDrawHolder, canvas, image, canvas.getWidth(), + canvas.getHeight()); + } + + public void setEditor(EditorRotate editorRotate) { + mEditorRotate = editorRotate; + } +} diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageShow.java b/src/com/android/gallery3d/filtershow/imageshow/ImageShow.java new file mode 100644 index 000000000..6278b2ad4 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/imageshow/ImageShow.java @@ -0,0 +1,578 @@ +/* + * Copyright (C) 2012 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.filtershow.imageshow; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.view.GestureDetector; +import android.view.GestureDetector.OnDoubleTapListener; +import android.view.GestureDetector.OnGestureListener; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; +import android.view.View; +import android.widget.LinearLayout; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.FilterShowActivity; +import com.android.gallery3d.filtershow.filters.ImageFilter; +import com.android.gallery3d.filtershow.pipeline.ImagePreset; +import com.android.gallery3d.filtershow.tools.SaveImage; + +import java.io.File; + +public class ImageShow extends View implements OnGestureListener, + ScaleGestureDetector.OnScaleGestureListener, + OnDoubleTapListener { + + private static final String LOGTAG = "ImageShow"; + private static final boolean ENABLE_ZOOMED_COMPARISON = false; + + protected Paint mPaint = new Paint(); + protected int mTextSize; + protected int mTextPadding; + + protected int mBackgroundColor; + + private GestureDetector mGestureDetector = null; + private ScaleGestureDetector mScaleGestureDetector = null; + + protected Rect mImageBounds = new Rect(); + private boolean mOriginalDisabled = false; + private boolean mTouchShowOriginal = false; + private long mTouchShowOriginalDate = 0; + private final long mTouchShowOriginalDelayMin = 200; // 200ms + private int mShowOriginalDirection = 0; + private static int UNVEIL_HORIZONTAL = 1; + private static int UNVEIL_VERTICAL = 2; + + private Point mTouchDown = new Point(); + private Point mTouch = new Point(); + private boolean mFinishedScalingOperation = false; + + private int mOriginalTextMargin; + private int mOriginalTextSize; + private String mOriginalText; + private boolean mZoomIn = false; + Point mOriginalTranslation = new Point(); + float mOriginalScale; + float mStartFocusX, mStartFocusY; + private enum InteractionMode { + NONE, + SCALE, + MOVE + } + InteractionMode mInteractionMode = InteractionMode.NONE; + + private FilterShowActivity mActivity = null; + + public FilterShowActivity getActivity() { + return mActivity; + } + + public boolean hasModifications() { + return MasterImage.getImage().hasModifications(); + } + + public void resetParameter() { + // TODO: implement reset + } + + public void onNewValue(int parameter) { + invalidate(); + } + + public ImageShow(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + setupImageShow(context); + } + + public ImageShow(Context context, AttributeSet attrs) { + super(context, attrs); + setupImageShow(context); + + } + + public ImageShow(Context context) { + super(context); + setupImageShow(context); + } + + private void setupImageShow(Context context) { + Resources res = context.getResources(); + mTextSize = res.getDimensionPixelSize(R.dimen.photoeditor_text_size); + mTextPadding = res.getDimensionPixelSize(R.dimen.photoeditor_text_padding); + mOriginalTextMargin = res.getDimensionPixelSize(R.dimen.photoeditor_original_text_margin); + mOriginalTextSize = res.getDimensionPixelSize(R.dimen.photoeditor_original_text_size); + mBackgroundColor = res.getColor(R.color.background_screen); + mOriginalText = res.getString(R.string.original_picture_text); + setupGestureDetector(context); + mActivity = (FilterShowActivity) context; + MasterImage.getImage().addObserver(this); + } + + public void setupGestureDetector(Context context) { + mGestureDetector = new GestureDetector(context, this); + mScaleGestureDetector = new ScaleGestureDetector(context, this); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int parentWidth = MeasureSpec.getSize(widthMeasureSpec); + int parentHeight = MeasureSpec.getSize(heightMeasureSpec); + setMeasuredDimension(parentWidth, parentHeight); + } + + public ImageFilter getCurrentFilter() { + return MasterImage.getImage().getCurrentFilter(); + } + + /* consider moving the following 2 methods into a subclass */ + /** + * This function calculates a Image to Screen Transformation matrix + * + * @param reflectRotation set true if you want the rotation encoded + * @return Image to Screen transformation matrix + */ + protected Matrix getImageToScreenMatrix(boolean reflectRotation) { + MasterImage master = MasterImage.getImage(); + if (master.getOriginalBounds() == null) { + return new Matrix(); + } + Matrix m = GeometryMathUtils.getImageToScreenMatrix(master.getPreset().getGeometryFilters(), + reflectRotation, master.getOriginalBounds(), getWidth(), getHeight()); + Point translate = master.getTranslation(); + float scaleFactor = master.getScaleFactor(); + m.postTranslate(translate.x, translate.y); + m.postScale(scaleFactor, scaleFactor, getWidth() / 2.0f, getHeight() / 2.0f); + return m; + } + + /** + * This function calculates a to Screen Image Transformation matrix + * + * @param reflectRotation set true if you want the rotation encoded + * @return Screen to Image transformation matrix + */ + protected Matrix getScreenToImageMatrix(boolean reflectRotation) { + Matrix m = getImageToScreenMatrix(reflectRotation); + Matrix invert = new Matrix(); + m.invert(invert); + return invert; + } + + public ImagePreset getImagePreset() { + return MasterImage.getImage().getPreset(); + } + + @Override + public void onDraw(Canvas canvas) { + MasterImage.getImage().setImageShowSize(getWidth(), getHeight()); + + float cx = canvas.getWidth()/2.0f; + float cy = canvas.getHeight()/2.0f; + float scaleFactor = MasterImage.getImage().getScaleFactor(); + Point translation = MasterImage.getImage().getTranslation(); + + Matrix scalingMatrix = new Matrix(); + scalingMatrix.postScale(scaleFactor, scaleFactor, cx, cy); + scalingMatrix.preTranslate(translation.x, translation.y); + + RectF unscaledClipRect = new RectF(mImageBounds); + scalingMatrix.mapRect(unscaledClipRect, unscaledClipRect); + + canvas.save(); + + boolean enablePartialRendering = false; + + // For now, partial rendering is disabled for all filters, + // so no need to clip. + if (enablePartialRendering && !unscaledClipRect.isEmpty()) { + canvas.clipRect(unscaledClipRect); + } + + canvas.save(); + // TODO: center scale on gesture + canvas.scale(scaleFactor, scaleFactor, cx, cy); + canvas.translate(translation.x, translation.y); + drawImage(canvas, getFilteredImage(), true); + Bitmap highresPreview = MasterImage.getImage().getHighresImage(); + if (highresPreview != null) { + drawImage(canvas, highresPreview, true); + } + canvas.restore(); + + Bitmap partialPreview = MasterImage.getImage().getPartialImage(); + if (partialPreview != null) { + Rect src = new Rect(0, 0, partialPreview.getWidth(), partialPreview.getHeight()); + Rect dest = new Rect(0, 0, getWidth(), getHeight()); + canvas.drawBitmap(partialPreview, src, dest, mPaint); + } + + canvas.save(); + canvas.scale(scaleFactor, scaleFactor, cx, cy); + canvas.translate(translation.x, translation.y); + drawPartialImage(canvas, getGeometryOnlyImage()); + canvas.restore(); + + canvas.restore(); + } + + public void resetImageCaches(ImageShow caller) { + MasterImage.getImage().updatePresets(true); + } + + public Bitmap getFiltersOnlyImage() { + return MasterImage.getImage().getFiltersOnlyImage(); + } + + public Bitmap getGeometryOnlyImage() { + return MasterImage.getImage().getGeometryOnlyImage(); + } + + public Bitmap getFilteredImage() { + return MasterImage.getImage().getFilteredImage(); + } + + public void drawImage(Canvas canvas, Bitmap image, boolean updateBounds) { + if (image != null) { + Rect s = new Rect(0, 0, image.getWidth(), + image.getHeight()); + + float scale = GeometryMathUtils.scale(image.getWidth(), image.getHeight(), getWidth(), + getHeight()); + + float w = image.getWidth() * scale; + float h = image.getHeight() * scale; + float ty = (getHeight() - h) / 2.0f; + float tx = (getWidth() - w) / 2.0f; + + Rect d = new Rect((int) tx, (int) ty, (int) (w + tx), + (int) (h + ty)); + if (updateBounds) { + mImageBounds = d; + } + canvas.drawBitmap(image, s, d, mPaint); + } + } + + public void drawPartialImage(Canvas canvas, Bitmap image) { + boolean showsOriginal = MasterImage.getImage().showsOriginal(); + if (!showsOriginal && !mTouchShowOriginal) + return; + canvas.save(); + if (image != null) { + if (mShowOriginalDirection == 0) { + if (Math.abs(mTouch.y - mTouchDown.y) > Math.abs(mTouch.x - mTouchDown.x)) { + mShowOriginalDirection = UNVEIL_VERTICAL; + } else { + mShowOriginalDirection = UNVEIL_HORIZONTAL; + } + } + + int px = 0; + int py = 0; + if (mShowOriginalDirection == UNVEIL_VERTICAL) { + px = mImageBounds.width(); + py = mTouch.y - mImageBounds.top; + } else { + px = mTouch.x - mImageBounds.left; + py = mImageBounds.height(); + if (showsOriginal) { + px = mImageBounds.width(); + } + } + + Rect d = new Rect(mImageBounds.left, mImageBounds.top, + mImageBounds.left + px, mImageBounds.top + py); + canvas.clipRect(d); + drawImage(canvas, image, false); + Paint paint = new Paint(); + paint.setColor(Color.BLACK); + paint.setStrokeWidth(3); + + if (mShowOriginalDirection == UNVEIL_VERTICAL) { + canvas.drawLine(mImageBounds.left, mTouch.y, + mImageBounds.right, mTouch.y, paint); + } else { + canvas.drawLine(mTouch.x, mImageBounds.top, + mTouch.x, mImageBounds.bottom, paint); + } + + Rect bounds = new Rect(); + paint.setAntiAlias(true); + paint.setTextSize(mOriginalTextSize); + paint.getTextBounds(mOriginalText, 0, mOriginalText.length(), bounds); + paint.setColor(Color.BLACK); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(3); + canvas.drawText(mOriginalText, mImageBounds.left + mOriginalTextMargin, + mImageBounds.top + bounds.height() + mOriginalTextMargin, paint); + paint.setStyle(Paint.Style.FILL); + paint.setStrokeWidth(1); + paint.setColor(Color.WHITE); + canvas.drawText(mOriginalText, mImageBounds.left + mOriginalTextMargin, + mImageBounds.top + bounds.height() + mOriginalTextMargin, paint); + } + canvas.restore(); + } + + public void bindAsImageLoadListener() { + MasterImage.getImage().addListener(this); + } + + public void updateImage() { + invalidate(); + } + + public void imageLoaded() { + updateImage(); + } + + public void saveImage(FilterShowActivity filterShowActivity, File file) { + SaveImage.saveImage(getImagePreset(), filterShowActivity, file); + } + + + public boolean scaleInProgress() { + return mScaleGestureDetector.isInProgress(); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + super.onTouchEvent(event); + int action = event.getAction(); + action = action & MotionEvent.ACTION_MASK; + + mGestureDetector.onTouchEvent(event); + boolean scaleInProgress = scaleInProgress(); + mScaleGestureDetector.onTouchEvent(event); + if (mInteractionMode == InteractionMode.SCALE) { + return true; + } + if (!scaleInProgress() && scaleInProgress) { + // If we were scaling, the scale will stop but we will + // still issue an ACTION_UP. Let the subclasses know. + mFinishedScalingOperation = true; + } + + int ex = (int) event.getX(); + int ey = (int) event.getY(); + if (action == MotionEvent.ACTION_DOWN) { + mInteractionMode = InteractionMode.MOVE; + mTouchDown.x = ex; + mTouchDown.y = ey; + mTouchShowOriginalDate = System.currentTimeMillis(); + mShowOriginalDirection = 0; + MasterImage.getImage().setOriginalTranslation(MasterImage.getImage().getTranslation()); + } + + if (action == MotionEvent.ACTION_MOVE && mInteractionMode == InteractionMode.MOVE) { + mTouch.x = ex; + mTouch.y = ey; + + float scaleFactor = MasterImage.getImage().getScaleFactor(); + if (scaleFactor > 1 && (!ENABLE_ZOOMED_COMPARISON || event.getPointerCount() == 2)) { + float translateX = (mTouch.x - mTouchDown.x) / scaleFactor; + float translateY = (mTouch.y - mTouchDown.y) / scaleFactor; + Point originalTranslation = MasterImage.getImage().getOriginalTranslation(); + Point translation = MasterImage.getImage().getTranslation(); + translation.x = (int) (originalTranslation.x + translateX); + translation.y = (int) (originalTranslation.y + translateY); + constrainTranslation(translation, scaleFactor); + MasterImage.getImage().setTranslation(translation); + mTouchShowOriginal = false; + } else if (enableComparison() && !mOriginalDisabled + && (System.currentTimeMillis() - mTouchShowOriginalDate + > mTouchShowOriginalDelayMin) + && event.getPointerCount() == 1) { + mTouchShowOriginal = true; + } + } + + if (action == MotionEvent.ACTION_UP) { + mInteractionMode = InteractionMode.NONE; + mTouchShowOriginal = false; + mTouchDown.x = 0; + mTouchDown.y = 0; + mTouch.x = 0; + mTouch.y = 0; + if (MasterImage.getImage().getScaleFactor() <= 1) { + MasterImage.getImage().setScaleFactor(1); + MasterImage.getImage().resetTranslation(); + } + } + invalidate(); + return true; + } + + protected boolean enableComparison() { + return true; + } + + @Override + public boolean onDoubleTap(MotionEvent arg0) { + mZoomIn = !mZoomIn; + float scale = 1.0f; + if (mZoomIn) { + scale = MasterImage.getImage().getMaxScaleFactor(); + } + if (scale != MasterImage.getImage().getScaleFactor()) { + MasterImage.getImage().setScaleFactor(scale); + float translateX = (getWidth() / 2 - arg0.getX()); + float translateY = (getHeight() / 2 - arg0.getY()); + Point translation = MasterImage.getImage().getTranslation(); + translation.x = (int) (mOriginalTranslation.x + translateX); + translation.y = (int) (mOriginalTranslation.y + translateY); + constrainTranslation(translation, scale); + MasterImage.getImage().setTranslation(translation); + invalidate(); + } + return true; + } + + private void constrainTranslation(Point translation, float scale) { + float maxTranslationX = getWidth() / scale; + float maxTranslationY = getHeight() / scale; + if (Math.abs(translation.x) > maxTranslationX) { + translation.x = (int) (Math.signum(translation.x) * + maxTranslationX); + if (Math.abs(translation.y) > maxTranslationY) { + translation.y = (int) (Math.signum(translation.y) * + maxTranslationY); + } + + } + } + + @Override + public boolean onDoubleTapEvent(MotionEvent arg0) { + return false; + } + + @Override + public boolean onSingleTapConfirmed(MotionEvent arg0) { + return false; + } + + @Override + public boolean onDown(MotionEvent arg0) { + return false; + } + + @Override + public boolean onFling(MotionEvent startEvent, MotionEvent endEvent, float arg2, float arg3) { + if (mActivity == null) { + return false; + } + if (endEvent.getPointerCount() == 2) { + return false; + } + return true; + } + + @Override + public void onLongPress(MotionEvent arg0) { + } + + @Override + public boolean onScroll(MotionEvent arg0, MotionEvent arg1, float arg2, float arg3) { + return false; + } + + @Override + public void onShowPress(MotionEvent arg0) { + } + + @Override + public boolean onSingleTapUp(MotionEvent arg0) { + return false; + } + + public boolean useUtilityPanel() { + return false; + } + + public void openUtilityPanel(final LinearLayout accessoryViewList) { + } + + @Override + public boolean onScale(ScaleGestureDetector detector) { + MasterImage img = MasterImage.getImage(); + float scaleFactor = img.getScaleFactor(); + + scaleFactor = scaleFactor * detector.getScaleFactor(); + if (scaleFactor > MasterImage.getImage().getMaxScaleFactor()) { + scaleFactor = MasterImage.getImage().getMaxScaleFactor(); + } + if (scaleFactor < 0.5) { + scaleFactor = 0.5f; + } + MasterImage.getImage().setScaleFactor(scaleFactor); + scaleFactor = img.getScaleFactor(); + float focusx = detector.getFocusX(); + float focusy = detector.getFocusY(); + float translateX = (focusx - mStartFocusX) / scaleFactor; + float translateY = (focusy - mStartFocusY) / scaleFactor; + Point translation = MasterImage.getImage().getTranslation(); + translation.x = (int) (mOriginalTranslation.x + translateX); + translation.y = (int) (mOriginalTranslation.y + translateY); + constrainTranslation(translation, scaleFactor); + MasterImage.getImage().setTranslation(translation); + + invalidate(); + return true; + } + + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + Point pos = MasterImage.getImage().getTranslation(); + mOriginalTranslation.x = pos.x; + mOriginalTranslation.y = pos.y; + mOriginalScale = MasterImage.getImage().getScaleFactor(); + mStartFocusX = detector.getFocusX(); + mStartFocusY = detector.getFocusY(); + mInteractionMode = InteractionMode.SCALE; + return true; + } + + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + mInteractionMode = InteractionMode.NONE; + if (MasterImage.getImage().getScaleFactor() < 1) { + MasterImage.getImage().setScaleFactor(1); + invalidate(); + } + } + + public boolean didFinishScalingOperation() { + if (mFinishedScalingOperation) { + mFinishedScalingOperation = false; + return true; + } + return false; + } + +} diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageStraighten.java b/src/com/android/gallery3d/filtershow/imageshow/ImageStraighten.java new file mode 100644 index 000000000..ff75dcc09 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/imageshow/ImageStraighten.java @@ -0,0 +1,260 @@ +/* + * Copyright (C) 2012 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.filtershow.imageshow; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.Path; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.view.MotionEvent; + +import com.android.gallery3d.filtershow.editors.EditorStraighten; +import com.android.gallery3d.filtershow.filters.FilterCropRepresentation; +import com.android.gallery3d.filtershow.filters.FilterRepresentation; +import com.android.gallery3d.filtershow.filters.FilterStraightenRepresentation; +import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils.GeometryHolder; + +import java.util.ArrayList; +import java.util.Collection; + + +public class ImageStraighten extends ImageShow { + private static final String TAG = ImageStraighten.class.getSimpleName(); + private float mBaseAngle = 0; + private float mAngle = 0; + private float mInitialAngle = 0; + private boolean mFirstDrawSinceUp = false; + private EditorStraighten mEditorStraighten; + private FilterStraightenRepresentation mLocalRep = new FilterStraightenRepresentation(); + private RectF mPriorCropAtUp = new RectF(); + private RectF mDrawRect = new RectF(); + private Path mDrawPath = new Path(); + private GeometryHolder mDrawHolder = new GeometryHolder(); + private enum MODES { + NONE, MOVE + } + private MODES mState = MODES.NONE; + private static final float MAX_STRAIGHTEN_ANGLE + = FilterStraightenRepresentation.MAX_STRAIGHTEN_ANGLE; + private static final float MIN_STRAIGHTEN_ANGLE + = FilterStraightenRepresentation.MIN_STRAIGHTEN_ANGLE; + private float mCurrentX; + private float mCurrentY; + private float mTouchCenterX; + private float mTouchCenterY; + private RectF mCrop = new RectF(); + private final Paint mPaint = new Paint(); + + public ImageStraighten(Context context) { + super(context); + } + + public ImageStraighten(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void setFilterStraightenRepresentation(FilterStraightenRepresentation rep) { + mLocalRep = (rep == null) ? new FilterStraightenRepresentation() : rep; + mInitialAngle = mBaseAngle = mAngle = mLocalRep.getStraighten(); + } + + public Collection<FilterRepresentation> getFinalRepresentation() { + ArrayList<FilterRepresentation> reps = new ArrayList<FilterRepresentation>(2); + reps.add(mLocalRep); + if (mInitialAngle != mLocalRep.getStraighten()) { + reps.add(new FilterCropRepresentation(mCrop)); + } + return reps; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + float x = event.getX(); + float y = event.getY(); + + switch (event.getActionMasked()) { + case (MotionEvent.ACTION_DOWN): + if (mState == MODES.NONE) { + mTouchCenterX = x; + mTouchCenterY = y; + mCurrentX = x; + mCurrentY = y; + mState = MODES.MOVE; + mBaseAngle = mAngle; + } + break; + case (MotionEvent.ACTION_UP): + if (mState == MODES.MOVE) { + mState = MODES.NONE; + mCurrentX = x; + mCurrentY = y; + computeValue(); + mFirstDrawSinceUp = true; + } + break; + case (MotionEvent.ACTION_MOVE): + if (mState == MODES.MOVE) { + mCurrentX = x; + mCurrentY = y; + computeValue(); + } + break; + default: + break; + } + invalidate(); + return true; + } + + private static float angleFor(float dx, float dy) { + return (float) (Math.atan2(dx, dy) * 180 / Math.PI); + } + + private float getCurrentTouchAngle() { + float centerX = getWidth() / 2f; + float centerY = getHeight() / 2f; + if (mCurrentX == mTouchCenterX && mCurrentY == mTouchCenterY) { + return 0; + } + float dX1 = mTouchCenterX - centerX; + float dY1 = mTouchCenterY - centerY; + float dX2 = mCurrentX - centerX; + float dY2 = mCurrentY - centerY; + float angleA = angleFor(dX1, dY1); + float angleB = angleFor(dX2, dY2); + return (angleB - angleA) % 360; + } + + private void computeValue() { + float angle = getCurrentTouchAngle(); + mAngle = (mBaseAngle - angle) % 360; + mAngle = Math.max(MIN_STRAIGHTEN_ANGLE, mAngle); + mAngle = Math.min(MAX_STRAIGHTEN_ANGLE, mAngle); + } + + private static void getUntranslatedStraightenCropBounds(RectF outRect, float straightenAngle) { + float deg = straightenAngle; + if (deg < 0) { + deg = -deg; + } + double a = Math.toRadians(deg); + double sina = Math.sin(a); + double cosa = Math.cos(a); + double rw = outRect.width(); + double rh = outRect.height(); + double h1 = rh * rh / (rw * sina + rh * cosa); + double h2 = rh * rw / (rw * cosa + rh * sina); + double hh = Math.min(h1, h2); + double ww = hh * rw / rh; + float left = (float) ((rw - ww) * 0.5f); + float top = (float) ((rh - hh) * 0.5f); + float right = (float) (left + ww); + float bottom = (float) (top + hh); + outRect.set(left, top, right, bottom); + } + + private void updateCurrentCrop(Matrix m, GeometryHolder h, RectF tmp, int imageWidth, + int imageHeight, int viewWidth, int viewHeight) { + if (GeometryMathUtils.needsDimensionSwap(h.rotation)) { + tmp.set(0, 0, imageHeight, imageWidth); + } else { + tmp.set(0, 0, imageWidth, imageHeight); + } + float scale = GeometryMathUtils.scale(imageWidth, imageHeight, viewWidth, viewHeight); + GeometryMathUtils.scaleRect(tmp, scale); + getUntranslatedStraightenCropBounds(tmp, mAngle); + tmp.offset(viewWidth / 2f - tmp.centerX(), viewHeight / 2f - tmp.centerY()); + h.straighten = 0; + Matrix m1 = GeometryMathUtils.getFullGeometryToScreenMatrix(h, imageWidth, + imageHeight, viewWidth, viewHeight); + m.reset(); + m1.invert(m); + mCrop.set(tmp); + m.mapRect(mCrop); + FilterCropRepresentation.findNormalizedCrop(mCrop, imageWidth, imageHeight); + } + + + @Override + public void onDraw(Canvas canvas) { + MasterImage master = MasterImage.getImage(); + Bitmap image = master.getFiltersOnlyImage(); + if (image == null) { + return; + } + GeometryMathUtils.initializeHolder(mDrawHolder, mLocalRep); + mDrawHolder.straighten = mAngle; + int imageWidth = image.getWidth(); + int imageHeight = image.getHeight(); + int viewWidth = canvas.getWidth(); + int viewHeight = canvas.getHeight(); + + // Get matrix for drawing bitmap + Matrix m = GeometryMathUtils.getFullGeometryToScreenMatrix(mDrawHolder, imageWidth, + imageHeight, viewWidth, viewHeight); + mPaint.reset(); + mPaint.setAntiAlias(true); + mPaint.setFilterBitmap(true); + canvas.drawBitmap(image, m, mPaint); + + mPaint.setFilterBitmap(false); + mPaint.setColor(Color.WHITE); + mPaint.setStrokeWidth(2); + mPaint.setStyle(Paint.Style.FILL_AND_STROKE); + updateCurrentCrop(m, mDrawHolder, mDrawRect, imageWidth, + imageHeight, viewWidth, viewHeight); + if (mFirstDrawSinceUp) { + mPriorCropAtUp.set(mCrop); + mLocalRep.setStraighten(mAngle); + mFirstDrawSinceUp = false; + } + + // Draw the grid + if (mState == MODES.MOVE) { + canvas.save(); + canvas.clipRect(mDrawRect); + int n = 16; + float step = viewWidth / n; + float p = 0; + for (int i = 1; i < n; i++) { + p = i * step; + mPaint.setAlpha(60); + canvas.drawLine(p, 0, p, viewHeight, mPaint); + canvas.drawLine(0, p, viewHeight, p, mPaint); + } + canvas.restore(); + } + mPaint.reset(); + mPaint.setColor(Color.WHITE); + mPaint.setStyle(Style.STROKE); + mPaint.setStrokeWidth(3); + mDrawPath.reset(); + mDrawPath.addRect(mDrawRect, Path.Direction.CW); + canvas.drawPath(mDrawPath, mPaint); + } + + public void setEditor(EditorStraighten editorStraighten) { + mEditorStraighten = editorStraighten; + } + +} diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageTinyPlanet.java b/src/com/android/gallery3d/filtershow/imageshow/ImageTinyPlanet.java new file mode 100644 index 000000000..25a0a9073 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/imageshow/ImageTinyPlanet.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2012 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.filtershow.imageshow; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; +import android.view.ScaleGestureDetector.OnScaleGestureListener; + +import com.android.gallery3d.filtershow.editors.BasicEditor; +import com.android.gallery3d.filtershow.editors.EditorTinyPlanet; +import com.android.gallery3d.filtershow.filters.FilterTinyPlanetRepresentation; + +public class ImageTinyPlanet extends ImageShow { + private static final String LOGTAG = "ImageTinyPlanet"; + + private float mTouchCenterX = 0; + private float mTouchCenterY = 0; + private float mCurrentX = 0; + private float mCurrentY = 0; + private float mCenterX = 0; + private float mCenterY = 0; + private float mStartAngle = 0; + private FilterTinyPlanetRepresentation mTinyPlanetRep; + private EditorTinyPlanet mEditorTinyPlanet; + private ScaleGestureDetector mScaleGestureDetector = null; + boolean mInScale = false; + RectF mDestRect = new RectF(); + + OnScaleGestureListener mScaleGestureListener = new OnScaleGestureListener() { + private float mScale = 100; + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + mInScale = false; + } + + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + mInScale = true; + mScale = mTinyPlanetRep.getValue(); + return true; + } + + @Override + public boolean onScale(ScaleGestureDetector detector) { + int value = mTinyPlanetRep.getValue(); + mScale *= detector.getScaleFactor(); + value = (int) (mScale); + value = Math.min(mTinyPlanetRep.getMaximum(), value); + value = Math.max(mTinyPlanetRep.getMinimum(), value); + mTinyPlanetRep.setValue(value); + invalidate(); + mEditorTinyPlanet.commitLocalRepresentation(); + mEditorTinyPlanet.updateUI(); + return true; + } + }; + + public ImageTinyPlanet(Context context) { + super(context); + mScaleGestureDetector = new ScaleGestureDetector(context, mScaleGestureListener); + } + + public ImageTinyPlanet(Context context, AttributeSet attrs) { + super(context, attrs); + mScaleGestureDetector = new ScaleGestureDetector(context,mScaleGestureListener ); + } + + protected static float angleFor(float dx, float dy) { + return (float) (Math.atan2(dx, dy) * 180 / Math.PI); + } + + protected float getCurrentTouchAngle() { + if (mCurrentX == mTouchCenterX && mCurrentY == mTouchCenterY) { + return 0; + } + float dX1 = mTouchCenterX - mCenterX; + float dY1 = mTouchCenterY - mCenterY; + float dX2 = mCurrentX - mCenterX; + float dY2 = mCurrentY - mCenterY; + + float angleA = angleFor(dX1, dY1); + float angleB = angleFor(dX2, dY2); + return (float) (((angleB - angleA) % 360) * Math.PI / 180); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + float x = event.getX(); + float y = event.getY(); + mCurrentX = x; + mCurrentY = y; + mCenterX = getWidth() / 2; + mCenterY = getHeight() / 2; + mScaleGestureDetector.onTouchEvent(event); + if (mInScale) { + return true; + } + switch (event.getActionMasked()) { + case (MotionEvent.ACTION_DOWN): + mTouchCenterX = x; + mTouchCenterY = y; + mStartAngle = mTinyPlanetRep.getAngle(); + break; + + case (MotionEvent.ACTION_MOVE): + mTinyPlanetRep.setAngle(mStartAngle + getCurrentTouchAngle()); + break; + } + invalidate(); + mEditorTinyPlanet.commitLocalRepresentation(); + return true; + } + + public void setRepresentation(FilterTinyPlanetRepresentation tinyPlanetRep) { + mTinyPlanetRep = tinyPlanetRep; + } + + public void setEditor(BasicEditor editorTinyPlanet) { + mEditorTinyPlanet = (EditorTinyPlanet) editorTinyPlanet; + } + + @Override + public void onDraw(Canvas canvas) { + Bitmap bitmap = MasterImage.getImage().getHighresImage(); + if (bitmap == null) { + bitmap = MasterImage.getImage().getFilteredImage(); + } + + if (bitmap != null) { + display(canvas, bitmap); + } + } + + private void display(Canvas canvas, Bitmap bitmap) { + float sw = canvas.getWidth(); + float sh = canvas.getHeight(); + float iw = bitmap.getWidth(); + float ih = bitmap.getHeight(); + float nsw = sw; + float nsh = sh; + + if (sw * ih > sh * iw) { + nsw = sh * iw / ih; + } else { + nsh = sw * ih / iw; + } + + mDestRect.left = (sw - nsw) / 2; + mDestRect.top = (sh - nsh) / 2; + mDestRect.right = sw - mDestRect.left; + mDestRect.bottom = sh - mDestRect.top; + + canvas.drawBitmap(bitmap, null, mDestRect, mPaint); + } +} diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageVignette.java b/src/com/android/gallery3d/filtershow/imageshow/ImageVignette.java new file mode 100644 index 000000000..518969ee1 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/imageshow/ImageVignette.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2013 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.filtershow.imageshow; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; + +import com.android.gallery3d.filtershow.editors.EditorVignette; +import com.android.gallery3d.filtershow.filters.FilterVignetteRepresentation; + +public class ImageVignette extends ImageShow { + private static final String LOGTAG = "ImageVignette"; + + private FilterVignetteRepresentation mVignetteRep; + private EditorVignette mEditorVignette; + + private int mActiveHandle = -1; + + EclipseControl mElipse; + + public ImageVignette(Context context) { + super(context); + mElipse = new EclipseControl(context); + } + + public ImageVignette(Context context, AttributeSet attrs) { + super(context, attrs); + mElipse = new EclipseControl(context); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + int mask = event.getActionMasked(); + if (mActiveHandle == -1) { + if (MotionEvent.ACTION_DOWN != mask) { + return super.onTouchEvent(event); + } + if (event.getPointerCount() == 1) { + mActiveHandle = mElipse.getCloseHandle(event.getX(), event.getY()); + } + if (mActiveHandle == -1) { + return super.onTouchEvent(event); + } + } else { + switch (mask) { + case MotionEvent.ACTION_UP: + mActiveHandle = -1; + break; + case MotionEvent.ACTION_DOWN: + break; + } + } + float x = event.getX(); + float y = event.getY(); + + mElipse.setScrToImageMatrix(getScreenToImageMatrix(true)); + + boolean didComputeEllipses = false; + switch (mask) { + case (MotionEvent.ACTION_DOWN): + mElipse.actionDown(x, y, mVignetteRep); + break; + case (MotionEvent.ACTION_UP): + case (MotionEvent.ACTION_MOVE): + mElipse.actionMove(mActiveHandle, x, y, mVignetteRep); + setRepresentation(mVignetteRep); + didComputeEllipses = true; + break; + } + if (!didComputeEllipses) { + computeEllipses(); + } + invalidate(); + return true; + } + + public void setRepresentation(FilterVignetteRepresentation vignetteRep) { + mVignetteRep = vignetteRep; + computeEllipses(); + } + + public void computeEllipses() { + if (mVignetteRep == null) { + return; + } + Matrix toImg = getScreenToImageMatrix(false); + Matrix toScr = new Matrix(); + toImg.invert(toScr); + + float[] c = new float[] { + mVignetteRep.getCenterX(), mVignetteRep.getCenterY() }; + if (Float.isNaN(c[0])) { + float cx = MasterImage.getImage().getOriginalBounds().width() / 2; + float cy = MasterImage.getImage().getOriginalBounds().height() / 2; + float rx = Math.min(cx, cy) * .8f; + float ry = rx; + mVignetteRep.setCenter(cx, cy); + mVignetteRep.setRadius(rx, ry); + + c[0] = cx; + c[1] = cy; + toScr.mapPoints(c); + if (getWidth() != 0) { + mElipse.setCenter(c[0], c[1]); + mElipse.setRadius(c[0] * 0.8f, c[1] * 0.8f); + } + } else { + + toScr.mapPoints(c); + + mElipse.setCenter(c[0], c[1]); + mElipse.setRadius(toScr.mapRadius(mVignetteRep.getRadiusX()), + toScr.mapRadius(mVignetteRep.getRadiusY())); + } + mEditorVignette.commitLocalRepresentation(); + } + + public void setEditor(EditorVignette editorVignette) { + mEditorVignette = editorVignette; + } + + @Override + public void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + computeEllipses(); + } + + @Override + public void onDraw(Canvas canvas) { + super.onDraw(canvas); + if (mVignetteRep == null) { + return; + } + Matrix toImg = getScreenToImageMatrix(false); + Matrix toScr = new Matrix(); + toImg.invert(toScr); + float[] c = new float[] { + mVignetteRep.getCenterX(), mVignetteRep.getCenterY() }; + toScr.mapPoints(c); + mElipse.setCenter(c[0], c[1]); + mElipse.setRadius(toScr.mapRadius(mVignetteRep.getRadiusX()), + toScr.mapRadius(mVignetteRep.getRadiusY())); + + mElipse.draw(canvas); + } + +} diff --git a/src/com/android/gallery3d/filtershow/imageshow/Line.java b/src/com/android/gallery3d/filtershow/imageshow/Line.java new file mode 100644 index 000000000..a767bd809 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/imageshow/Line.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2012 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.filtershow.imageshow; + +public interface Line { + void setPoint1(float x, float y); + void setPoint2(float x, float y); + float getPoint1X(); + float getPoint1Y(); + float getPoint2X(); + float getPoint2Y(); +} diff --git a/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java b/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java new file mode 100644 index 000000000..92e57bfc1 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java @@ -0,0 +1,581 @@ +/* + * Copyright (C) 2013 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.filtershow.imageshow; + +import android.graphics.Bitmap; +import android.graphics.Matrix; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.RectF; +import android.net.Uri; +import android.os.Handler; +import android.os.Message; + +import com.android.gallery3d.filtershow.FilterShowActivity; +import com.android.gallery3d.filtershow.cache.ImageLoader; +import com.android.gallery3d.filtershow.filters.FilterRepresentation; +import com.android.gallery3d.filtershow.filters.ImageFilter; +import com.android.gallery3d.filtershow.history.HistoryItem; +import com.android.gallery3d.filtershow.history.HistoryManager; +import com.android.gallery3d.filtershow.pipeline.Buffer; +import com.android.gallery3d.filtershow.pipeline.ImagePreset; +import com.android.gallery3d.filtershow.pipeline.RenderingRequest; +import com.android.gallery3d.filtershow.pipeline.RenderingRequestCaller; +import com.android.gallery3d.filtershow.pipeline.SharedBuffer; +import com.android.gallery3d.filtershow.pipeline.SharedPreset; +import com.android.gallery3d.filtershow.state.StateAdapter; + +import java.util.Vector; + +public class MasterImage implements RenderingRequestCaller { + + private static final String LOGTAG = "MasterImage"; + private boolean DEBUG = false; + private static final boolean DISABLEZOOM = false; + public static final int SMALL_BITMAP_DIM = 160; + public static final int MAX_BITMAP_DIM = 900; + private static MasterImage sMasterImage = null; + + private boolean mSupportsHighRes = false; + + private ImageFilter mCurrentFilter = null; + private ImagePreset mPreset = null; + private ImagePreset mLoadedPreset = null; + private ImagePreset mGeometryOnlyPreset = null; + private ImagePreset mFiltersOnlyPreset = null; + + private SharedBuffer mPreviewBuffer = new SharedBuffer(); + private SharedPreset mPreviewPreset = new SharedPreset(); + + private Bitmap mOriginalBitmapSmall = null; + private Bitmap mOriginalBitmapLarge = null; + private Bitmap mOriginalBitmapHighres = null; + private int mOrientation; + private Rect mOriginalBounds; + private final Vector<ImageShow> mLoadListeners = new Vector<ImageShow>(); + private Uri mUri = null; + private int mZoomOrientation = ImageLoader.ORI_NORMAL; + + private Bitmap mGeometryOnlyBitmap = null; + private Bitmap mFiltersOnlyBitmap = null; + private Bitmap mPartialBitmap = null; + private Bitmap mHighresBitmap = null; + + private HistoryManager mHistory = null; + private StateAdapter mState = null; + + private FilterShowActivity mActivity = null; + + private Vector<ImageShow> mObservers = new Vector<ImageShow>(); + private FilterRepresentation mCurrentFilterRepresentation; + + private float mScaleFactor = 1.0f; + private float mMaxScaleFactor = 3.0f; // TODO: base this on the current view / image + private Point mTranslation = new Point(); + private Point mOriginalTranslation = new Point(); + + private Point mImageShowSize = new Point(); + + private boolean mShowsOriginal; + + private MasterImage() { + } + + // TODO: remove singleton + public static void setMaster(MasterImage master) { + sMasterImage = master; + } + + public static MasterImage getImage() { + if (sMasterImage == null) { + sMasterImage = new MasterImage(); + } + return sMasterImage; + } + + public Bitmap getOriginalBitmapSmall() { + return mOriginalBitmapSmall; + } + + public Bitmap getOriginalBitmapLarge() { + return mOriginalBitmapLarge; + } + + public Bitmap getOriginalBitmapHighres() { + return mOriginalBitmapHighres; + } + + public void setOriginalBitmapHighres(Bitmap mOriginalBitmapHighres) { + this.mOriginalBitmapHighres = mOriginalBitmapHighres; + } + + public int getOrientation() { + return mOrientation; + } + + public Rect getOriginalBounds() { + return mOriginalBounds; + } + + public void setOriginalBounds(Rect r) { + mOriginalBounds = r; + } + + public Uri getUri() { + return mUri; + } + + public void setUri(Uri uri) { + mUri = uri; + } + + public int getZoomOrientation() { + return mZoomOrientation; + } + + public void addListener(ImageShow imageShow) { + if (!mLoadListeners.contains(imageShow)) { + mLoadListeners.add(imageShow); + } + } + + public void warnListeners() { + mActivity.runOnUiThread(mWarnListenersRunnable); + } + + private Runnable mWarnListenersRunnable = new Runnable() { + @Override + public void run() { + for (int i = 0; i < mLoadListeners.size(); i++) { + ImageShow imageShow = mLoadListeners.elementAt(i); + imageShow.imageLoaded(); + } + invalidatePreview(); + } + }; + + public boolean loadBitmap(Uri uri, int size) { + setUri(uri); + mOrientation = ImageLoader.getMetadataOrientation(mActivity, uri); + Rect originalBounds = new Rect(); + mOriginalBitmapLarge = ImageLoader.loadOrientedConstrainedBitmap(uri, mActivity, + Math.min(MAX_BITMAP_DIM, size), + mOrientation, originalBounds); + setOriginalBounds(originalBounds); + if (mOriginalBitmapLarge == null) { + return false; + } + int sw = SMALL_BITMAP_DIM; + int sh = (int) (sw * (float) mOriginalBitmapLarge.getHeight() / mOriginalBitmapLarge + .getWidth()); + mOriginalBitmapSmall = Bitmap.createScaledBitmap(mOriginalBitmapLarge, sw, sh, true); + mZoomOrientation = mOrientation; + warnListeners(); + return true; + } + + public void setSupportsHighRes(boolean value) { + mSupportsHighRes = value; + } + + public void addObserver(ImageShow observer) { + if (mObservers.contains(observer)) { + return; + } + mObservers.add(observer); + } + + public void setActivity(FilterShowActivity activity) { + mActivity = activity; + } + + public FilterShowActivity getActivity() { + return mActivity; + } + + public synchronized ImagePreset getPreset() { + return mPreset; + } + + public synchronized ImagePreset getGeometryPreset() { + return mGeometryOnlyPreset; + } + + public synchronized ImagePreset getFiltersOnlyPreset() { + return mFiltersOnlyPreset; + } + + public synchronized void setPreset(ImagePreset preset, + FilterRepresentation change, + boolean addToHistory) { + if (DEBUG) { + preset.showFilters(); + } + mPreset = preset; + mPreset.fillImageStateAdapter(mState); + if (addToHistory) { + HistoryItem historyItem = new HistoryItem(mPreset, change); + mHistory.addHistoryItem(historyItem); + } + updatePresets(true); + mActivity.updateCategories(); + } + + public void onHistoryItemClick(int position) { + HistoryItem historyItem = mHistory.getItem(position); + // We need a copy from the history + ImagePreset newPreset = new ImagePreset(historyItem.getImagePreset()); + // don't need to add it to the history + setPreset(newPreset, historyItem.getFilterRepresentation(), false); + mHistory.setCurrentPreset(position); + } + + public HistoryManager getHistory() { + return mHistory; + } + + public StateAdapter getState() { + return mState; + } + + public void setHistoryManager(HistoryManager adapter) { + mHistory = adapter; + } + + public void setStateAdapter(StateAdapter adapter) { + mState = adapter; + } + + public void setCurrentFilter(ImageFilter filter) { + mCurrentFilter = filter; + } + + public ImageFilter getCurrentFilter() { + return mCurrentFilter; + } + + public synchronized boolean hasModifications() { + // TODO: We need to have a better same effects check to see if two + // presets are functionally the same. Right now, we are relying on a + // stricter check as equals(). + ImagePreset loadedPreset = getLoadedPreset(); + if (mPreset == null) { + if (loadedPreset == null) { + return false; + } else { + return loadedPreset.hasModifications(); + } + } else { + if (loadedPreset == null) { + return mPreset.hasModifications(); + } else { + return !mPreset.equals(loadedPreset); + } + } + } + + public SharedBuffer getPreviewBuffer() { + return mPreviewBuffer; + } + + public SharedPreset getPreviewPreset() { + return mPreviewPreset; + } + + public Bitmap getFilteredImage() { + mPreviewBuffer.swapConsumerIfNeeded(); // get latest bitmap + Buffer consumer = mPreviewBuffer.getConsumer(); + if (consumer != null) { + return consumer.getBitmap(); + } + return null; + } + + public Bitmap getFiltersOnlyImage() { + return mFiltersOnlyBitmap; + } + + public Bitmap getGeometryOnlyImage() { + return mGeometryOnlyBitmap; + } + + public Bitmap getPartialImage() { + return mPartialBitmap; + } + + public Bitmap getHighresImage() { + return mHighresBitmap; + } + + public void notifyObservers() { + for (ImageShow observer : mObservers) { + observer.invalidate(); + } + } + + public void updatePresets(boolean force) { + if (force || mGeometryOnlyPreset == null) { + ImagePreset newPreset = new ImagePreset(mPreset); + newPreset.setDoApplyFilters(false); + newPreset.setDoApplyGeometry(true); + if (force || mGeometryOnlyPreset == null + || !newPreset.same(mGeometryOnlyPreset)) { + mGeometryOnlyPreset = newPreset; + RenderingRequest.post(mActivity, getOriginalBitmapLarge(), + mGeometryOnlyPreset, RenderingRequest.GEOMETRY_RENDERING, this); + } + } + if (force || mFiltersOnlyPreset == null) { + ImagePreset newPreset = new ImagePreset(mPreset); + newPreset.setDoApplyFilters(true); + newPreset.setDoApplyGeometry(false); + if (force || mFiltersOnlyPreset == null + || !newPreset.same(mFiltersOnlyPreset)) { + mFiltersOnlyPreset = newPreset; + RenderingRequest.post(mActivity, MasterImage.getImage().getOriginalBitmapLarge(), + mFiltersOnlyPreset, RenderingRequest.FILTERS_RENDERING, this); + } + } + invalidatePreview(); + } + + public FilterRepresentation getCurrentFilterRepresentation() { + return mCurrentFilterRepresentation; + } + + public void setCurrentFilterRepresentation(FilterRepresentation currentFilterRepresentation) { + mCurrentFilterRepresentation = currentFilterRepresentation; + } + + public void invalidateFiltersOnly() { + mFiltersOnlyPreset = null; + updatePresets(false); + } + + public void invalidatePartialPreview() { + if (mPartialBitmap != null) { + mPartialBitmap = null; + notifyObservers(); + } + } + + public void invalidateHighresPreview() { + if (mHighresBitmap != null) { + mHighresBitmap = null; + notifyObservers(); + } + } + + public void invalidatePreview() { + mPreviewPreset.enqueuePreset(mPreset); + mPreviewBuffer.invalidate(); + invalidatePartialPreview(); + invalidateHighresPreview(); + needsUpdatePartialPreview(); + needsUpdateHighResPreview(); + mActivity.getProcessingService().updatePreviewBuffer(); + } + + public void setImageShowSize(int w, int h) { + if (mImageShowSize.x != w || mImageShowSize.y != h) { + mImageShowSize.set(w, h); + needsUpdatePartialPreview(); + needsUpdateHighResPreview(); + } + } + + private Matrix getImageToScreenMatrix(boolean reflectRotation) { + if (getOriginalBounds() == null || mImageShowSize.x == 0 || mImageShowSize.y == 0) { + return new Matrix(); + } + Matrix m = GeometryMathUtils.getImageToScreenMatrix(mPreset.getGeometryFilters(), + reflectRotation, getOriginalBounds(), mImageShowSize.x, mImageShowSize.y); + if (m == null) { + m = new Matrix(); + m.reset(); + return m; + } + Point translate = getTranslation(); + float scaleFactor = getScaleFactor(); + m.postTranslate(translate.x, translate.y); + m.postScale(scaleFactor, scaleFactor, mImageShowSize.x / 2.0f, mImageShowSize.y / 2.0f); + return m; + } + + private Matrix getScreenToImageMatrix(boolean reflectRotation) { + Matrix m = getImageToScreenMatrix(reflectRotation); + Matrix invert = new Matrix(); + m.invert(invert); + return invert; + } + + public void needsUpdateHighResPreview() { + if (!mSupportsHighRes) { + return; + } + if (mActivity.getProcessingService() == null) { + return; + } + mActivity.getProcessingService().postHighresRenderingRequest(mPreset, + getScaleFactor(), this); + invalidateHighresPreview(); + } + + public void needsUpdatePartialPreview() { + if (mPreset == null) { + return; + } + if (!mPreset.canDoPartialRendering()) { + invalidatePartialPreview(); + return; + } + Matrix m = getScreenToImageMatrix(true); + RectF r = new RectF(0, 0, mImageShowSize.x, mImageShowSize.y); + RectF dest = new RectF(); + m.mapRect(dest, r); + Rect bounds = new Rect(); + dest.roundOut(bounds); + RenderingRequest.post(mActivity, null, mPreset, RenderingRequest.PARTIAL_RENDERING, + this, bounds, new Rect(0, 0, mImageShowSize.x, mImageShowSize.y)); + invalidatePartialPreview(); + } + + @Override + public void available(RenderingRequest request) { + if (request.getBitmap() == null) { + return; + } + + boolean needsCheckModification = false; + if (request.getType() == RenderingRequest.GEOMETRY_RENDERING) { + mGeometryOnlyBitmap = request.getBitmap(); + needsCheckModification = true; + } + if (request.getType() == RenderingRequest.FILTERS_RENDERING) { + mFiltersOnlyBitmap = request.getBitmap(); + notifyObservers(); + needsCheckModification = true; + } + if (request.getType() == RenderingRequest.PARTIAL_RENDERING + && request.getScaleFactor() == getScaleFactor()) { + mPartialBitmap = request.getBitmap(); + notifyObservers(); + needsCheckModification = true; + } + if (request.getType() == RenderingRequest.HIGHRES_RENDERING) { + mHighresBitmap = request.getBitmap(); + notifyObservers(); + needsCheckModification = true; + } + if (needsCheckModification) { + mActivity.enableSave(hasModifications()); + } + } + + public static void reset() { + sMasterImage = null; + } + + public float getScaleFactor() { + return mScaleFactor; + } + + public void setScaleFactor(float scaleFactor) { + if (DISABLEZOOM) { + return; + } + if (scaleFactor == mScaleFactor) { + return; + } + mScaleFactor = scaleFactor; + invalidatePartialPreview(); + } + + public Point getTranslation() { + return mTranslation; + } + + public void setTranslation(Point translation) { + if (DISABLEZOOM) { + mTranslation.x = 0; + mTranslation.y = 0; + return; + } + mTranslation.x = translation.x; + mTranslation.y = translation.y; + needsUpdatePartialPreview(); + } + + public Point getOriginalTranslation() { + return mOriginalTranslation; + } + + public void setOriginalTranslation(Point originalTranslation) { + if (DISABLEZOOM) { + return; + } + mOriginalTranslation.x = originalTranslation.x; + mOriginalTranslation.y = originalTranslation.y; + } + + public void resetTranslation() { + mTranslation.x = 0; + mTranslation.y = 0; + needsUpdatePartialPreview(); + } + + public Bitmap getThumbnailBitmap() { + return getOriginalBitmapSmall(); + } + + public Bitmap getLargeThumbnailBitmap() { + return getOriginalBitmapLarge(); + } + + public float getMaxScaleFactor() { + if (DISABLEZOOM) { + return 1; + } + return mMaxScaleFactor; + } + + public void setMaxScaleFactor(float maxScaleFactor) { + mMaxScaleFactor = maxScaleFactor; + } + + public boolean supportsHighRes() { + return mSupportsHighRes; + } + + public void setShowsOriginal(boolean value) { + mShowsOriginal = value; + notifyObservers(); + } + + public boolean showsOriginal() { + return mShowsOriginal; + } + + public void setLoadedPreset(ImagePreset preset) { + mLoadedPreset = preset; + } + + public ImagePreset getLoadedPreset() { + return mLoadedPreset; + } + +} diff --git a/src/com/android/gallery3d/filtershow/imageshow/Oval.java b/src/com/android/gallery3d/filtershow/imageshow/Oval.java new file mode 100644 index 000000000..28f278f1c --- /dev/null +++ b/src/com/android/gallery3d/filtershow/imageshow/Oval.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2012 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.filtershow.imageshow; + +public interface Oval { + void setCenter(float x, float y); + void setRadius(float w, float h); + float getCenterX(); + float getCenterY(); + float getRadiusX(); + float getRadiusY(); + void setRadiusY(float y); + void setRadiusX(float x); + +} diff --git a/src/com/android/gallery3d/filtershow/imageshow/Spline.java b/src/com/android/gallery3d/filtershow/imageshow/Spline.java new file mode 100644 index 000000000..3c27a4d0f --- /dev/null +++ b/src/com/android/gallery3d/filtershow/imageshow/Spline.java @@ -0,0 +1,450 @@ +/* + * Copyright (C) 2012 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.filtershow.imageshow; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.drawable.Drawable; +import android.util.Log; + +import java.util.Collections; +import java.util.Vector; + +public class Spline { + private final Vector<ControlPoint> mPoints; + private static Drawable mCurveHandle; + private static int mCurveHandleSize; + private static int mCurveWidth; + + public static final int RGB = 0; + public static final int RED = 1; + public static final int GREEN = 2; + public static final int BLUE = 3; + private static final String LOGTAG = "Spline"; + + private final Paint gPaint = new Paint(); + private ControlPoint mCurrentControlPoint = null; + + public Spline() { + mPoints = new Vector<ControlPoint>(); + } + + public Spline(Spline spline) { + mPoints = new Vector<ControlPoint>(); + for (int i = 0; i < spline.mPoints.size(); i++) { + ControlPoint p = spline.mPoints.elementAt(i); + ControlPoint newPoint = new ControlPoint(p); + mPoints.add(newPoint); + if (spline.mCurrentControlPoint == p) { + mCurrentControlPoint = newPoint; + } + } + Collections.sort(mPoints); + } + + public static void setCurveHandle(Drawable drawable, int size) { + mCurveHandle = drawable; + mCurveHandleSize = size; + } + + public static void setCurveWidth(int width) { + mCurveWidth = width; + } + + public static int curveHandleSize() { + return mCurveHandleSize; + } + + public static int colorForCurve(int curveIndex) { + switch (curveIndex) { + case Spline.RED: + return Color.RED; + case GREEN: + return Color.GREEN; + case BLUE: + return Color.BLUE; + } + return Color.WHITE; + } + + public boolean sameValues(Spline other) { + if (this == other) { + return true; + } + if (other == null) { + return false; + } + + if (getNbPoints() != other.getNbPoints()) { + return false; + } + + for (int i = 0; i < getNbPoints(); i++) { + ControlPoint p = mPoints.elementAt(i); + ControlPoint otherPoint = other.mPoints.elementAt(i); + if (!p.sameValues(otherPoint)) { + return false; + } + } + return true; + } + + private void didMovePoint(ControlPoint point) { + mCurrentControlPoint = point; + } + + public void movePoint(int pick, float x, float y) { + if (pick < 0 || pick > mPoints.size() - 1) { + return; + } + ControlPoint point = mPoints.elementAt(pick); + point.x = x; + point.y = y; + didMovePoint(point); + } + + public boolean isOriginal() { + if (this.getNbPoints() != 2) { + return false; + } + if (mPoints.elementAt(0).x != 0 || mPoints.elementAt(0).y != 1) { + return false; + } + if (mPoints.elementAt(1).x != 1 || mPoints.elementAt(1).y != 0) { + return false; + } + return true; + } + + public void reset() { + mPoints.clear(); + addPoint(0.0f, 1.0f); + addPoint(1.0f, 0.0f); + } + + private void drawHandles(Canvas canvas, Drawable indicator, float centerX, float centerY) { + int left = (int) centerX - mCurveHandleSize / 2; + int top = (int) centerY - mCurveHandleSize / 2; + indicator.setBounds(left, top, left + mCurveHandleSize, top + mCurveHandleSize); + indicator.draw(canvas); + } + + public float[] getAppliedCurve() { + float[] curve = new float[256]; + ControlPoint[] points = new ControlPoint[mPoints.size()]; + for (int i = 0; i < mPoints.size(); i++) { + ControlPoint p = mPoints.get(i); + points[i] = new ControlPoint(p.x, p.y); + } + double[] derivatives = solveSystem(points); + int start = 0; + int end = 256; + if (points[0].x != 0) { + start = (int) (points[0].x * 256); + } + if (points[points.length - 1].x != 1) { + end = (int) (points[points.length - 1].x * 256); + } + for (int i = 0; i < start; i++) { + curve[i] = 1.0f - points[0].y; + } + for (int i = end; i < 256; i++) { + curve[i] = 1.0f - points[points.length - 1].y; + } + for (int i = start; i < end; i++) { + ControlPoint cur = null; + ControlPoint next = null; + double x = i / 256.0; + int pivot = 0; + for (int j = 0; j < points.length - 1; j++) { + if (x >= points[j].x && x <= points[j + 1].x) { + pivot = j; + } + } + cur = points[pivot]; + next = points[pivot + 1]; + if (x <= next.x) { + double x1 = cur.x; + double x2 = next.x; + double y1 = cur.y; + double y2 = next.y; + + // Use the second derivatives to apply the cubic spline + // equation: + double delta = (x2 - x1); + double delta2 = delta * delta; + double b = (x - x1) / delta; + double a = 1 - b; + double ta = a * y1; + double tb = b * y2; + double tc = (a * a * a - a) * derivatives[pivot]; + double td = (b * b * b - b) * derivatives[pivot + 1]; + double y = ta + tb + (delta2 / 6) * (tc + td); + if (y > 1.0f) { + y = 1.0f; + } + if (y < 0) { + y = 0; + } + curve[i] = (float) (1.0f - y); + } else { + curve[i] = 1.0f - next.y; + } + } + return curve; + } + + private void drawGrid(Canvas canvas, float w, float h) { + // Grid + gPaint.setARGB(128, 150, 150, 150); + gPaint.setStrokeWidth(1); + + float stepH = h / 9; + float stepW = w / 9; + + // central diagonal + gPaint.setARGB(255, 100, 100, 100); + gPaint.setStrokeWidth(2); + canvas.drawLine(0, h, w, 0, gPaint); + + gPaint.setARGB(128, 200, 200, 200); + gPaint.setStrokeWidth(4); + stepH = h / 3; + stepW = w / 3; + for (int j = 1; j < 3; j++) { + canvas.drawLine(0, j * stepH, w, j * stepH, gPaint); + canvas.drawLine(j * stepW, 0, j * stepW, h, gPaint); + } + canvas.drawLine(0, 0, 0, h, gPaint); + canvas.drawLine(w, 0, w, h, gPaint); + canvas.drawLine(0, 0, w, 0, gPaint); + canvas.drawLine(0, h, w, h, gPaint); + } + + public void draw(Canvas canvas, int color, int canvasWidth, int canvasHeight, + boolean showHandles, boolean moving) { + float w = canvasWidth - mCurveHandleSize; + float h = canvasHeight - mCurveHandleSize; + float dx = mCurveHandleSize / 2; + float dy = mCurveHandleSize / 2; + + // The cubic spline equation is (from numerical recipes in C): + // y = a(y_i) + b(y_i+1) + c(y"_i) + d(y"_i+1) + // + // with c(y"_i) and d(y"_i+1): + // c(y"_i) = 1/6 (a^3 - a) delta^2 (y"_i) + // d(y"_i_+1) = 1/6 (b^3 - b) delta^2 (y"_i+1) + // + // and delta: + // delta = x_i+1 - x_i + // + // To find the second derivatives y", we can rearrange the equation as: + // A(y"_i-1) + B(y"_i) + C(y"_i+1) = D + // + // With the coefficients A, B, C, D: + // A = 1/6 (x_i - x_i-1) + // B = 1/3 (x_i+1 - x_i-1) + // C = 1/6 (x_i+1 - x_i) + // D = (y_i+1 - y_i)/(x_i+1 - x_i) - (y_i - y_i-1)/(x_i - x_i-1) + // + // We can now easily solve the equation to find the second derivatives: + ControlPoint[] points = new ControlPoint[mPoints.size()]; + for (int i = 0; i < mPoints.size(); i++) { + ControlPoint p = mPoints.get(i); + points[i] = new ControlPoint(p.x * w, p.y * h); + } + double[] derivatives = solveSystem(points); + + Path path = new Path(); + path.moveTo(0, points[0].y); + for (int i = 0; i < points.length - 1; i++) { + double x1 = points[i].x; + double x2 = points[i + 1].x; + double y1 = points[i].y; + double y2 = points[i + 1].y; + + for (double x = x1; x < x2; x += 20) { + // Use the second derivatives to apply the cubic spline + // equation: + double delta = (x2 - x1); + double delta2 = delta * delta; + double b = (x - x1) / delta; + double a = 1 - b; + double ta = a * y1; + double tb = b * y2; + double tc = (a * a * a - a) * derivatives[i]; + double td = (b * b * b - b) * derivatives[i + 1]; + double y = ta + tb + (delta2 / 6) * (tc + td); + if (y > h) { + y = h; + } + if (y < 0) { + y = 0; + } + path.lineTo((float) x, (float) y); + } + } + canvas.save(); + canvas.translate(dx, dy); + drawGrid(canvas, w, h); + ControlPoint lastPoint = points[points.length - 1]; + path.lineTo(lastPoint.x, lastPoint.y); + path.lineTo(w, lastPoint.y); + Paint paint = new Paint(); + paint.setAntiAlias(true); + paint.setFilterBitmap(true); + paint.setDither(true); + paint.setStyle(Paint.Style.STROKE); + int curveWidth = mCurveWidth; + if (showHandles) { + curveWidth *= 1.5; + } + paint.setStrokeWidth(curveWidth + 2); + paint.setColor(Color.BLACK); + canvas.drawPath(path, paint); + + if (moving && mCurrentControlPoint != null) { + float px = mCurrentControlPoint.x * w; + float py = mCurrentControlPoint.y * h; + paint.setStrokeWidth(3); + paint.setColor(Color.BLACK); + canvas.drawLine(px, py, px, h, paint); + canvas.drawLine(0, py, px, py, paint); + paint.setStrokeWidth(1); + paint.setColor(color); + canvas.drawLine(px, py, px, h, paint); + canvas.drawLine(0, py, px, py, paint); + } + + paint.setStrokeWidth(curveWidth); + paint.setColor(color); + canvas.drawPath(path, paint); + if (showHandles) { + for (int i = 0; i < points.length; i++) { + float x = points[i].x; + float y = points[i].y; + drawHandles(canvas, mCurveHandle, x, y); + } + } + canvas.restore(); + } + + double[] solveSystem(ControlPoint[] points) { + int n = points.length; + double[][] system = new double[n][3]; + double[] result = new double[n]; // d + double[] solution = new double[n]; // returned coefficients + system[0][1] = 1; + system[n - 1][1] = 1; + double d6 = 1.0 / 6.0; + double d3 = 1.0 / 3.0; + + // let's create a tridiagonal matrix representing the + // system, and apply the TDMA algorithm to solve it + // (see http://en.wikipedia.org/wiki/Tridiagonal_matrix_algorithm) + for (int i = 1; i < n - 1; i++) { + double deltaPrevX = points[i].x - points[i - 1].x; + double deltaX = points[i + 1].x - points[i - 1].x; + double deltaNextX = points[i + 1].x - points[i].x; + double deltaNextY = points[i + 1].y - points[i].y; + double deltaPrevY = points[i].y - points[i - 1].y; + system[i][0] = d6 * deltaPrevX; // a_i + system[i][1] = d3 * deltaX; // b_i + system[i][2] = d6 * deltaNextX; // c_i + result[i] = (deltaNextY / deltaNextX) - (deltaPrevY / deltaPrevX); // d_i + } + + // Forward sweep + for (int i = 1; i < n; i++) { + // m = a_i/b_i-1 + double m = system[i][0] / system[i - 1][1]; + // b_i = b_i - m(c_i-1) + system[i][1] = system[i][1] - m * system[i - 1][2]; + // d_i = d_i - m(d_i-1) + result[i] = result[i] - m * result[i - 1]; + } + + // Back substitution + solution[n - 1] = result[n - 1] / system[n - 1][1]; + for (int i = n - 2; i >= 0; --i) { + solution[i] = (result[i] - system[i][2] * solution[i + 1]) / system[i][1]; + } + return solution; + } + + public int addPoint(float x, float y) { + return addPoint(new ControlPoint(x, y)); + } + + public int addPoint(ControlPoint v) { + mPoints.add(v); + Collections.sort(mPoints); + return mPoints.indexOf(v); + } + + public void deletePoint(int n) { + mPoints.remove(n); + if (mPoints.size() < 2) { + reset(); + } + Collections.sort(mPoints); + } + + public int getNbPoints() { + return mPoints.size(); + } + + public ControlPoint getPoint(int n) { + return mPoints.elementAt(n); + } + + public boolean isPointContained(float x, int n) { + for (int i = 0; i < n; i++) { + ControlPoint point = mPoints.elementAt(i); + if (point.x > x) { + return false; + } + } + for (int i = n + 1; i < mPoints.size(); i++) { + ControlPoint point = mPoints.elementAt(i); + if (point.x < x) { + return false; + } + } + return true; + } + + public Spline copy() { + Spline spline = new Spline(); + for (int i = 0; i < mPoints.size(); i++) { + ControlPoint point = mPoints.elementAt(i); + spline.addPoint(point.copy()); + } + return spline; + } + + public void show() { + Log.v(LOGTAG, "show curve " + this); + for (int i = 0; i < mPoints.size(); i++) { + ControlPoint point = mPoints.elementAt(i); + Log.v(LOGTAG, "point " + i + " is (" + point.x + ", " + point.y + ")"); + } + } + +} diff --git a/src/com/android/gallery3d/filtershow/pipeline/Buffer.java b/src/com/android/gallery3d/filtershow/pipeline/Buffer.java new file mode 100644 index 000000000..744451229 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/pipeline/Buffer.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2013 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.filtershow.pipeline; + +import android.graphics.Bitmap; +import android.support.v8.renderscript.Allocation; +import android.support.v8.renderscript.RenderScript; + +public class Buffer { + private static final String LOGTAG = "Buffer"; + private Bitmap mBitmap; + private Allocation mAllocation; + private boolean mUseAllocation = false; + private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888; + private ImagePreset mPreset; + + public Buffer(Bitmap bitmap) { + RenderScript rs = CachingPipeline.getRenderScriptContext(); + if (bitmap != null) { + mBitmap = bitmap.copy(BITMAP_CONFIG, true); + } + if (mUseAllocation) { + // TODO: recreate the allocation when the RS context changes + mAllocation = Allocation.createFromBitmap(rs, mBitmap, + Allocation.MipmapControl.MIPMAP_NONE, + Allocation.USAGE_SHARED | Allocation.USAGE_SCRIPT); + } + } + + public void setBitmap(Bitmap bitmap) { + mBitmap = bitmap.copy(BITMAP_CONFIG, true); + } + + public Bitmap getBitmap() { + return mBitmap; + } + + public Allocation getAllocation() { + return mAllocation; + } + + public void sync() { + if (mUseAllocation) { + mAllocation.copyTo(mBitmap); + } + } + + public ImagePreset getPreset() { + return mPreset; + } + + public void setPreset(ImagePreset preset) { + if ((mPreset == null) || (!mPreset.same(preset))) { + mPreset = new ImagePreset(preset); + } else { + mPreset.updateWith(preset); + } + } +} + diff --git a/src/com/android/gallery3d/filtershow/pipeline/CacheProcessing.java b/src/com/android/gallery3d/filtershow/pipeline/CacheProcessing.java new file mode 100644 index 000000000..e0269e9bb --- /dev/null +++ b/src/com/android/gallery3d/filtershow/pipeline/CacheProcessing.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2013 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.filtershow.pipeline; + +import android.graphics.Bitmap; +import android.util.Log; +import com.android.gallery3d.filtershow.filters.FilterRepresentation; + +import java.util.Vector; + +public class CacheProcessing { + private static final String LOGTAG = "CacheProcessing"; + private static final boolean DEBUG = false; + private Vector<CacheStep> mSteps = new Vector<CacheStep>(); + + static class CacheStep { + FilterRepresentation representation; + Bitmap cache; + } + + public Bitmap process(Bitmap originalBitmap, + Vector<FilterRepresentation> filters, + FilterEnvironment environment) { + + if (filters.size() == 0) { + return originalBitmap; + } + + // New set of filters, let's clear the cache and rebuild it. + if (filters.size() != mSteps.size()) { + mSteps.clear(); + for (int i = 0; i < filters.size(); i++) { + FilterRepresentation representation = filters.elementAt(i); + CacheStep step = new CacheStep(); + step.representation = representation.copy(); + mSteps.add(step); + } + } + + if (DEBUG) { + displayFilters(filters); + } + + // First, let's find how similar we are in our cache + // compared to the current list of filters + int similarUpToIndex = -1; + for (int i = 0; i < filters.size(); i++) { + FilterRepresentation representation = filters.elementAt(i); + CacheStep step = mSteps.elementAt(i); + boolean similar = step.representation.equals(representation); + if (similar) { + similarUpToIndex = i; + } else { + break; + } + } + if (DEBUG) { + Log.v(LOGTAG, "similar up to index " + similarUpToIndex); + } + + // Now, let's get the earliest cached result in our pipeline + Bitmap cacheBitmap = null; + int findBaseImageIndex = similarUpToIndex; + if (findBaseImageIndex > -1) { + while (findBaseImageIndex > 0 + && mSteps.elementAt(findBaseImageIndex).cache == null) { + findBaseImageIndex--; + } + cacheBitmap = mSteps.elementAt(findBaseImageIndex).cache; + } + boolean emptyStack = false; + if (cacheBitmap == null) { + emptyStack = true; + // Damn, it's an empty stack, we have to start from scratch + // TODO: use a bitmap cache + RS allocation instead of Bitmap.copy() + cacheBitmap = originalBitmap.copy(Bitmap.Config.ARGB_8888, true); + if (findBaseImageIndex > -1) { + FilterRepresentation representation = filters.elementAt(findBaseImageIndex); + if (representation.getFilterType() != FilterRepresentation.TYPE_GEOMETRY) { + cacheBitmap = environment.applyRepresentation(representation, cacheBitmap); + } + mSteps.elementAt(findBaseImageIndex).representation = representation.copy(); + mSteps.elementAt(findBaseImageIndex).cache = cacheBitmap; + } + if (DEBUG) { + Log.v(LOGTAG, "empty stack"); + } + } + + // Ok, so sadly the earliest cached result is before the index we want. + // We have to rebuild a new result for this position, and then cache it. + if (findBaseImageIndex != similarUpToIndex) { + if (DEBUG) { + Log.v(LOGTAG, "rebuild cacheBitmap from " + findBaseImageIndex + + " to " + similarUpToIndex); + } + // rebuild the cache image for this step + if (!emptyStack) { + cacheBitmap = cacheBitmap.copy(Bitmap.Config.ARGB_8888, true); + } else { + // if it was an empty stack, we already applied it + findBaseImageIndex ++; + } + for (int i = findBaseImageIndex; i <= similarUpToIndex; i++) { + FilterRepresentation representation = filters.elementAt(i); + if (representation.getFilterType() != FilterRepresentation.TYPE_GEOMETRY) { + cacheBitmap = environment.applyRepresentation(representation, cacheBitmap); + } + if (DEBUG) { + Log.v(LOGTAG, " - " + i + " => apply " + representation.getName()); + } + } + // Let's cache it! + mSteps.elementAt(similarUpToIndex).cache = cacheBitmap; + } + + if (DEBUG) { + Log.v(LOGTAG, "process pipeline from " + similarUpToIndex + + " to " + (filters.size() - 1)); + } + + // Now we are good to go, let's use the cacheBitmap as a starting point + for (int i = similarUpToIndex + 1; i < filters.size(); i++) { + FilterRepresentation representation = filters.elementAt(i); + CacheStep currentStep = mSteps.elementAt(i); + cacheBitmap = cacheBitmap.copy(Bitmap.Config.ARGB_8888, true); + if (representation.getFilterType() != FilterRepresentation.TYPE_GEOMETRY) { + cacheBitmap = environment.applyRepresentation(representation, cacheBitmap); + } + currentStep.representation = representation.copy(); + currentStep.cache = cacheBitmap; + if (DEBUG) { + Log.v(LOGTAG, " - " + i + " => apply " + representation.getName()); + } + } + + if (DEBUG) { + Log.v(LOGTAG, "now let's cleanup the cache..."); + displayNbBitmapsInCache(); + } + + // Let's see if we can cleanup the cache for unused bitmaps + for (int i = 0; i < similarUpToIndex; i++) { + CacheStep currentStep = mSteps.elementAt(i); + currentStep.cache = null; + } + + if (DEBUG) { + Log.v(LOGTAG, "cleanup done..."); + displayNbBitmapsInCache(); + } + return cacheBitmap; + } + + private void displayFilters(Vector<FilterRepresentation> filters) { + Log.v(LOGTAG, "------>>>"); + for (int i = 0; i < filters.size(); i++) { + FilterRepresentation representation = filters.elementAt(i); + CacheStep step = mSteps.elementAt(i); + boolean similar = step.representation.equals(representation); + Log.v(LOGTAG, "[" + i + "] - " + representation.getName() + + " similar rep ? " + (similar ? "YES" : "NO") + + " -- bitmap: " + step.cache); + } + Log.v(LOGTAG, "<<<------"); + } + + private void displayNbBitmapsInCache() { + int nbBitmapsCached = 0; + for (int i = 0; i < mSteps.size(); i++) { + CacheStep step = mSteps.elementAt(i); + if (step.cache != null) { + nbBitmapsCached++; + } + } + Log.v(LOGTAG, "nb bitmaps in cache: " + nbBitmapsCached + " / " + mSteps.size()); + } + +} diff --git a/src/com/android/gallery3d/filtershow/pipeline/CachingPipeline.java b/src/com/android/gallery3d/filtershow/pipeline/CachingPipeline.java new file mode 100644 index 000000000..fc0d6ce49 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/pipeline/CachingPipeline.java @@ -0,0 +1,469 @@ +/* + * Copyright (C) 2013 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.filtershow.pipeline; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.support.v8.renderscript.Allocation; +import android.support.v8.renderscript.RenderScript; +import android.util.Log; + +import com.android.gallery3d.filtershow.cache.ImageLoader; +import com.android.gallery3d.filtershow.filters.FilterRepresentation; +import com.android.gallery3d.filtershow.filters.FiltersManager; +import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils; +import com.android.gallery3d.filtershow.imageshow.MasterImage; + +import java.util.Vector; + +public class CachingPipeline implements PipelineInterface { + private static final String LOGTAG = "CachingPipeline"; + private boolean DEBUG = false; + + private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888; + + private static volatile RenderScript sRS = null; + + private FiltersManager mFiltersManager = null; + private volatile Bitmap mOriginalBitmap = null; + private volatile Bitmap mResizedOriginalBitmap = null; + + private FilterEnvironment mEnvironment = new FilterEnvironment(); + private CacheProcessing mCachedProcessing = new CacheProcessing(); + + + private volatile Allocation mOriginalAllocation = null; + private volatile Allocation mFiltersOnlyOriginalAllocation = null; + + protected volatile Allocation mInPixelsAllocation; + protected volatile Allocation mOutPixelsAllocation; + private volatile int mWidth = 0; + private volatile int mHeight = 0; + + private volatile float mPreviewScaleFactor = 1.0f; + private volatile float mHighResPreviewScaleFactor = 1.0f; + private volatile String mName = ""; + + public CachingPipeline(FiltersManager filtersManager, String name) { + mFiltersManager = filtersManager; + mName = name; + } + + public static synchronized RenderScript getRenderScriptContext() { + return sRS; + } + + public static synchronized void createRenderscriptContext(Context context) { + if (sRS != null) { + Log.w(LOGTAG, "A prior RS context exists when calling setRenderScriptContext"); + destroyRenderScriptContext(); + } + sRS = RenderScript.create(context); + } + + public static synchronized void destroyRenderScriptContext() { + if (sRS != null) { + sRS.destroy(); + } + sRS = null; + } + + public void stop() { + mEnvironment.setStop(true); + } + + public synchronized void reset() { + synchronized (CachingPipeline.class) { + if (getRenderScriptContext() == null) { + return; + } + mOriginalBitmap = null; // just a reference to the bitmap in ImageLoader + if (mResizedOriginalBitmap != null) { + mResizedOriginalBitmap.recycle(); + mResizedOriginalBitmap = null; + } + if (mOriginalAllocation != null) { + mOriginalAllocation.destroy(); + mOriginalAllocation = null; + } + if (mFiltersOnlyOriginalAllocation != null) { + mFiltersOnlyOriginalAllocation.destroy(); + mFiltersOnlyOriginalAllocation = null; + } + mPreviewScaleFactor = 1.0f; + mHighResPreviewScaleFactor = 1.0f; + + destroyPixelAllocations(); + } + } + + public Resources getResources() { + return sRS.getApplicationContext().getResources(); + } + + private synchronized void destroyPixelAllocations() { + if (DEBUG) { + Log.v(LOGTAG, "destroyPixelAllocations in " + getName()); + } + if (mInPixelsAllocation != null) { + mInPixelsAllocation.destroy(); + mInPixelsAllocation = null; + } + if (mOutPixelsAllocation != null) { + mOutPixelsAllocation.destroy(); + mOutPixelsAllocation = null; + } + mWidth = 0; + mHeight = 0; + } + + private String getType(RenderingRequest request) { + if (request.getType() == RenderingRequest.ICON_RENDERING) { + return "ICON_RENDERING"; + } + if (request.getType() == RenderingRequest.FILTERS_RENDERING) { + return "FILTERS_RENDERING"; + } + if (request.getType() == RenderingRequest.FULL_RENDERING) { + return "FULL_RENDERING"; + } + if (request.getType() == RenderingRequest.GEOMETRY_RENDERING) { + return "GEOMETRY_RENDERING"; + } + if (request.getType() == RenderingRequest.PARTIAL_RENDERING) { + return "PARTIAL_RENDERING"; + } + if (request.getType() == RenderingRequest.HIGHRES_RENDERING) { + return "HIGHRES_RENDERING"; + } + return "UNKNOWN TYPE!"; + } + + private void setupEnvironment(ImagePreset preset, boolean highResPreview) { + mEnvironment.setPipeline(this); + mEnvironment.setFiltersManager(mFiltersManager); + if (highResPreview) { + mEnvironment.setScaleFactor(mHighResPreviewScaleFactor); + } else { + mEnvironment.setScaleFactor(mPreviewScaleFactor); + } + mEnvironment.setQuality(FilterEnvironment.QUALITY_PREVIEW); + mEnvironment.setImagePreset(preset); + mEnvironment.setStop(false); + } + + public void setOriginal(Bitmap bitmap) { + mOriginalBitmap = bitmap; + Log.v(LOGTAG,"setOriginal, size " + bitmap.getWidth() + " x " + bitmap.getHeight()); + ImagePreset preset = MasterImage.getImage().getPreset(); + setupEnvironment(preset, false); + updateOriginalAllocation(preset); + } + + private synchronized boolean updateOriginalAllocation(ImagePreset preset) { + Bitmap originalBitmap = mOriginalBitmap; + + if (originalBitmap == null) { + return false; + } + + RenderScript RS = getRenderScriptContext(); + + Allocation filtersOnlyOriginalAllocation = mFiltersOnlyOriginalAllocation; + mFiltersOnlyOriginalAllocation = Allocation.createFromBitmap(RS, originalBitmap, + Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT); + if (filtersOnlyOriginalAllocation != null) { + filtersOnlyOriginalAllocation.destroy(); + } + + Allocation originalAllocation = mOriginalAllocation; + mResizedOriginalBitmap = preset.applyGeometry(originalBitmap, mEnvironment); + mOriginalAllocation = Allocation.createFromBitmap(RS, mResizedOriginalBitmap, + Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT); + if (originalAllocation != null) { + originalAllocation.destroy(); + } + + return true; + } + + public void renderHighres(RenderingRequest request) { + synchronized (CachingPipeline.class) { + if (getRenderScriptContext() == null) { + return; + } + ImagePreset preset = request.getImagePreset(); + setupEnvironment(preset, false); + Bitmap bitmap = MasterImage.getImage().getOriginalBitmapHighres(); + if (bitmap == null) { + return; + } + // TODO: use a cache of bitmaps + bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true); + bitmap = preset.applyGeometry(bitmap, mEnvironment); + + mEnvironment.setQuality(FilterEnvironment.QUALITY_PREVIEW); + Bitmap bmp = preset.apply(bitmap, mEnvironment); + if (!mEnvironment.needsStop()) { + request.setBitmap(bmp); + } + mFiltersManager.freeFilterResources(preset); + } + } + + public synchronized void render(RenderingRequest request) { + synchronized (CachingPipeline.class) { + if (getRenderScriptContext() == null) { + return; + } + if (((request.getType() != RenderingRequest.PARTIAL_RENDERING + && request.getType() != RenderingRequest.HIGHRES_RENDERING) + && request.getBitmap() == null) + || request.getImagePreset() == null) { + return; + } + + if (DEBUG) { + Log.v(LOGTAG, "render image of type " + getType(request)); + } + + Bitmap bitmap = request.getBitmap(); + ImagePreset preset = request.getImagePreset(); + setupEnvironment(preset, + request.getType() != RenderingRequest.HIGHRES_RENDERING); + mFiltersManager.freeFilterResources(preset); + + if (request.getType() == RenderingRequest.PARTIAL_RENDERING) { + MasterImage master = MasterImage.getImage(); + bitmap = ImageLoader.getScaleOneImageForPreset(master.getActivity(), + master.getUri(), request.getBounds(), + request.getDestination()); + if (bitmap == null) { + Log.w(LOGTAG, "could not get bitmap for: " + getType(request)); + return; + } + } + + if (request.getType() == RenderingRequest.HIGHRES_RENDERING) { + bitmap = MasterImage.getImage().getOriginalBitmapHighres(); + if (bitmap != null) { + bitmap = preset.applyGeometry(bitmap, mEnvironment); + } + } + + if (request.getType() == RenderingRequest.FULL_RENDERING + || request.getType() == RenderingRequest.GEOMETRY_RENDERING + || request.getType() == RenderingRequest.FILTERS_RENDERING) { + updateOriginalAllocation(preset); + } + + if (DEBUG) { + Log.v(LOGTAG, "after update, req bitmap (" + bitmap.getWidth() + "x" + bitmap.getHeight() + + " ? resizeOriginal (" + mResizedOriginalBitmap.getWidth() + "x" + + mResizedOriginalBitmap.getHeight()); + } + + if (request.getType() == RenderingRequest.FULL_RENDERING + || request.getType() == RenderingRequest.GEOMETRY_RENDERING) { + mOriginalAllocation.copyTo(bitmap); + } else if (request.getType() == RenderingRequest.FILTERS_RENDERING) { + mFiltersOnlyOriginalAllocation.copyTo(bitmap); + } + + if (request.getType() == RenderingRequest.FULL_RENDERING + || request.getType() == RenderingRequest.FILTERS_RENDERING + || request.getType() == RenderingRequest.ICON_RENDERING + || request.getType() == RenderingRequest.PARTIAL_RENDERING + || request.getType() == RenderingRequest.HIGHRES_RENDERING + || request.getType() == RenderingRequest.STYLE_ICON_RENDERING) { + + if (request.getType() == RenderingRequest.ICON_RENDERING) { + mEnvironment.setQuality(FilterEnvironment.QUALITY_ICON); + } else { + mEnvironment.setQuality(FilterEnvironment.QUALITY_PREVIEW); + } + + Bitmap bmp = preset.apply(bitmap, mEnvironment); + if (!mEnvironment.needsStop()) { + request.setBitmap(bmp); + } + mFiltersManager.freeFilterResources(preset); + } + } + } + + public synchronized void renderImage(ImagePreset preset, Allocation in, Allocation out) { + synchronized (CachingPipeline.class) { + if (getRenderScriptContext() == null) { + return; + } + setupEnvironment(preset, false); + mFiltersManager.freeFilterResources(preset); + preset.applyFilters(-1, -1, in, out, mEnvironment); + boolean copyOut = false; + if (preset.nbFilters() > 0) { + copyOut = true; + } + preset.applyBorder(in, out, copyOut, mEnvironment); + } + } + + public synchronized Bitmap renderFinalImage(Bitmap bitmap, ImagePreset preset) { + synchronized (CachingPipeline.class) { + if (getRenderScriptContext() == null) { + return bitmap; + } + setupEnvironment(preset, false); + mEnvironment.setQuality(FilterEnvironment.QUALITY_FINAL); + mEnvironment.setScaleFactor(1.0f); + mFiltersManager.freeFilterResources(preset); + bitmap = preset.applyGeometry(bitmap, mEnvironment); + bitmap = preset.apply(bitmap, mEnvironment); + return bitmap; + } + } + + public Bitmap renderGeometryIcon(Bitmap bitmap, ImagePreset preset) { + return GeometryMathUtils.applyGeometryRepresentations(preset.getGeometryFilters(), bitmap); + } + + public void compute(SharedBuffer buffer, ImagePreset preset, int type) { + if (getRenderScriptContext() == null) { + return; + } + setupEnvironment(preset, false); + Vector<FilterRepresentation> filters = preset.getFilters(); + Bitmap result = mCachedProcessing.process(mOriginalBitmap, filters, mEnvironment); + buffer.setProducer(result); + } + + public synchronized void computeOld(SharedBuffer buffer, ImagePreset preset, int type) { + synchronized (CachingPipeline.class) { + if (getRenderScriptContext() == null) { + return; + } + if (DEBUG) { + Log.v(LOGTAG, "compute preset " + preset); + preset.showFilters(); + } + + String thread = Thread.currentThread().getName(); + long time = System.currentTimeMillis(); + setupEnvironment(preset, false); + mFiltersManager.freeFilterResources(preset); + + Bitmap resizedOriginalBitmap = mResizedOriginalBitmap; + if (updateOriginalAllocation(preset) || buffer.getProducer() == null) { + resizedOriginalBitmap = mResizedOriginalBitmap; + buffer.setProducer(resizedOriginalBitmap); + mEnvironment.cache(buffer.getProducer()); + } + + Bitmap bitmap = buffer.getProducer().getBitmap(); + long time2 = System.currentTimeMillis(); + + if (bitmap == null || (bitmap.getWidth() != resizedOriginalBitmap.getWidth()) + || (bitmap.getHeight() != resizedOriginalBitmap.getHeight())) { + mEnvironment.cache(buffer.getProducer()); + buffer.setProducer(resizedOriginalBitmap); + bitmap = buffer.getProducer().getBitmap(); + } + mOriginalAllocation.copyTo(bitmap); + + Bitmap tmpbitmap = preset.apply(bitmap, mEnvironment); + if (tmpbitmap != bitmap) { + mEnvironment.cache(buffer.getProducer()); + buffer.setProducer(tmpbitmap); + } + + mFiltersManager.freeFilterResources(preset); + + time = System.currentTimeMillis() - time; + time2 = System.currentTimeMillis() - time2; + if (DEBUG) { + Log.v(LOGTAG, "Applying type " + type + " filters to bitmap " + + bitmap + " (" + bitmap.getWidth() + " x " + bitmap.getHeight() + + ") took " + time + " ms, " + time2 + " ms for the filter, on thread " + thread); + } + } + } + + public boolean needsRepaint() { + SharedBuffer buffer = MasterImage.getImage().getPreviewBuffer(); + return buffer.checkRepaintNeeded(); + } + + public void setPreviewScaleFactor(float previewScaleFactor) { + mPreviewScaleFactor = previewScaleFactor; + } + + public void setHighResPreviewScaleFactor(float highResPreviewScaleFactor) { + mHighResPreviewScaleFactor = highResPreviewScaleFactor; + } + + public synchronized boolean isInitialized() { + return getRenderScriptContext() != null && mOriginalBitmap != null; + } + + public boolean prepareRenderscriptAllocations(Bitmap bitmap) { + RenderScript RS = getRenderScriptContext(); + boolean needsUpdate = false; + if (mOutPixelsAllocation == null || mInPixelsAllocation == null || + bitmap.getWidth() != mWidth || bitmap.getHeight() != mHeight) { + destroyPixelAllocations(); + Bitmap bitmapBuffer = bitmap; + if (bitmap.getConfig() == null || bitmap.getConfig() != BITMAP_CONFIG) { + bitmapBuffer = bitmap.copy(BITMAP_CONFIG, true); + } + mOutPixelsAllocation = Allocation.createFromBitmap(RS, bitmapBuffer, + Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT); + mInPixelsAllocation = Allocation.createTyped(RS, + mOutPixelsAllocation.getType()); + needsUpdate = true; + } + if (RS != null) { + mInPixelsAllocation.copyFrom(bitmap); + } + if (bitmap.getWidth() != mWidth + || bitmap.getHeight() != mHeight) { + mWidth = bitmap.getWidth(); + mHeight = bitmap.getHeight(); + needsUpdate = true; + } + if (DEBUG) { + Log.v(LOGTAG, "prepareRenderscriptAllocations: " + needsUpdate + " in " + getName()); + } + return needsUpdate; + } + + public synchronized Allocation getInPixelsAllocation() { + return mInPixelsAllocation; + } + + public synchronized Allocation getOutPixelsAllocation() { + return mOutPixelsAllocation; + } + + public String getName() { + return mName; + } + + public RenderScript getRSContext() { + return CachingPipeline.getRenderScriptContext(); + } +} diff --git a/src/com/android/gallery3d/filtershow/pipeline/FilterEnvironment.java b/src/com/android/gallery3d/filtershow/pipeline/FilterEnvironment.java new file mode 100644 index 000000000..4fac956be --- /dev/null +++ b/src/com/android/gallery3d/filtershow/pipeline/FilterEnvironment.java @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2013 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.filtershow.pipeline; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.support.v8.renderscript.Allocation; + +import com.android.gallery3d.filtershow.filters.FilterRepresentation; +import com.android.gallery3d.filtershow.filters.FilterUserPresetRepresentation; +import com.android.gallery3d.filtershow.filters.FilterRotateRepresentation.Rotation; +import com.android.gallery3d.filtershow.filters.FiltersManagerInterface; +import com.android.gallery3d.filtershow.filters.ImageFilter; + +import java.lang.ref.WeakReference; +import java.util.HashMap; + +public class FilterEnvironment { + private static final String LOGTAG = "FilterEnvironment"; + private ImagePreset mImagePreset; + private float mScaleFactor; + private int mQuality; + private FiltersManagerInterface mFiltersManager; + private PipelineInterface mPipeline; + private volatile boolean mStop = false; + + public static final int QUALITY_ICON = 0; + public static final int QUALITY_PREVIEW = 1; + public static final int QUALITY_FINAL = 2; + + public synchronized boolean needsStop() { + return mStop; + } + + public synchronized void setStop(boolean stop) { + this.mStop = stop; + } + + private HashMap<Long, WeakReference<Bitmap>> + bitmapCach = new HashMap<Long, WeakReference<Bitmap>>(); + + private HashMap<Integer, Integer> + generalParameters = new HashMap<Integer, Integer>(); + + public void cache(Buffer buffer) { + if (buffer == null) { + return; + } + Bitmap bitmap = buffer.getBitmap(); + if (bitmap == null) { + return; + } + Long key = calcKey(bitmap.getWidth(), bitmap.getHeight()); + bitmapCach.put(key, new WeakReference<Bitmap>(bitmap)); + } + + public Bitmap getBitmap(int w, int h) { + Long key = calcKey(w, h); + WeakReference<Bitmap> ref = bitmapCach.remove(key); + Bitmap bitmap = null; + if (ref != null) { + bitmap = ref.get(); + } + if (bitmap == null) { + bitmap = Bitmap.createBitmap( + w, h, Bitmap.Config.ARGB_8888); + } + return bitmap; + } + + private Long calcKey(long w, long h) { + return (w << 32) | (h << 32); + } + + public void setImagePreset(ImagePreset imagePreset) { + mImagePreset = imagePreset; + } + + public ImagePreset getImagePreset() { + return mImagePreset; + } + + public void setScaleFactor(float scaleFactor) { + mScaleFactor = scaleFactor; + } + + public float getScaleFactor() { + return mScaleFactor; + } + + public void setQuality(int quality) { + mQuality = quality; + } + + public int getQuality() { + return mQuality; + } + + public void setFiltersManager(FiltersManagerInterface filtersManager) { + mFiltersManager = filtersManager; + } + + public FiltersManagerInterface getFiltersManager() { + return mFiltersManager; + } + + public void applyRepresentation(FilterRepresentation representation, + Allocation in, Allocation out) { + ImageFilter filter = mFiltersManager.getFilterForRepresentation(representation); + filter.useRepresentation(representation); + filter.setEnvironment(this); + if (filter.supportsAllocationInput()) { + filter.apply(in, out); + } + filter.setGeneralParameters(); + filter.setEnvironment(null); + } + + public Bitmap applyRepresentation(FilterRepresentation representation, Bitmap bitmap) { + if (representation instanceof FilterUserPresetRepresentation) { + // we allow instances of FilterUserPresetRepresentation in a preset only to know if one + // has been applied (so we can show this in the UI). But as all the filters in them are + // applied directly they do not themselves need to do any kind of filtering. + return bitmap; + } + ImageFilter filter = mFiltersManager.getFilterForRepresentation(representation); + filter.useRepresentation(representation); + filter.setEnvironment(this); + Bitmap ret = filter.apply(bitmap, mScaleFactor, mQuality); + filter.setGeneralParameters(); + filter.setEnvironment(null); + return ret; + } + + public PipelineInterface getPipeline() { + return mPipeline; + } + + public void setPipeline(PipelineInterface cachingPipeline) { + mPipeline = cachingPipeline; + } + + public synchronized void clearGeneralParameters() { + generalParameters = null; + } + + public synchronized Integer getGeneralParameter(int id) { + if (generalParameters == null || !generalParameters.containsKey(id)) { + return null; + } + return generalParameters.get(id); + } + + public synchronized void setGeneralParameter(int id, int value) { + if (generalParameters == null) { + generalParameters = new HashMap<Integer, Integer>(); + } + + generalParameters.put(id, value); + } + +} diff --git a/src/com/android/gallery3d/filtershow/pipeline/HighresRenderingRequestTask.java b/src/com/android/gallery3d/filtershow/pipeline/HighresRenderingRequestTask.java new file mode 100644 index 000000000..5a0eb4d45 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/pipeline/HighresRenderingRequestTask.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2013 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.filtershow.pipeline; + +import android.graphics.Bitmap; +import com.android.gallery3d.filtershow.filters.FiltersManager; + +public class HighresRenderingRequestTask extends ProcessingTask { + + private CachingPipeline mHighresPreviewPipeline = null; + private boolean mPipelineIsOn = false; + + public void setHighresPreviewScaleFactor(float highResPreviewScale) { + mHighresPreviewPipeline.setHighResPreviewScaleFactor(highResPreviewScale); + } + + public void setPreviewScaleFactor(float previewScale) { + mHighresPreviewPipeline.setPreviewScaleFactor(previewScale); + } + + static class Render implements Request { + RenderingRequest request; + } + + static class RenderResult implements Result { + RenderingRequest request; + } + + public HighresRenderingRequestTask() { + mHighresPreviewPipeline = new CachingPipeline( + FiltersManager.getHighresManager(), "Highres"); + } + + public void setOriginal(Bitmap bitmap) { + mHighresPreviewPipeline.setOriginal(bitmap); + } + + public void setOriginalBitmapHighres(Bitmap originalHires) { + mPipelineIsOn = true; + } + + public void stop() { + mHighresPreviewPipeline.stop(); + } + + public void postRenderingRequest(RenderingRequest request) { + if (!mPipelineIsOn) { + return; + } + Render render = new Render(); + render.request = request; + postRequest(render); + } + + @Override + public Result doInBackground(Request message) { + RenderingRequest request = ((Render) message).request; + RenderResult result = null; + mHighresPreviewPipeline.renderHighres(request); + result = new RenderResult(); + result.request = request; + return result; + } + + @Override + public void onResult(Result message) { + if (message == null) { + return; + } + RenderingRequest request = ((RenderResult) message).request; + request.markAvailable(); + } + + @Override + public boolean isDelayedTask() { return true; } +} diff --git a/src/com/android/gallery3d/filtershow/pipeline/ImagePreset.java b/src/com/android/gallery3d/filtershow/pipeline/ImagePreset.java new file mode 100644 index 000000000..d34216ad6 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/pipeline/ImagePreset.java @@ -0,0 +1,694 @@ +/* + * Copyright (C) 2012 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.filtershow.pipeline; + +import android.graphics.Bitmap; +import android.graphics.Rect; +import android.support.v8.renderscript.Allocation; +import android.util.JsonReader; +import android.util.JsonWriter; +import android.util.Log; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.cache.ImageLoader; +import com.android.gallery3d.filtershow.filters.BaseFiltersManager; +import com.android.gallery3d.filtershow.filters.FilterCropRepresentation; +import com.android.gallery3d.filtershow.filters.FilterFxRepresentation; +import com.android.gallery3d.filtershow.filters.FilterImageBorderRepresentation; +import com.android.gallery3d.filtershow.filters.FilterMirrorRepresentation; +import com.android.gallery3d.filtershow.filters.FilterRepresentation; +import com.android.gallery3d.filtershow.filters.FilterRotateRepresentation; +import com.android.gallery3d.filtershow.filters.FilterStraightenRepresentation; +import com.android.gallery3d.filtershow.filters.FilterUserPresetRepresentation; +import com.android.gallery3d.filtershow.filters.FiltersManager; +import com.android.gallery3d.filtershow.filters.ImageFilter; +import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils; +import com.android.gallery3d.filtershow.imageshow.MasterImage; +import com.android.gallery3d.filtershow.state.State; +import com.android.gallery3d.filtershow.state.StateAdapter; +import com.android.gallery3d.util.UsageStatistics; + +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Vector; + +public class ImagePreset { + + private static final String LOGTAG = "ImagePreset"; + + private Vector<FilterRepresentation> mFilters = new Vector<FilterRepresentation>(); + + private boolean mDoApplyGeometry = true; + private boolean mDoApplyFilters = true; + + private boolean mPartialRendering = false; + private Rect mPartialRenderingBounds; + private static final boolean DEBUG = false; + + public ImagePreset() { + } + + public ImagePreset(ImagePreset source) { + for (int i = 0; i < source.mFilters.size(); i++) { + FilterRepresentation sourceRepresentation = source.mFilters.elementAt(i); + mFilters.add(sourceRepresentation.copy()); + } + } + + public Vector<FilterRepresentation> getFilters() { + return mFilters; + } + + public FilterRepresentation getFilterRepresentation(int position) { + FilterRepresentation representation = null; + + representation = mFilters.elementAt(position).copy(); + + return representation; + } + + private static boolean sameSerializationName(String a, String b) { + if (a != null && b != null) { + return a.equals(b); + } else { + return a == null && b == null; + } + } + + public static boolean sameSerializationName(FilterRepresentation a, FilterRepresentation b) { + if (a == null || b == null) { + return false; + } + return sameSerializationName(a.getSerializationName(), b.getSerializationName()); + } + + public int getPositionForRepresentation(FilterRepresentation representation) { + for (int i = 0; i < mFilters.size(); i++) { + if (sameSerializationName(mFilters.elementAt(i), representation)) { + return i; + } + } + return -1; + } + + private FilterRepresentation getFilterRepresentationForType(int type) { + for (int i = 0; i < mFilters.size(); i++) { + if (mFilters.elementAt(i).getFilterType() == type) { + return mFilters.elementAt(i); + } + } + return null; + } + + public int getPositionForType(int type) { + for (int i = 0; i < mFilters.size(); i++) { + if (mFilters.elementAt(i).getFilterType() == type) { + return i; + } + } + return -1; + } + + public FilterRepresentation getFilterRepresentationCopyFrom( + FilterRepresentation filterRepresentation) { + // TODO: add concept of position in the filters (to allow multiple instances) + if (filterRepresentation == null) { + return null; + } + int position = getPositionForRepresentation(filterRepresentation); + if (position == -1) { + return null; + } + FilterRepresentation representation = mFilters.elementAt(position); + if (representation != null) { + representation = representation.copy(); + } + return representation; + } + + public void updateFilterRepresentations(Collection<FilterRepresentation> reps) { + for (FilterRepresentation r : reps) { + updateOrAddFilterRepresentation(r); + } + } + + public void updateOrAddFilterRepresentation(FilterRepresentation rep) { + int pos = getPositionForRepresentation(rep); + if (pos != -1) { + mFilters.elementAt(pos).useParametersFrom(rep); + } else { + addFilter(rep.copy()); + } + } + + public void setDoApplyGeometry(boolean value) { + mDoApplyGeometry = value; + } + + public void setDoApplyFilters(boolean value) { + mDoApplyFilters = value; + } + + public boolean getDoApplyFilters() { + return mDoApplyFilters; + } + + public boolean hasModifications() { + for (int i = 0; i < mFilters.size(); i++) { + FilterRepresentation filter = mFilters.elementAt(i); + if (!filter.isNil()) { + return true; + } + } + return false; + } + + public boolean isPanoramaSafe() { + for (FilterRepresentation representation : mFilters) { + if (representation.getFilterType() == FilterRepresentation.TYPE_GEOMETRY + && !representation.isNil()) { + return false; + } + if (representation.getFilterType() == FilterRepresentation.TYPE_BORDER + && !representation.isNil()) { + return false; + } + if (representation.getFilterType() == FilterRepresentation.TYPE_VIGNETTE + && !representation.isNil()) { + return false; + } + if (representation.getFilterType() == FilterRepresentation.TYPE_TINYPLANET + && !representation.isNil()) { + return false; + } + } + return true; + } + + public boolean same(ImagePreset preset) { + if (preset == null) { + return false; + } + + if (preset.mFilters.size() != mFilters.size()) { + return false; + } + + if (mDoApplyGeometry != preset.mDoApplyGeometry) { + return false; + } + + if (mDoApplyFilters != preset.mDoApplyFilters) { + if (mFilters.size() > 0 || preset.mFilters.size() > 0) { + return false; + } + } + + if (mDoApplyFilters && preset.mDoApplyFilters) { + for (int i = 0; i < preset.mFilters.size(); i++) { + FilterRepresentation a = preset.mFilters.elementAt(i); + FilterRepresentation b = mFilters.elementAt(i); + + if (!a.same(b)) { + return false; + } + } + } + + return true; + } + + public int similarUpTo(ImagePreset preset) { + for (int i = 0; i < preset.mFilters.size(); i++) { + FilterRepresentation a = preset.mFilters.elementAt(i); + if (i < mFilters.size()) { + FilterRepresentation b = mFilters.elementAt(i); + if (!a.same(b)) { + return i; + } + if (!a.equals(b)) { + return i; + } + } else { + return i; + } + } + return preset.mFilters.size(); + } + + public void showFilters() { + Log.v(LOGTAG, "\\\\\\ showFilters -- " + mFilters.size() + " filters"); + int n = 0; + for (FilterRepresentation representation : mFilters) { + Log.v(LOGTAG, " filter " + n + " : " + representation.toString()); + n++; + } + Log.v(LOGTAG, "/// showFilters -- " + mFilters.size() + " filters"); + } + + public FilterRepresentation getLastRepresentation() { + if (mFilters.size() > 0) { + return mFilters.lastElement(); + } + return null; + } + + public void removeFilter(FilterRepresentation filterRepresentation) { + if (filterRepresentation.getFilterType() == FilterRepresentation.TYPE_BORDER) { + for (int i = 0; i < mFilters.size(); i++) { + if (mFilters.elementAt(i).getFilterType() + == filterRepresentation.getFilterType()) { + mFilters.remove(i); + break; + } + } + } else { + for (int i = 0; i < mFilters.size(); i++) { + if (sameSerializationName(mFilters.elementAt(i), filterRepresentation)) { + mFilters.remove(i); + break; + } + } + } + } + + // If the filter is an "None" effect or border, then just don't add this filter. + public void addFilter(FilterRepresentation representation) { + if (representation instanceof FilterUserPresetRepresentation) { + ImagePreset preset = ((FilterUserPresetRepresentation) representation).getImagePreset(); + // user preset replace everything but geometry + mFilters.clear(); + for (int i = 0; i < preset.nbFilters(); i++) { + addFilter(preset.getFilterRepresentation(i)); + } + mFilters.add(representation); + } else if (representation.getFilterType() == FilterRepresentation.TYPE_GEOMETRY) { + // Add geometry filter, removing duplicates and do-nothing operations. + for (int i = 0; i < mFilters.size(); i++) { + if (sameSerializationName(representation, mFilters.elementAt(i))) { + mFilters.remove(i); + } + } + if (!representation.isNil()) { + mFilters.add(representation); + } + } else if (representation.getFilterType() == FilterRepresentation.TYPE_BORDER) { + removeFilter(representation); + if (!isNoneBorderFilter(representation)) { + mFilters.add(representation); + } + } else if (representation.getFilterType() == FilterRepresentation.TYPE_FX) { + boolean found = false; + for (int i = 0; i < mFilters.size(); i++) { + FilterRepresentation current = mFilters.elementAt(i); + int type = current.getFilterType(); + if (found) { + if (type != FilterRepresentation.TYPE_VIGNETTE) { + mFilters.remove(i); + continue; + } + } + if (type == FilterRepresentation.TYPE_FX) { + if (current instanceof FilterUserPresetRepresentation) { + ImagePreset preset = ((FilterUserPresetRepresentation) current) + .getImagePreset(); + // If we had an existing user preset, let's remove all the presets that + // were added by it + for (int j = 0; j < preset.nbFilters(); j++) { + FilterRepresentation rep = preset.getFilterRepresentation(j); + int pos = getPositionForRepresentation(rep); + if (pos != -1) { + mFilters.remove(pos); + } + } + int pos = getPositionForRepresentation(current); + if (pos != -1) { + mFilters.remove(pos); + } else { + pos = 0; + } + if (!isNoneFxFilter(representation)) { + mFilters.add(pos, representation); + } + + } else { + mFilters.remove(i); + if (!isNoneFxFilter(representation)) { + mFilters.add(i, representation); + } + } + found = true; + } + } + if (!found) { + if (!isNoneFxFilter(representation)) { + mFilters.add(representation); + } + } + } else { + mFilters.add(representation); + } + } + + private boolean isNoneBorderFilter(FilterRepresentation representation) { + return representation instanceof FilterImageBorderRepresentation && + ((FilterImageBorderRepresentation) representation).getDrawableResource() == 0; + } + + private boolean isNoneFxFilter(FilterRepresentation representation) { + return representation instanceof FilterFxRepresentation && + ((FilterFxRepresentation) representation).getNameResource() == R.string.none; + } + + public FilterRepresentation getRepresentation(FilterRepresentation filterRepresentation) { + for (int i = 0; i < mFilters.size(); i++) { + FilterRepresentation representation = mFilters.elementAt(i); + if (sameSerializationName(representation, filterRepresentation)) { + return representation; + } + } + return null; + } + + public Bitmap apply(Bitmap original, FilterEnvironment environment) { + Bitmap bitmap = original; + bitmap = applyFilters(bitmap, -1, -1, environment); + return applyBorder(bitmap, environment); + } + + public Collection<FilterRepresentation> getGeometryFilters() { + ArrayList<FilterRepresentation> geometry = new ArrayList<FilterRepresentation>(); + for (FilterRepresentation r : mFilters) { + if (r.getFilterType() == FilterRepresentation.TYPE_GEOMETRY) { + geometry.add(r); + } + } + return geometry; + } + + public FilterRepresentation getFilterWithSerializationName(String serializationName) { + for (FilterRepresentation r : mFilters) { + if (r != null) { + if (sameSerializationName(r.getSerializationName(), serializationName)) { + return r.copy(); + } + } + } + return null; + } + + public Bitmap applyGeometry(Bitmap bitmap, FilterEnvironment environment) { + // Apply any transform -- 90 rotate, flip, straighten, crop + // Returns a new bitmap. + if (mDoApplyGeometry) { + bitmap = GeometryMathUtils.applyGeometryRepresentations(getGeometryFilters(), bitmap); + } + return bitmap; + } + + public Bitmap applyBorder(Bitmap bitmap, FilterEnvironment environment) { + // get the border from the list of filters. + FilterRepresentation border = getFilterRepresentationForType( + FilterRepresentation.TYPE_BORDER); + if (border != null && mDoApplyGeometry) { + bitmap = environment.applyRepresentation(border, bitmap); + if (environment.getQuality() == FilterEnvironment.QUALITY_FINAL) { + UsageStatistics.onEvent(UsageStatistics.COMPONENT_EDITOR, + "SaveBorder", border.getSerializationName(), 1); + } + } + return bitmap; + } + + public int nbFilters() { + return mFilters.size(); + } + + public Bitmap applyFilters(Bitmap bitmap, int from, int to, FilterEnvironment environment) { + if (mDoApplyFilters) { + if (from < 0) { + from = 0; + } + if (to == -1) { + to = mFilters.size(); + } + if (environment.getQuality() == FilterEnvironment.QUALITY_FINAL) { + UsageStatistics.onEvent(UsageStatistics.COMPONENT_EDITOR, + "SaveFilters", "Total", to - from + 1); + } + for (int i = from; i < to; i++) { + FilterRepresentation representation = mFilters.elementAt(i); + if (representation.getFilterType() == FilterRepresentation.TYPE_GEOMETRY) { + // skip the geometry as it's already applied. + continue; + } + if (representation.getFilterType() == FilterRepresentation.TYPE_BORDER) { + // for now, let's skip the border as it will be applied in + // applyBorder() + // TODO: might be worth getting rid of applyBorder. + continue; + } + bitmap = environment.applyRepresentation(representation, bitmap); + if (environment.getQuality() == FilterEnvironment.QUALITY_FINAL) { + UsageStatistics.onEvent(UsageStatistics.COMPONENT_EDITOR, + "SaveFilter", representation.getSerializationName(), 1); + } + if (environment.needsStop()) { + return bitmap; + } + } + } + + return bitmap; + } + + public void applyBorder(Allocation in, Allocation out, + boolean copyOut, FilterEnvironment environment) { + FilterRepresentation border = getFilterRepresentationForType( + FilterRepresentation.TYPE_BORDER); + if (border != null && mDoApplyGeometry) { + // TODO: should keep the bitmap around + Allocation bitmapIn = in; + if (copyOut) { + bitmapIn = Allocation.createTyped( + CachingPipeline.getRenderScriptContext(), in.getType()); + bitmapIn.copyFrom(out); + } + environment.applyRepresentation(border, bitmapIn, out); + } + } + + public void applyFilters(int from, int to, Allocation in, Allocation out, + FilterEnvironment environment) { + if (mDoApplyFilters) { + if (from < 0) { + from = 0; + } + if (to == -1) { + to = mFilters.size(); + } + for (int i = from; i < to; i++) { + FilterRepresentation representation = mFilters.elementAt(i); + if (representation.getFilterType() == FilterRepresentation.TYPE_GEOMETRY + || representation.getFilterType() == FilterRepresentation.TYPE_BORDER) { + continue; + } + if (i > from) { + in.copyFrom(out); + } + environment.applyRepresentation(representation, in, out); + } + } + } + + public boolean canDoPartialRendering() { + if (MasterImage.getImage().getZoomOrientation() != ImageLoader.ORI_NORMAL) { + return false; + } + for (int i = 0; i < mFilters.size(); i++) { + FilterRepresentation representation = mFilters.elementAt(i); + if (representation.getFilterType() == FilterRepresentation.TYPE_GEOMETRY + && !representation.isNil()) { + return false; + } + if (!representation.supportsPartialRendering()) { + return false; + } + } + return true; + } + + public void fillImageStateAdapter(StateAdapter imageStateAdapter) { + if (imageStateAdapter == null) { + return; + } + Vector<State> states = new Vector<State>(); + for (FilterRepresentation filter : mFilters) { + if (filter.getFilterType() == FilterRepresentation.TYPE_GEOMETRY) { + // TODO: supports Geometry representations in the state panel. + continue; + } + if (filter instanceof FilterUserPresetRepresentation) { + // do not show the user preset itself in the state panel + continue; + } + State state = new State(filter.getName()); + state.setFilterRepresentation(filter); + states.add(state); + } + imageStateAdapter.fill(states); + } + + public void setPartialRendering(boolean partialRendering, Rect bounds) { + mPartialRendering = partialRendering; + mPartialRenderingBounds = bounds; + } + + public boolean isPartialRendering() { + return mPartialRendering; + } + + public Rect getPartialRenderingBounds() { + return mPartialRenderingBounds; + } + + public Vector<ImageFilter> getUsedFilters(BaseFiltersManager filtersManager) { + Vector<ImageFilter> usedFilters = new Vector<ImageFilter>(); + for (int i = 0; i < mFilters.size(); i++) { + FilterRepresentation representation = mFilters.elementAt(i); + ImageFilter filter = filtersManager.getFilterForRepresentation(representation); + usedFilters.add(filter); + } + return usedFilters; + } + + public String getJsonString(String name) { + StringWriter swriter = new StringWriter(); + try { + JsonWriter writer = new JsonWriter(swriter); + writeJson(writer, name); + writer.close(); + } catch (IOException e) { + return null; + } + return swriter.toString(); + } + + public void writeJson(JsonWriter writer, String name) { + int numFilters = mFilters.size(); + try { + writer.beginObject(); + for (int i = 0; i < numFilters; i++) { + FilterRepresentation filter = mFilters.get(i); + if (filter instanceof FilterUserPresetRepresentation) { + continue; + } + String sname = filter.getSerializationName(); + if (DEBUG) { + Log.v(LOGTAG, "Serialization: " + sname); + if (sname == null) { + Log.v(LOGTAG, "Serialization name null for filter: " + filter); + } + } + writer.name(sname); + filter.serializeRepresentation(writer); + } + writer.endObject(); + + } catch (IOException e) { + Log.e(LOGTAG,"Error encoding JASON",e); + } + } + + /** + * populates preset from JSON string + * + * @param filterString a JSON string + * @return true on success if false ImagePreset is undefined + */ + public boolean readJsonFromString(String filterString) { + if (DEBUG) { + Log.v(LOGTAG, "reading preset: \"" + filterString + "\""); + } + StringReader sreader = new StringReader(filterString); + try { + JsonReader reader = new JsonReader(sreader); + boolean ok = readJson(reader); + if (!ok) { + reader.close(); + return false; + } + reader.close(); + } catch (Exception e) { + Log.e(LOGTAG, "parsing the filter parameters:", e); + return false; + } + return true; + } + + /** + * populates preset from JSON stream + * + * @param sreader a JSON string + * @return true on success if false ImagePreset is undefined + */ + public boolean readJson(JsonReader sreader) throws IOException { + sreader.beginObject(); + + while (sreader.hasNext()) { + String name = sreader.nextName(); + FilterRepresentation filter = creatFilterFromName(name); + if (filter == null) { + Log.w(LOGTAG, "UNKNOWN FILTER! " + name); + return false; + } + filter.deSerializeRepresentation(sreader); + addFilter(filter); + } + sreader.endObject(); + return true; + } + + FilterRepresentation creatFilterFromName(String name) { + if (FilterRotateRepresentation.SERIALIZATION_NAME.equals(name)) { + return new FilterRotateRepresentation(); + } else if (FilterMirrorRepresentation.SERIALIZATION_NAME.equals(name)) { + return new FilterMirrorRepresentation(); + } else if (FilterStraightenRepresentation.SERIALIZATION_NAME.equals(name)) { + return new FilterStraightenRepresentation(); + } else if (FilterCropRepresentation.SERIALIZATION_NAME.equals(name)) { + return new FilterCropRepresentation(); + } + FiltersManager filtersManager = FiltersManager.getManager(); + return filtersManager.createFilterFromName(name); + } + + public void updateWith(ImagePreset preset) { + if (preset.mFilters.size() != mFilters.size()) { + Log.e(LOGTAG, "Updating a preset with an incompatible one"); + return; + } + for (int i = 0; i < mFilters.size(); i++) { + FilterRepresentation destRepresentation = mFilters.elementAt(i); + FilterRepresentation sourceRepresentation = preset.mFilters.elementAt(i); + destRepresentation.useParametersFrom(sourceRepresentation); + } + } +} diff --git a/src/com/android/gallery3d/filtershow/pipeline/ImageSavingTask.java b/src/com/android/gallery3d/filtershow/pipeline/ImageSavingTask.java new file mode 100644 index 000000000..b760edd5a --- /dev/null +++ b/src/com/android/gallery3d/filtershow/pipeline/ImageSavingTask.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2013 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.filtershow.pipeline; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.net.Uri; +import com.android.gallery3d.filtershow.cache.ImageLoader; +import com.android.gallery3d.filtershow.filters.FiltersManager; +import com.android.gallery3d.filtershow.tools.SaveImage; + +import java.io.File; + +public class ImageSavingTask extends ProcessingTask { + private ProcessingService mProcessingService; + + static class SaveRequest implements Request { + Uri sourceUri; + Uri selectedUri; + File destinationFile; + ImagePreset preset; + boolean flatten; + int quality; + } + + static class UpdateBitmap implements Update { + Bitmap bitmap; + } + + static class UpdateProgress implements Update { + int max; + int current; + } + + static class URIResult implements Result { + Uri uri; + } + + public ImageSavingTask(ProcessingService service) { + mProcessingService = service; + } + + public void saveImage(Uri sourceUri, Uri selectedUri, + File destinationFile, ImagePreset preset, boolean flatten, int quality) { + SaveRequest request = new SaveRequest(); + request.sourceUri = sourceUri; + request.selectedUri = selectedUri; + request.destinationFile = destinationFile; + request.preset = preset; + request.flatten = flatten; + request.quality = quality; + postRequest(request); + } + + public Result doInBackground(Request message) { + SaveRequest request = (SaveRequest) message; + Uri sourceUri = request.sourceUri; + Uri selectedUri = request.selectedUri; + File destinationFile = request.destinationFile; + ImagePreset preset = request.preset; + boolean flatten = request.flatten; + // We create a small bitmap showing the result that we can + // give to the notification + UpdateBitmap updateBitmap = new UpdateBitmap(); + updateBitmap.bitmap = createNotificationBitmap(sourceUri, preset); + postUpdate(updateBitmap); + SaveImage saveImage = new SaveImage(mProcessingService, sourceUri, + selectedUri, destinationFile, + new SaveImage.Callback() { + @Override + public void onProgress(int max, int current) { + UpdateProgress updateProgress = new UpdateProgress(); + updateProgress.max = max; + updateProgress.current = current; + postUpdate(updateProgress); + } + }); + Uri uri = saveImage.processAndSaveImage(preset, !flatten, request.quality); + URIResult result = new URIResult(); + result.uri = uri; + return result; + } + + @Override + public void onResult(Result message) { + URIResult result = (URIResult) message; + mProcessingService.completeSaveImage(result.uri); + } + + @Override + public void onUpdate(Update message) { + if (message instanceof UpdateBitmap) { + Bitmap bitmap = ((UpdateBitmap) message).bitmap; + mProcessingService.updateNotificationWithBitmap(bitmap); + } + if (message instanceof UpdateProgress) { + UpdateProgress progress = (UpdateProgress) message; + mProcessingService.updateProgress(progress.max, progress.current); + } + } + + private Bitmap createNotificationBitmap(Uri sourceUri, ImagePreset preset) { + int notificationBitmapSize = Resources.getSystem().getDimensionPixelSize( + android.R.dimen.notification_large_icon_width); + Bitmap bitmap = ImageLoader.loadConstrainedBitmap(sourceUri, getContext(), + notificationBitmapSize, null, true); + CachingPipeline pipeline = new CachingPipeline(FiltersManager.getManager(), "Thumb"); + return pipeline.renderFinalImage(bitmap, preset); + } + +} diff --git a/src/com/android/gallery3d/filtershow/pipeline/PipelineInterface.java b/src/com/android/gallery3d/filtershow/pipeline/PipelineInterface.java new file mode 100644 index 000000000..d53768c95 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/pipeline/PipelineInterface.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2013 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.filtershow.pipeline; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.support.v8.renderscript.Allocation; +import android.support.v8.renderscript.RenderScript; + +public interface PipelineInterface { + public String getName(); + public Resources getResources(); + public Allocation getInPixelsAllocation(); + public Allocation getOutPixelsAllocation(); + public boolean prepareRenderscriptAllocations(Bitmap bitmap); + public RenderScript getRSContext(); +} diff --git a/src/com/android/gallery3d/filtershow/pipeline/ProcessingService.java b/src/com/android/gallery3d/filtershow/pipeline/ProcessingService.java new file mode 100644 index 000000000..d0504d11f --- /dev/null +++ b/src/com/android/gallery3d/filtershow/pipeline/ProcessingService.java @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2013 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.filtershow.pipeline; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Binder; +import android.os.IBinder; +import android.util.Log; +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.FilterShowActivity; +import com.android.gallery3d.filtershow.filters.FiltersManager; +import com.android.gallery3d.filtershow.filters.ImageFilter; +import com.android.gallery3d.filtershow.imageshow.MasterImage; +import com.android.gallery3d.filtershow.tools.SaveImage; + +import java.io.File; + +public class ProcessingService extends Service { + private static final String LOGTAG = "ProcessingService"; + private static final boolean SHOW_IMAGE = false; + private int mNotificationId; + private NotificationManager mNotifyMgr = null; + private Notification.Builder mBuilder = null; + + private static final String PRESET = "preset"; + private static final String QUALITY = "quality"; + private static final String SOURCE_URI = "sourceUri"; + private static final String SELECTED_URI = "selectedUri"; + private static final String DESTINATION_FILE = "destinationFile"; + private static final String SAVING = "saving"; + private static final String FLATTEN = "flatten"; + + private ProcessingTaskController mProcessingTaskController; + private ImageSavingTask mImageSavingTask; + private UpdatePreviewTask mUpdatePreviewTask; + private HighresRenderingRequestTask mHighresRenderingRequestTask; + private RenderingRequestTask mRenderingRequestTask; + + private final IBinder mBinder = new LocalBinder(); + private FilterShowActivity mFiltershowActivity; + + private boolean mSaving = false; + private boolean mNeedsAlive = false; + + public void setFiltershowActivity(FilterShowActivity filtershowActivity) { + mFiltershowActivity = filtershowActivity; + } + + public void setOriginalBitmap(Bitmap originalBitmap) { + if (mUpdatePreviewTask == null) { + return; + } + mUpdatePreviewTask.setOriginal(originalBitmap); + mHighresRenderingRequestTask.setOriginal(originalBitmap); + mRenderingRequestTask.setOriginal(originalBitmap); + } + + public void updatePreviewBuffer() { + mHighresRenderingRequestTask.stop(); + mUpdatePreviewTask.updatePreview(); + } + + public void postRenderingRequest(RenderingRequest request) { + mRenderingRequestTask.postRenderingRequest(request); + } + + public void postHighresRenderingRequest(ImagePreset preset, float scaleFactor, + RenderingRequestCaller caller) { + RenderingRequest request = new RenderingRequest(); + // TODO: use the triple buffer preset as UpdatePreviewTask does instead of creating a copy + ImagePreset passedPreset = new ImagePreset(preset); + request.setOriginalImagePreset(preset); + request.setScaleFactor(scaleFactor); + request.setImagePreset(passedPreset); + request.setType(RenderingRequest.HIGHRES_RENDERING); + request.setCaller(caller); + mHighresRenderingRequestTask.postRenderingRequest(request); + } + + public void setHighresPreviewScaleFactor(float highResPreviewScale) { + mHighresRenderingRequestTask.setHighresPreviewScaleFactor(highResPreviewScale); + } + + public void setPreviewScaleFactor(float previewScale) { + mHighresRenderingRequestTask.setPreviewScaleFactor(previewScale); + mRenderingRequestTask.setPreviewScaleFactor(previewScale); + } + + public void setOriginalBitmapHighres(Bitmap originalHires) { + mHighresRenderingRequestTask.setOriginalBitmapHighres(originalHires); + } + + public class LocalBinder extends Binder { + public ProcessingService getService() { + return ProcessingService.this; + } + } + + public static Intent getSaveIntent(Context context, ImagePreset preset, File destination, + Uri selectedImageUri, Uri sourceImageUri, boolean doFlatten, int quality) { + Intent processIntent = new Intent(context, ProcessingService.class); + processIntent.putExtra(ProcessingService.SOURCE_URI, + sourceImageUri.toString()); + processIntent.putExtra(ProcessingService.SELECTED_URI, + selectedImageUri.toString()); + processIntent.putExtra(ProcessingService.QUALITY, quality); + if (destination != null) { + processIntent.putExtra(ProcessingService.DESTINATION_FILE, destination.toString()); + } + processIntent.putExtra(ProcessingService.PRESET, + preset.getJsonString(context.getString(R.string.saved))); + processIntent.putExtra(ProcessingService.SAVING, true); + if (doFlatten) { + processIntent.putExtra(ProcessingService.FLATTEN, true); + } + return processIntent; + } + + + @Override + public void onCreate() { + mProcessingTaskController = new ProcessingTaskController(this); + mImageSavingTask = new ImageSavingTask(this); + mUpdatePreviewTask = new UpdatePreviewTask(); + mHighresRenderingRequestTask = new HighresRenderingRequestTask(); + mRenderingRequestTask = new RenderingRequestTask(); + mProcessingTaskController.add(mImageSavingTask); + mProcessingTaskController.add(mUpdatePreviewTask); + mProcessingTaskController.add(mHighresRenderingRequestTask); + mProcessingTaskController.add(mRenderingRequestTask); + setupPipeline(); + } + + @Override + public void onDestroy() { + tearDownPipeline(); + mProcessingTaskController.quit(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + mNeedsAlive = true; + if (intent != null && intent.getBooleanExtra(SAVING, false)) { + // we save using an intent to keep the service around after the + // activity has been destroyed. + String presetJson = intent.getStringExtra(PRESET); + String source = intent.getStringExtra(SOURCE_URI); + String selected = intent.getStringExtra(SELECTED_URI); + String destination = intent.getStringExtra(DESTINATION_FILE); + int quality = intent.getIntExtra(QUALITY, 100); + boolean flatten = intent.getBooleanExtra(FLATTEN, false); + Uri sourceUri = Uri.parse(source); + Uri selectedUri = null; + if (selected != null) { + selectedUri = Uri.parse(selected); + } + File destinationFile = null; + if (destination != null) { + destinationFile = new File(destination); + } + ImagePreset preset = new ImagePreset(); + preset.readJsonFromString(presetJson); + mNeedsAlive = false; + mSaving = true; + handleSaveRequest(sourceUri, selectedUri, destinationFile, preset, flatten, quality); + } + return START_REDELIVER_INTENT; + } + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + public void onStart() { + mNeedsAlive = true; + if (!mSaving && mFiltershowActivity != null) { + mFiltershowActivity.updateUIAfterServiceStarted(); + } + } + + public void handleSaveRequest(Uri sourceUri, Uri selectedUri, + File destinationFile, ImagePreset preset, boolean flatten, int quality) { + mNotifyMgr = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + + mNotificationId++; + + mBuilder = + new Notification.Builder(this) + .setSmallIcon(R.drawable.filtershow_button_fx) + .setContentTitle(getString(R.string.filtershow_notification_label)) + .setContentText(getString(R.string.filtershow_notification_message)); + + startForeground(mNotificationId, mBuilder.build()); + + updateProgress(SaveImage.MAX_PROCESSING_STEPS, 0); + + // Process the image + + mImageSavingTask.saveImage(sourceUri, selectedUri, destinationFile, + preset, flatten, quality); + } + + public void updateNotificationWithBitmap(Bitmap bitmap) { + mBuilder.setLargeIcon(bitmap); + mNotifyMgr.notify(mNotificationId, mBuilder.build()); + } + + public void updateProgress(int max, int current) { + mBuilder.setProgress(max, current, false); + mNotifyMgr.notify(mNotificationId, mBuilder.build()); + } + + public void completeSaveImage(Uri result) { + if (SHOW_IMAGE) { + // TODO: we should update the existing image in Gallery instead + Intent viewImage = new Intent(Intent.ACTION_VIEW, result); + viewImage.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(viewImage); + } + stopForeground(true); + stopSelf(); + if (mNeedsAlive) { + // If the app has been restarted while we were saving... + mFiltershowActivity.updateUIAfterServiceStarted(); + } else if (mFiltershowActivity.isSimpleEditAction()) { + // terminate now + mFiltershowActivity.completeSaveImage(result); + } + } + + private void setupPipeline() { + Resources res = getResources(); + FiltersManager.setResources(res); + CachingPipeline.createRenderscriptContext(this); + + FiltersManager filtersManager = FiltersManager.getManager(); + filtersManager.addLooks(this); + filtersManager.addBorders(this); + filtersManager.addTools(this); + filtersManager.addEffects(); + + FiltersManager highresFiltersManager = FiltersManager.getHighresManager(); + highresFiltersManager.addLooks(this); + highresFiltersManager.addBorders(this); + highresFiltersManager.addTools(this); + highresFiltersManager.addEffects(); + } + + private void tearDownPipeline() { + ImageFilter.resetStatics(); + FiltersManager.getPreviewManager().freeRSFilterScripts(); + FiltersManager.getManager().freeRSFilterScripts(); + FiltersManager.getHighresManager().freeRSFilterScripts(); + FiltersManager.reset(); + CachingPipeline.destroyRenderScriptContext(); + } + + static { + System.loadLibrary("jni_filtershow_filters"); + } +} diff --git a/src/com/android/gallery3d/filtershow/pipeline/ProcessingTask.java b/src/com/android/gallery3d/filtershow/pipeline/ProcessingTask.java new file mode 100644 index 000000000..8d3e8110f --- /dev/null +++ b/src/com/android/gallery3d/filtershow/pipeline/ProcessingTask.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2013 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.filtershow.pipeline; + +import android.content.Context; +import android.os.Handler; +import android.os.Message; + +public abstract class ProcessingTask { + private ProcessingTaskController mTaskController; + private Handler mProcessingHandler; + private Handler mResultHandler; + private int mType; + private static final int DELAY = 300; + + static interface Request {} + static interface Update {} + static interface Result {} + + public boolean postRequest(Request message) { + Message msg = mProcessingHandler.obtainMessage(mType); + msg.obj = message; + if (isPriorityTask()) { + if (mProcessingHandler.hasMessages(getType())) { + return false; + } + mProcessingHandler.sendMessageAtFrontOfQueue(msg); + } else if (isDelayedTask()) { + if (mProcessingHandler.hasMessages(getType())) { + mProcessingHandler.removeMessages(getType()); + } + mProcessingHandler.sendMessageDelayed(msg, DELAY); + } else { + mProcessingHandler.sendMessage(msg); + } + return true; + } + + public void postUpdate(Update message) { + Message msg = mResultHandler.obtainMessage(mType); + msg.obj = message; + msg.arg1 = ProcessingTaskController.UPDATE; + mResultHandler.sendMessage(msg); + } + + public void processRequest(Request message) { + Object result = doInBackground(message); + Message msg = mResultHandler.obtainMessage(mType); + msg.obj = result; + msg.arg1 = ProcessingTaskController.RESULT; + mResultHandler.sendMessage(msg); + } + + public void added(ProcessingTaskController taskController) { + mTaskController = taskController; + mResultHandler = taskController.getResultHandler(); + mProcessingHandler = taskController.getProcessingHandler(); + mType = taskController.getReservedType(); + } + + public int getType() { + return mType; + } + + public Context getContext() { + return mTaskController.getContext(); + } + + public abstract Result doInBackground(Request message); + public abstract void onResult(Result message); + public void onUpdate(Update message) {} + public boolean isPriorityTask() { return false; } + public boolean isDelayedTask() { return false; } +} diff --git a/src/com/android/gallery3d/filtershow/pipeline/ProcessingTaskController.java b/src/com/android/gallery3d/filtershow/pipeline/ProcessingTaskController.java new file mode 100644 index 000000000..b54bbb044 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/pipeline/ProcessingTaskController.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2013 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.filtershow.pipeline; + +import android.content.Context; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Message; +import android.util.Log; + +import java.util.HashMap; + +public class ProcessingTaskController implements Handler.Callback { + private static final String LOGTAG = "ProcessingTaskController"; + + private Context mContext; + private HandlerThread mHandlerThread = null; + private Handler mProcessingHandler = null; + private int mCurrentType; + private HashMap<Integer, ProcessingTask> mTasks = new HashMap<Integer, ProcessingTask>(); + + public final static int RESULT = 1; + public final static int UPDATE = 2; + + private final Handler mResultHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + ProcessingTask task = mTasks.get(msg.what); + if (task != null) { + if (msg.arg1 == RESULT) { + task.onResult((ProcessingTask.Result) msg.obj); + } else if (msg.arg1 == UPDATE) { + task.onUpdate((ProcessingTask.Update) msg.obj); + } else { + Log.w(LOGTAG, "received unknown message! " + msg.arg1); + } + } + } + }; + + @Override + public boolean handleMessage(Message msg) { + ProcessingTask task = mTasks.get(msg.what); + if (task != null) { + task.processRequest((ProcessingTask.Request) msg.obj); + return true; + } + return false; + } + + public ProcessingTaskController(Context context) { + mContext = context; + mHandlerThread = new HandlerThread("ProcessingTaskController", + android.os.Process.THREAD_PRIORITY_FOREGROUND); + mHandlerThread.start(); + mProcessingHandler = new Handler(mHandlerThread.getLooper(), this); + } + + public Handler getProcessingHandler() { + return mProcessingHandler; + } + + public Handler getResultHandler() { + return mResultHandler; + } + + public int getReservedType() { + return mCurrentType++; + } + + public Context getContext() { + return mContext; + } + + public void add(ProcessingTask task) { + task.added(this); + mTasks.put(task.getType(), task); + } + + public void quit() { + mHandlerThread.quit(); + } +} diff --git a/src/com/android/gallery3d/filtershow/pipeline/RenderingRequest.java b/src/com/android/gallery3d/filtershow/pipeline/RenderingRequest.java new file mode 100644 index 000000000..ef4bb9bc0 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/pipeline/RenderingRequest.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2013 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.filtershow.pipeline; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Rect; +import com.android.gallery3d.app.Log; +import com.android.gallery3d.filtershow.FilterShowActivity; +import com.android.gallery3d.filtershow.filters.FiltersManager; +import com.android.gallery3d.filtershow.imageshow.MasterImage; + +public class RenderingRequest { + private static final String LOGTAG = "RenderingRequest"; + private boolean mIsDirect = false; + private Bitmap mBitmap = null; + private ImagePreset mImagePreset = null; + private ImagePreset mOriginalImagePreset = null; + private RenderingRequestCaller mCaller = null; + private float mScaleFactor = 1.0f; + private Rect mBounds = null; + private Rect mDestination = null; + private int mType = FULL_RENDERING; + public static final int FULL_RENDERING = 0; + public static final int FILTERS_RENDERING = 1; + public static final int GEOMETRY_RENDERING = 2; + public static final int ICON_RENDERING = 3; + public static final int PARTIAL_RENDERING = 4; + public static final int HIGHRES_RENDERING = 5; + public static final int STYLE_ICON_RENDERING = 6; + + private static final Bitmap.Config mConfig = Bitmap.Config.ARGB_8888; + + public static void post(Context context, Bitmap source, ImagePreset preset, + int type, RenderingRequestCaller caller) { + RenderingRequest.post(context, source, preset, type, caller, null, null); + } + + public static void post(Context context, Bitmap source, ImagePreset preset, int type, + RenderingRequestCaller caller, Rect bounds, Rect destination) { + if (((type != PARTIAL_RENDERING && type != HIGHRES_RENDERING) && source == null) + || preset == null || caller == null) { + Log.v(LOGTAG, "something null: source: " + source + + " or preset: " + preset + " or caller: " + caller); + return; + } + RenderingRequest request = new RenderingRequest(); + Bitmap bitmap = null; + if (type == FULL_RENDERING + || type == GEOMETRY_RENDERING + || type == ICON_RENDERING + || type == STYLE_ICON_RENDERING) { + CachingPipeline pipeline = new CachingPipeline( + FiltersManager.getManager(), "Icon"); + bitmap = pipeline.renderGeometryIcon(source, preset); + } else if (type != PARTIAL_RENDERING && type != HIGHRES_RENDERING) { + bitmap = Bitmap.createBitmap(source.getWidth(), source.getHeight(), mConfig); + } + + request.setBitmap(bitmap); + ImagePreset passedPreset = new ImagePreset(preset); + request.setOriginalImagePreset(preset); + request.setScaleFactor(MasterImage.getImage().getScaleFactor()); + + if (type == PARTIAL_RENDERING) { + request.setBounds(bounds); + request.setDestination(destination); + passedPreset.setPartialRendering(true, bounds); + } + + request.setImagePreset(passedPreset); + request.setType(type); + request.setCaller(caller); + request.post(context); + } + + public void post(Context context) { + if (context instanceof FilterShowActivity) { + FilterShowActivity activity = (FilterShowActivity) context; + ProcessingService service = activity.getProcessingService(); + service.postRenderingRequest(this); + } + } + + public void markAvailable() { + if (mBitmap == null || mImagePreset == null + || mCaller == null) { + return; + } + mCaller.available(this); + } + + public boolean isDirect() { + return mIsDirect; + } + + public void setDirect(boolean isDirect) { + mIsDirect = isDirect; + } + + public Bitmap getBitmap() { + return mBitmap; + } + + public void setBitmap(Bitmap bitmap) { + mBitmap = bitmap; + } + + public ImagePreset getImagePreset() { + return mImagePreset; + } + + public void setImagePreset(ImagePreset imagePreset) { + mImagePreset = imagePreset; + } + + public int getType() { + return mType; + } + + public void setType(int type) { + mType = type; + } + + public void setCaller(RenderingRequestCaller caller) { + mCaller = caller; + } + + public Rect getBounds() { + return mBounds; + } + + public void setBounds(Rect bounds) { + mBounds = bounds; + } + + public void setScaleFactor(float scaleFactor) { + mScaleFactor = scaleFactor; + } + + public float getScaleFactor() { + return mScaleFactor; + } + + public Rect getDestination() { + return mDestination; + } + + public void setDestination(Rect destination) { + mDestination = destination; + } + + public ImagePreset getOriginalImagePreset() { + return mOriginalImagePreset; + } + + public void setOriginalImagePreset(ImagePreset originalImagePreset) { + mOriginalImagePreset = originalImagePreset; + } +} diff --git a/src/com/android/gallery3d/filtershow/pipeline/RenderingRequestCaller.java b/src/com/android/gallery3d/filtershow/pipeline/RenderingRequestCaller.java new file mode 100644 index 000000000..b978e7040 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/pipeline/RenderingRequestCaller.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2013 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.filtershow.pipeline; + +public interface RenderingRequestCaller { + public void available(RenderingRequest request); +} diff --git a/src/com/android/gallery3d/filtershow/pipeline/RenderingRequestTask.java b/src/com/android/gallery3d/filtershow/pipeline/RenderingRequestTask.java new file mode 100644 index 000000000..7a83f7072 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/pipeline/RenderingRequestTask.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2013 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.filtershow.pipeline; + +import android.graphics.Bitmap; +import com.android.gallery3d.filtershow.filters.FiltersManager; + +public class RenderingRequestTask extends ProcessingTask { + + private CachingPipeline mPreviewPipeline = null; + private boolean mPipelineIsOn = false; + + public void setPreviewScaleFactor(float previewScale) { + mPreviewPipeline.setPreviewScaleFactor(previewScale); + } + + static class Render implements Request { + RenderingRequest request; + } + + static class RenderResult implements Result { + RenderingRequest request; + } + + public RenderingRequestTask() { + mPreviewPipeline = new CachingPipeline( + FiltersManager.getManager(), "Normal"); + } + + public void setOriginal(Bitmap bitmap) { + mPreviewPipeline.setOriginal(bitmap); + mPipelineIsOn = true; + } + + public void stop() { + mPreviewPipeline.stop(); + } + + public void postRenderingRequest(RenderingRequest request) { + if (!mPipelineIsOn) { + return; + } + Render render = new Render(); + render.request = request; + postRequest(render); + } + + @Override + public Result doInBackground(Request message) { + RenderingRequest request = ((Render) message).request; + RenderResult result = null; + mPreviewPipeline.render(request); + result = new RenderResult(); + result.request = request; + return result; + } + + @Override + public void onResult(Result message) { + if (message == null) { + return; + } + RenderingRequest request = ((RenderResult) message).request; + request.markAvailable(); + } + +} diff --git a/src/com/android/gallery3d/filtershow/pipeline/SharedBuffer.java b/src/com/android/gallery3d/filtershow/pipeline/SharedBuffer.java new file mode 100644 index 000000000..98e69f60e --- /dev/null +++ b/src/com/android/gallery3d/filtershow/pipeline/SharedBuffer.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2013 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.filtershow.pipeline; + +import android.graphics.Bitmap; + +public class SharedBuffer { + + private static final String LOGTAG = "SharedBuffer"; + + private volatile Buffer mProducer = null; + private volatile Buffer mConsumer = null; + private volatile Buffer mIntermediate = null; + + private volatile boolean mNeedsSwap = false; + private volatile boolean mNeedsRepaint = true; + + public void setProducer(Bitmap producer) { + Buffer buffer = new Buffer(producer); + synchronized (this) { + mProducer = buffer; + } + } + + public synchronized Buffer getProducer() { + return mProducer; + } + + public synchronized Buffer getConsumer() { + return mConsumer; + } + + public synchronized void swapProducer() { + Buffer intermediate = mIntermediate; + mIntermediate = mProducer; + mProducer = intermediate; + mNeedsSwap = true; + } + + public synchronized void swapConsumerIfNeeded() { + if (!mNeedsSwap) { + return; + } + Buffer intermediate = mIntermediate; + mIntermediate = mConsumer; + mConsumer = intermediate; + mNeedsSwap = false; + } + + public synchronized void invalidate() { + mNeedsRepaint = true; + } + + public synchronized boolean checkRepaintNeeded() { + if (mNeedsRepaint) { + mNeedsRepaint = false; + return true; + } + return false; + } + +} + diff --git a/src/com/android/gallery3d/filtershow/pipeline/SharedPreset.java b/src/com/android/gallery3d/filtershow/pipeline/SharedPreset.java new file mode 100644 index 000000000..3f850fed2 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/pipeline/SharedPreset.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2013 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.filtershow.pipeline; + +public class SharedPreset { + + private volatile ImagePreset mProducerPreset = null; + private volatile ImagePreset mConsumerPreset = null; + private volatile ImagePreset mIntermediatePreset = null; + + public synchronized void enqueuePreset(ImagePreset preset) { + if (mProducerPreset == null || (!mProducerPreset.same(preset))) { + mProducerPreset = new ImagePreset(preset); + } else { + mProducerPreset.updateWith(preset); + } + ImagePreset temp = mIntermediatePreset; + mIntermediatePreset = mProducerPreset; + mProducerPreset = temp; + } + + public synchronized ImagePreset dequeuePreset() { + ImagePreset temp = mConsumerPreset; + mConsumerPreset = mIntermediatePreset; + mIntermediatePreset = temp; + return mConsumerPreset; + } +} diff --git a/src/com/android/gallery3d/filtershow/pipeline/UpdatePreviewTask.java b/src/com/android/gallery3d/filtershow/pipeline/UpdatePreviewTask.java new file mode 100644 index 000000000..406cc9bf5 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/pipeline/UpdatePreviewTask.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2013 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.filtershow.pipeline; + +import android.graphics.Bitmap; +import com.android.gallery3d.filtershow.filters.FiltersManager; +import com.android.gallery3d.filtershow.imageshow.MasterImage; + +public class UpdatePreviewTask extends ProcessingTask { + private CachingPipeline mPreviewPipeline = null; + private boolean mHasUnhandledPreviewRequest = false; + private boolean mPipelineIsOn = false; + + public UpdatePreviewTask() { + mPreviewPipeline = new CachingPipeline( + FiltersManager.getPreviewManager(), "Preview"); + } + + public void setOriginal(Bitmap bitmap) { + mPreviewPipeline.setOriginal(bitmap); + mPipelineIsOn = true; + } + + public void updatePreview() { + if (!mPipelineIsOn) { + return; + } + mHasUnhandledPreviewRequest = true; + if (postRequest(null)) { + mHasUnhandledPreviewRequest = false; + } + } + + @Override + public boolean isPriorityTask() { + return true; + } + + @Override + public Result doInBackground(Request message) { + SharedBuffer buffer = MasterImage.getImage().getPreviewBuffer(); + SharedPreset preset = MasterImage.getImage().getPreviewPreset(); + ImagePreset renderingPreset = preset.dequeuePreset(); + if (renderingPreset != null) { + mPreviewPipeline.compute(buffer, renderingPreset, 0); + // set the preset we used in the buffer for later inspection UI-side + buffer.getProducer().setPreset(renderingPreset); + buffer.getProducer().sync(); + buffer.swapProducer(); // push back the result + } + return null; + } + + @Override + public void onResult(Result message) { + MasterImage.getImage().notifyObservers(); + if (mHasUnhandledPreviewRequest) { + updatePreview(); + } + } + + public void setPipelineIsOn(boolean pipelineIsOn) { + mPipelineIsOn = pipelineIsOn; + } +} diff --git a/src/com/android/gallery3d/filtershow/presets/PresetManagementDialog.java b/src/com/android/gallery3d/filtershow/presets/PresetManagementDialog.java new file mode 100644 index 000000000..7ab61fcc9 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/presets/PresetManagementDialog.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2013 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.filtershow.presets; + +import android.os.Bundle; +import android.support.v4.app.DialogFragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ListView; +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.FilterShowActivity; + +public class PresetManagementDialog extends DialogFragment implements View.OnClickListener { + private UserPresetsAdapter mAdapter; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.filtershow_presets_management_dialog, container); + + FilterShowActivity activity = (FilterShowActivity) getActivity(); + mAdapter = activity.getUserPresetsAdapter(); + ListView panel = (ListView) view.findViewById(R.id.listItems); + panel.setAdapter(mAdapter); + + view.findViewById(R.id.cancel).setOnClickListener(this); + view.findViewById(R.id.addpreset).setOnClickListener(this); + view.findViewById(R.id.ok).setOnClickListener(this); + getDialog().setTitle(getString(R.string.filtershow_manage_preset)); + return view; + } + + @Override + public void onClick(View v) { + FilterShowActivity activity = (FilterShowActivity) getActivity(); + switch (v.getId()) { + case R.id.cancel: + mAdapter.clearChangedRepresentations(); + mAdapter.clearDeletedRepresentations(); + activity.updateUserPresetsFromAdapter(mAdapter); + dismiss(); + break; + case R.id.addpreset: + activity.saveCurrentImagePreset(); + dismiss(); + break; + case R.id.ok: + mAdapter.updateCurrent(); + activity.updateUserPresetsFromAdapter(mAdapter); + dismiss(); + break; + } + } +} diff --git a/src/com/android/gallery3d/filtershow/presets/UserPresetsAdapter.java b/src/com/android/gallery3d/filtershow/presets/UserPresetsAdapter.java new file mode 100644 index 000000000..dab9ea454 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/presets/UserPresetsAdapter.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2013 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.filtershow.presets; + +import android.content.Context; +import android.graphics.Rect; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.ImageView; +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.category.Action; +import com.android.gallery3d.filtershow.category.CategoryView; +import com.android.gallery3d.filtershow.filters.FilterRepresentation; +import com.android.gallery3d.filtershow.filters.FilterUserPresetRepresentation; + +import java.util.ArrayList; + +public class UserPresetsAdapter extends ArrayAdapter<Action> + implements View.OnClickListener, View.OnFocusChangeListener { + private static final String LOGTAG = "UserPresetsAdapter"; + private LayoutInflater mInflater; + private int mIconSize = 160; + private ArrayList<FilterUserPresetRepresentation> mDeletedRepresentations = + new ArrayList<FilterUserPresetRepresentation>(); + private ArrayList<FilterUserPresetRepresentation> mChangedRepresentations = + new ArrayList<FilterUserPresetRepresentation>(); + private EditText mCurrentEditText; + + public UserPresetsAdapter(Context context, int textViewResourceId) { + super(context, textViewResourceId); + mInflater = LayoutInflater.from(context); + mIconSize = context.getResources().getDimensionPixelSize(R.dimen.category_panel_icon_size); + } + + public UserPresetsAdapter(Context context) { + this(context, 0); + } + + @Override + public void add(Action action) { + super.add(action); + action.setAdapter(this); + } + + private void deletePreset(Action action) { + FilterRepresentation rep = action.getRepresentation(); + if (rep instanceof FilterUserPresetRepresentation) { + mDeletedRepresentations.add((FilterUserPresetRepresentation) rep); + } + remove(action); + notifyDataSetChanged(); + } + + private void changePreset(Action action) { + FilterRepresentation rep = action.getRepresentation(); + rep.setName(action.getName()); + if (rep instanceof FilterUserPresetRepresentation) { + mChangedRepresentations.add((FilterUserPresetRepresentation) rep); + } + } + + public void updateCurrent() { + if (mCurrentEditText != null) { + updateActionFromEditText(mCurrentEditText); + } + } + + static class UserPresetViewHolder { + ImageView imageView; + EditText editText; + ImageButton deleteButton; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + UserPresetViewHolder viewHolder; + if (convertView == null) { + convertView = mInflater.inflate(R.layout.filtershow_presets_management_row, null); + viewHolder = new UserPresetViewHolder(); + viewHolder.imageView = (ImageView) convertView.findViewById(R.id.imageView); + viewHolder.editText = (EditText) convertView.findViewById(R.id.editView); + viewHolder.deleteButton = (ImageButton) convertView.findViewById(R.id.deleteUserPreset); + viewHolder.editText.setOnClickListener(this); + viewHolder.editText.setOnFocusChangeListener(this); + viewHolder.deleteButton.setOnClickListener(this); + convertView.setTag(viewHolder); + } else { + viewHolder = (UserPresetViewHolder) convertView.getTag(); + } + Action action = getItem(position); + viewHolder.imageView.setImageBitmap(action.getImage()); + if (action.getImage() == null) { + // queue image rendering for this action + action.setImageFrame(new Rect(0, 0, mIconSize, mIconSize), CategoryView.VERTICAL); + } + viewHolder.deleteButton.setTag(action); + viewHolder.editText.setTag(action); + viewHolder.editText.setHint(action.getName()); + + return convertView; + } + + public ArrayList<FilterUserPresetRepresentation> getDeletedRepresentations() { + return mDeletedRepresentations; + } + + public void clearDeletedRepresentations() { + mDeletedRepresentations.clear(); + } + + public ArrayList<FilterUserPresetRepresentation> getChangedRepresentations() { + return mChangedRepresentations; + } + + public void clearChangedRepresentations() { + mChangedRepresentations.clear(); + } + + @Override + public void onClick(View v) { + switch (v.getId()) { + case R.id.editView: + v.requestFocus(); + break; + case R.id.deleteUserPreset: + Action action = (Action) v.getTag(); + deletePreset(action); + break; + } + } + + @Override + public void onFocusChange(View v, boolean hasFocus) { + if (v.getId() != R.id.editView) { + return; + } + EditText editText = (EditText) v; + if (!hasFocus) { + updateActionFromEditText(editText); + } else { + mCurrentEditText = editText; + } + } + + private void updateActionFromEditText(EditText editText) { + Action action = (Action) editText.getTag(); + String newName = editText.getText().toString(); + if (newName.length() > 0) { + action.setName(editText.getText().toString()); + changePreset(action); + } + } +} diff --git a/src/com/android/gallery3d/filtershow/provider/SharedImageProvider.java b/src/com/android/gallery3d/filtershow/provider/SharedImageProvider.java new file mode 100644 index 000000000..bc17a6e03 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/provider/SharedImageProvider.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2012 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.filtershow.provider; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.net.Uri; +import android.os.ConditionVariable; +import android.os.ParcelFileDescriptor; +import android.provider.BaseColumns; +import android.provider.MediaStore; +import android.provider.OpenableColumns; + +import java.io.File; +import java.io.FileNotFoundException; + +public class SharedImageProvider extends ContentProvider { + + private static final String LOGTAG = "SharedImageProvider"; + + public static final String MIME_TYPE = "image/jpeg"; + public static final String AUTHORITY = "com.android.gallery3d.filtershow.provider.SharedImageProvider"; + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/image"); + public static final String PREPARE = "prepare"; + + private final String[] mMimeStreamType = { + MIME_TYPE + }; + + private static ConditionVariable mImageReadyCond = new ConditionVariable(false); + + @Override + public int delete(Uri arg0, String arg1, String[] arg2) { + return 0; + } + + @Override + public String getType(Uri arg0) { + return MIME_TYPE; + } + + @Override + public String[] getStreamTypes(Uri arg0, String mimeTypeFilter) { + return mMimeStreamType; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + if (values.containsKey(PREPARE)) { + if (values.getAsBoolean(PREPARE)) { + mImageReadyCond.close(); + } else { + mImageReadyCond.open(); + } + } + return null; + } + + @Override + public int update(Uri arg0, ContentValues arg1, String arg2, String[] arg3) { + return 0; + } + + @Override + public boolean onCreate() { + return true; + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + String uriPath = uri.getLastPathSegment(); + if (uriPath == null) { + return null; + } + if (projection == null) { + projection = new String[] { + BaseColumns._ID, + MediaStore.MediaColumns.DATA, + OpenableColumns.DISPLAY_NAME, + OpenableColumns.SIZE + }; + } + // If we receive a query on display name or size, + // we should block until the image is ready + mImageReadyCond.block(); + + File path = new File(uriPath); + + MatrixCursor cursor = new MatrixCursor(projection); + Object[] columns = new Object[projection.length]; + for (int i = 0; i < projection.length; i++) { + if (projection[i].equalsIgnoreCase(BaseColumns._ID)) { + columns[i] = 0; + } else if (projection[i].equalsIgnoreCase(MediaStore.MediaColumns.DATA)) { + columns[i] = uri; + } else if (projection[i].equalsIgnoreCase(OpenableColumns.DISPLAY_NAME)) { + columns[i] = path.getName(); + } else if (projection[i].equalsIgnoreCase(OpenableColumns.SIZE)) { + columns[i] = path.length(); + } + } + cursor.addRow(columns); + + return cursor; + } + + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode) + throws FileNotFoundException { + String uriPath = uri.getLastPathSegment(); + if (uriPath == null) { + return null; + } + // Here we need to block until the image is ready + mImageReadyCond.block(); + File path = new File(uriPath); + int imode = 0; + imode |= ParcelFileDescriptor.MODE_READ_ONLY; + return ParcelFileDescriptor.open(path, imode); + } +} diff --git a/src/com/android/gallery3d/filtershow/state/DragListener.java b/src/com/android/gallery3d/filtershow/state/DragListener.java new file mode 100644 index 000000000..1aa81ed69 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/state/DragListener.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2013 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.filtershow.state; + +import android.view.DragEvent; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.LinearLayout; + +class DragListener implements View.OnDragListener { + + private static final String LOGTAG = "DragListener"; + private PanelTrack mStatePanelTrack; + private static float sSlope = 0.2f; + + public DragListener(PanelTrack statePanelTrack) { + mStatePanelTrack = statePanelTrack; + } + + private void setState(DragEvent event) { + float translation = event.getY() - mStatePanelTrack.getTouchPoint().y; + float alpha = 1.0f - (Math.abs(translation) + / mStatePanelTrack.getCurrentView().getHeight()); + if (mStatePanelTrack.getOrientation() == LinearLayout.VERTICAL) { + translation = event.getX() - mStatePanelTrack.getTouchPoint().x; + alpha = 1.0f - (Math.abs(translation) + / mStatePanelTrack.getCurrentView().getWidth()); + mStatePanelTrack.getCurrentView().setTranslationX(translation); + } else { + mStatePanelTrack.getCurrentView().setTranslationY(translation); + } + mStatePanelTrack.getCurrentView().setBackgroundAlpha(alpha); + } + + @Override + public boolean onDrag(View v, DragEvent event) { + switch (event.getAction()) { + case DragEvent.ACTION_DRAG_STARTED: { + break; + } + case DragEvent.ACTION_DRAG_LOCATION: { + if (mStatePanelTrack.getCurrentView() != null) { + setState(event); + View over = mStatePanelTrack.findChildAt((int) event.getX(), + (int) event.getY()); + if (over != null && over != mStatePanelTrack.getCurrentView()) { + StateView stateView = (StateView) over; + if (stateView != mStatePanelTrack.getCurrentView()) { + int pos = mStatePanelTrack.findChild(over); + int origin = mStatePanelTrack.findChild( + mStatePanelTrack.getCurrentView()); + ArrayAdapter array = (ArrayAdapter) mStatePanelTrack.getAdapter(); + if (origin != -1 && pos != -1) { + State current = (State) array.getItem(origin); + array.remove(current); + array.insert(current, pos); + mStatePanelTrack.fillContent(false); + mStatePanelTrack.setCurrentView(mStatePanelTrack.getChildAt(pos)); + } + } + } + } + break; + } + case DragEvent.ACTION_DRAG_ENTERED: { + mStatePanelTrack.setExited(false); + if (mStatePanelTrack.getCurrentView() != null) { + mStatePanelTrack.getCurrentView().setVisibility(View.VISIBLE); + } + return true; + } + case DragEvent.ACTION_DRAG_EXITED: { + if (mStatePanelTrack.getCurrentView() != null) { + setState(event); + mStatePanelTrack.getCurrentView().setVisibility(View.INVISIBLE); + } + mStatePanelTrack.setExited(true); + break; + } + case DragEvent.ACTION_DROP: { + break; + } + case DragEvent.ACTION_DRAG_ENDED: { + if (mStatePanelTrack.getCurrentView() != null + && mStatePanelTrack.getCurrentView().getAlpha() > sSlope) { + setState(event); + } + mStatePanelTrack.checkEndState(); + break; + } + default: + break; + } + return true; + } +} diff --git a/src/com/android/gallery3d/filtershow/state/PanelTrack.java b/src/com/android/gallery3d/filtershow/state/PanelTrack.java new file mode 100644 index 000000000..d02207d9b --- /dev/null +++ b/src/com/android/gallery3d/filtershow/state/PanelTrack.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2013 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.filtershow.state; + +import android.graphics.Point; +import android.view.MotionEvent; +import android.view.View; +import android.widget.Adapter; + +public interface PanelTrack { + public int getOrientation(); + public void onTouch(MotionEvent event, StateView view); + public StateView getCurrentView(); + public void setCurrentView(View view); + public Point getTouchPoint(); + public View findChildAt(int x, int y); + public int findChild(View view); + public Adapter getAdapter(); + public void fillContent(boolean value); + public View getChildAt(int pos); + public void setExited(boolean value); + public void checkEndState(); +} diff --git a/src/com/android/gallery3d/filtershow/state/State.java b/src/com/android/gallery3d/filtershow/state/State.java new file mode 100644 index 000000000..e7dedd6a2 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/state/State.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2013 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.filtershow.state; + +import com.android.gallery3d.filtershow.filters.FilterFxRepresentation; +import com.android.gallery3d.filtershow.filters.FilterRepresentation; + +public class State { + private String mText; + private int mType; + private FilterRepresentation mFilterRepresentation; + + public State(State state) { + this(state.getText(), state.getType()); + } + + public State(String text) { + this(text, StateView.DEFAULT); + } + + public State(String text, int type) { + mText = text; + mType = type; + } + + public boolean equals(State state) { + if (mFilterRepresentation.getFilterClass() + != state.mFilterRepresentation.getFilterClass()) { + return false; + } + if (mFilterRepresentation instanceof FilterFxRepresentation) { + return mFilterRepresentation.equals(state.getFilterRepresentation()); + } + return true; + } + + public boolean isDraggable() { + return mFilterRepresentation != null; + } + + String getText() { + return mText; + } + + void setText(String text) { + mText = text; + } + + int getType() { + return mType; + } + + void setType(int type) { + mType = type; + } + + public FilterRepresentation getFilterRepresentation() { + return mFilterRepresentation; + } + + public void setFilterRepresentation(FilterRepresentation filterRepresentation) { + mFilterRepresentation = filterRepresentation; + } +} diff --git a/src/com/android/gallery3d/filtershow/state/StateAdapter.java b/src/com/android/gallery3d/filtershow/state/StateAdapter.java new file mode 100644 index 000000000..522585280 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/state/StateAdapter.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2013 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.filtershow.state; + +import android.content.Context; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.FilterShowActivity; +import com.android.gallery3d.filtershow.editors.ImageOnlyEditor; +import com.android.gallery3d.filtershow.filters.FilterRepresentation; +import com.android.gallery3d.filtershow.imageshow.MasterImage; + +import java.util.Vector; + +public class StateAdapter extends ArrayAdapter<State> { + + private static final String LOGTAG = "StateAdapter"; + private int mOrientation; + private String mOriginalText; + private String mResultText; + + public StateAdapter(Context context, int textViewResourceId) { + super(context, textViewResourceId); + mOriginalText = context.getString(R.string.state_panel_original); + mResultText = context.getString(R.string.state_panel_result); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + StateView view = null; + if (convertView == null) { + convertView = new StateView(getContext()); + } + view = (StateView) convertView; + State state = getItem(position); + view.setState(state); + view.setOrientation(mOrientation); + FilterRepresentation currentRep = MasterImage.getImage().getCurrentFilterRepresentation(); + FilterRepresentation stateRep = state.getFilterRepresentation(); + if (currentRep != null && stateRep != null + && currentRep.getFilterClass() == stateRep.getFilterClass() + && currentRep.getEditorId() != ImageOnlyEditor.ID) { + view.setSelected(true); + } else { + view.setSelected(false); + } + return view; + } + + public boolean contains(State state) { + for (int i = 0; i < getCount(); i++) { + if (state == getItem(i)) { + return true; + } + } + return false; + } + + public void setOrientation(int orientation) { + mOrientation = orientation; + } + + public void addOriginal() { + add(new State(mOriginalText)); + } + + public boolean same(Vector<State> states) { + // we have the original state in addition + if (states.size() + 1 != getCount()) { + return false; + } + for (int i = 1; i < getCount(); i++) { + State state = getItem(i); + if (!state.equals(states.elementAt(i-1))) { + return false; + } + } + return true; + } + + public void fill(Vector<State> states) { + if (same(states)) { + return; + } + clear(); + addOriginal(); + addAll(states); + notifyDataSetChanged(); + } + + @Override + public void remove(State state) { + super.remove(state); + FilterRepresentation filterRepresentation = state.getFilterRepresentation(); + FilterShowActivity activity = (FilterShowActivity) getContext(); + activity.removeFilterRepresentation(filterRepresentation); + } +} diff --git a/src/com/android/gallery3d/filtershow/state/StatePanel.java b/src/com/android/gallery3d/filtershow/state/StatePanel.java new file mode 100644 index 000000000..df470f23e --- /dev/null +++ b/src/com/android/gallery3d/filtershow/state/StatePanel.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2013 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.filtershow.state; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.imageshow.MasterImage; + +public class StatePanel extends Fragment { + private static final String LOGTAG = "StatePanel"; + private StatePanelTrack track; + private LinearLayout mMainView; + public static final String FRAGMENT_TAG = "StatePanel"; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + mMainView = (LinearLayout) inflater.inflate(R.layout.filtershow_state_panel_new, null); + View panel = mMainView.findViewById(R.id.listStates); + track = (StatePanelTrack) panel; + track.setAdapter(MasterImage.getImage().getState()); + return mMainView; + } +} diff --git a/src/com/android/gallery3d/filtershow/state/StatePanelTrack.java b/src/com/android/gallery3d/filtershow/state/StatePanelTrack.java new file mode 100644 index 000000000..fff7e7f5f --- /dev/null +++ b/src/com/android/gallery3d/filtershow/state/StatePanelTrack.java @@ -0,0 +1,351 @@ +/* + * Copyright (C) 2013 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.filtershow.state; + +import android.animation.LayoutTransition; +import android.content.Context; +import android.content.res.TypedArray; +import android.database.DataSetObserver; +import android.graphics.Canvas; +import android.graphics.Point; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.util.Log; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Adapter; +import android.widget.LinearLayout; +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.FilterShowActivity; +import com.android.gallery3d.filtershow.editors.ImageOnlyEditor; +import com.android.gallery3d.filtershow.filters.FilterRepresentation; +import com.android.gallery3d.filtershow.imageshow.MasterImage; + +public class StatePanelTrack extends LinearLayout implements PanelTrack { + + private static final String LOGTAG = "StatePanelTrack"; + private Point mTouchPoint; + private StateView mCurrentView; + private StateView mCurrentSelectedView; + private boolean mExited = false; + private boolean mStartedDrag = false; + private StateAdapter mAdapter; + private DragListener mDragListener = new DragListener(this); + private float mDeleteSlope = 0.2f; + private GestureDetector mGestureDetector; + private int mElemWidth; + private int mElemHeight; + private int mElemSize; + private int mElemEndSize; + private int mEndElemWidth; + private int mEndElemHeight; + private long mTouchTime; + private int mMaxTouchDelay = 300; // 300ms delay for touch + private static final boolean ALLOWS_DRAG = false; + private DataSetObserver mObserver = new DataSetObserver() { + @Override + public void onChanged() { + super.onChanged(); + fillContent(false); + } + + @Override + public void onInvalidated() { + super.onInvalidated(); + fillContent(false); + } + }; + + public StatePanelTrack(Context context, AttributeSet attrs) { + super(context, attrs); + TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.StatePanelTrack); + mElemSize = a.getDimensionPixelSize(R.styleable.StatePanelTrack_elemSize, 0); + mElemEndSize = a.getDimensionPixelSize(R.styleable.StatePanelTrack_elemEndSize, 0); + if (getOrientation() == LinearLayout.HORIZONTAL) { + mElemWidth = mElemSize; + mElemHeight = LayoutParams.MATCH_PARENT; + mEndElemWidth = mElemEndSize; + mEndElemHeight = LayoutParams.MATCH_PARENT; + } else { + mElemWidth = LayoutParams.MATCH_PARENT; + mElemHeight = mElemSize; + mEndElemWidth = LayoutParams.MATCH_PARENT; + mEndElemHeight = mElemEndSize; + } + GestureDetector.SimpleOnGestureListener simpleOnGestureListener + = new GestureDetector.SimpleOnGestureListener(){ + @Override + public void onLongPress(MotionEvent e) { + longPress(e); + } + @Override + public boolean onDoubleTap(MotionEvent e) { + addDuplicate(e); + return true; + } + }; + mGestureDetector = new GestureDetector(context, simpleOnGestureListener); + } + + private void addDuplicate(MotionEvent e) { + if (mCurrentSelectedView == null) { + return; + } + int pos = findChild(mCurrentSelectedView); + if (pos != -1) { + mAdapter.insert(new State(mCurrentSelectedView.getState()), pos); + fillContent(true); + } + } + + private void longPress(MotionEvent e) { + View view = findChildAt((int) e.getX(), (int) e.getY()); + if (view == null) { + return; + } + if (view instanceof StateView) { + StateView stateView = (StateView) view; + stateView.setDuplicateButton(true); + } + } + + public void setAdapter(StateAdapter adapter) { + mAdapter = adapter; + mAdapter.registerDataSetObserver(mObserver); + mAdapter.setOrientation(getOrientation()); + fillContent(false); + requestLayout(); + } + + public StateView findChildWithState(State state) { + for (int i = 0; i < getChildCount(); i++) { + StateView view = (StateView) getChildAt(i); + if (view.getState() == state) { + return view; + } + } + return null; + } + + public void fillContent(boolean animate) { + if (!animate) { + this.setLayoutTransition(null); + } + int n = mAdapter.getCount(); + for (int i = 0; i < getChildCount(); i++) { + StateView child = (StateView) getChildAt(i); + child.resetPosition(); + if (!mAdapter.contains(child.getState())) { + removeView(child); + } + } + LayoutParams params = new LayoutParams(mElemWidth, mElemHeight); + for (int i = 0; i < n; i++) { + State s = mAdapter.getItem(i); + if (findChildWithState(s) == null) { + View view = mAdapter.getView(i, null, this); + addView(view, i, params); + } + } + + for (int i = 0; i < n; i++) { + State state = mAdapter.getItem(i); + StateView view = (StateView) getChildAt(i); + view.setState(state); + if (i == 0) { + view.setType(StateView.BEGIN); + } else if (i == n - 1) { + view.setType(StateView.END); + } else { + view.setType(StateView.DEFAULT); + } + view.resetPosition(); + } + + if (!animate) { + this.setLayoutTransition(new LayoutTransition()); + } + } + + public void onTouch(MotionEvent event, StateView view) { + if (!view.isDraggable()) { + return; + } + mCurrentView = view; + if (mCurrentSelectedView == mCurrentView) { + return; + } + if (mCurrentSelectedView != null) { + mCurrentSelectedView.setSelected(false); + } + // We changed the current view -- let's reset the + // gesture detector. + MotionEvent cancelEvent = MotionEvent.obtain(event); + cancelEvent.setAction(MotionEvent.ACTION_CANCEL); + mGestureDetector.onTouchEvent(cancelEvent); + mCurrentSelectedView = mCurrentView; + // We have to send the event to the gesture detector + mGestureDetector.onTouchEvent(event); + mTouchTime = System.currentTimeMillis(); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + if (mCurrentView != null) { + return true; + } + return false; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (mCurrentView == null) { + return false; + } + if (mTouchTime == 0) { + mTouchTime = System.currentTimeMillis(); + } + mGestureDetector.onTouchEvent(event); + if (mTouchPoint == null) { + mTouchPoint = new Point(); + mTouchPoint.x = (int) event.getX(); + mTouchPoint.y = (int) event.getY(); + } + + if (event.getActionMasked() == MotionEvent.ACTION_MOVE) { + float translation = event.getY() - mTouchPoint.y; + float alpha = 1.0f - (Math.abs(translation) / mCurrentView.getHeight()); + if (getOrientation() == LinearLayout.VERTICAL) { + translation = event.getX() - mTouchPoint.x; + alpha = 1.0f - (Math.abs(translation) / mCurrentView.getWidth()); + mCurrentView.setTranslationX(translation); + } else { + mCurrentView.setTranslationY(translation); + } + mCurrentView.setBackgroundAlpha(alpha); + if (ALLOWS_DRAG && alpha < 0.7) { + setOnDragListener(mDragListener); + DragShadowBuilder shadowBuilder = new DragShadowBuilder(mCurrentView); + mCurrentView.startDrag(null, shadowBuilder, mCurrentView, 0); + mStartedDrag = true; + } + } + if (!mExited && mCurrentView != null + && mCurrentView.getBackgroundAlpha() > mDeleteSlope + && event.getActionMasked() == MotionEvent.ACTION_UP + && System.currentTimeMillis() - mTouchTime < mMaxTouchDelay) { + FilterRepresentation representation = mCurrentView.getState().getFilterRepresentation(); + mCurrentView.setSelected(true); + if (representation != MasterImage.getImage().getCurrentFilterRepresentation()) { + FilterShowActivity activity = (FilterShowActivity) getContext(); + activity.showRepresentation(representation); + mCurrentView.setSelected(false); + } + } + if (event.getActionMasked() == MotionEvent.ACTION_UP + || (!mStartedDrag && event.getActionMasked() == MotionEvent.ACTION_CANCEL)) { + checkEndState(); + if (mCurrentView != null) { + FilterRepresentation representation = mCurrentView.getState().getFilterRepresentation(); + if (representation.getEditorId() == ImageOnlyEditor.ID) { + mCurrentView.setSelected(false); + } + } + } + return true; + } + + public void checkEndState() { + mTouchPoint = null; + mTouchTime = 0; + if (mExited || mCurrentView.getBackgroundAlpha() < mDeleteSlope) { + int origin = findChild(mCurrentView); + if (origin != -1) { + State current = mAdapter.getItem(origin); + FilterRepresentation currentRep = MasterImage.getImage().getCurrentFilterRepresentation(); + FilterRepresentation removedRep = current.getFilterRepresentation(); + mAdapter.remove(current); + fillContent(true); + if (currentRep != null && removedRep != null + && currentRep.getFilterClass() == removedRep.getFilterClass()) { + FilterShowActivity activity = (FilterShowActivity) getContext(); + activity.backToMain(); + return; + } + } + } else { + mCurrentView.setBackgroundAlpha(1.0f); + mCurrentView.setTranslationX(0); + mCurrentView.setTranslationY(0); + } + if (mCurrentSelectedView != null) { + mCurrentSelectedView.invalidate(); + } + if (mCurrentView != null) { + mCurrentView.invalidate(); + } + mCurrentView = null; + mExited = false; + mStartedDrag = false; + } + + public View findChildAt(int x, int y) { + Rect frame = new Rect(); + int scrolledXInt = getScrollX() + x; + int scrolledYInt = getScrollY() + y; + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + child.getHitRect(frame); + if (frame.contains(scrolledXInt, scrolledYInt)) { + return child; + } + } + return null; + } + + public int findChild(View view) { + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + if (child == view) { + return i; + } + } + return -1; + } + + public StateView getCurrentView() { + return mCurrentView; + } + + public void setCurrentView(View currentView) { + mCurrentView = (StateView) currentView; + } + + public void setExited(boolean value) { + mExited = value; + } + + public Point getTouchPoint() { + return mTouchPoint; + } + + public Adapter getAdapter() { + return mAdapter; + } +} diff --git a/src/com/android/gallery3d/filtershow/state/StateView.java b/src/com/android/gallery3d/filtershow/state/StateView.java new file mode 100644 index 000000000..73d57846a --- /dev/null +++ b/src/com/android/gallery3d/filtershow/state/StateView.java @@ -0,0 +1,291 @@ +/* + * Copyright (C) 2013 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.filtershow.state; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.*; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewParent; +import android.widget.LinearLayout; +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.FilterShowActivity; +import com.android.gallery3d.filtershow.imageshow.MasterImage; + +public class StateView extends View { + + private static final String LOGTAG = "StateView"; + private Path mPath = new Path(); + private Paint mPaint = new Paint(); + + public static int DEFAULT = 0; + public static int BEGIN = 1; + public static int END = 2; + + public static int UP = 1; + public static int DOWN = 2; + public static int LEFT = 3; + public static int RIGHT = 4; + + private int mType = DEFAULT; + private float mAlpha = 1.0f; + private String mText = "Default"; + private float mTextSize = 32; + private static int sMargin = 16; + private static int sArrowHeight = 16; + private static int sArrowWidth = 8; + private int mOrientation = LinearLayout.VERTICAL; + private int mDirection = DOWN; + private boolean mDuplicateButton; + private State mState; + + private int mEndsBackgroundColor; + private int mEndsTextColor; + private int mBackgroundColor; + private int mTextColor; + private int mSelectedBackgroundColor; + private int mSelectedTextColor; + private Rect mTextBounds = new Rect(); + + public StateView(Context context) { + this(context, DEFAULT); + } + + public StateView(Context context, int type) { + super(context); + mType = type; + Resources res = getResources(); + mEndsBackgroundColor = res.getColor(R.color.filtershow_stateview_end_background); + mEndsTextColor = res.getColor(R.color.filtershow_stateview_end_text); + mBackgroundColor = res.getColor(R.color.filtershow_stateview_background); + mTextColor = res.getColor(R.color.filtershow_stateview_text); + mSelectedBackgroundColor = res.getColor(R.color.filtershow_stateview_selected_background); + mSelectedTextColor = res.getColor(R.color.filtershow_stateview_selected_text); + mTextSize = res.getDimensionPixelSize(R.dimen.state_panel_text_size); + } + + public String getText() { + return mText; + } + + public void setText(String text) { + mText = text; + invalidate(); + } + + public void setType(int type) { + mType = type; + invalidate(); + } + + @Override + public void setSelected(boolean value) { + super.setSelected(value); + if (!value) { + mDuplicateButton = false; + } + invalidate(); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { + ViewParent parent = getParent(); + if (parent instanceof PanelTrack) { + ((PanelTrack) getParent()).onTouch(event, this); + } + if (mType == BEGIN) { + MasterImage.getImage().setShowsOriginal(true); + } + } + if (event.getActionMasked() == MotionEvent.ACTION_UP + || event.getActionMasked() == MotionEvent.ACTION_CANCEL) { + MasterImage.getImage().setShowsOriginal(false); + } + return true; + } + + public void drawText(Canvas canvas) { + if (mText == null) { + return; + } + mPaint.reset(); + if (isSelected()) { + mPaint.setColor(mSelectedTextColor); + } else { + mPaint.setColor(mTextColor); + } + if (mType == BEGIN) { + mPaint.setColor(mEndsTextColor); + } + mPaint.setTypeface(Typeface.DEFAULT_BOLD); + mPaint.setAntiAlias(true); + mPaint.setTextSize(mTextSize); + mPaint.getTextBounds(mText, 0, mText.length(), mTextBounds); + int x = (canvas.getWidth() - mTextBounds.width()) / 2; + int y = mTextBounds.height() + (canvas.getHeight() - mTextBounds.height()) / 2; + canvas.drawText(mText, x, y, mPaint); + } + + public void onDraw(Canvas canvas) { + canvas.drawARGB(0, 0, 0, 0); + mPaint.reset(); + mPath.reset(); + + float w = canvas.getWidth(); + float h = canvas.getHeight(); + float r = sArrowHeight; + float d = sArrowWidth; + + if (mOrientation == LinearLayout.HORIZONTAL) { + drawHorizontalPath(w, h, r, d); + } else { + if (mDirection == DOWN) { + drawVerticalDownPath(w, h, r, d); + } else { + drawVerticalPath(w, h, r, d); + } + } + + if (mType == DEFAULT || mType == END) { + if (mDuplicateButton) { + mPaint.setARGB(255, 200, 0, 0); + } else if (isSelected()) { + mPaint.setColor(mSelectedBackgroundColor); + } else { + mPaint.setColor(mBackgroundColor); + } + } else { + mPaint.setColor(mEndsBackgroundColor); + } + canvas.drawPath(mPath, mPaint); + drawText(canvas); + } + + private void drawHorizontalPath(float w, float h, float r, float d) { + mPath.moveTo(0, 0); + if (mType == END) { + mPath.lineTo(w, 0); + mPath.lineTo(w, h); + } else { + mPath.lineTo(w - d, 0); + mPath.lineTo(w - d, r); + mPath.lineTo(w, r + d); + mPath.lineTo(w - d, r + d + r); + mPath.lineTo(w - d, h); + } + mPath.lineTo(0, h); + if (mType != BEGIN) { + mPath.lineTo(0, r + d + r); + mPath.lineTo(d, r + d); + mPath.lineTo(0, r); + } + mPath.close(); + } + + private void drawVerticalPath(float w, float h, float r, float d) { + if (mType == BEGIN) { + mPath.moveTo(0, 0); + mPath.lineTo(w, 0); + } else { + mPath.moveTo(0, d); + mPath.lineTo(r, d); + mPath.lineTo(r + d, 0); + mPath.lineTo(r + d + r, d); + mPath.lineTo(w, d); + } + mPath.lineTo(w, h); + if (mType != END) { + mPath.lineTo(r + d + r, h); + mPath.lineTo(r + d, h - d); + mPath.lineTo(r, h); + } + mPath.lineTo(0, h); + mPath.close(); + } + + private void drawVerticalDownPath(float w, float h, float r, float d) { + mPath.moveTo(0, 0); + if (mType != BEGIN) { + mPath.lineTo(r, 0); + mPath.lineTo(r + d, d); + mPath.lineTo(r + d + r, 0); + } + mPath.lineTo(w, 0); + + if (mType != END) { + mPath.lineTo(w, h - d); + + mPath.lineTo(r + d + r, h - d); + mPath.lineTo(r + d, h); + mPath.lineTo(r, h - d); + + mPath.lineTo(0, h - d); + } else { + mPath.lineTo(w, h); + mPath.lineTo(0, h); + } + + mPath.close(); + } + + public void setBackgroundAlpha(float alpha) { + if (mType == BEGIN) { + return; + } + mAlpha = alpha; + setAlpha(alpha); + invalidate(); + } + + public float getBackgroundAlpha() { + return mAlpha; + } + + public void setOrientation(int orientation) { + mOrientation = orientation; + } + + public void setDuplicateButton(boolean b) { + mDuplicateButton = b; + invalidate(); + } + + public State getState() { + return mState; + } + + public void setState(State state) { + mState = state; + mText = mState.getText().toUpperCase(); + mType = mState.getType(); + invalidate(); + } + + public void resetPosition() { + setTranslationX(0); + setTranslationY(0); + setBackgroundAlpha(1.0f); + } + + public boolean isDraggable() { + return mState.isDraggable(); + } +} diff --git a/src/com/android/gallery3d/filtershow/tools/IconFactory.java b/src/com/android/gallery3d/filtershow/tools/IconFactory.java new file mode 100644 index 000000000..9e39f27fc --- /dev/null +++ b/src/com/android/gallery3d/filtershow/tools/IconFactory.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2013 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.filtershow.tools; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; + +/** + * A factory class for producing bitmaps to use as UI icons. + */ +public class IconFactory { + + /** + * Builds an icon with the dimensions iconWidth:iconHeight. If scale is set + * the source image is stretched to fit within the given dimensions; + * otherwise, the source image is cropped to the proper aspect ratio. + * + * @param sourceImage image to create an icon from. + * @param iconWidth width of the icon bitmap. + * @param iconHeight height of the icon bitmap. + * @param scale if true, stretch sourceImage to fit the icon dimensions. + * @return an icon bitmap with the dimensions iconWidth:iconHeight. + */ + public static Bitmap createIcon(Bitmap sourceImage, int iconWidth, int iconHeight, + boolean scale) { + if (sourceImage == null) { + throw new IllegalArgumentException("Null argument to buildIcon"); + } + + int sourceWidth = sourceImage.getWidth(); + int sourceHeight = sourceImage.getHeight(); + + if (sourceWidth == 0 || sourceHeight == 0 || iconWidth == 0 || iconHeight == 0) { + throw new IllegalArgumentException("Bitmap with dimension 0 used as input"); + } + + Bitmap icon = Bitmap.createBitmap(iconWidth, iconHeight, + Bitmap.Config.ARGB_8888); + drawIcon(icon, sourceImage, scale); + return icon; + } + + /** + * Draws an icon in the destination bitmap. If scale is set the source image + * is stretched to fit within the destination dimensions; otherwise, the + * source image is cropped to the proper aspect ratio. + * + * @param dest bitmap into which to draw the icon. + * @param sourceImage image to create an icon from. + * @param scale if true, stretch sourceImage to fit the destination. + */ + public static void drawIcon(Bitmap dest, Bitmap sourceImage, boolean scale) { + if (dest == null || sourceImage == null) { + throw new IllegalArgumentException("Null argument to buildIcon"); + } + + int sourceWidth = sourceImage.getWidth(); + int sourceHeight = sourceImage.getHeight(); + int iconWidth = dest.getWidth(); + int iconHeight = dest.getHeight(); + + if (sourceWidth == 0 || sourceHeight == 0 || iconWidth == 0 || iconHeight == 0) { + throw new IllegalArgumentException("Bitmap with dimension 0 used as input"); + } + + Rect destRect = new Rect(0, 0, iconWidth, iconHeight); + Canvas canvas = new Canvas(dest); + + Rect srcRect = null; + if (scale) { + // scale image to fit in icon (stretches if aspect isn't the same) + srcRect = new Rect(0, 0, sourceWidth, sourceHeight); + } else { + // crop image to aspect ratio iconWidth:iconHeight + float wScale = sourceWidth / (float) iconWidth; + float hScale = sourceHeight / (float) iconHeight; + float s = Math.min(hScale, wScale); + + float iw = iconWidth * s; + float ih = iconHeight * s; + + float borderW = (sourceWidth - iw) / 2.0f; + float borderH = (sourceHeight - ih) / 2.0f; + RectF rec = new RectF(borderW, borderH, borderW + iw, borderH + ih); + srcRect = new Rect(); + rec.roundOut(srcRect); + } + + canvas.drawBitmap(sourceImage, srcRect, destRect, new Paint(Paint.FILTER_BITMAP_FLAG)); + } +} diff --git a/src/com/android/gallery3d/filtershow/tools/MatrixFit.java b/src/com/android/gallery3d/filtershow/tools/MatrixFit.java new file mode 100644 index 000000000..3b815673c --- /dev/null +++ b/src/com/android/gallery3d/filtershow/tools/MatrixFit.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2013 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.filtershow.tools; + +import android.util.Log; + +public class MatrixFit { + // Simple implementation of a matrix fit in N dimensions. + + private static final String LOGTAG = "MatrixFit"; + + private double[][] mMatrix; + private int mDimension; + private boolean mValid = false; + private static double sEPS = 1.0f/10000000000.0f; + + public MatrixFit(double[][] from, double[][] to) { + mValid = fit(from, to); + } + + public int getDimension() { + return mDimension; + } + + public boolean isValid() { + return mValid; + } + + public double[][] getMatrix() { + return mMatrix; + } + + public boolean fit(double[][] from, double[][] to) { + if ((from.length != to.length) || (from.length < 1)) { + Log.e(LOGTAG, "from and to must be of same size"); + return false; + } + + mDimension = from[0].length; + mMatrix = new double[mDimension +1][mDimension + mDimension +1]; + + if (from.length < mDimension) { + Log.e(LOGTAG, "Too few points => under-determined system"); + return false; + } + + double[][] q = new double[from.length][mDimension]; + for (int i = 0; i < from.length; i++) { + for (int j = 0; j < mDimension; j++) { + q[i][j] = from[i][j]; + } + } + + double[][] p = new double[to.length][mDimension]; + for (int i = 0; i < to.length; i++) { + for (int j = 0; j < mDimension; j++) { + p[i][j] = to[i][j]; + } + } + + // Make an empty (dim) x (dim + 1) matrix and fill it + double[][] c = new double[mDimension+1][mDimension]; + for (int j = 0; j < mDimension; j++) { + for (int k = 0; k < mDimension + 1; k++) { + for (int i = 0; i < q.length; i++) { + double qt = 1; + if (k < mDimension) { + qt = q[i][k]; + } + c[k][j] += qt * p[i][j]; + } + } + } + + // Make an empty (dim+1) x (dim+1) matrix and fill it + double[][] Q = new double[mDimension+1][mDimension+1]; + for (int qi = 0; qi < q.length; qi++) { + double[] qt = new double[mDimension + 1]; + for (int i = 0; i < mDimension; i++) { + qt[i] = q[qi][i]; + } + qt[mDimension] = 1; + for (int i = 0; i < mDimension + 1; i++) { + for (int j = 0; j < mDimension + 1; j++) { + Q[i][j] += qt[i] * qt[j]; + } + } + } + + // Use a gaussian elimination to solve the linear system + for (int i = 0; i < mDimension + 1; i++) { + for (int j = 0; j < mDimension + 1; j++) { + mMatrix[i][j] = Q[i][j]; + } + for (int j = 0; j < mDimension; j++) { + mMatrix[i][mDimension + 1 + j] = c[i][j]; + } + } + if (!gaussianElimination(mMatrix)) { + return false; + } + return true; + } + + public double[] apply(double[] point) { + if (mDimension != point.length) { + return null; + } + double[] res = new double[mDimension]; + for (int j = 0; j < mDimension; j++) { + for (int i = 0; i < mDimension; i++) { + res[j] += point[i] * mMatrix[i][j+ mDimension +1]; + } + res[j] += mMatrix[mDimension][j+ mDimension +1]; + } + return res; + } + + public void printEquation() { + for (int j = 0; j < mDimension; j++) { + String str = "x" + j + "' = "; + for (int i = 0; i < mDimension; i++) { + str += "x" + i + " * " + mMatrix[i][j+mDimension+1] + " + "; + } + str += mMatrix[mDimension][j+mDimension+1]; + Log.v(LOGTAG, str); + } + } + + private void printMatrix(String name, double[][] matrix) { + Log.v(LOGTAG, "name: " + name); + for (int i = 0; i < matrix.length; i++) { + String str = ""; + for (int j = 0; j < matrix[0].length; j++) { + str += "" + matrix[i][j] + " "; + } + Log.v(LOGTAG, str); + } + } + + /* + * Transforms the given matrix into a row echelon matrix + */ + private boolean gaussianElimination(double[][] m) { + int h = m.length; + int w = m[0].length; + + for (int y = 0; y < h; y++) { + int maxrow = y; + for (int y2 = y + 1; y2 < h; y2++) { // Find max pivot + if (Math.abs(m[y2][y]) > Math.abs(m[maxrow][y])) { + maxrow = y2; + } + } + // swap + for (int i = 0; i < mDimension; i++) { + double t = m[y][i]; + m[y][i] = m[maxrow][i]; + m[maxrow][i] = t; + } + + if (Math.abs(m[y][y]) <= sEPS) { // Singular Matrix + return false; + } + for (int y2 = y + 1; y2 < h; y2++) { // Eliminate column y + double c = m[y2][y] / m[y][y]; + for (int x = y; x < w; x++) { + m[y2][x] -= m[y][x] * c; + } + } + } + for (int y = h -1; y > -1; y--) { // Back substitution + double c = m[y][y]; + for (int y2 = 0; y2 < y; y2++) { + for (int x = w - 1; x > y - 1; x--) { + m[y2][x] -= m[y][x] * m[y2][y] / c; + } + } + m[y][y] /= c; + for (int x = h; x < w; x++) { // Normalize row y + m[y][x] /= c; + } + } + return true; + } +} diff --git a/src/com/android/gallery3d/filtershow/tools/SaveImage.java b/src/com/android/gallery3d/filtershow/tools/SaveImage.java new file mode 100644 index 000000000..83cbd0136 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/tools/SaveImage.java @@ -0,0 +1,632 @@ +/* + * 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.filtershow.tools; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Environment; +import android.provider.MediaStore; +import android.provider.MediaStore.Images; +import android.provider.MediaStore.Images.ImageColumns; +import android.util.Log; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.exif.ExifInterface; +import com.android.gallery3d.filtershow.FilterShowActivity; +import com.android.gallery3d.filtershow.cache.ImageLoader; +import com.android.gallery3d.filtershow.filters.FiltersManager; +import com.android.gallery3d.filtershow.imageshow.MasterImage; +import com.android.gallery3d.filtershow.pipeline.CachingPipeline; +import com.android.gallery3d.filtershow.pipeline.ImagePreset; +import com.android.gallery3d.filtershow.pipeline.ProcessingService; +import com.android.gallery3d.util.UsageStatistics; +import com.android.gallery3d.util.XmpUtilHelper; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.sql.Date; +import java.text.SimpleDateFormat; +import java.util.TimeZone; + +/** + * Handles saving edited photo + */ +public class SaveImage { + private static final String LOGTAG = "SaveImage"; + + /** + * Callback for updates + */ + public interface Callback { + void onProgress(int max, int current); + } + + public interface ContentResolverQueryCallback { + void onCursorResult(Cursor cursor); + } + + private static final String TIME_STAMP_NAME = "_yyyyMMdd_HHmmss"; + private static final String PREFIX_PANO = "PANO"; + private static final String PREFIX_IMG = "IMG"; + private static final String POSTFIX_JPG = ".jpg"; + private static final String AUX_DIR_NAME = ".aux"; + + private final Context mContext; + private final Uri mSourceUri; + private final Callback mCallback; + private final File mDestinationFile; + private final Uri mSelectedImageUri; + + private int mCurrentProcessingStep = 1; + + public static final int MAX_PROCESSING_STEPS = 6; + public static final String DEFAULT_SAVE_DIRECTORY = "EditedOnlinePhotos"; + + // In order to support the new edit-save behavior such that user won't see + // the edited image together with the original image, we are adding a new + // auxiliary directory for the edited image. Basically, the original image + // will be hidden in that directory after edit and user will see the edited + // image only. + // Note that deletion on the edited image will also cause the deletion of + // the original image under auxiliary directory. + // + // There are several situations we need to consider: + // 1. User edit local image local01.jpg. A local02.jpg will be created in the + // same directory, and original image will be moved to auxiliary directory as + // ./.aux/local02.jpg. + // If user edit the local02.jpg, local03.jpg will be created in the local + // directory and ./.aux/local02.jpg will be renamed to ./.aux/local03.jpg + // + // 2. User edit remote image remote01.jpg from picassa or other server. + // remoteSavedLocal01.jpg will be saved under proper local directory. + // In remoteSavedLocal01.jpg, there will be a reference pointing to the + // remote01.jpg. There will be no local copy of remote01.jpg. + // If user edit remoteSavedLocal01.jpg, then a new remoteSavedLocal02.jpg + // will be generated and still pointing to the remote01.jpg + // + // 3. User delete any local image local.jpg. + // Since the filenames are kept consistent in auxiliary directory, every + // time a local.jpg get deleted, the files in auxiliary directory whose + // names starting with "local." will be deleted. + // This pattern will facilitate the multiple images deletion in the auxiliary + // directory. + + /** + * @param context + * @param sourceUri The Uri for the original image, which can be the hidden + * image under the auxiliary directory or the same as selectedImageUri. + * @param selectedImageUri The Uri for the image selected by the user. + * In most cases, it is a content Uri for local image or remote image. + * @param destination Destinaton File, if this is null, a new file will be + * created under the same directory as selectedImageUri. + * @param callback Let the caller know the saving has completed. + * @return the newSourceUri + */ + public SaveImage(Context context, Uri sourceUri, Uri selectedImageUri, + File destination, Callback callback) { + mContext = context; + mSourceUri = sourceUri; + mCallback = callback; + if (destination == null) { + mDestinationFile = getNewFile(context, selectedImageUri); + } else { + mDestinationFile = destination; + } + + mSelectedImageUri = selectedImageUri; + } + + public static File getFinalSaveDirectory(Context context, Uri sourceUri) { + File saveDirectory = SaveImage.getSaveDirectory(context, sourceUri); + if ((saveDirectory == null) || !saveDirectory.canWrite()) { + saveDirectory = new File(Environment.getExternalStorageDirectory(), + SaveImage.DEFAULT_SAVE_DIRECTORY); + } + // Create the directory if it doesn't exist + if (!saveDirectory.exists()) + saveDirectory.mkdirs(); + return saveDirectory; + } + + public static File getNewFile(Context context, Uri sourceUri) { + File saveDirectory = getFinalSaveDirectory(context, sourceUri); + String filename = new SimpleDateFormat(TIME_STAMP_NAME).format(new Date( + System.currentTimeMillis())); + if (hasPanoPrefix(context, sourceUri)) { + return new File(saveDirectory, PREFIX_PANO + filename + POSTFIX_JPG); + } + return new File(saveDirectory, PREFIX_IMG + filename + POSTFIX_JPG); + } + + /** + * Remove the files in the auxiliary directory whose names are the same as + * the source image. + * @param contentResolver The application's contentResolver + * @param srcContentUri The content Uri for the source image. + */ + public static void deleteAuxFiles(ContentResolver contentResolver, + Uri srcContentUri) { + final String[] fullPath = new String[1]; + String[] queryProjection = new String[] { ImageColumns.DATA }; + querySourceFromContentResolver(contentResolver, + srcContentUri, queryProjection, + new ContentResolverQueryCallback() { + @Override + public void onCursorResult(Cursor cursor) { + fullPath[0] = cursor.getString(0); + } + } + ); + if (fullPath[0] != null) { + // Construct the auxiliary directory given the source file's path. + // Then select and delete all the files starting with the same name + // under the auxiliary directory. + File currentFile = new File(fullPath[0]); + + String filename = currentFile.getName(); + int firstDotPos = filename.indexOf("."); + final String filenameNoExt = (firstDotPos == -1) ? filename : + filename.substring(0, firstDotPos); + File auxDir = getLocalAuxDirectory(currentFile); + if (auxDir.exists()) { + FilenameFilter filter = new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + if (name.startsWith(filenameNoExt + ".")) { + return true; + } else { + return false; + } + } + }; + + // Delete all auxiliary files whose name is matching the + // current local image. + File[] auxFiles = auxDir.listFiles(filter); + for (File file : auxFiles) { + file.delete(); + } + } + } + } + + public Object getPanoramaXMPData(Uri source, ImagePreset preset) { + Object xmp = null; + if (preset.isPanoramaSafe()) { + InputStream is = null; + try { + is = mContext.getContentResolver().openInputStream(source); + xmp = XmpUtilHelper.extractXMPMeta(is); + } catch (FileNotFoundException e) { + Log.w(LOGTAG, "Failed to get XMP data from image: ", e); + } finally { + Utils.closeSilently(is); + } + } + return xmp; + } + + public boolean putPanoramaXMPData(File file, Object xmp) { + if (xmp != null) { + return XmpUtilHelper.writeXMPMeta(file.getAbsolutePath(), xmp); + } + return false; + } + + public ExifInterface getExifData(Uri source) { + ExifInterface exif = new ExifInterface(); + String mimeType = mContext.getContentResolver().getType(mSelectedImageUri); + if (mimeType == null) { + mimeType = ImageLoader.getMimeType(mSelectedImageUri); + } + if (mimeType.equals(ImageLoader.JPEG_MIME_TYPE)) { + InputStream inStream = null; + try { + inStream = mContext.getContentResolver().openInputStream(source); + exif.readExif(inStream); + } catch (FileNotFoundException e) { + Log.w(LOGTAG, "Cannot find file: " + source, e); + } catch (IOException e) { + Log.w(LOGTAG, "Cannot read exif for: " + source, e); + } finally { + Utils.closeSilently(inStream); + } + } + return exif; + } + + public boolean putExifData(File file, ExifInterface exif, Bitmap image, + int jpegCompressQuality) { + boolean ret = false; + OutputStream s = null; + try { + s = exif.getExifWriterStream(file.getAbsolutePath()); + image.compress(Bitmap.CompressFormat.JPEG, + (jpegCompressQuality > 0) ? jpegCompressQuality : 1, s); + s.flush(); + s.close(); + s = null; + ret = true; + } catch (FileNotFoundException e) { + Log.w(LOGTAG, "File not found: " + file.getAbsolutePath(), e); + } catch (IOException e) { + Log.w(LOGTAG, "Could not write exif: ", e); + } finally { + Utils.closeSilently(s); + } + return ret; + } + + private Uri resetToOriginalImageIfNeeded(ImagePreset preset, boolean doAuxBackup) { + Uri uri = null; + if (!preset.hasModifications()) { + // This can happen only when preset has no modification but save + // button is enabled, it means the file is loaded with filters in + // the XMP, then all the filters are removed or restore to default. + // In this case, when mSourceUri exists, rename it to the + // destination file. + File srcFile = getLocalFileFromUri(mContext, mSourceUri); + // If the source is not a local file, then skip this renaming and + // create a local copy as usual. + if (srcFile != null) { + srcFile.renameTo(mDestinationFile); + uri = SaveImage.linkNewFileToUri(mContext, mSelectedImageUri, + mDestinationFile, System.currentTimeMillis(), doAuxBackup); + } + } + return uri; + } + + private void resetProgress() { + mCurrentProcessingStep = 0; + } + + private void updateProgress() { + if (mCallback != null) { + mCallback.onProgress(MAX_PROCESSING_STEPS, ++mCurrentProcessingStep); + } + } + + public Uri processAndSaveImage(ImagePreset preset, boolean doAuxBackup, int quality) { + + Uri uri = resetToOriginalImageIfNeeded(preset, doAuxBackup); + if (uri != null) { + return null; + } + + resetProgress(); + + boolean noBitmap = true; + int num_tries = 0; + int sampleSize = 1; + + // If necessary, move the source file into the auxiliary directory, + // newSourceUri is then pointing to the new location. + // If no file is moved, newSourceUri will be the same as mSourceUri. + Uri newSourceUri = mSourceUri; + if (doAuxBackup) { + newSourceUri = moveSrcToAuxIfNeeded(mSourceUri, mDestinationFile); + } + + // Stopgap fix for low-memory devices. + while (noBitmap) { + try { + updateProgress(); + // Try to do bitmap operations, downsample if low-memory + Bitmap bitmap = ImageLoader.loadOrientedBitmapWithBackouts(mContext, newSourceUri, + sampleSize); + if (bitmap == null) { + return null; + } + updateProgress(); + CachingPipeline pipeline = new CachingPipeline(FiltersManager.getManager(), + "Saving"); + + bitmap = pipeline.renderFinalImage(bitmap, preset); + updateProgress(); + + Object xmp = getPanoramaXMPData(newSourceUri, preset); + ExifInterface exif = getExifData(newSourceUri); + + updateProgress(); + // Set tags + long time = System.currentTimeMillis(); + exif.addDateTimeStampTag(ExifInterface.TAG_DATE_TIME, time, + TimeZone.getDefault()); + exif.setTag(exif.buildTag(ExifInterface.TAG_ORIENTATION, + ExifInterface.Orientation.TOP_LEFT)); + // Remove old thumbnail + exif.removeCompressedThumbnail(); + + updateProgress(); + + // If we succeed in writing the bitmap as a jpeg, return a uri. + if (putExifData(mDestinationFile, exif, bitmap, quality)) { + putPanoramaXMPData(mDestinationFile, xmp); + // mDestinationFile will save the newSourceUri info in the XMP. + XmpPresets.writeFilterXMP(mContext, newSourceUri, + mDestinationFile, preset); + + // After this call, mSelectedImageUri will be actually + // pointing at the new file mDestinationFile. + uri = SaveImage.linkNewFileToUri(mContext, mSelectedImageUri, + mDestinationFile, time, doAuxBackup); + } + updateProgress(); + + noBitmap = false; + UsageStatistics.onEvent(UsageStatistics.COMPONENT_EDITOR, + "SaveComplete", null); + } catch (OutOfMemoryError e) { + // Try 5 times before failing for good. + if (++num_tries >= 5) { + throw e; + } + System.gc(); + sampleSize *= 2; + resetProgress(); + } + } + return uri; + } + + /** + * Move the source file to auxiliary directory if needed and return the Uri + * pointing to this new source file. + * @param srcUri Uri to the source image. + * @param dstFile Providing the destination file info to help to build the + * auxiliary directory and new source file's name. + * @return the newSourceUri pointing to the new source image. + */ + private Uri moveSrcToAuxIfNeeded(Uri srcUri, File dstFile) { + File srcFile = getLocalFileFromUri(mContext, srcUri); + if (srcFile == null) { + Log.d(LOGTAG, "Source file is not a local file, no update."); + return srcUri; + } + + // Get the destination directory and create the auxilliary directory + // if necessary. + File auxDiretory = getLocalAuxDirectory(dstFile); + if (!auxDiretory.exists()) { + auxDiretory.mkdirs(); + } + + // Make sure there is a .nomedia file in the auxiliary directory, such + // that MediaScanner will not report those files under this directory. + File noMedia = new File(auxDiretory, ".nomedia"); + if (!noMedia.exists()) { + try { + noMedia.createNewFile(); + } catch (IOException e) { + Log.e(LOGTAG, "Can't create the nomedia"); + return srcUri; + } + } + // We are using the destination file name such that photos sitting in + // the auxiliary directory are matching the parent directory. + File newSrcFile = new File(auxDiretory, dstFile.getName()); + + if (!newSrcFile.exists()) { + srcFile.renameTo(newSrcFile); + } + + return Uri.fromFile(newSrcFile); + + } + + private static File getLocalAuxDirectory(File dstFile) { + File dstDirectory = dstFile.getParentFile(); + File auxDiretory = new File(dstDirectory + "/" + AUX_DIR_NAME); + return auxDiretory; + } + + public static Uri makeAndInsertUri(Context context, Uri sourceUri) { + long time = System.currentTimeMillis(); + String filename = new SimpleDateFormat(TIME_STAMP_NAME).format(new Date(time)); + File saveDirectory = getFinalSaveDirectory(context, sourceUri); + File file = new File(saveDirectory, filename + ".JPG"); + return linkNewFileToUri(context, sourceUri, file, time, false); + } + + public static void saveImage(ImagePreset preset, final FilterShowActivity filterShowActivity, + File destination) { + Uri selectedImageUri = filterShowActivity.getSelectedImageUri(); + Uri sourceImageUri = MasterImage.getImage().getUri(); + + Intent processIntent = ProcessingService.getSaveIntent(filterShowActivity, preset, + destination, selectedImageUri, sourceImageUri, false, 90); + + filterShowActivity.startService(processIntent); + + if (!filterShowActivity.isSimpleEditAction()) { + // terminate for now + filterShowActivity.completeSaveImage(selectedImageUri); + } + } + + public static void querySource(Context context, Uri sourceUri, String[] projection, + ContentResolverQueryCallback callback) { + ContentResolver contentResolver = context.getContentResolver(); + querySourceFromContentResolver(contentResolver, sourceUri, projection, callback); + } + + private static void querySourceFromContentResolver( + ContentResolver contentResolver, Uri sourceUri, String[] projection, + ContentResolverQueryCallback callback) { + Cursor cursor = null; + try { + cursor = contentResolver.query(sourceUri, projection, null, null, + null); + if ((cursor != null) && cursor.moveToNext()) { + callback.onCursorResult(cursor); + } + } catch (Exception e) { + // Ignore error for lacking the data column from the source. + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + + private static File getSaveDirectory(Context context, Uri sourceUri) { + File file = getLocalFileFromUri(context, sourceUri); + if (file != null) { + return file.getParentFile(); + } else { + return null; + } + } + + /** + * Construct a File object based on the srcUri. + * @return The file object. Return null if srcUri is invalid or not a local + * file. + */ + private static File getLocalFileFromUri(Context context, Uri srcUri) { + if (srcUri == null) { + Log.e(LOGTAG, "srcUri is null."); + return null; + } + + String scheme = srcUri.getScheme(); + if (scheme == null) { + Log.e(LOGTAG, "scheme is null."); + return null; + } + + final File[] file = new File[1]; + // sourceUri can be a file path or a content Uri, it need to be handled + // differently. + if (scheme.equals(ContentResolver.SCHEME_CONTENT)) { + if (srcUri.getAuthority().equals(MediaStore.AUTHORITY)) { + querySource(context, srcUri, new String[] { + ImageColumns.DATA + }, + new ContentResolverQueryCallback() { + + @Override + public void onCursorResult(Cursor cursor) { + file[0] = new File(cursor.getString(0)); + } + }); + } + } else if (scheme.equals(ContentResolver.SCHEME_FILE)) { + file[0] = new File(srcUri.getPath()); + } + return file[0]; + } + + /** + * Gets the actual filename for a Uri from Gallery's ContentProvider. + */ + private static String getTrueFilename(Context context, Uri src) { + if (context == null || src == null) { + return null; + } + final String[] trueName = new String[1]; + querySource(context, src, new String[] { + ImageColumns.DATA + }, new ContentResolverQueryCallback() { + @Override + public void onCursorResult(Cursor cursor) { + trueName[0] = new File(cursor.getString(0)).getName(); + } + }); + return trueName[0]; + } + + /** + * Checks whether the true filename has the panorama image prefix. + */ + private static boolean hasPanoPrefix(Context context, Uri src) { + String name = getTrueFilename(context, src); + return name != null && name.startsWith(PREFIX_PANO); + } + + /** + * If the <code>sourceUri</code> is a local content Uri, update the + * <code>sourceUri</code> to point to the <code>file</code>. + * At the same time, the old file <code>sourceUri</code> used to point to + * will be removed if it is local. + * If the <code>sourceUri</code> is not a local content Uri, then the + * <code>file</code> will be inserted as a new content Uri. + * @return the final Uri referring to the <code>file</code>. + */ + public static Uri linkNewFileToUri(Context context, Uri sourceUri, + File file, long time, boolean deleteOriginal) { + File oldSelectedFile = getLocalFileFromUri(context, sourceUri); + final ContentValues values = new ContentValues(); + + time /= 1000; + values.put(Images.Media.TITLE, file.getName()); + values.put(Images.Media.DISPLAY_NAME, file.getName()); + values.put(Images.Media.MIME_TYPE, "image/jpeg"); + values.put(Images.Media.DATE_TAKEN, time); + values.put(Images.Media.DATE_MODIFIED, time); + values.put(Images.Media.DATE_ADDED, time); + values.put(Images.Media.ORIENTATION, 0); + values.put(Images.Media.DATA, file.getAbsolutePath()); + values.put(Images.Media.SIZE, file.length()); + + final String[] projection = new String[] { + ImageColumns.DATE_TAKEN, + ImageColumns.LATITUDE, ImageColumns.LONGITUDE, + }; + SaveImage.querySource(context, sourceUri, projection, + new SaveImage.ContentResolverQueryCallback() { + + @Override + public void onCursorResult(Cursor cursor) { + values.put(Images.Media.DATE_TAKEN, cursor.getLong(0)); + + double latitude = cursor.getDouble(1); + double longitude = cursor.getDouble(2); + // TODO: Change || to && after the default location + // issue is fixed. + if ((latitude != 0f) || (longitude != 0f)) { + values.put(Images.Media.LATITUDE, latitude); + values.put(Images.Media.LONGITUDE, longitude); + } + } + }); + + Uri result = sourceUri; + if (oldSelectedFile == null || !deleteOriginal) { + result = context.getContentResolver().insert( + Images.Media.EXTERNAL_CONTENT_URI, values); + } else { + context.getContentResolver().update(sourceUri, values, null, null); + if (oldSelectedFile.exists()) { + oldSelectedFile.delete(); + } + } + + return result; + } + +} diff --git a/src/com/android/gallery3d/filtershow/tools/XmpPresets.java b/src/com/android/gallery3d/filtershow/tools/XmpPresets.java new file mode 100644 index 000000000..3995eeb85 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/tools/XmpPresets.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2013 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.filtershow.tools; + +import android.content.Context; +import android.net.Uri; +import android.util.Log; + +import com.adobe.xmp.XMPException; +import com.adobe.xmp.XMPMeta; +import com.adobe.xmp.XMPMetaFactory; +import com.android.gallery3d.R; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.filtershow.imageshow.MasterImage; +import com.android.gallery3d.filtershow.pipeline.ImagePreset; +import com.android.gallery3d.util.XmpUtilHelper; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.InputStream; + +public class XmpPresets { + public static final String + XMP_GOOGLE_FILTER_NAMESPACE = "http://ns.google.com/photos/1.0/filter/"; + public static final String XMP_GOOGLE_FILTER_PREFIX = "AFltr"; + public static final String XMP_SRC_FILE_URI = "SourceFileUri"; + public static final String XMP_FILTERSTACK = "filterstack"; + private static final String LOGTAG = "XmpPresets"; + + public static class XMresults { + public String presetString; + public ImagePreset preset; + public Uri originalimage; + } + + static { + try { + XMPMetaFactory.getSchemaRegistry().registerNamespace( + XMP_GOOGLE_FILTER_NAMESPACE, XMP_GOOGLE_FILTER_PREFIX); + } catch (XMPException e) { + Log.e(LOGTAG, "Register XMP name space failed", e); + } + } + + public static void writeFilterXMP( + Context context, Uri srcUri, File dstFile, ImagePreset preset) { + InputStream is = null; + XMPMeta xmpMeta = null; + try { + is = context.getContentResolver().openInputStream(srcUri); + xmpMeta = XmpUtilHelper.extractXMPMeta(is); + } catch (FileNotFoundException e) { + + } finally { + Utils.closeSilently(is); + } + + if (xmpMeta == null) { + xmpMeta = XMPMetaFactory.create(); + } + try { + xmpMeta.setProperty(XMP_GOOGLE_FILTER_NAMESPACE, + XMP_SRC_FILE_URI, srcUri.toString()); + xmpMeta.setProperty(XMP_GOOGLE_FILTER_NAMESPACE, + XMP_FILTERSTACK, preset.getJsonString(context.getString(R.string.saved))); + } catch (XMPException e) { + Log.v(LOGTAG, "Write XMP meta to file failed:" + dstFile.getAbsolutePath()); + return; + } + + if (!XmpUtilHelper.writeXMPMeta(dstFile.getAbsolutePath(), xmpMeta)) { + Log.v(LOGTAG, "Write XMP meta to file failed:" + dstFile.getAbsolutePath()); + } + } + + public static XMresults extractXMPData( + Context context, MasterImage mMasterImage, Uri uriToEdit) { + XMresults ret = new XMresults(); + + InputStream is = null; + XMPMeta xmpMeta = null; + try { + is = context.getContentResolver().openInputStream(uriToEdit); + xmpMeta = XmpUtilHelper.extractXMPMeta(is); + } catch (FileNotFoundException e) { + } finally { + Utils.closeSilently(is); + } + + if (xmpMeta == null) { + return null; + } + + try { + String strSrcUri = xmpMeta.getPropertyString(XMP_GOOGLE_FILTER_NAMESPACE, + XMP_SRC_FILE_URI); + + if (strSrcUri != null) { + String filterString = xmpMeta.getPropertyString(XMP_GOOGLE_FILTER_NAMESPACE, + XMP_FILTERSTACK); + + Uri srcUri = Uri.parse(strSrcUri); + ret.originalimage = srcUri; + + ret.preset = new ImagePreset(mMasterImage.getPreset()); + ret.presetString = filterString; + boolean ok = ret.preset.readJsonFromString(filterString); + if (!ok) { + return null; + } + return ret; + } + } catch (XMPException e) { + e.printStackTrace(); + } + + return null; + } +} diff --git a/src/com/android/gallery3d/filtershow/ui/ExportDialog.java b/src/com/android/gallery3d/filtershow/ui/ExportDialog.java new file mode 100644 index 000000000..4b30e7b18 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/ui/ExportDialog.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2013 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.filtershow.ui; + +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.app.DialogFragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.SeekBar; +import android.widget.TextView; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.FilterShowActivity; +import com.android.gallery3d.filtershow.imageshow.MasterImage; +import com.android.gallery3d.filtershow.pipeline.ProcessingService; +import com.android.gallery3d.filtershow.tools.SaveImage; + +import java.io.File; + +public class ExportDialog extends DialogFragment implements View.OnClickListener, SeekBar.OnSeekBarChangeListener{ + SeekBar mSeekBar; + TextView mSeekVal; + String mSliderLabel; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.filtershow_export_dialog, container); + mSeekBar = (SeekBar) view.findViewById(R.id.qualitySeekBar); + mSeekVal = (TextView) view.findViewById(R.id.qualityTextView); + mSliderLabel = getString(R.string.quality) + ": "; + mSeekVal.setText(mSliderLabel + mSeekBar.getProgress()); + mSeekBar.setOnSeekBarChangeListener(this); + view.findViewById(R.id.cancel).setOnClickListener(this); + view.findViewById(R.id.done).setOnClickListener(this); + getDialog().setTitle(R.string.export_flattened); + return view; + } + + @Override + public void onStopTrackingTouch(SeekBar arg0) { + // Do nothing + } + + @Override + public void onStartTrackingTouch(SeekBar arg0) { + // Do nothing + } + + @Override + public void onProgressChanged(SeekBar arg0, int arg1, boolean arg2) { + mSeekVal.setText(mSliderLabel + arg1); + } + + @Override + public void onClick(View v) { + switch (v.getId()) { + case R.id.cancel: + dismiss(); + break; + case R.id.done: + FilterShowActivity activity = (FilterShowActivity) getActivity(); + Uri sourceUri = MasterImage.getImage().getUri(); + File dest = SaveImage.getNewFile(activity, sourceUri); + Intent processIntent = ProcessingService.getSaveIntent(activity, MasterImage + .getImage().getPreset(), dest, activity.getSelectedImageUri(), sourceUri, + true, mSeekBar.getProgress()); + activity.startService(processIntent); + dismiss(); + break; + } + } +} diff --git a/src/com/android/gallery3d/filtershow/ui/FramedTextButton.java b/src/com/android/gallery3d/filtershow/ui/FramedTextButton.java new file mode 100644 index 000000000..c1e4109d2 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/ui/FramedTextButton.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2012 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.filtershow.ui; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.widget.ImageButton; + +import com.android.gallery3d.R; + +public class FramedTextButton extends ImageButton { + private static final String LOGTAG = "FramedTextButton"; + private String mText = null; + private static int mTextSize = 24; + private static int mTextPadding = 20; + private static Paint gPaint = new Paint(); + private static Path gPath = new Path(); + private static int mTrianglePadding = 2; + private static int mTriangleSize = 30; + + public static void setTextSize(int value) { + mTextSize = value; + } + + public static void setTextPadding(int value) { + mTextPadding = value; + } + + public static void setTrianglePadding(int value) { + mTrianglePadding = value; + } + + public static void setTriangleSize(int value) { + mTriangleSize = value; + } + + public void setText(String text) { + mText = text; + invalidate(); + } + + public void setTextFrom(int itemId) { + switch (itemId) { + case R.id.curve_menu_rgb: { + setText(getContext().getString(R.string.curves_channel_rgb)); + break; + } + case R.id.curve_menu_red: { + setText(getContext().getString(R.string.curves_channel_red)); + break; + } + case R.id.curve_menu_green: { + setText(getContext().getString(R.string.curves_channel_green)); + break; + } + case R.id.curve_menu_blue: { + setText(getContext().getString(R.string.curves_channel_blue)); + break; + } + } + invalidate(); + } + + public FramedTextButton(Context context) { + this(context, null); + } + + public FramedTextButton(Context context, AttributeSet attrs) { + super(context, attrs); + if (attrs == null) { + return; + } + TypedArray a = getContext().obtainStyledAttributes( + attrs, R.styleable.ImageButtonTitle); + + mText = a.getString(R.styleable.ImageButtonTitle_android_text); + } + + public String getText(){ + return mText; + } + + @Override + public void onDraw(Canvas canvas) { + gPaint.setARGB(96, 255, 255, 255); + gPaint.setStrokeWidth(2); + gPaint.setStyle(Paint.Style.STROKE); + int w = getWidth(); + int h = getHeight(); + canvas.drawRect(mTextPadding, mTextPadding, w - mTextPadding, + h - mTextPadding, gPaint); + gPath.reset(); + gPath.moveTo(w - mTextPadding - mTrianglePadding - mTriangleSize, + h - mTextPadding - mTrianglePadding); + gPath.lineTo(w - mTextPadding - mTrianglePadding, + h - mTextPadding - mTrianglePadding - mTriangleSize); + gPath.lineTo(w - mTextPadding - mTrianglePadding, + h - mTextPadding - mTrianglePadding); + gPath.close(); + gPaint.setARGB(128, 255, 255, 255); + gPaint.setStrokeWidth(1); + gPaint.setStyle(Paint.Style.FILL_AND_STROKE); + canvas.drawPath(gPath, gPaint); + if (mText != null) { + gPaint.reset(); + gPaint.setARGB(255, 255, 255, 255); + gPaint.setTextSize(mTextSize); + float textWidth = gPaint.measureText(mText); + Rect bounds = new Rect(); + gPaint.getTextBounds(mText, 0, mText.length(), bounds); + int x = (int) ((w - textWidth) / 2); + int y = (h + bounds.height()) / 2; + + canvas.drawText(mText, x, y, gPaint); + } + } + +} diff --git a/src/com/android/gallery3d/filtershow/ui/SelectionRenderer.java b/src/com/android/gallery3d/filtershow/ui/SelectionRenderer.java new file mode 100644 index 000000000..ef40c5e44 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/ui/SelectionRenderer.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2013 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.filtershow.ui; + +import android.graphics.Canvas; +import android.graphics.Paint; + +public class SelectionRenderer { + + public static void drawSelection(Canvas canvas, int left, int top, int right, int bottom, + int stroke, Paint paint) { + canvas.drawRect(left, top, right, top + stroke, paint); + canvas.drawRect(left, bottom - stroke, right, bottom, paint); + canvas.drawRect(left, top, left + stroke, bottom, paint); + canvas.drawRect(right - stroke, top, right, bottom, paint); + } + + public static void drawSelection(Canvas canvas, int left, int top, int right, int bottom, + int stroke, Paint selectPaint, int border, Paint borderPaint) { + canvas.drawRect(left, top, right, top + stroke, selectPaint); + canvas.drawRect(left, bottom - stroke, right, bottom, selectPaint); + canvas.drawRect(left, top, left + stroke, bottom, selectPaint); + canvas.drawRect(right - stroke, top, right, bottom, selectPaint); + canvas.drawRect(left + stroke, top + stroke, right - stroke, + top + stroke + border, borderPaint); + canvas.drawRect(left + stroke, bottom - stroke - border, right - stroke, + bottom - stroke, borderPaint); + canvas.drawRect(left + stroke, top + stroke, left + stroke + border, + bottom - stroke, borderPaint); + canvas.drawRect(right - stroke - border, top + stroke, right - stroke, + bottom - stroke, borderPaint); + } + +} diff --git a/src/com/android/gallery3d/gadget/LocalPhotoSource.java b/src/com/android/gallery3d/gadget/LocalPhotoSource.java new file mode 100644 index 000000000..4e94e8d75 --- /dev/null +++ b/src/com/android/gallery3d/gadget/LocalPhotoSource.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2011 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.gadget; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Environment; +import android.os.Handler; +import android.provider.MediaStore.Images.Media; + +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.ContentListener; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.util.GalleryUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Random; + +public class LocalPhotoSource implements WidgetSource { + + @SuppressWarnings("unused") + private static final String TAG = "LocalPhotoSource"; + + private static final int MAX_PHOTO_COUNT = 128; + + /* Static fields used to query for the correct set of images */ + private static final Uri CONTENT_URI = Media.EXTERNAL_CONTENT_URI; + private static final String DATE_TAKEN = Media.DATE_TAKEN; + private static final String[] PROJECTION = {Media._ID}; + private static final String[] COUNT_PROJECTION = {"count(*)"}; + /* We don't want to include the download directory */ + private static final String SELECTION = + String.format("%s != %s", Media.BUCKET_ID, getDownloadBucketId()); + private static final String ORDER = String.format("%s DESC", DATE_TAKEN); + + private Context mContext; + private ArrayList<Long> mPhotos = new ArrayList<Long>(); + private ContentListener mContentListener; + private ContentObserver mContentObserver; + private boolean mContentDirty = true; + private DataManager mDataManager; + private static final Path LOCAL_IMAGE_ROOT = Path.fromString("/local/image/item"); + + public LocalPhotoSource(Context context) { + mContext = context; + mDataManager = ((GalleryApp) context.getApplicationContext()).getDataManager(); + mContentObserver = new ContentObserver(new Handler()) { + @Override + public void onChange(boolean selfChange) { + mContentDirty = true; + if (mContentListener != null) mContentListener.onContentDirty(); + } + }; + mContext.getContentResolver() + .registerContentObserver(CONTENT_URI, true, mContentObserver); + } + + @Override + public void close() { + mContext.getContentResolver().unregisterContentObserver(mContentObserver); + } + + @Override + public Uri getContentUri(int index) { + if (index < mPhotos.size()) { + return CONTENT_URI.buildUpon() + .appendPath(String.valueOf(mPhotos.get(index))) + .build(); + } + return null; + } + + @Override + public Bitmap getImage(int index) { + if (index >= mPhotos.size()) return null; + long id = mPhotos.get(index); + MediaItem image = (MediaItem) + mDataManager.getMediaObject(LOCAL_IMAGE_ROOT.getChild(id)); + if (image == null) return null; + + return WidgetUtils.createWidgetBitmap(image); + } + + private int[] getExponentialIndice(int total, int count) { + Random random = new Random(); + if (count > total) count = total; + HashSet<Integer> selected = new HashSet<Integer>(count); + while (selected.size() < count) { + int row = (int)(-Math.log(random.nextDouble()) * total / 2); + if (row < total) selected.add(row); + } + int values[] = new int[count]; + int index = 0; + for (int value : selected) { + values[index++] = value; + } + return values; + } + + private int getPhotoCount(ContentResolver resolver) { + Cursor cursor = resolver.query( + CONTENT_URI, COUNT_PROJECTION, SELECTION, null, null); + if (cursor == null) return 0; + try { + Utils.assertTrue(cursor.moveToNext()); + return cursor.getInt(0); + } finally { + cursor.close(); + } + } + + private boolean isContentSound(int totalCount) { + if (mPhotos.size() < Math.min(totalCount, MAX_PHOTO_COUNT)) return false; + if (mPhotos.size() == 0) return true; // totalCount is also 0 + + StringBuilder builder = new StringBuilder(); + for (Long imageId : mPhotos) { + if (builder.length() > 0) builder.append(","); + builder.append(imageId); + } + Cursor cursor = mContext.getContentResolver().query( + CONTENT_URI, COUNT_PROJECTION, + String.format("%s in (%s)", Media._ID, builder.toString()), + null, null); + if (cursor == null) return false; + try { + Utils.assertTrue(cursor.moveToNext()); + return cursor.getInt(0) == mPhotos.size(); + } finally { + cursor.close(); + } + } + + @Override + public void reload() { + if (!mContentDirty) return; + mContentDirty = false; + + ContentResolver resolver = mContext.getContentResolver(); + int photoCount = getPhotoCount(resolver); + if (isContentSound(photoCount)) return; + + int choosedIds[] = getExponentialIndice(photoCount, MAX_PHOTO_COUNT); + Arrays.sort(choosedIds); + + mPhotos.clear(); + Cursor cursor = mContext.getContentResolver().query( + CONTENT_URI, PROJECTION, SELECTION, null, ORDER); + if (cursor == null) return; + try { + for (int index : choosedIds) { + if (cursor.moveToPosition(index)) { + mPhotos.add(cursor.getLong(0)); + } + } + } finally { + cursor.close(); + } + } + + @Override + public int size() { + reload(); + return mPhotos.size(); + } + + /** + * Builds the bucket ID for the public external storage Downloads directory + * @return the bucket ID + */ + private static int getDownloadBucketId() { + String downloadsPath = Environment + .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + .getAbsolutePath(); + return GalleryUtils.getBucketId(downloadsPath); + } + + @Override + public void setContentListener(ContentListener listener) { + mContentListener = listener; + } +} diff --git a/src/com/android/gallery3d/gadget/MediaSetSource.java b/src/com/android/gallery3d/gadget/MediaSetSource.java new file mode 100644 index 000000000..458651c98 --- /dev/null +++ b/src/com/android/gallery3d/gadget/MediaSetSource.java @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2011 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.gadget; + +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Binder; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.ContentListener; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.Path; + +import java.util.ArrayList; +import java.util.Arrays; + +public class MediaSetSource implements WidgetSource, ContentListener { + private static final String TAG = "MediaSetSource"; + + private DataManager mDataManager; + private Path mAlbumPath; + + private WidgetSource mSource; + + private MediaSet mRootSet; + private ContentListener mListener; + + public MediaSetSource(DataManager manager, String albumPath) { + MediaSet mediaSet = (MediaSet) manager.getMediaObject(albumPath); + if (mediaSet != null) { + mSource = new CheckedMediaSetSource(mediaSet); + return; + } + + // Initialize source to an empty source until the album path can be resolved + mDataManager = Utils.checkNotNull(manager); + mAlbumPath = Path.fromString(albumPath); + mSource = new EmptySource(); + monitorRootPath(); + } + + @Override + public int size() { + return mSource.size(); + } + + @Override + public Bitmap getImage(int index) { + return mSource.getImage(index); + } + + @Override + public Uri getContentUri(int index) { + return mSource.getContentUri(index); + } + + @Override + public synchronized void setContentListener(ContentListener listener) { + if (mRootSet != null) { + mListener = listener; + } else { + mSource.setContentListener(listener); + } + } + + @Override + public void reload() { + mSource.reload(); + } + + @Override + public void close() { + mSource.close(); + } + + @Override + public void onContentDirty() { + resolveAlbumPath(); + } + + private void monitorRootPath() { + String rootPath = mDataManager.getTopSetPath(DataManager.INCLUDE_ALL); + mRootSet = (MediaSet) mDataManager.getMediaObject(rootPath); + mRootSet.addContentListener(this); + } + + private synchronized void resolveAlbumPath() { + if (mDataManager == null) return; + MediaSet mediaSet = (MediaSet) mDataManager.getMediaObject(mAlbumPath); + if (mediaSet != null) { + // Clear the reference instead of removing the listener + // to get around a concurrent modification exception. + mRootSet = null; + + mSource = new CheckedMediaSetSource(mediaSet); + if (mListener != null) { + mListener.onContentDirty(); + mSource.setContentListener(mListener); + mListener = null; + } + mDataManager = null; + mAlbumPath = null; + } + } + + private static class CheckedMediaSetSource implements WidgetSource, ContentListener { + private static final int CACHE_SIZE = 32; + + @SuppressWarnings("unused") + private static final String TAG = "CheckedMediaSetSource"; + + private MediaSet mSource; + private MediaItem mCache[] = new MediaItem[CACHE_SIZE]; + private int mCacheStart; + private int mCacheEnd; + private long mSourceVersion = MediaObject.INVALID_DATA_VERSION; + + private ContentListener mContentListener; + + public CheckedMediaSetSource(MediaSet source) { + mSource = Utils.checkNotNull(source); + mSource.addContentListener(this); + } + + @Override + public void close() { + mSource.removeContentListener(this); + } + + private void ensureCacheRange(int index) { + if (index >= mCacheStart && index < mCacheEnd) return; + + long token = Binder.clearCallingIdentity(); + try { + mCacheStart = index; + ArrayList<MediaItem> items = mSource.getMediaItem(mCacheStart, CACHE_SIZE); + mCacheEnd = mCacheStart + items.size(); + items.toArray(mCache); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public synchronized Uri getContentUri(int index) { + ensureCacheRange(index); + if (index < mCacheStart || index >= mCacheEnd) return null; + return mCache[index - mCacheStart].getContentUri(); + } + + @Override + public synchronized Bitmap getImage(int index) { + ensureCacheRange(index); + if (index < mCacheStart || index >= mCacheEnd) return null; + return WidgetUtils.createWidgetBitmap(mCache[index - mCacheStart]); + } + + @Override + public void reload() { + long version = mSource.reload(); + if (mSourceVersion != version) { + mSourceVersion = version; + mCacheStart = 0; + mCacheEnd = 0; + Arrays.fill(mCache, null); + } + } + + @Override + public void setContentListener(ContentListener listener) { + mContentListener = listener; + } + + @Override + public int size() { + long token = Binder.clearCallingIdentity(); + try { + return mSource.getMediaItemCount(); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public void onContentDirty() { + if (mContentListener != null) mContentListener.onContentDirty(); + } + } + + private static class EmptySource implements WidgetSource { + + @Override + public int size() { + return 0; + } + + @Override + public Bitmap getImage(int index) { + throw new UnsupportedOperationException(); + } + + @Override + public Uri getContentUri(int index) { + throw new UnsupportedOperationException(); + } + + @Override + public void setContentListener(ContentListener listener) {} + + @Override + public void reload() {} + + @Override + public void close() {} + } +} diff --git a/src/com/android/gallery3d/gadget/PhotoAppWidgetProvider.java b/src/com/android/gallery3d/gadget/PhotoAppWidgetProvider.java new file mode 100644 index 000000000..58466bf01 --- /dev/null +++ b/src/com/android/gallery3d/gadget/PhotoAppWidgetProvider.java @@ -0,0 +1,139 @@ +/* + * 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.gadget; + +import android.annotation.TargetApi; +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.util.Log; +import android.widget.RemoteViews; + +import com.android.gallery3d.R; +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.gadget.WidgetDatabaseHelper.Entry; +import com.android.gallery3d.onetimeinitializer.GalleryWidgetMigrator; + +public class PhotoAppWidgetProvider extends AppWidgetProvider { + + private static final String TAG = "WidgetProvider"; + + static RemoteViews buildWidget(Context context, int id, Entry entry) { + + switch (entry.type) { + case WidgetDatabaseHelper.TYPE_ALBUM: + case WidgetDatabaseHelper.TYPE_SHUFFLE: + return buildStackWidget(context, id, entry); + case WidgetDatabaseHelper.TYPE_SINGLE_PHOTO: + return buildFrameWidget(context, id, entry); + } + throw new RuntimeException("invalid type - " + entry.type); + } + + @Override + public void onUpdate(Context context, + AppWidgetManager appWidgetManager, int[] appWidgetIds) { + + if (ApiHelper.HAS_REMOTE_VIEWS_SERVICE) { + // migrate gallery widgets from pre-JB releases to JB due to bucket ID change + GalleryWidgetMigrator.migrateGalleryWidgets(context); + } + + WidgetDatabaseHelper helper = new WidgetDatabaseHelper(context); + try { + for (int id : appWidgetIds) { + Entry entry = helper.getEntry(id); + if (entry != null) { + RemoteViews views = buildWidget(context, id, entry); + appWidgetManager.updateAppWidget(id, views); + } else { + Log.e(TAG, "cannot load widget: " + id); + } + } + } finally { + helper.close(); + } + super.onUpdate(context, appWidgetManager, appWidgetIds); + } + + @SuppressWarnings("deprecation") + @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) + private static RemoteViews buildStackWidget(Context context, int widgetId, Entry entry) { + RemoteViews views = new RemoteViews( + context.getPackageName(), R.layout.appwidget_main); + + Intent intent = new Intent(context, WidgetService.class); + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId); + intent.putExtra(WidgetService.EXTRA_WIDGET_TYPE, entry.type); + intent.putExtra(WidgetService.EXTRA_ALBUM_PATH, entry.albumPath); + intent.setData(Uri.parse("widget://gallery/" + widgetId)); + + // We use the deprecated API for backward compatibility + // The new API is available in ICE_CREAM_SANDWICH (15) + views.setRemoteAdapter(widgetId, R.id.appwidget_stack_view, intent); + + views.setEmptyView(R.id.appwidget_stack_view, R.id.appwidget_empty_view); + + Intent clickIntent = new Intent(context, WidgetClickHandler.class); + PendingIntent pendingIntent = PendingIntent.getActivity( + context, 0, clickIntent, PendingIntent.FLAG_UPDATE_CURRENT); + views.setPendingIntentTemplate(R.id.appwidget_stack_view, pendingIntent); + + return views; + } + + static RemoteViews buildFrameWidget(Context context, int appWidgetId, Entry entry) { + RemoteViews views = new RemoteViews( + context.getPackageName(), R.layout.photo_frame); + try { + byte[] data = entry.imageData; + Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); + views.setImageViewBitmap(R.id.photo, bitmap); + } catch (Throwable t) { + Log.w(TAG, "cannot load widget image: " + appWidgetId, t); + } + + if (entry.imageUri != null) { + try { + Uri uri = Uri.parse(entry.imageUri); + Intent clickIntent = new Intent(context, WidgetClickHandler.class) + .setData(uri); + PendingIntent pendingClickIntent = PendingIntent.getActivity(context, 0, + clickIntent, PendingIntent.FLAG_CANCEL_CURRENT); + views.setOnClickPendingIntent(R.id.photo, pendingClickIntent); + } catch (Throwable t) { + Log.w(TAG, "cannot load widget uri: " + appWidgetId, t); + } + } + return views; + } + + @Override + public void onDeleted(Context context, int[] appWidgetIds) { + // Clean deleted photos out of our database + WidgetDatabaseHelper helper = new WidgetDatabaseHelper(context); + for (int appWidgetId : appWidgetIds) { + helper.deleteEntry(appWidgetId); + } + helper.close(); + } +} diff --git a/src/com/android/gallery3d/gadget/WidgetClickHandler.java b/src/com/android/gallery3d/gadget/WidgetClickHandler.java new file mode 100644 index 000000000..37ee1a651 --- /dev/null +++ b/src/com/android/gallery3d/gadget/WidgetClickHandler.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2009 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.gadget; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Intent; +import android.content.res.AssetFileDescriptor; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; +import android.widget.Toast; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.Gallery; +import com.android.gallery3d.app.PhotoPage; +import com.android.gallery3d.common.ApiHelper; + +public class WidgetClickHandler extends Activity { + private static final String TAG = "PhotoAppWidgetClickHandler"; + + private boolean isValidDataUri(Uri dataUri) { + if (dataUri == null) return false; + try { + AssetFileDescriptor f = getContentResolver() + .openAssetFileDescriptor(dataUri, "r"); + f.close(); + return true; + } catch (Throwable e) { + Log.w(TAG, "cannot open uri: " + dataUri, e); + return false; + } + } + + @Override + @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) + protected void onCreate(Bundle savedState) { + super.onCreate(savedState); + // The behavior is changed in JB, refer to b/6384492 for more details + boolean tediousBack = Build.VERSION.SDK_INT >= ApiHelper.VERSION_CODES.JELLY_BEAN; + Uri uri = getIntent().getData(); + Intent intent; + if (isValidDataUri(uri)) { + intent = new Intent(Intent.ACTION_VIEW, uri); + if (tediousBack) { + intent.putExtra(PhotoPage.KEY_TREAT_BACK_AS_UP, true); + } + } else { + Toast.makeText(this, + R.string.no_such_item, Toast.LENGTH_LONG).show(); + intent = new Intent(this, Gallery.class); + } + if (tediousBack) { + intent.setFlags( + Intent.FLAG_ACTIVITY_NEW_TASK | + Intent.FLAG_ACTIVITY_CLEAR_TASK | + Intent.FLAG_ACTIVITY_TASK_ON_HOME); + } + startActivity(intent); + finish(); + } +} diff --git a/src/com/android/gallery3d/gadget/WidgetConfigure.java b/src/com/android/gallery3d/gadget/WidgetConfigure.java new file mode 100644 index 000000000..2a4c6cfe4 --- /dev/null +++ b/src/com/android/gallery3d/gadget/WidgetConfigure.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2011 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.gadget; + +import android.app.Activity; +import android.appwidget.AppWidgetManager; +import android.content.Intent; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; +import android.widget.RemoteViews; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.AlbumPicker; +import com.android.gallery3d.app.DialogPicker; +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.LocalAlbum; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.filtershow.crop.CropActivity; +import com.android.gallery3d.filtershow.crop.CropExtras; + +public class WidgetConfigure extends Activity { + @SuppressWarnings("unused") + private static final String TAG = "WidgetConfigure"; + + public static final String KEY_WIDGET_TYPE = "widget-type"; + private static final String KEY_PICKED_ITEM = "picked-item"; + + private static final int REQUEST_WIDGET_TYPE = 1; + private static final int REQUEST_CHOOSE_ALBUM = 2; + private static final int REQUEST_CROP_IMAGE = 3; + private static final int REQUEST_GET_PHOTO = 4; + + public static final int RESULT_ERROR = RESULT_FIRST_USER; + + // Scale up the widget size since we only specified the minimized + // size of the gadget. The real size could be larger. + // Note: There is also a limit on the size of data that can be + // passed in Binder's transaction. + private static float WIDGET_SCALE_FACTOR = 1.5f; + private static int MAX_WIDGET_SIDE = 360; + + private int mAppWidgetId = -1; + private Uri mPickedItem; + + @Override + protected void onCreate(Bundle savedState) { + super.onCreate(savedState); + mAppWidgetId = getIntent().getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1); + + if (mAppWidgetId == -1) { + setResult(Activity.RESULT_CANCELED); + finish(); + return; + } + + if (savedState == null) { + if (ApiHelper.HAS_REMOTE_VIEWS_SERVICE) { + Intent intent = new Intent(this, WidgetTypeChooser.class); + startActivityForResult(intent, REQUEST_WIDGET_TYPE); + } else { // Choose the photo type widget + setWidgetType(new Intent() + .putExtra(KEY_WIDGET_TYPE, R.id.widget_type_photo)); + } + } else { + mPickedItem = savedState.getParcelable(KEY_PICKED_ITEM); + } + } + + protected void onSaveInstanceStates(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putParcelable(KEY_PICKED_ITEM, mPickedItem); + } + + private void updateWidgetAndFinish(WidgetDatabaseHelper.Entry entry) { + AppWidgetManager manager = AppWidgetManager.getInstance(this); + RemoteViews views = PhotoAppWidgetProvider.buildWidget(this, mAppWidgetId, entry); + manager.updateAppWidget(mAppWidgetId, views); + setResult(RESULT_OK, new Intent().putExtra( + AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId)); + finish(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode != RESULT_OK) { + setResult(resultCode, new Intent().putExtra( + AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId)); + finish(); + return; + } + + if (requestCode == REQUEST_WIDGET_TYPE) { + setWidgetType(data); + } else if (requestCode == REQUEST_CHOOSE_ALBUM) { + setChoosenAlbum(data); + } else if (requestCode == REQUEST_GET_PHOTO) { + setChoosenPhoto(data); + } else if (requestCode == REQUEST_CROP_IMAGE) { + setPhotoWidget(data); + } else { + throw new AssertionError("unknown request: " + requestCode); + } + } + + private void setPhotoWidget(Intent data) { + // Store the cropped photo in our database + Bitmap bitmap = (Bitmap) data.getParcelableExtra("data"); + WidgetDatabaseHelper helper = new WidgetDatabaseHelper(this); + try { + helper.setPhoto(mAppWidgetId, mPickedItem, bitmap); + updateWidgetAndFinish(helper.getEntry(mAppWidgetId)); + } finally { + helper.close(); + } + } + + private void setChoosenPhoto(Intent data) { + Resources res = getResources(); + + float width = res.getDimension(R.dimen.appwidget_width); + float height = res.getDimension(R.dimen.appwidget_height); + + // We try to crop a larger image (by scale factor), but there is still + // a bound on the binder limit. + float scale = Math.min(WIDGET_SCALE_FACTOR, + MAX_WIDGET_SIDE / Math.max(width, height)); + + int widgetWidth = Math.round(width * scale); + int widgetHeight = Math.round(height * scale); + + mPickedItem = data.getData(); + Intent request = new Intent(CropActivity.CROP_ACTION, mPickedItem) + .putExtra(CropExtras.KEY_OUTPUT_X, widgetWidth) + .putExtra(CropExtras.KEY_OUTPUT_Y, widgetHeight) + .putExtra(CropExtras.KEY_ASPECT_X, widgetWidth) + .putExtra(CropExtras.KEY_ASPECT_Y, widgetHeight) + .putExtra(CropExtras.KEY_SCALE_UP_IF_NEEDED, true) + .putExtra(CropExtras.KEY_SCALE, true) + .putExtra(CropExtras.KEY_RETURN_DATA, true); + startActivityForResult(request, REQUEST_CROP_IMAGE); + } + + private void setChoosenAlbum(Intent data) { + String albumPath = data.getStringExtra(AlbumPicker.KEY_ALBUM_PATH); + WidgetDatabaseHelper helper = new WidgetDatabaseHelper(this); + try { + String relativePath = null; + GalleryApp galleryApp = (GalleryApp) getApplicationContext(); + DataManager manager = galleryApp.getDataManager(); + Path path = Path.fromString(albumPath); + MediaSet mediaSet = (MediaSet) manager.getMediaObject(path); + if (mediaSet instanceof LocalAlbum) { + int bucketId = Integer.parseInt(path.getSuffix()); + // If the chosen album is a local album, find relative path + // Otherwise, leave the relative path field empty + relativePath = LocalAlbum.getRelativePath(bucketId); + Log.i(TAG, "Setting widget, album path: " + albumPath + + ", relative path: " + relativePath); + } + helper.setWidget(mAppWidgetId, + WidgetDatabaseHelper.TYPE_ALBUM, albumPath, relativePath); + updateWidgetAndFinish(helper.getEntry(mAppWidgetId)); + } finally { + helper.close(); + } + } + + private void setWidgetType(Intent data) { + int widgetType = data.getIntExtra(KEY_WIDGET_TYPE, R.id.widget_type_shuffle); + if (widgetType == R.id.widget_type_album) { + Intent intent = new Intent(this, AlbumPicker.class); + startActivityForResult(intent, REQUEST_CHOOSE_ALBUM); + } else if (widgetType == R.id.widget_type_shuffle) { + WidgetDatabaseHelper helper = new WidgetDatabaseHelper(this); + try { + helper.setWidget(mAppWidgetId, WidgetDatabaseHelper.TYPE_SHUFFLE, null, null); + updateWidgetAndFinish(helper.getEntry(mAppWidgetId)); + } finally { + helper.close(); + } + } else { + // Explicitly send the intent to the DialogPhotoPicker + Intent request = new Intent(this, DialogPicker.class) + .setAction(Intent.ACTION_GET_CONTENT) + .setType("image/*"); + startActivityForResult(request, REQUEST_GET_PHOTO); + } + } +} diff --git a/src/com/android/gallery3d/gadget/WidgetDatabaseHelper.java b/src/com/android/gallery3d/gadget/WidgetDatabaseHelper.java new file mode 100644 index 000000000..c0145843b --- /dev/null +++ b/src/com/android/gallery3d/gadget/WidgetDatabaseHelper.java @@ -0,0 +1,309 @@ +/* + * 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.gadget; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import android.database.sqlite.SQLiteOpenHelper; +import android.graphics.Bitmap; +import android.net.Uri; +import android.util.Log; + +import com.android.gallery3d.common.Utils; + +import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.List; + +public class WidgetDatabaseHelper extends SQLiteOpenHelper { + private static final String TAG = "PhotoDatabaseHelper"; + private static final String DATABASE_NAME = "launcher.db"; + + // Increment the database version to 5. In version 5, we + // add a column in widgets table to record relative paths. + private static final int DATABASE_VERSION = 5; + + private static final String TABLE_WIDGETS = "widgets"; + + private static final String FIELD_APPWIDGET_ID = "appWidgetId"; + private static final String FIELD_IMAGE_URI = "imageUri"; + private static final String FIELD_PHOTO_BLOB = "photoBlob"; + private static final String FIELD_WIDGET_TYPE = "widgetType"; + private static final String FIELD_ALBUM_PATH = "albumPath"; + private static final String FIELD_RELATIVE_PATH = "relativePath"; + + public static final int TYPE_SINGLE_PHOTO = 0; + public static final int TYPE_SHUFFLE = 1; + public static final int TYPE_ALBUM = 2; + + private static final String[] PROJECTION = { + FIELD_WIDGET_TYPE, FIELD_IMAGE_URI, FIELD_PHOTO_BLOB, FIELD_ALBUM_PATH, + FIELD_APPWIDGET_ID, FIELD_RELATIVE_PATH}; + private static final int INDEX_WIDGET_TYPE = 0; + private static final int INDEX_IMAGE_URI = 1; + private static final int INDEX_PHOTO_BLOB = 2; + private static final int INDEX_ALBUM_PATH = 3; + private static final int INDEX_APPWIDGET_ID = 4; + private static final int INDEX_RELATIVE_PATH = 5; + private static final String WHERE_APPWIDGET_ID = FIELD_APPWIDGET_ID + " = ?"; + private static final String WHERE_WIDGET_TYPE = FIELD_WIDGET_TYPE + " = ?"; + + public static class Entry { + public int widgetId; + public int type; + public String imageUri; + public byte imageData[]; + public String albumPath; + public String relativePath; + + private Entry() {} + + private Entry(int id, Cursor cursor) { + widgetId = id; + type = cursor.getInt(INDEX_WIDGET_TYPE); + if (type == TYPE_SINGLE_PHOTO) { + imageUri = cursor.getString(INDEX_IMAGE_URI); + imageData = cursor.getBlob(INDEX_PHOTO_BLOB); + } else if (type == TYPE_ALBUM) { + albumPath = cursor.getString(INDEX_ALBUM_PATH); + relativePath = cursor.getString(INDEX_RELATIVE_PATH); + } + } + + private Entry(Cursor cursor) { + this(cursor.getInt(INDEX_APPWIDGET_ID), cursor); + } + } + + public WidgetDatabaseHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL("CREATE TABLE " + TABLE_WIDGETS + " (" + + FIELD_APPWIDGET_ID + " INTEGER PRIMARY KEY, " + + FIELD_WIDGET_TYPE + " INTEGER DEFAULT 0, " + + FIELD_IMAGE_URI + " TEXT, " + + FIELD_ALBUM_PATH + " TEXT, " + + FIELD_PHOTO_BLOB + " BLOB, " + + FIELD_RELATIVE_PATH + " TEXT)"); + } + + private void saveData(SQLiteDatabase db, int oldVersion, ArrayList<Entry> data) { + if (oldVersion <= 2) { + Cursor cursor = db.query("photos", + new String[] {FIELD_APPWIDGET_ID, FIELD_PHOTO_BLOB}, + null, null, null, null, null); + if (cursor == null) return; + try { + while (cursor.moveToNext()) { + Entry entry = new Entry(); + entry.type = TYPE_SINGLE_PHOTO; + entry.widgetId = cursor.getInt(0); + entry.imageData = cursor.getBlob(1); + data.add(entry); + } + } finally { + cursor.close(); + } + } else if (oldVersion == 3) { + Cursor cursor = db.query("photos", + new String[] {FIELD_APPWIDGET_ID, FIELD_PHOTO_BLOB, FIELD_IMAGE_URI}, + null, null, null, null, null); + if (cursor == null) return; + try { + while (cursor.moveToNext()) { + Entry entry = new Entry(); + entry.type = TYPE_SINGLE_PHOTO; + entry.widgetId = cursor.getInt(0); + entry.imageData = cursor.getBlob(1); + entry.imageUri = cursor.getString(2); + data.add(entry); + } + } finally { + cursor.close(); + } + } + } + + private void restoreData(SQLiteDatabase db, ArrayList<Entry> data) { + db.beginTransaction(); + try { + for (Entry entry : data) { + ContentValues values = new ContentValues(); + values.put(FIELD_APPWIDGET_ID, entry.widgetId); + values.put(FIELD_WIDGET_TYPE, entry.type); + values.put(FIELD_IMAGE_URI, entry.imageUri); + values.put(FIELD_PHOTO_BLOB, entry.imageData); + values.put(FIELD_ALBUM_PATH, entry.albumPath); + db.insert(TABLE_WIDGETS, null, values); + } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion < 4) { + // Table "photos" is renamed to "widget" in version 4 + ArrayList<Entry> data = new ArrayList<Entry>(); + saveData(db, oldVersion, data); + + Log.w(TAG, "destroying all old data."); + db.execSQL("DROP TABLE IF EXISTS photos"); + db.execSQL("DROP TABLE IF EXISTS " + TABLE_WIDGETS); + onCreate(db); + + restoreData(db, data); + } + // Add a column for relative path + if (oldVersion < DATABASE_VERSION) { + try { + db.execSQL("ALTER TABLE widgets ADD COLUMN relativePath TEXT"); + } catch (Throwable t) { + Log.e(TAG, "Failed to add the column for relative path."); + return; + } + } + } + + /** + * Store the given bitmap in this database for the given appWidgetId. + */ + public boolean setPhoto(int appWidgetId, Uri imageUri, Bitmap bitmap) { + try { + // Try go guesstimate how much space the icon will take when + // serialized to avoid unnecessary allocations/copies during + // the write. + int size = bitmap.getWidth() * bitmap.getHeight() * 4; + ByteArrayOutputStream out = new ByteArrayOutputStream(size); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); + out.close(); + + ContentValues values = new ContentValues(); + values.put(FIELD_APPWIDGET_ID, appWidgetId); + values.put(FIELD_WIDGET_TYPE, TYPE_SINGLE_PHOTO); + values.put(FIELD_IMAGE_URI, imageUri.toString()); + values.put(FIELD_PHOTO_BLOB, out.toByteArray()); + + SQLiteDatabase db = getWritableDatabase(); + db.replaceOrThrow(TABLE_WIDGETS, null, values); + return true; + } catch (Throwable e) { + Log.e(TAG, "set widget photo fail", e); + return false; + } + } + + public boolean setWidget(int id, int type, String albumPath, String relativePath) { + try { + ContentValues values = new ContentValues(); + values.put(FIELD_APPWIDGET_ID, id); + values.put(FIELD_WIDGET_TYPE, type); + values.put(FIELD_ALBUM_PATH, Utils.ensureNotNull(albumPath)); + values.put(FIELD_RELATIVE_PATH, relativePath); + getWritableDatabase().replaceOrThrow(TABLE_WIDGETS, null, values); + return true; + } catch (Throwable e) { + Log.e(TAG, "set widget fail", e); + return false; + } + } + + public Entry getEntry(int appWidgetId) { + Cursor cursor = null; + try { + SQLiteDatabase db = getReadableDatabase(); + cursor = db.query(TABLE_WIDGETS, PROJECTION, + WHERE_APPWIDGET_ID, new String[] {String.valueOf(appWidgetId)}, + null, null, null); + if (cursor == null || !cursor.moveToNext()) { + Log.e(TAG, "query fail: empty cursor: " + cursor + " appWidgetId: " + + appWidgetId); + return null; + } + return new Entry(appWidgetId, cursor); + } catch (Throwable e) { + Log.e(TAG, "Could not load photo from database", e); + return null; + } finally { + Utils.closeSilently(cursor); + } + } + + public List<Entry> getEntries(int type) { + Cursor cursor = null; + try { + SQLiteDatabase db = getReadableDatabase(); + cursor = db.query(TABLE_WIDGETS, PROJECTION, + WHERE_WIDGET_TYPE, new String[] {String.valueOf(type)}, + null, null, null); + if (cursor == null) { + Log.e(TAG, "query fail: null cursor: " + cursor); + return null; + } + ArrayList<Entry> result = new ArrayList<Entry>(cursor.getCount()); + while (cursor.moveToNext()) { + result.add(new Entry(cursor)); + } + return result; + } catch (Throwable e) { + Log.e(TAG, "Could not load widget from database", e); + return null; + } finally { + Utils.closeSilently(cursor); + } + } + + /** + * Updates the entry in the widget database. + */ + public void updateEntry(Entry entry) { + deleteEntry(entry.widgetId); + try { + ContentValues values = new ContentValues(); + values.put(FIELD_APPWIDGET_ID, entry.widgetId); + values.put(FIELD_WIDGET_TYPE, entry.type); + values.put(FIELD_ALBUM_PATH, entry.albumPath); + values.put(FIELD_IMAGE_URI, entry.imageUri); + values.put(FIELD_PHOTO_BLOB, entry.imageData); + values.put(FIELD_RELATIVE_PATH, entry.relativePath); + getWritableDatabase().insert(TABLE_WIDGETS, null, values); + } catch (Throwable e) { + Log.e(TAG, "set widget fail", e); + } + } + + /** + * Remove any bitmap associated with the given appWidgetId. + */ + public void deleteEntry(int appWidgetId) { + try { + SQLiteDatabase db = getWritableDatabase(); + db.delete(TABLE_WIDGETS, WHERE_APPWIDGET_ID, + new String[] {String.valueOf(appWidgetId)}); + } catch (SQLiteException e) { + Log.e(TAG, "Could not delete photo from database", e); + } + } +} diff --git a/src/com/android/gallery3d/gadget/WidgetService.java b/src/com/android/gallery3d/gadget/WidgetService.java new file mode 100644 index 000000000..94dd16439 --- /dev/null +++ b/src/com/android/gallery3d/gadget/WidgetService.java @@ -0,0 +1,143 @@ +/* + * 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.gadget; + +import android.annotation.TargetApi; +import android.appwidget.AppWidgetManager; +import android.content.Intent; +import android.graphics.Bitmap; +import android.net.Uri; +import android.widget.RemoteViews; +import android.widget.RemoteViewsService; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.data.ContentListener; + +@TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) +public class WidgetService extends RemoteViewsService { + + @SuppressWarnings("unused") + private static final String TAG = "GalleryAppWidgetService"; + + public static final String EXTRA_WIDGET_TYPE = "widget-type"; + public static final String EXTRA_ALBUM_PATH = "album-path"; + + @Override + public RemoteViewsFactory onGetViewFactory(Intent intent) { + int id = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, + AppWidgetManager.INVALID_APPWIDGET_ID); + int type = intent.getIntExtra(EXTRA_WIDGET_TYPE, 0); + String albumPath = intent.getStringExtra(EXTRA_ALBUM_PATH); + + return new PhotoRVFactory((GalleryApp) getApplicationContext(), + id, type, albumPath); + } + + private static class PhotoRVFactory implements + RemoteViewsService.RemoteViewsFactory, ContentListener { + + private final int mAppWidgetId; + private final int mType; + private final String mAlbumPath; + private final GalleryApp mApp; + + private WidgetSource mSource; + + public PhotoRVFactory(GalleryApp app, int id, int type, String albumPath) { + mApp = app; + mAppWidgetId = id; + mType = type; + mAlbumPath = albumPath; + } + + @Override + public void onCreate() { + if (mType == WidgetDatabaseHelper.TYPE_ALBUM) { + mSource = new MediaSetSource(mApp.getDataManager(), mAlbumPath); + } else { + mSource = new LocalPhotoSource(mApp.getAndroidContext()); + } + mSource.setContentListener(this); + AppWidgetManager.getInstance(mApp.getAndroidContext()) + .notifyAppWidgetViewDataChanged( + mAppWidgetId, R.id.appwidget_stack_view); + } + + @Override + public void onDestroy() { + mSource.close(); + mSource = null; + } + + @Override + public int getCount() { + return mSource.size(); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public int getViewTypeCount() { + return 1; + } + + @Override + public boolean hasStableIds() { + return true; + } + + @Override + public RemoteViews getLoadingView() { + RemoteViews rv = new RemoteViews( + mApp.getAndroidContext().getPackageName(), + R.layout.appwidget_loading_item); + rv.setProgressBar(R.id.appwidget_loading_item, 0, 0, true); + return rv; + } + + @Override + public RemoteViews getViewAt(int position) { + Bitmap bitmap = mSource.getImage(position); + if (bitmap == null) return getLoadingView(); + RemoteViews views = new RemoteViews( + mApp.getAndroidContext().getPackageName(), + R.layout.appwidget_photo_item); + views.setImageViewBitmap(R.id.appwidget_photo_item, bitmap); + views.setOnClickFillInIntent(R.id.appwidget_photo_item, new Intent() + .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + .setData(mSource.getContentUri(position))); + return views; + } + + @Override + public void onDataSetChanged() { + mSource.reload(); + } + + @Override + public void onContentDirty() { + AppWidgetManager.getInstance(mApp.getAndroidContext()) + .notifyAppWidgetViewDataChanged( + mAppWidgetId, R.id.appwidget_stack_view); + } + } +} diff --git a/src/com/android/gallery3d/gadget/WidgetSource.java b/src/com/android/gallery3d/gadget/WidgetSource.java new file mode 100644 index 000000000..92874c740 --- /dev/null +++ b/src/com/android/gallery3d/gadget/WidgetSource.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2011 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.gadget; + +import android.graphics.Bitmap; +import android.net.Uri; + +import com.android.gallery3d.data.ContentListener; + +public interface WidgetSource { + public int size(); + public Bitmap getImage(int index); + public Uri getContentUri(int index); + public void setContentListener(ContentListener listener); + public void reload(); + public void close(); +} diff --git a/src/com/android/gallery3d/gadget/WidgetTypeChooser.java b/src/com/android/gallery3d/gadget/WidgetTypeChooser.java new file mode 100644 index 000000000..1694f1c04 --- /dev/null +++ b/src/com/android/gallery3d/gadget/WidgetTypeChooser.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2011 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.gadget; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.RadioGroup; +import android.widget.RadioGroup.OnCheckedChangeListener; + +import com.android.gallery3d.R; + +public class WidgetTypeChooser extends Activity { + + private OnCheckedChangeListener mListener = new OnCheckedChangeListener() { + @Override + public void onCheckedChanged(RadioGroup group, int checkedId) { + Intent data = new Intent() + .putExtra(WidgetConfigure.KEY_WIDGET_TYPE, checkedId); + setResult(RESULT_OK, data); + finish(); + } + }; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setTitle(R.string.widget_type); + setContentView(R.layout.choose_widget_type); + RadioGroup rg = (RadioGroup) findViewById(R.id.widget_type); + rg.setOnCheckedChangeListener(mListener); + + Button cancel = (Button) findViewById(R.id.cancel); + cancel.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + setResult(RESULT_CANCELED); + finish(); + } + }); + } +} diff --git a/src/com/android/gallery3d/gadget/WidgetUtils.java b/src/com/android/gallery3d/gadget/WidgetUtils.java new file mode 100644 index 000000000..c20c186df --- /dev/null +++ b/src/com/android/gallery3d/gadget/WidgetUtils.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2011 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.gadget; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.util.Log; + +import com.android.gallery3d.R; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.util.ThreadPool; + +public class WidgetUtils { + + private static final String TAG = "WidgetUtils"; + + private static int sStackPhotoWidth = 220; + private static int sStackPhotoHeight = 170; + + private WidgetUtils() { + } + + public static void initialize(Context context) { + Resources r = context.getResources(); + sStackPhotoWidth = r.getDimensionPixelSize(R.dimen.stack_photo_width); + sStackPhotoHeight = r.getDimensionPixelSize(R.dimen.stack_photo_height); + } + + public static Bitmap createWidgetBitmap(MediaItem image) { + Bitmap bitmap = image.requestImage(MediaItem.TYPE_THUMBNAIL) + .run(ThreadPool.JOB_CONTEXT_STUB); + if (bitmap == null) { + Log.w(TAG, "fail to get image of " + image.toString()); + return null; + } + return createWidgetBitmap(bitmap, image.getRotation()); + } + + public static Bitmap createWidgetBitmap(Bitmap bitmap, int rotation) { + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + + float scale; + if (((rotation / 90) & 1) == 0) { + scale = Math.max((float) sStackPhotoWidth / w, + (float) sStackPhotoHeight / h); + } else { + scale = Math.max((float) sStackPhotoWidth / h, + (float) sStackPhotoHeight / w); + } + + Bitmap target = Bitmap.createBitmap( + sStackPhotoWidth, sStackPhotoHeight, Config.ARGB_8888); + Canvas canvas = new Canvas(target); + canvas.translate(sStackPhotoWidth / 2, sStackPhotoHeight / 2); + canvas.rotate(rotation); + canvas.scale(scale, scale); + Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG); + canvas.drawBitmap(bitmap, -w / 2, -h / 2, paint); + return target; + } +} diff --git a/src/com/android/gallery3d/glrenderer/BasicTexture.java b/src/com/android/gallery3d/glrenderer/BasicTexture.java new file mode 100644 index 000000000..2e77b903f --- /dev/null +++ b/src/com/android/gallery3d/glrenderer/BasicTexture.java @@ -0,0 +1,212 @@ +/* + * 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.glrenderer; + +import android.util.Log; + +import com.android.gallery3d.common.Utils; + +import java.util.WeakHashMap; + +// BasicTexture is a Texture corresponds to a real GL texture. +// The state of a BasicTexture indicates whether its data is loaded to GL memory. +// If a BasicTexture is loaded into GL memory, it has a GL texture id. +public abstract class BasicTexture implements Texture { + + @SuppressWarnings("unused") + private static final String TAG = "BasicTexture"; + protected static final int UNSPECIFIED = -1; + + protected static final int STATE_UNLOADED = 0; + protected static final int STATE_LOADED = 1; + protected static final int STATE_ERROR = -1; + + // Log a warning if a texture is larger along a dimension + private static final int MAX_TEXTURE_SIZE = 4096; + + protected int mId = -1; + protected int mState; + + protected int mWidth = UNSPECIFIED; + protected int mHeight = UNSPECIFIED; + + protected int mTextureWidth; + protected int mTextureHeight; + + private boolean mHasBorder; + + protected GLCanvas mCanvasRef = null; + private static WeakHashMap<BasicTexture, Object> sAllTextures + = new WeakHashMap<BasicTexture, Object>(); + private static ThreadLocal sInFinalizer = new ThreadLocal(); + + protected BasicTexture(GLCanvas canvas, int id, int state) { + setAssociatedCanvas(canvas); + mId = id; + mState = state; + synchronized (sAllTextures) { + sAllTextures.put(this, null); + } + } + + protected BasicTexture() { + this(null, 0, STATE_UNLOADED); + } + + protected void setAssociatedCanvas(GLCanvas canvas) { + mCanvasRef = canvas; + } + + /** + * Sets the content size of this texture. In OpenGL, the actual texture + * size must be of power of 2, the size of the content may be smaller. + */ + public void setSize(int width, int height) { + mWidth = width; + mHeight = height; + mTextureWidth = width > 0 ? Utils.nextPowerOf2(width) : 0; + mTextureHeight = height > 0 ? Utils.nextPowerOf2(height) : 0; + if (mTextureWidth > MAX_TEXTURE_SIZE || mTextureHeight > MAX_TEXTURE_SIZE) { + Log.w(TAG, String.format("texture is too large: %d x %d", + mTextureWidth, mTextureHeight), new Exception()); + } + } + + public boolean isFlippedVertically() { + return false; + } + + public int getId() { + return mId; + } + + @Override + public int getWidth() { + return mWidth; + } + + @Override + public int getHeight() { + return mHeight; + } + + // Returns the width rounded to the next power of 2. + public int getTextureWidth() { + return mTextureWidth; + } + + // Returns the height rounded to the next power of 2. + public int getTextureHeight() { + return mTextureHeight; + } + + // Returns true if the texture has one pixel transparent border around the + // actual content. This is used to avoid jigged edges. + // + // The jigged edges appear because we use GL_CLAMP_TO_EDGE for texture wrap + // mode (GL_CLAMP is not available in OpenGL ES), so a pixel partially + // covered by the texture will use the color of the edge texel. If we add + // the transparent border, the color of the edge texel will be mixed with + // appropriate amount of transparent. + // + // Currently our background is black, so we can draw the thumbnails without + // enabling blending. + public boolean hasBorder() { + return mHasBorder; + } + + protected void setBorder(boolean hasBorder) { + mHasBorder = hasBorder; + } + + @Override + public void draw(GLCanvas canvas, int x, int y) { + canvas.drawTexture(this, x, y, getWidth(), getHeight()); + } + + @Override + public void draw(GLCanvas canvas, int x, int y, int w, int h) { + canvas.drawTexture(this, x, y, w, h); + } + + // onBind is called before GLCanvas binds this texture. + // It should make sure the data is uploaded to GL memory. + abstract protected boolean onBind(GLCanvas canvas); + + // Returns the GL texture target for this texture (e.g. GL_TEXTURE_2D). + abstract protected int getTarget(); + + public boolean isLoaded() { + return mState == STATE_LOADED; + } + + // recycle() is called when the texture will never be used again, + // so it can free all resources. + public void recycle() { + freeResource(); + } + + // yield() is called when the texture will not be used temporarily, + // so it can free some resources. + // The default implementation unloads the texture from GL memory, so + // the subclass should make sure it can reload the texture to GL memory + // later, or it will have to override this method. + public void yield() { + freeResource(); + } + + private void freeResource() { + GLCanvas canvas = mCanvasRef; + if (canvas != null && mId != -1) { + canvas.unloadTexture(this); + mId = -1; // Don't free it again. + } + mState = STATE_UNLOADED; + setAssociatedCanvas(null); + } + + @Override + protected void finalize() { + sInFinalizer.set(BasicTexture.class); + recycle(); + sInFinalizer.set(null); + } + + // This is for deciding if we can call Bitmap's recycle(). + // We cannot call Bitmap's recycle() in finalizer because at that point + // the finalizer of Bitmap may already be called so recycle() will crash. + public static boolean inFinalizer() { + return sInFinalizer.get() != null; + } + + public static void yieldAllTextures() { + synchronized (sAllTextures) { + for (BasicTexture t : sAllTextures.keySet()) { + t.yield(); + } + } + } + + public static void invalidateAllTextures() { + synchronized (sAllTextures) { + for (BasicTexture t : sAllTextures.keySet()) { + t.mState = STATE_UNLOADED; + t.setAssociatedCanvas(null); + } + } + } +} diff --git a/src/com/android/gallery3d/glrenderer/BitmapTexture.java b/src/com/android/gallery3d/glrenderer/BitmapTexture.java new file mode 100644 index 000000000..100b0b3b9 --- /dev/null +++ b/src/com/android/gallery3d/glrenderer/BitmapTexture.java @@ -0,0 +1,54 @@ +/* + * 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.glrenderer; + +import android.graphics.Bitmap; + +import junit.framework.Assert; + +// BitmapTexture is a texture whose content is specified by a fixed Bitmap. +// +// The texture does not own the Bitmap. The user should make sure the Bitmap +// is valid during the texture's lifetime. When the texture is recycled, it +// does not free the Bitmap. +public class BitmapTexture extends UploadedTexture { + protected Bitmap mContentBitmap; + + public BitmapTexture(Bitmap bitmap) { + this(bitmap, false); + } + + public BitmapTexture(Bitmap bitmap, boolean hasBorder) { + super(hasBorder); + Assert.assertTrue(bitmap != null && !bitmap.isRecycled()); + mContentBitmap = bitmap; + } + + @Override + protected void onFreeBitmap(Bitmap bitmap) { + // Do nothing. + } + + @Override + protected Bitmap onGetBitmap() { + return mContentBitmap; + } + + public Bitmap getBitmap() { + return mContentBitmap; + } +} diff --git a/src/com/android/gallery3d/glrenderer/CanvasTexture.java b/src/com/android/gallery3d/glrenderer/CanvasTexture.java new file mode 100644 index 000000000..bff9d4baa --- /dev/null +++ b/src/com/android/gallery3d/glrenderer/CanvasTexture.java @@ -0,0 +1,52 @@ +/* + * 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.glrenderer; + +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.Canvas; + +// CanvasTexture is a texture whose content is the drawing on a Canvas. +// The subclasses should override onDraw() to draw on the bitmap. +// By default CanvasTexture is not opaque. +abstract class CanvasTexture extends UploadedTexture { + protected Canvas mCanvas; + private final Config mConfig; + + public CanvasTexture(int width, int height) { + mConfig = Config.ARGB_8888; + setSize(width, height); + setOpaque(false); + } + + @Override + protected Bitmap onGetBitmap() { + Bitmap bitmap = Bitmap.createBitmap(mWidth, mHeight, mConfig); + mCanvas = new Canvas(bitmap); + onDraw(mCanvas, bitmap); + return bitmap; + } + + @Override + protected void onFreeBitmap(Bitmap bitmap) { + if (!inFinalizer()) { + bitmap.recycle(); + } + } + + abstract protected void onDraw(Canvas canvas, Bitmap backing); +} diff --git a/src/com/android/gallery3d/glrenderer/ColorTexture.java b/src/com/android/gallery3d/glrenderer/ColorTexture.java new file mode 100644 index 000000000..904c78e1b --- /dev/null +++ b/src/com/android/gallery3d/glrenderer/ColorTexture.java @@ -0,0 +1,63 @@ +/* + * 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.glrenderer; + +import com.android.gallery3d.common.Utils; + +// ColorTexture is a texture which fills the rectangle with the specified color. +public class ColorTexture implements Texture { + + private final int mColor; + private int mWidth; + private int mHeight; + + public ColorTexture(int color) { + mColor = color; + mWidth = 1; + mHeight = 1; + } + + @Override + public void draw(GLCanvas canvas, int x, int y) { + draw(canvas, x, y, mWidth, mHeight); + } + + @Override + public void draw(GLCanvas canvas, int x, int y, int w, int h) { + canvas.fillRect(x, y, w, h, mColor); + } + + @Override + public boolean isOpaque() { + return Utils.isOpaque(mColor); + } + + public void setSize(int width, int height) { + mWidth = width; + mHeight = height; + } + + @Override + public int getWidth() { + return mWidth; + } + + @Override + public int getHeight() { + return mHeight; + } +} diff --git a/src/com/android/gallery3d/glrenderer/ExtTexture.java b/src/com/android/gallery3d/glrenderer/ExtTexture.java new file mode 100644 index 000000000..af76300b1 --- /dev/null +++ b/src/com/android/gallery3d/glrenderer/ExtTexture.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2012 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.glrenderer; + +// ExtTexture is a texture whose content comes from a external texture. +// Before drawing, setSize() should be called. +public class ExtTexture extends BasicTexture { + + private int mTarget; + + public ExtTexture(GLCanvas canvas, int target) { + GLId glId = canvas.getGLId(); + mId = glId.generateTexture(); + mTarget = target; + } + + private void uploadToCanvas(GLCanvas canvas) { + canvas.setTextureParameters(this); + setAssociatedCanvas(canvas); + mState = STATE_LOADED; + } + + @Override + protected boolean onBind(GLCanvas canvas) { + if (!isLoaded()) { + uploadToCanvas(canvas); + } + + return true; + } + + @Override + public int getTarget() { + return mTarget; + } + + @Override + public boolean isOpaque() { + return true; + } + + @Override + public void yield() { + // we cannot free the texture because we have no backup. + } +} diff --git a/src/com/android/gallery3d/glrenderer/FadeInTexture.java b/src/com/android/gallery3d/glrenderer/FadeInTexture.java new file mode 100644 index 000000000..838d465f5 --- /dev/null +++ b/src/com/android/gallery3d/glrenderer/FadeInTexture.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2011 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.glrenderer; + + +// FadeInTexture is a texture which begins with a color, then gradually animates +// into a given texture. +public class FadeInTexture extends FadeTexture implements Texture { + @SuppressWarnings("unused") + private static final String TAG = "FadeInTexture"; + + private final int mColor; + private final TiledTexture mTexture; + + public FadeInTexture(int color, TiledTexture texture) { + super(texture.getWidth(), texture.getHeight(), texture.isOpaque()); + mColor = color; + mTexture = texture; + } + + @Override + public void draw(GLCanvas canvas, int x, int y, int w, int h) { + if (isAnimating()) { + mTexture.drawMixed(canvas, mColor, getRatio(), x, y, w, h); + } else { + mTexture.draw(canvas, x, y, w, h); + } + } +} diff --git a/src/com/android/gallery3d/glrenderer/FadeOutTexture.java b/src/com/android/gallery3d/glrenderer/FadeOutTexture.java new file mode 100644 index 000000000..b05f3b631 --- /dev/null +++ b/src/com/android/gallery3d/glrenderer/FadeOutTexture.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2011 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.glrenderer; + + +// FadeOutTexture is a texture which begins with a given texture, then gradually animates +// into fading out totally. +public class FadeOutTexture extends FadeTexture { + @SuppressWarnings("unused") + private static final String TAG = "FadeOutTexture"; + + private final BasicTexture mTexture; + + public FadeOutTexture(BasicTexture texture) { + super(texture.getWidth(), texture.getHeight(), texture.isOpaque()); + mTexture = texture; + } + + @Override + public void draw(GLCanvas canvas, int x, int y, int w, int h) { + if (isAnimating()) { + canvas.save(GLCanvas.SAVE_FLAG_ALPHA); + canvas.setAlpha(getRatio()); + mTexture.draw(canvas, x, y, w, h); + canvas.restore(); + } + } +} diff --git a/src/com/android/gallery3d/glrenderer/FadeTexture.java b/src/com/android/gallery3d/glrenderer/FadeTexture.java new file mode 100644 index 000000000..002c90f5c --- /dev/null +++ b/src/com/android/gallery3d/glrenderer/FadeTexture.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2011 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.glrenderer; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.ui.AnimationTime; + +// FadeTexture is a texture which fades the given texture along the time. +public abstract class FadeTexture implements Texture { + @SuppressWarnings("unused") + private static final String TAG = "FadeTexture"; + + // The duration of the fading animation in milliseconds + public static final int DURATION = 180; + + private final long mStartTime; + private final int mWidth; + private final int mHeight; + private final boolean mIsOpaque; + private boolean mIsAnimating; + + public FadeTexture(int width, int height, boolean opaque) { + mWidth = width; + mHeight = height; + mIsOpaque = opaque; + mStartTime = now(); + mIsAnimating = true; + } + + @Override + public void draw(GLCanvas canvas, int x, int y) { + draw(canvas, x, y, mWidth, mHeight); + } + + @Override + public boolean isOpaque() { + return mIsOpaque; + } + + @Override + public int getWidth() { + return mWidth; + } + + @Override + public int getHeight() { + return mHeight; + } + + public boolean isAnimating() { + if (mIsAnimating) { + if (now() - mStartTime >= DURATION) { + mIsAnimating = false; + } + } + return mIsAnimating; + } + + protected float getRatio() { + float r = (float)(now() - mStartTime) / DURATION; + return Utils.clamp(1.0f - r, 0.0f, 1.0f); + } + + private long now() { + return AnimationTime.get(); + } +} diff --git a/src/com/android/gallery3d/glrenderer/GLCanvas.java b/src/com/android/gallery3d/glrenderer/GLCanvas.java new file mode 100644 index 000000000..305e90521 --- /dev/null +++ b/src/com/android/gallery3d/glrenderer/GLCanvas.java @@ -0,0 +1,217 @@ +/* + * 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.glrenderer; + +import android.graphics.Bitmap; +import android.graphics.Rect; +import android.graphics.RectF; + +import javax.microedition.khronos.opengles.GL11; + +// +// GLCanvas gives a convenient interface to draw using OpenGL. +// +// When a rectangle is specified in this interface, it means the region +// [x, x+width) * [y, y+height) +// +public interface GLCanvas { + + public GLId getGLId(); + + // Tells GLCanvas the size of the underlying GL surface. This should be + // called before first drawing and when the size of GL surface is changed. + // This is called by GLRoot and should not be called by the clients + // who only want to draw on the GLCanvas. Both width and height must be + // nonnegative. + public abstract void setSize(int width, int height); + + // Clear the drawing buffers. This should only be used by GLRoot. + public abstract void clearBuffer(); + + public abstract void clearBuffer(float[] argb); + + // Sets and gets the current alpha, alpha must be in [0, 1]. + public abstract void setAlpha(float alpha); + + public abstract float getAlpha(); + + // (current alpha) = (current alpha) * alpha + public abstract void multiplyAlpha(float alpha); + + // Change the current transform matrix. + public abstract void translate(float x, float y, float z); + + public abstract void translate(float x, float y); + + public abstract void scale(float sx, float sy, float sz); + + public abstract void rotate(float angle, float x, float y, float z); + + public abstract void multiplyMatrix(float[] mMatrix, int offset); + + // Pushes the configuration state (matrix, and alpha) onto + // a private stack. + public abstract void save(); + + // Same as save(), but only save those specified in saveFlags. + public abstract void save(int saveFlags); + + public static final int SAVE_FLAG_ALL = 0xFFFFFFFF; + public static final int SAVE_FLAG_ALPHA = 0x01; + public static final int SAVE_FLAG_MATRIX = 0x02; + + // Pops from the top of the stack as current configuration state (matrix, + // alpha, and clip). This call balances a previous call to save(), and is + // used to remove all modifications to the configuration state since the + // last save call. + public abstract void restore(); + + // Draws a line using the specified paint from (x1, y1) to (x2, y2). + // (Both end points are included). + public abstract void drawLine(float x1, float y1, float x2, float y2, GLPaint paint); + + // Draws a rectangle using the specified paint from (x1, y1) to (x2, y2). + // (Both end points are included). + public abstract void drawRect(float x1, float y1, float x2, float y2, GLPaint paint); + + // Fills the specified rectangle with the specified color. + public abstract void fillRect(float x, float y, float width, float height, int color); + + // Draws a texture to the specified rectangle. + public abstract void drawTexture( + BasicTexture texture, int x, int y, int width, int height); + + public abstract void drawMesh(BasicTexture tex, int x, int y, int xyBuffer, + int uvBuffer, int indexBuffer, int indexCount); + + // Draws the source rectangle part of the texture to the target rectangle. + public abstract void drawTexture(BasicTexture texture, RectF source, RectF target); + + // Draw a texture with a specified texture transform. + public abstract void drawTexture(BasicTexture texture, float[] mTextureTransform, + int x, int y, int w, int h); + + // Draw two textures to the specified rectangle. The actual texture used is + // from * (1 - ratio) + to * ratio + // The two textures must have the same size. + public abstract void drawMixed(BasicTexture from, int toColor, + float ratio, int x, int y, int w, int h); + + // Draw a region of a texture and a specified color to the specified + // rectangle. The actual color used is from * (1 - ratio) + to * ratio. + // The region of the texture is defined by parameter "src". The target + // rectangle is specified by parameter "target". + public abstract void drawMixed(BasicTexture from, int toColor, + float ratio, RectF src, RectF target); + + // Unloads the specified texture from the canvas. The resource allocated + // to draw the texture will be released. The specified texture will return + // to the unloaded state. This function should be called only from + // BasicTexture or its descendant + public abstract boolean unloadTexture(BasicTexture texture); + + // Delete the specified buffer object, similar to unloadTexture. + public abstract void deleteBuffer(int bufferId); + + // Delete the textures and buffers in GL side. This function should only be + // called in the GL thread. + public abstract void deleteRecycledResources(); + + // Dump statistics information and clear the counters. For debug only. + public abstract void dumpStatisticsAndClear(); + + public abstract void beginRenderTarget(RawTexture texture); + + public abstract void endRenderTarget(); + + /** + * Sets texture parameters to use GL_CLAMP_TO_EDGE for both + * GL_TEXTURE_WRAP_S and GL_TEXTURE_WRAP_T. Sets texture parameters to be + * GL_LINEAR for GL_TEXTURE_MIN_FILTER and GL_TEXTURE_MAG_FILTER. + * bindTexture() must be called prior to this. + * + * @param texture The texture to set parameters on. + */ + public abstract void setTextureParameters(BasicTexture texture); + + /** + * Initializes the texture to a size by calling texImage2D on it. + * + * @param texture The texture to initialize the size. + * @param format The texture format (e.g. GL_RGBA) + * @param type The texture type (e.g. GL_UNSIGNED_BYTE) + */ + public abstract void initializeTextureSize(BasicTexture texture, int format, int type); + + /** + * Initializes the texture to a size by calling texImage2D on it. + * + * @param texture The texture to initialize the size. + * @param bitmap The bitmap to initialize the bitmap with. + */ + public abstract void initializeTexture(BasicTexture texture, Bitmap bitmap); + + /** + * Calls glTexSubImage2D to upload a bitmap to the texture. + * + * @param texture The target texture to write to. + * @param xOffset Specifies a texel offset in the x direction within the + * texture array. + * @param yOffset Specifies a texel offset in the y direction within the + * texture array. + * @param format The texture format (e.g. GL_RGBA) + * @param type The texture type (e.g. GL_UNSIGNED_BYTE) + */ + public abstract void texSubImage2D(BasicTexture texture, int xOffset, int yOffset, + Bitmap bitmap, + int format, int type); + + /** + * Generates buffers and uploads the buffer data. + * + * @param buffer The buffer to upload + * @return The buffer ID that was generated. + */ + public abstract int uploadBuffer(java.nio.FloatBuffer buffer); + + /** + * Generates buffers and uploads the element array buffer data. + * + * @param buffer The buffer to upload + * @return The buffer ID that was generated. + */ + public abstract int uploadBuffer(java.nio.ByteBuffer buffer); + + /** + * After LightCycle makes GL calls, this method is called to restore the GL + * configuration to the one expected by GLCanvas. + */ + public abstract void recoverFromLightCycle(); + + /** + * Gets the bounds given by x, y, width, and height as well as the internal + * matrix state. There is no special handling for non-90-degree rotations. + * It only considers the lower-left and upper-right corners as the bounds. + * + * @param bounds The output bounds to write to. + * @param x The left side of the input rectangle. + * @param y The bottom of the input rectangle. + * @param width The width of the input rectangle. + * @param height The height of the input rectangle. + */ + public abstract void getBounds(Rect bounds, int x, int y, int width, int height); +} diff --git a/src/com/android/gallery3d/glrenderer/GLES11Canvas.java b/src/com/android/gallery3d/glrenderer/GLES11Canvas.java new file mode 100644 index 000000000..7013c3d1f --- /dev/null +++ b/src/com/android/gallery3d/glrenderer/GLES11Canvas.java @@ -0,0 +1,997 @@ +/* + * 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.glrenderer; + +import android.graphics.Bitmap; +import android.graphics.Rect; +import android.graphics.RectF; +import android.opengl.GLU; +import android.opengl.GLUtils; +import android.opengl.Matrix; +import android.util.Log; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.util.IntArray; + +import junit.framework.Assert; + +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.util.ArrayList; + +import javax.microedition.khronos.opengles.GL10; +import javax.microedition.khronos.opengles.GL11; +import javax.microedition.khronos.opengles.GL11Ext; +import javax.microedition.khronos.opengles.GL11ExtensionPack; + +public class GLES11Canvas implements GLCanvas { + @SuppressWarnings("unused") + private static final String TAG = "GLCanvasImp"; + + private static final float OPAQUE_ALPHA = 0.95f; + + private static final int OFFSET_FILL_RECT = 0; + private static final int OFFSET_DRAW_LINE = 4; + private static final int OFFSET_DRAW_RECT = 6; + private static final float[] BOX_COORDINATES = { + 0, 0, 1, 0, 0, 1, 1, 1, // used for filling a rectangle + 0, 0, 1, 1, // used for drawing a line + 0, 0, 0, 1, 1, 1, 1, 0}; // used for drawing the outline of a rectangle + + private GL11 mGL; + + private final float mMatrixValues[] = new float[16]; + private final float mTextureMatrixValues[] = new float[16]; + + // The results of mapPoints are stored in this buffer, and the order is + // x1, y1, x2, y2. + private final float mMapPointsBuffer[] = new float[4]; + + private final float mTextureColor[] = new float[4]; + + private int mBoxCoords; + + private GLState mGLState; + private final ArrayList<RawTexture> mTargetStack = new ArrayList<RawTexture>(); + + private float mAlpha; + private final ArrayList<ConfigState> mRestoreStack = new ArrayList<ConfigState>(); + private ConfigState mRecycledRestoreAction; + + private final RectF mDrawTextureSourceRect = new RectF(); + private final RectF mDrawTextureTargetRect = new RectF(); + private final float[] mTempMatrix = new float[32]; + private final IntArray mUnboundTextures = new IntArray(); + private final IntArray mDeleteBuffers = new IntArray(); + private int mScreenWidth; + private int mScreenHeight; + private boolean mBlendEnabled = true; + private int mFrameBuffer[] = new int[1]; + private static float[] sCropRect = new float[4]; + + private RawTexture mTargetTexture; + + // Drawing statistics + int mCountDrawLine; + int mCountFillRect; + int mCountDrawMesh; + int mCountTextureRect; + int mCountTextureOES; + + private static GLId mGLId = new GLES11IdImpl(); + + public GLES11Canvas(GL11 gl) { + mGL = gl; + mGLState = new GLState(gl); + // First create an nio buffer, then create a VBO from it. + int size = BOX_COORDINATES.length * Float.SIZE / Byte.SIZE; + FloatBuffer xyBuffer = allocateDirectNativeOrderBuffer(size).asFloatBuffer(); + xyBuffer.put(BOX_COORDINATES, 0, BOX_COORDINATES.length).position(0); + + int[] name = new int[1]; + mGLId.glGenBuffers(1, name, 0); + mBoxCoords = name[0]; + + gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, mBoxCoords); + gl.glBufferData(GL11.GL_ARRAY_BUFFER, xyBuffer.capacity() * (Float.SIZE / Byte.SIZE), + xyBuffer, GL11.GL_STATIC_DRAW); + + gl.glVertexPointer(2, GL11.GL_FLOAT, 0, 0); + gl.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0); + + // Enable the texture coordinate array for Texture 1 + gl.glClientActiveTexture(GL11.GL_TEXTURE1); + gl.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0); + gl.glClientActiveTexture(GL11.GL_TEXTURE0); + gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY); + + // mMatrixValues and mAlpha will be initialized in setSize() + } + + @Override + public void setSize(int width, int height) { + Assert.assertTrue(width >= 0 && height >= 0); + + if (mTargetTexture == null) { + mScreenWidth = width; + mScreenHeight = height; + } + mAlpha = 1.0f; + + GL11 gl = mGL; + gl.glViewport(0, 0, width, height); + gl.glMatrixMode(GL11.GL_PROJECTION); + gl.glLoadIdentity(); + GLU.gluOrtho2D(gl, 0, width, 0, height); + + gl.glMatrixMode(GL11.GL_MODELVIEW); + gl.glLoadIdentity(); + + float matrix[] = mMatrixValues; + Matrix.setIdentityM(matrix, 0); + // to match the graphic coordinate system in android, we flip it vertically. + if (mTargetTexture == null) { + Matrix.translateM(matrix, 0, 0, height, 0); + Matrix.scaleM(matrix, 0, 1, -1, 1); + } + } + + @Override + public void setAlpha(float alpha) { + Assert.assertTrue(alpha >= 0 && alpha <= 1); + mAlpha = alpha; + } + + @Override + public float getAlpha() { + return mAlpha; + } + + @Override + public void multiplyAlpha(float alpha) { + Assert.assertTrue(alpha >= 0 && alpha <= 1); + mAlpha *= alpha; + } + + private static ByteBuffer allocateDirectNativeOrderBuffer(int size) { + return ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder()); + } + + @Override + public void drawRect(float x, float y, float width, float height, GLPaint paint) { + GL11 gl = mGL; + + mGLState.setColorMode(paint.getColor(), mAlpha); + mGLState.setLineWidth(paint.getLineWidth()); + + saveTransform(); + translate(x, y); + scale(width, height, 1); + + gl.glLoadMatrixf(mMatrixValues, 0); + gl.glDrawArrays(GL11.GL_LINE_LOOP, OFFSET_DRAW_RECT, 4); + + restoreTransform(); + mCountDrawLine++; + } + + @Override + public void drawLine(float x1, float y1, float x2, float y2, GLPaint paint) { + GL11 gl = mGL; + + mGLState.setColorMode(paint.getColor(), mAlpha); + mGLState.setLineWidth(paint.getLineWidth()); + + saveTransform(); + translate(x1, y1); + scale(x2 - x1, y2 - y1, 1); + + gl.glLoadMatrixf(mMatrixValues, 0); + gl.glDrawArrays(GL11.GL_LINE_STRIP, OFFSET_DRAW_LINE, 2); + + restoreTransform(); + mCountDrawLine++; + } + + @Override + public void fillRect(float x, float y, float width, float height, int color) { + mGLState.setColorMode(color, mAlpha); + GL11 gl = mGL; + + saveTransform(); + translate(x, y); + scale(width, height, 1); + + gl.glLoadMatrixf(mMatrixValues, 0); + gl.glDrawArrays(GL11.GL_TRIANGLE_STRIP, OFFSET_FILL_RECT, 4); + + restoreTransform(); + mCountFillRect++; + } + + @Override + public void translate(float x, float y, float z) { + Matrix.translateM(mMatrixValues, 0, x, y, z); + } + + // This is a faster version of translate(x, y, z) because + // (1) we knows z = 0, (2) we inline the Matrix.translateM call, + // (3) we unroll the loop + @Override + public void translate(float x, float y) { + float[] m = mMatrixValues; + m[12] += m[0] * x + m[4] * y; + m[13] += m[1] * x + m[5] * y; + m[14] += m[2] * x + m[6] * y; + m[15] += m[3] * x + m[7] * y; + } + + @Override + public void scale(float sx, float sy, float sz) { + Matrix.scaleM(mMatrixValues, 0, sx, sy, sz); + } + + @Override + public void rotate(float angle, float x, float y, float z) { + if (angle == 0) return; + float[] temp = mTempMatrix; + Matrix.setRotateM(temp, 0, angle, x, y, z); + Matrix.multiplyMM(temp, 16, mMatrixValues, 0, temp, 0); + System.arraycopy(temp, 16, mMatrixValues, 0, 16); + } + + @Override + public void multiplyMatrix(float matrix[], int offset) { + float[] temp = mTempMatrix; + Matrix.multiplyMM(temp, 0, mMatrixValues, 0, matrix, offset); + System.arraycopy(temp, 0, mMatrixValues, 0, 16); + } + + private void textureRect(float x, float y, float width, float height) { + GL11 gl = mGL; + + saveTransform(); + translate(x, y); + scale(width, height, 1); + + gl.glLoadMatrixf(mMatrixValues, 0); + gl.glDrawArrays(GL11.GL_TRIANGLE_STRIP, OFFSET_FILL_RECT, 4); + + restoreTransform(); + mCountTextureRect++; + } + + @Override + public void drawMesh(BasicTexture tex, int x, int y, int xyBuffer, + int uvBuffer, int indexBuffer, int indexCount) { + float alpha = mAlpha; + if (!bindTexture(tex)) return; + + mGLState.setBlendEnabled(mBlendEnabled + && (!tex.isOpaque() || alpha < OPAQUE_ALPHA)); + mGLState.setTextureAlpha(alpha); + + // Reset the texture matrix. We will set our own texture coordinates + // below. + setTextureCoords(0, 0, 1, 1); + + saveTransform(); + translate(x, y); + + mGL.glLoadMatrixf(mMatrixValues, 0); + + mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, xyBuffer); + mGL.glVertexPointer(2, GL11.GL_FLOAT, 0, 0); + + mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, uvBuffer); + mGL.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0); + + mGL.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, indexBuffer); + mGL.glDrawElements(GL11.GL_TRIANGLE_STRIP, + indexCount, GL11.GL_UNSIGNED_BYTE, 0); + + mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, mBoxCoords); + mGL.glVertexPointer(2, GL11.GL_FLOAT, 0, 0); + mGL.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0); + + restoreTransform(); + mCountDrawMesh++; + } + + // Transforms two points by the given matrix m. The result + // {x1', y1', x2', y2'} are stored in mMapPointsBuffer and also returned. + private float[] mapPoints(float m[], int x1, int y1, int x2, int y2) { + float[] r = mMapPointsBuffer; + + // Multiply m and (x1 y1 0 1) to produce (x3 y3 z3 w3). z3 is unused. + float x3 = m[0] * x1 + m[4] * y1 + m[12]; + float y3 = m[1] * x1 + m[5] * y1 + m[13]; + float w3 = m[3] * x1 + m[7] * y1 + m[15]; + r[0] = x3 / w3; + r[1] = y3 / w3; + + // Same for x2 y2. + float x4 = m[0] * x2 + m[4] * y2 + m[12]; + float y4 = m[1] * x2 + m[5] * y2 + m[13]; + float w4 = m[3] * x2 + m[7] * y2 + m[15]; + r[2] = x4 / w4; + r[3] = y4 / w4; + + return r; + } + + private void drawBoundTexture( + BasicTexture texture, int x, int y, int width, int height) { + // Test whether it has been rotated or flipped, if so, glDrawTexiOES + // won't work + if (isMatrixRotatedOrFlipped(mMatrixValues)) { + if (texture.hasBorder()) { + setTextureCoords( + 1.0f / texture.getTextureWidth(), + 1.0f / texture.getTextureHeight(), + (texture.getWidth() - 1.0f) / texture.getTextureWidth(), + (texture.getHeight() - 1.0f) / texture.getTextureHeight()); + } else { + setTextureCoords(0, 0, + (float) texture.getWidth() / texture.getTextureWidth(), + (float) texture.getHeight() / texture.getTextureHeight()); + } + textureRect(x, y, width, height); + } else { + // draw the rect from bottom-left to top-right + float points[] = mapPoints( + mMatrixValues, x, y + height, x + width, y); + x = (int) (points[0] + 0.5f); + y = (int) (points[1] + 0.5f); + width = (int) (points[2] + 0.5f) - x; + height = (int) (points[3] + 0.5f) - y; + if (width > 0 && height > 0) { + ((GL11Ext) mGL).glDrawTexiOES(x, y, 0, width, height); + mCountTextureOES++; + } + } + } + + @Override + public void drawTexture( + BasicTexture texture, int x, int y, int width, int height) { + drawTexture(texture, x, y, width, height, mAlpha); + } + + private void drawTexture(BasicTexture texture, + int x, int y, int width, int height, float alpha) { + if (width <= 0 || height <= 0) return; + + mGLState.setBlendEnabled(mBlendEnabled + && (!texture.isOpaque() || alpha < OPAQUE_ALPHA)); + if (!bindTexture(texture)) return; + mGLState.setTextureAlpha(alpha); + drawBoundTexture(texture, x, y, width, height); + } + + @Override + public void drawTexture(BasicTexture texture, RectF source, RectF target) { + if (target.width() <= 0 || target.height() <= 0) return; + + // Copy the input to avoid changing it. + mDrawTextureSourceRect.set(source); + mDrawTextureTargetRect.set(target); + source = mDrawTextureSourceRect; + target = mDrawTextureTargetRect; + + mGLState.setBlendEnabled(mBlendEnabled + && (!texture.isOpaque() || mAlpha < OPAQUE_ALPHA)); + if (!bindTexture(texture)) return; + convertCoordinate(source, target, texture); + setTextureCoords(source); + mGLState.setTextureAlpha(mAlpha); + textureRect(target.left, target.top, target.width(), target.height()); + } + + @Override + public void drawTexture(BasicTexture texture, float[] mTextureTransform, + int x, int y, int w, int h) { + mGLState.setBlendEnabled(mBlendEnabled + && (!texture.isOpaque() || mAlpha < OPAQUE_ALPHA)); + if (!bindTexture(texture)) return; + setTextureCoords(mTextureTransform); + mGLState.setTextureAlpha(mAlpha); + textureRect(x, y, w, h); + } + + // This function changes the source coordinate to the texture coordinates. + // It also clips the source and target coordinates if it is beyond the + // bound of the texture. + private static void convertCoordinate(RectF source, RectF target, + BasicTexture texture) { + + int width = texture.getWidth(); + int height = texture.getHeight(); + int texWidth = texture.getTextureWidth(); + int texHeight = texture.getTextureHeight(); + // Convert to texture coordinates + source.left /= texWidth; + source.right /= texWidth; + source.top /= texHeight; + source.bottom /= texHeight; + + // Clip if the rendering range is beyond the bound of the texture. + float xBound = (float) width / texWidth; + if (source.right > xBound) { + target.right = target.left + target.width() * + (xBound - source.left) / source.width(); + source.right = xBound; + } + float yBound = (float) height / texHeight; + if (source.bottom > yBound) { + target.bottom = target.top + target.height() * + (yBound - source.top) / source.height(); + source.bottom = yBound; + } + } + + @Override + public void drawMixed(BasicTexture from, + int toColor, float ratio, int x, int y, int w, int h) { + drawMixed(from, toColor, ratio, x, y, w, h, mAlpha); + } + + private boolean bindTexture(BasicTexture texture) { + if (!texture.onBind(this)) return false; + int target = texture.getTarget(); + mGLState.setTextureTarget(target); + mGL.glBindTexture(target, texture.getId()); + return true; + } + + private void setTextureColor(float r, float g, float b, float alpha) { + float[] color = mTextureColor; + color[0] = r; + color[1] = g; + color[2] = b; + color[3] = alpha; + } + + private void setMixedColor(int toColor, float ratio, float alpha) { + // + // The formula we want: + // alpha * ((1 - ratio) * from + ratio * to) + // + // The formula that GL supports is in the form of: + // combo * from + (1 - combo) * to * scale + // + // So, we have combo = alpha * (1 - ratio) + // and scale = alpha * ratio / (1 - combo) + // + float combo = alpha * (1 - ratio); + float scale = alpha * ratio / (1 - combo); + + // Specify the interpolation factor via the alpha component of + // GL_TEXTURE_ENV_COLORs. + // RGB component are get from toColor and will used as SRC1 + float colorScale = scale * (toColor >>> 24) / (0xff * 0xff); + setTextureColor(((toColor >>> 16) & 0xff) * colorScale, + ((toColor >>> 8) & 0xff) * colorScale, + (toColor & 0xff) * colorScale, combo); + GL11 gl = mGL; + gl.glTexEnvfv(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_COLOR, mTextureColor, 0); + + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_RGB, GL11.GL_INTERPOLATE); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_ALPHA, GL11.GL_INTERPOLATE); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC1_RGB, GL11.GL_CONSTANT); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND1_RGB, GL11.GL_SRC_COLOR); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC1_ALPHA, GL11.GL_CONSTANT); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND1_ALPHA, GL11.GL_SRC_ALPHA); + + // Wire up the interpolation factor for RGB. + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_RGB, GL11.GL_CONSTANT); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_RGB, GL11.GL_SRC_ALPHA); + + // Wire up the interpolation factor for alpha. + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_ALPHA, GL11.GL_CONSTANT); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_ALPHA, GL11.GL_SRC_ALPHA); + + } + + @Override + public void drawMixed(BasicTexture from, int toColor, float ratio, + RectF source, RectF target) { + if (target.width() <= 0 || target.height() <= 0) return; + + if (ratio <= 0.01f) { + drawTexture(from, source, target); + return; + } else if (ratio >= 1) { + fillRect(target.left, target.top, target.width(), target.height(), toColor); + return; + } + + float alpha = mAlpha; + + // Copy the input to avoid changing it. + mDrawTextureSourceRect.set(source); + mDrawTextureTargetRect.set(target); + source = mDrawTextureSourceRect; + target = mDrawTextureTargetRect; + + mGLState.setBlendEnabled(mBlendEnabled && (!from.isOpaque() + || !Utils.isOpaque(toColor) || alpha < OPAQUE_ALPHA)); + + if (!bindTexture(from)) return; + + // Interpolate the RGB and alpha values between both textures. + mGLState.setTexEnvMode(GL11.GL_COMBINE); + setMixedColor(toColor, ratio, alpha); + convertCoordinate(source, target, from); + setTextureCoords(source); + textureRect(target.left, target.top, target.width(), target.height()); + mGLState.setTexEnvMode(GL11.GL_REPLACE); + } + + private void drawMixed(BasicTexture from, int toColor, + float ratio, int x, int y, int width, int height, float alpha) { + // change from 0 to 0.01f to prevent getting divided by zero below + if (ratio <= 0.01f) { + drawTexture(from, x, y, width, height, alpha); + return; + } else if (ratio >= 1) { + fillRect(x, y, width, height, toColor); + return; + } + + mGLState.setBlendEnabled(mBlendEnabled && (!from.isOpaque() + || !Utils.isOpaque(toColor) || alpha < OPAQUE_ALPHA)); + + final GL11 gl = mGL; + if (!bindTexture(from)) return; + + // Interpolate the RGB and alpha values between both textures. + mGLState.setTexEnvMode(GL11.GL_COMBINE); + setMixedColor(toColor, ratio, alpha); + + drawBoundTexture(from, x, y, width, height); + mGLState.setTexEnvMode(GL11.GL_REPLACE); + } + + // TODO: the code only work for 2D should get fixed for 3D or removed + private static final int MSKEW_X = 4; + private static final int MSKEW_Y = 1; + private static final int MSCALE_X = 0; + private static final int MSCALE_Y = 5; + + private static boolean isMatrixRotatedOrFlipped(float matrix[]) { + final float eps = 1e-5f; + return Math.abs(matrix[MSKEW_X]) > eps + || Math.abs(matrix[MSKEW_Y]) > eps + || matrix[MSCALE_X] < -eps + || matrix[MSCALE_Y] > eps; + } + + private static class GLState { + + private final GL11 mGL; + + private int mTexEnvMode = GL11.GL_REPLACE; + private float mTextureAlpha = 1.0f; + private int mTextureTarget = GL11.GL_TEXTURE_2D; + private boolean mBlendEnabled = true; + private float mLineWidth = 1.0f; + private boolean mLineSmooth = false; + + public GLState(GL11 gl) { + mGL = gl; + + // Disable unused state + gl.glDisable(GL11.GL_LIGHTING); + + // Enable used features + gl.glEnable(GL11.GL_DITHER); + + gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); + gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY); + gl.glEnable(GL11.GL_TEXTURE_2D); + + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, + GL11.GL_TEXTURE_ENV_MODE, GL11.GL_REPLACE); + + // Set the background color + gl.glClearColor(0f, 0f, 0f, 0f); + + gl.glEnable(GL11.GL_BLEND); + gl.glBlendFunc(GL11.GL_ONE, GL11.GL_ONE_MINUS_SRC_ALPHA); + + // We use 565 or 8888 format, so set the alignment to 2 bytes/pixel. + gl.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 2); + } + + public void setTexEnvMode(int mode) { + if (mTexEnvMode == mode) return; + mTexEnvMode = mode; + mGL.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, mode); + } + + public void setLineWidth(float width) { + if (mLineWidth == width) return; + mLineWidth = width; + mGL.glLineWidth(width); + } + + public void setTextureAlpha(float alpha) { + if (mTextureAlpha == alpha) return; + mTextureAlpha = alpha; + if (alpha >= OPAQUE_ALPHA) { + // The alpha is need for those texture without alpha channel + mGL.glColor4f(1, 1, 1, 1); + setTexEnvMode(GL11.GL_REPLACE); + } else { + mGL.glColor4f(alpha, alpha, alpha, alpha); + setTexEnvMode(GL11.GL_MODULATE); + } + } + + public void setColorMode(int color, float alpha) { + setBlendEnabled(!Utils.isOpaque(color) || alpha < OPAQUE_ALPHA); + + // Set mTextureAlpha to an invalid value, so that it will reset + // again in setTextureAlpha(float) later. + mTextureAlpha = -1.0f; + + setTextureTarget(0); + + float prealpha = (color >>> 24) * alpha * 65535f / 255f / 255f; + mGL.glColor4x( + Math.round(((color >> 16) & 0xFF) * prealpha), + Math.round(((color >> 8) & 0xFF) * prealpha), + Math.round((color & 0xFF) * prealpha), + Math.round(255 * prealpha)); + } + + // target is a value like GL_TEXTURE_2D. If target = 0, texturing is disabled. + public void setTextureTarget(int target) { + if (mTextureTarget == target) return; + if (mTextureTarget != 0) { + mGL.glDisable(mTextureTarget); + } + mTextureTarget = target; + if (mTextureTarget != 0) { + mGL.glEnable(mTextureTarget); + } + } + + public void setBlendEnabled(boolean enabled) { + if (mBlendEnabled == enabled) return; + mBlendEnabled = enabled; + if (enabled) { + mGL.glEnable(GL11.GL_BLEND); + } else { + mGL.glDisable(GL11.GL_BLEND); + } + } + } + + @Override + public void clearBuffer(float[] argb) { + if(argb != null && argb.length == 4) { + mGL.glClearColor(argb[1], argb[2], argb[3], argb[0]); + } else { + mGL.glClearColor(0, 0, 0, 1); + } + mGL.glClear(GL10.GL_COLOR_BUFFER_BIT); + } + + @Override + public void clearBuffer() { + clearBuffer(null); + } + + private void setTextureCoords(RectF source) { + setTextureCoords(source.left, source.top, source.right, source.bottom); + } + + private void setTextureCoords(float left, float top, + float right, float bottom) { + mGL.glMatrixMode(GL11.GL_TEXTURE); + mTextureMatrixValues[0] = right - left; + mTextureMatrixValues[5] = bottom - top; + mTextureMatrixValues[10] = 1; + mTextureMatrixValues[12] = left; + mTextureMatrixValues[13] = top; + mTextureMatrixValues[15] = 1; + mGL.glLoadMatrixf(mTextureMatrixValues, 0); + mGL.glMatrixMode(GL11.GL_MODELVIEW); + } + + private void setTextureCoords(float[] mTextureTransform) { + mGL.glMatrixMode(GL11.GL_TEXTURE); + mGL.glLoadMatrixf(mTextureTransform, 0); + mGL.glMatrixMode(GL11.GL_MODELVIEW); + } + + // unloadTexture and deleteBuffer can be called from the finalizer thread, + // so we synchronized on the mUnboundTextures object. + @Override + public boolean unloadTexture(BasicTexture t) { + synchronized (mUnboundTextures) { + if (!t.isLoaded()) return false; + mUnboundTextures.add(t.mId); + return true; + } + } + + @Override + public void deleteBuffer(int bufferId) { + synchronized (mUnboundTextures) { + mDeleteBuffers.add(bufferId); + } + } + + @Override + public void deleteRecycledResources() { + synchronized (mUnboundTextures) { + IntArray ids = mUnboundTextures; + if (ids.size() > 0) { + mGLId.glDeleteTextures(mGL, ids.size(), ids.getInternalArray(), 0); + ids.clear(); + } + + ids = mDeleteBuffers; + if (ids.size() > 0) { + mGLId.glDeleteBuffers(mGL, ids.size(), ids.getInternalArray(), 0); + ids.clear(); + } + } + } + + @Override + public void save() { + save(SAVE_FLAG_ALL); + } + + @Override + public void save(int saveFlags) { + ConfigState config = obtainRestoreConfig(); + + if ((saveFlags & SAVE_FLAG_ALPHA) != 0) { + config.mAlpha = mAlpha; + } else { + config.mAlpha = -1; + } + + if ((saveFlags & SAVE_FLAG_MATRIX) != 0) { + System.arraycopy(mMatrixValues, 0, config.mMatrix, 0, 16); + } else { + config.mMatrix[0] = Float.NEGATIVE_INFINITY; + } + + mRestoreStack.add(config); + } + + @Override + public void restore() { + if (mRestoreStack.isEmpty()) throw new IllegalStateException(); + ConfigState config = mRestoreStack.remove(mRestoreStack.size() - 1); + config.restore(this); + freeRestoreConfig(config); + } + + private void freeRestoreConfig(ConfigState action) { + action.mNextFree = mRecycledRestoreAction; + mRecycledRestoreAction = action; + } + + private ConfigState obtainRestoreConfig() { + if (mRecycledRestoreAction != null) { + ConfigState result = mRecycledRestoreAction; + mRecycledRestoreAction = result.mNextFree; + return result; + } + return new ConfigState(); + } + + private static class ConfigState { + float mAlpha; + float mMatrix[] = new float[16]; + ConfigState mNextFree; + + public void restore(GLES11Canvas canvas) { + if (mAlpha >= 0) canvas.setAlpha(mAlpha); + if (mMatrix[0] != Float.NEGATIVE_INFINITY) { + System.arraycopy(mMatrix, 0, canvas.mMatrixValues, 0, 16); + } + } + } + + @Override + public void dumpStatisticsAndClear() { + String line = String.format( + "MESH:%d, TEX_OES:%d, TEX_RECT:%d, FILL_RECT:%d, LINE:%d", + mCountDrawMesh, mCountTextureRect, mCountTextureOES, + mCountFillRect, mCountDrawLine); + mCountDrawMesh = 0; + mCountTextureRect = 0; + mCountTextureOES = 0; + mCountFillRect = 0; + mCountDrawLine = 0; + Log.d(TAG, line); + } + + private void saveTransform() { + System.arraycopy(mMatrixValues, 0, mTempMatrix, 0, 16); + } + + private void restoreTransform() { + System.arraycopy(mTempMatrix, 0, mMatrixValues, 0, 16); + } + + private void setRenderTarget(RawTexture texture) { + GL11ExtensionPack gl11ep = (GL11ExtensionPack) mGL; + + if (mTargetTexture == null && texture != null) { + mGLId.glGenBuffers(1, mFrameBuffer, 0); + gl11ep.glBindFramebufferOES( + GL11ExtensionPack.GL_FRAMEBUFFER_OES, mFrameBuffer[0]); + } + if (mTargetTexture != null && texture == null) { + gl11ep.glBindFramebufferOES(GL11ExtensionPack.GL_FRAMEBUFFER_OES, 0); + gl11ep.glDeleteFramebuffersOES(1, mFrameBuffer, 0); + } + + mTargetTexture = texture; + if (texture == null) { + setSize(mScreenWidth, mScreenHeight); + } else { + setSize(texture.getWidth(), texture.getHeight()); + + if (!texture.isLoaded()) texture.prepare(this); + + gl11ep.glFramebufferTexture2DOES( + GL11ExtensionPack.GL_FRAMEBUFFER_OES, + GL11ExtensionPack.GL_COLOR_ATTACHMENT0_OES, + GL11.GL_TEXTURE_2D, texture.getId(), 0); + + checkFramebufferStatus(gl11ep); + } + } + + @Override + public void endRenderTarget() { + RawTexture texture = mTargetStack.remove(mTargetStack.size() - 1); + setRenderTarget(texture); + restore(); // restore matrix and alpha + } + + @Override + public void beginRenderTarget(RawTexture texture) { + save(); // save matrix and alpha + mTargetStack.add(mTargetTexture); + setRenderTarget(texture); + } + + private static void checkFramebufferStatus(GL11ExtensionPack gl11ep) { + int status = gl11ep.glCheckFramebufferStatusOES(GL11ExtensionPack.GL_FRAMEBUFFER_OES); + if (status != GL11ExtensionPack.GL_FRAMEBUFFER_COMPLETE_OES) { + String msg = ""; + switch (status) { + case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_FORMATS_OES: + msg = "FRAMEBUFFER_FORMATS"; + break; + case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT_OES: + msg = "FRAMEBUFFER_ATTACHMENT"; + break; + case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_OES: + msg = "FRAMEBUFFER_MISSING_ATTACHMENT"; + break; + case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER_OES: + msg = "FRAMEBUFFER_DRAW_BUFFER"; + break; + case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_OES: + msg = "FRAMEBUFFER_READ_BUFFER"; + break; + case GL11ExtensionPack.GL_FRAMEBUFFER_UNSUPPORTED_OES: + msg = "FRAMEBUFFER_UNSUPPORTED"; + break; + case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_OES: + msg = "FRAMEBUFFER_INCOMPLETE_DIMENSIONS"; + break; + } + throw new RuntimeException(msg + ":" + Integer.toHexString(status)); + } + } + + @Override + public void setTextureParameters(BasicTexture texture) { + int width = texture.getWidth(); + int height = texture.getHeight(); + // Define a vertically flipped crop rectangle for OES_draw_texture. + // The four values in sCropRect are: left, bottom, width, and + // height. Negative value of width or height means flip. + sCropRect[0] = 0; + sCropRect[1] = height; + sCropRect[2] = width; + sCropRect[3] = -height; + + // Set texture parameters. + int target = texture.getTarget(); + mGL.glBindTexture(target, texture.getId()); + mGL.glTexParameterfv(target, GL11Ext.GL_TEXTURE_CROP_RECT_OES, sCropRect, 0); + mGL.glTexParameteri(target, GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP_TO_EDGE); + mGL.glTexParameteri(target, GL11.GL_TEXTURE_WRAP_T, GL11.GL_CLAMP_TO_EDGE); + mGL.glTexParameterf(target, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); + mGL.glTexParameterf(target, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR); + } + + @Override + public void initializeTextureSize(BasicTexture texture, int format, int type) { + int target = texture.getTarget(); + mGL.glBindTexture(target, texture.getId()); + int width = texture.getTextureWidth(); + int height = texture.getTextureHeight(); + mGL.glTexImage2D(target, 0, format, width, height, 0, format, type, null); + } + + @Override + public void initializeTexture(BasicTexture texture, Bitmap bitmap) { + int target = texture.getTarget(); + mGL.glBindTexture(target, texture.getId()); + GLUtils.texImage2D(target, 0, bitmap, 0); + } + + @Override + public void texSubImage2D(BasicTexture texture, int xOffset, int yOffset, Bitmap bitmap, + int format, int type) { + int target = texture.getTarget(); + mGL.glBindTexture(target, texture.getId()); + GLUtils.texSubImage2D(target, 0, xOffset, yOffset, bitmap, format, type); + } + + @Override + public int uploadBuffer(FloatBuffer buf) { + return uploadBuffer(buf, Float.SIZE / Byte.SIZE); + } + + @Override + public int uploadBuffer(ByteBuffer buf) { + return uploadBuffer(buf, 1); + } + + private int uploadBuffer(Buffer buf, int elementSize) { + int[] bufferIds = new int[1]; + mGLId.glGenBuffers(bufferIds.length, bufferIds, 0); + int bufferId = bufferIds[0]; + mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, bufferId); + mGL.glBufferData(GL11.GL_ARRAY_BUFFER, buf.capacity() * elementSize, buf, + GL11.GL_STATIC_DRAW); + return bufferId; + } + + @Override + public void recoverFromLightCycle() { + // This is only required for GLES20 + } + + @Override + public void getBounds(Rect bounds, int x, int y, int width, int height) { + // This is only required for GLES20 + } + + @Override + public GLId getGLId() { + return mGLId; + } +} diff --git a/src/com/android/gallery3d/glrenderer/GLES11IdImpl.java b/src/com/android/gallery3d/glrenderer/GLES11IdImpl.java new file mode 100644 index 000000000..e4793730f --- /dev/null +++ b/src/com/android/gallery3d/glrenderer/GLES11IdImpl.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2012 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.glrenderer; + +import javax.microedition.khronos.opengles.GL11; +import javax.microedition.khronos.opengles.GL11ExtensionPack; + +/** + * Open GL ES 1.1 implementation for generating and destroying texture IDs and + * buffer IDs + */ +public class GLES11IdImpl implements GLId { + private static int sNextId = 1; + // Mutex for sNextId + private static Object sLock = new Object(); + + @Override + public int generateTexture() { + synchronized (sLock) { + return sNextId++; + } + } + + @Override + public void glGenBuffers(int n, int[] buffers, int offset) { + synchronized (sLock) { + while (n-- > 0) { + buffers[offset + n] = sNextId++; + } + } + } + + @Override + public void glDeleteTextures(GL11 gl, int n, int[] textures, int offset) { + synchronized (sLock) { + gl.glDeleteTextures(n, textures, offset); + } + } + + @Override + public void glDeleteBuffers(GL11 gl, int n, int[] buffers, int offset) { + synchronized (sLock) { + gl.glDeleteBuffers(n, buffers, offset); + } + } + + @Override + public void glDeleteFramebuffers(GL11ExtensionPack gl11ep, int n, int[] buffers, int offset) { + synchronized (sLock) { + gl11ep.glDeleteFramebuffersOES(n, buffers, offset); + } + } + + +} diff --git a/src/com/android/gallery3d/glrenderer/GLES20Canvas.java b/src/com/android/gallery3d/glrenderer/GLES20Canvas.java new file mode 100644 index 000000000..4ead1315e --- /dev/null +++ b/src/com/android/gallery3d/glrenderer/GLES20Canvas.java @@ -0,0 +1,1009 @@ +/* + * Copyright (C) 2012 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.glrenderer; + +import android.graphics.Bitmap; +import android.graphics.Rect; +import android.graphics.RectF; +import android.opengl.GLES20; +import android.opengl.GLUtils; +import android.opengl.Matrix; +import android.util.Log; + +import com.android.gallery3d.util.IntArray; + +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.util.ArrayList; +import java.util.Arrays; + +public class GLES20Canvas implements GLCanvas { + // ************** Constants ********************** + private static final String TAG = GLES20Canvas.class.getSimpleName(); + private static final int FLOAT_SIZE = Float.SIZE / Byte.SIZE; + private static final float OPAQUE_ALPHA = 0.95f; + + private static final int COORDS_PER_VERTEX = 2; + private static final int VERTEX_STRIDE = COORDS_PER_VERTEX * FLOAT_SIZE; + + private static final int COUNT_FILL_VERTEX = 4; + private static final int COUNT_LINE_VERTEX = 2; + private static final int COUNT_RECT_VERTEX = 4; + private static final int OFFSET_FILL_RECT = 0; + private static final int OFFSET_DRAW_LINE = OFFSET_FILL_RECT + COUNT_FILL_VERTEX; + private static final int OFFSET_DRAW_RECT = OFFSET_DRAW_LINE + COUNT_LINE_VERTEX; + + private static final float[] BOX_COORDINATES = { + 0, 0, // Fill rectangle + 1, 0, + 0, 1, + 1, 1, + 0, 0, // Draw line + 1, 1, + 0, 0, // Draw rectangle outline + 0, 1, + 1, 1, + 1, 0, + }; + + private static final float[] BOUNDS_COORDINATES = { + 0, 0, 0, 1, + 1, 1, 0, 1, + }; + + private static final String POSITION_ATTRIBUTE = "aPosition"; + private static final String COLOR_UNIFORM = "uColor"; + private static final String MATRIX_UNIFORM = "uMatrix"; + private static final String TEXTURE_MATRIX_UNIFORM = "uTextureMatrix"; + private static final String TEXTURE_SAMPLER_UNIFORM = "uTextureSampler"; + private static final String ALPHA_UNIFORM = "uAlpha"; + private static final String TEXTURE_COORD_ATTRIBUTE = "aTextureCoordinate"; + + private static final String DRAW_VERTEX_SHADER = "" + + "uniform mat4 " + MATRIX_UNIFORM + ";\n" + + "attribute vec2 " + POSITION_ATTRIBUTE + ";\n" + + "void main() {\n" + + " vec4 pos = vec4(" + POSITION_ATTRIBUTE + ", 0.0, 1.0);\n" + + " gl_Position = " + MATRIX_UNIFORM + " * pos;\n" + + "}\n"; + + private static final String DRAW_FRAGMENT_SHADER = "" + + "precision mediump float;\n" + + "uniform vec4 " + COLOR_UNIFORM + ";\n" + + "void main() {\n" + + " gl_FragColor = " + COLOR_UNIFORM + ";\n" + + "}\n"; + + private static final String TEXTURE_VERTEX_SHADER = "" + + "uniform mat4 " + MATRIX_UNIFORM + ";\n" + + "uniform mat4 " + TEXTURE_MATRIX_UNIFORM + ";\n" + + "attribute vec2 " + POSITION_ATTRIBUTE + ";\n" + + "varying vec2 vTextureCoord;\n" + + "void main() {\n" + + " vec4 pos = vec4(" + POSITION_ATTRIBUTE + ", 0.0, 1.0);\n" + + " gl_Position = " + MATRIX_UNIFORM + " * pos;\n" + + " vTextureCoord = (" + TEXTURE_MATRIX_UNIFORM + " * pos).xy;\n" + + "}\n"; + + private static final String MESH_VERTEX_SHADER = "" + + "uniform mat4 " + MATRIX_UNIFORM + ";\n" + + "attribute vec2 " + POSITION_ATTRIBUTE + ";\n" + + "attribute vec2 " + TEXTURE_COORD_ATTRIBUTE + ";\n" + + "varying vec2 vTextureCoord;\n" + + "void main() {\n" + + " vec4 pos = vec4(" + POSITION_ATTRIBUTE + ", 0.0, 1.0);\n" + + " gl_Position = " + MATRIX_UNIFORM + " * pos;\n" + + " vTextureCoord = " + TEXTURE_COORD_ATTRIBUTE + ";\n" + + "}\n"; + + private static final String TEXTURE_FRAGMENT_SHADER = "" + + "precision mediump float;\n" + + "varying vec2 vTextureCoord;\n" + + "uniform float " + ALPHA_UNIFORM + ";\n" + + "uniform sampler2D " + TEXTURE_SAMPLER_UNIFORM + ";\n" + + "void main() {\n" + + " gl_FragColor = texture2D(" + TEXTURE_SAMPLER_UNIFORM + ", vTextureCoord);\n" + + " gl_FragColor *= " + ALPHA_UNIFORM + ";\n" + + "}\n"; + + private static final String OES_TEXTURE_FRAGMENT_SHADER = "" + + "#extension GL_OES_EGL_image_external : require\n" + + "precision mediump float;\n" + + "varying vec2 vTextureCoord;\n" + + "uniform float " + ALPHA_UNIFORM + ";\n" + + "uniform samplerExternalOES " + TEXTURE_SAMPLER_UNIFORM + ";\n" + + "void main() {\n" + + " gl_FragColor = texture2D(" + TEXTURE_SAMPLER_UNIFORM + ", vTextureCoord);\n" + + " gl_FragColor *= " + ALPHA_UNIFORM + ";\n" + + "}\n"; + + private static final int INITIAL_RESTORE_STATE_SIZE = 8; + private static final int MATRIX_SIZE = 16; + + // Keep track of restore state + private float[] mMatrices = new float[INITIAL_RESTORE_STATE_SIZE * MATRIX_SIZE]; + private float[] mAlphas = new float[INITIAL_RESTORE_STATE_SIZE]; + private IntArray mSaveFlags = new IntArray(); + + private int mCurrentAlphaIndex = 0; + private int mCurrentMatrixIndex = 0; + + // Viewport size + private int mWidth; + private int mHeight; + + // Projection matrix + private float[] mProjectionMatrix = new float[MATRIX_SIZE]; + + // Screen size for when we aren't bound to a texture + private int mScreenWidth; + private int mScreenHeight; + + // GL programs + private int mDrawProgram; + private int mTextureProgram; + private int mOesTextureProgram; + private int mMeshProgram; + + // GL buffer containing BOX_COORDINATES + private int mBoxCoordinates; + + // Handle indices -- common + private static final int INDEX_POSITION = 0; + private static final int INDEX_MATRIX = 1; + + // Handle indices -- draw + private static final int INDEX_COLOR = 2; + + // Handle indices -- texture + private static final int INDEX_TEXTURE_MATRIX = 2; + private static final int INDEX_TEXTURE_SAMPLER = 3; + private static final int INDEX_ALPHA = 4; + + // Handle indices -- mesh + private static final int INDEX_TEXTURE_COORD = 2; + + private abstract static class ShaderParameter { + public int handle; + protected final String mName; + + public ShaderParameter(String name) { + mName = name; + } + + public abstract void loadHandle(int program); + } + + private static class UniformShaderParameter extends ShaderParameter { + public UniformShaderParameter(String name) { + super(name); + } + + @Override + public void loadHandle(int program) { + handle = GLES20.glGetUniformLocation(program, mName); + checkError(); + } + } + + private static class AttributeShaderParameter extends ShaderParameter { + public AttributeShaderParameter(String name) { + super(name); + } + + @Override + public void loadHandle(int program) { + handle = GLES20.glGetAttribLocation(program, mName); + checkError(); + } + } + + ShaderParameter[] mDrawParameters = { + new AttributeShaderParameter(POSITION_ATTRIBUTE), // INDEX_POSITION + new UniformShaderParameter(MATRIX_UNIFORM), // INDEX_MATRIX + new UniformShaderParameter(COLOR_UNIFORM), // INDEX_COLOR + }; + ShaderParameter[] mTextureParameters = { + new AttributeShaderParameter(POSITION_ATTRIBUTE), // INDEX_POSITION + new UniformShaderParameter(MATRIX_UNIFORM), // INDEX_MATRIX + new UniformShaderParameter(TEXTURE_MATRIX_UNIFORM), // INDEX_TEXTURE_MATRIX + new UniformShaderParameter(TEXTURE_SAMPLER_UNIFORM), // INDEX_TEXTURE_SAMPLER + new UniformShaderParameter(ALPHA_UNIFORM), // INDEX_ALPHA + }; + ShaderParameter[] mOesTextureParameters = { + new AttributeShaderParameter(POSITION_ATTRIBUTE), // INDEX_POSITION + new UniformShaderParameter(MATRIX_UNIFORM), // INDEX_MATRIX + new UniformShaderParameter(TEXTURE_MATRIX_UNIFORM), // INDEX_TEXTURE_MATRIX + new UniformShaderParameter(TEXTURE_SAMPLER_UNIFORM), // INDEX_TEXTURE_SAMPLER + new UniformShaderParameter(ALPHA_UNIFORM), // INDEX_ALPHA + }; + ShaderParameter[] mMeshParameters = { + new AttributeShaderParameter(POSITION_ATTRIBUTE), // INDEX_POSITION + new UniformShaderParameter(MATRIX_UNIFORM), // INDEX_MATRIX + new AttributeShaderParameter(TEXTURE_COORD_ATTRIBUTE), // INDEX_TEXTURE_COORD + new UniformShaderParameter(TEXTURE_SAMPLER_UNIFORM), // INDEX_TEXTURE_SAMPLER + new UniformShaderParameter(ALPHA_UNIFORM), // INDEX_ALPHA + }; + + private final IntArray mUnboundTextures = new IntArray(); + private final IntArray mDeleteBuffers = new IntArray(); + + // Keep track of statistics for debugging + private int mCountDrawMesh = 0; + private int mCountTextureRect = 0; + private int mCountFillRect = 0; + private int mCountDrawLine = 0; + + // Buffer for framebuffer IDs -- we keep track so we can switch the attached + // texture. + private int[] mFrameBuffer = new int[1]; + + // Bound textures. + private ArrayList<RawTexture> mTargetTextures = new ArrayList<RawTexture>(); + + // Temporary variables used within calculations + private final float[] mTempMatrix = new float[32]; + private final float[] mTempColor = new float[4]; + private final RectF mTempSourceRect = new RectF(); + private final RectF mTempTargetRect = new RectF(); + private final float[] mTempTextureMatrix = new float[MATRIX_SIZE]; + private final int[] mTempIntArray = new int[1]; + + private static final GLId mGLId = new GLES20IdImpl(); + + public GLES20Canvas() { + Matrix.setIdentityM(mTempTextureMatrix, 0); + Matrix.setIdentityM(mMatrices, mCurrentMatrixIndex); + mAlphas[mCurrentAlphaIndex] = 1f; + mTargetTextures.add(null); + + FloatBuffer boxBuffer = createBuffer(BOX_COORDINATES); + mBoxCoordinates = uploadBuffer(boxBuffer); + + int drawVertexShader = loadShader(GLES20.GL_VERTEX_SHADER, DRAW_VERTEX_SHADER); + int textureVertexShader = loadShader(GLES20.GL_VERTEX_SHADER, TEXTURE_VERTEX_SHADER); + int meshVertexShader = loadShader(GLES20.GL_VERTEX_SHADER, MESH_VERTEX_SHADER); + int drawFragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, DRAW_FRAGMENT_SHADER); + int textureFragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, TEXTURE_FRAGMENT_SHADER); + int oesTextureFragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, + OES_TEXTURE_FRAGMENT_SHADER); + + mDrawProgram = assembleProgram(drawVertexShader, drawFragmentShader, mDrawParameters); + mTextureProgram = assembleProgram(textureVertexShader, textureFragmentShader, + mTextureParameters); + mOesTextureProgram = assembleProgram(textureVertexShader, oesTextureFragmentShader, + mOesTextureParameters); + mMeshProgram = assembleProgram(meshVertexShader, textureFragmentShader, mMeshParameters); + GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA); + checkError(); + } + + private static FloatBuffer createBuffer(float[] values) { + // First create an nio buffer, then create a VBO from it. + int size = values.length * FLOAT_SIZE; + FloatBuffer buffer = ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + buffer.put(values, 0, values.length).position(0); + return buffer; + } + + private int assembleProgram(int vertexShader, int fragmentShader, ShaderParameter[] params) { + int program = GLES20.glCreateProgram(); + checkError(); + if (program == 0) { + throw new RuntimeException("Cannot create GL program: " + GLES20.glGetError()); + } + GLES20.glAttachShader(program, vertexShader); + checkError(); + GLES20.glAttachShader(program, fragmentShader); + checkError(); + GLES20.glLinkProgram(program); + checkError(); + int[] mLinkStatus = mTempIntArray; + GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, mLinkStatus, 0); + if (mLinkStatus[0] != GLES20.GL_TRUE) { + Log.e(TAG, "Could not link program: "); + Log.e(TAG, GLES20.glGetProgramInfoLog(program)); + GLES20.glDeleteProgram(program); + program = 0; + } + for (int i = 0; i < params.length; i++) { + params[i].loadHandle(program); + } + return program; + } + + private static int loadShader(int type, String shaderCode) { + // create a vertex shader type (GLES20.GL_VERTEX_SHADER) + // or a fragment shader type (GLES20.GL_FRAGMENT_SHADER) + int shader = GLES20.glCreateShader(type); + + // add the source code to the shader and compile it + GLES20.glShaderSource(shader, shaderCode); + checkError(); + GLES20.glCompileShader(shader); + checkError(); + + return shader; + } + + @Override + public void setSize(int width, int height) { + mWidth = width; + mHeight = height; + GLES20.glViewport(0, 0, mWidth, mHeight); + checkError(); + Matrix.setIdentityM(mMatrices, mCurrentMatrixIndex); + Matrix.orthoM(mProjectionMatrix, 0, 0, width, 0, height, -1, 1); + if (getTargetTexture() == null) { + mScreenWidth = width; + mScreenHeight = height; + Matrix.translateM(mMatrices, mCurrentMatrixIndex, 0, height, 0); + Matrix.scaleM(mMatrices, mCurrentMatrixIndex, 1, -1, 1); + } + } + + @Override + public void clearBuffer() { + GLES20.glClearColor(0f, 0f, 0f, 1f); + checkError(); + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); + checkError(); + } + + @Override + public void clearBuffer(float[] argb) { + GLES20.glClearColor(argb[1], argb[2], argb[3], argb[0]); + checkError(); + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); + checkError(); + } + + @Override + public float getAlpha() { + return mAlphas[mCurrentAlphaIndex]; + } + + @Override + public void setAlpha(float alpha) { + mAlphas[mCurrentAlphaIndex] = alpha; + } + + @Override + public void multiplyAlpha(float alpha) { + setAlpha(getAlpha() * alpha); + } + + @Override + public void translate(float x, float y, float z) { + Matrix.translateM(mMatrices, mCurrentMatrixIndex, x, y, z); + } + + // This is a faster version of translate(x, y, z) because + // (1) we knows z = 0, (2) we inline the Matrix.translateM call, + // (3) we unroll the loop + @Override + public void translate(float x, float y) { + int index = mCurrentMatrixIndex; + float[] m = mMatrices; + m[index + 12] += m[index + 0] * x + m[index + 4] * y; + m[index + 13] += m[index + 1] * x + m[index + 5] * y; + m[index + 14] += m[index + 2] * x + m[index + 6] * y; + m[index + 15] += m[index + 3] * x + m[index + 7] * y; + } + + @Override + public void scale(float sx, float sy, float sz) { + Matrix.scaleM(mMatrices, mCurrentMatrixIndex, sx, sy, sz); + } + + @Override + public void rotate(float angle, float x, float y, float z) { + if (angle == 0f) { + return; + } + float[] temp = mTempMatrix; + Matrix.setRotateM(temp, 0, angle, x, y, z); + float[] matrix = mMatrices; + int index = mCurrentMatrixIndex; + Matrix.multiplyMM(temp, MATRIX_SIZE, matrix, index, temp, 0); + System.arraycopy(temp, MATRIX_SIZE, matrix, index, MATRIX_SIZE); + } + + @Override + public void multiplyMatrix(float[] matrix, int offset) { + float[] temp = mTempMatrix; + float[] currentMatrix = mMatrices; + int index = mCurrentMatrixIndex; + Matrix.multiplyMM(temp, 0, currentMatrix, index, matrix, offset); + System.arraycopy(temp, 0, currentMatrix, index, 16); + } + + @Override + public void save() { + save(SAVE_FLAG_ALL); + } + + @Override + public void save(int saveFlags) { + boolean saveAlpha = (saveFlags & SAVE_FLAG_ALPHA) == SAVE_FLAG_ALPHA; + if (saveAlpha) { + float currentAlpha = getAlpha(); + mCurrentAlphaIndex++; + if (mAlphas.length <= mCurrentAlphaIndex) { + mAlphas = Arrays.copyOf(mAlphas, mAlphas.length * 2); + } + mAlphas[mCurrentAlphaIndex] = currentAlpha; + } + boolean saveMatrix = (saveFlags & SAVE_FLAG_MATRIX) == SAVE_FLAG_MATRIX; + if (saveMatrix) { + int currentIndex = mCurrentMatrixIndex; + mCurrentMatrixIndex += MATRIX_SIZE; + if (mMatrices.length <= mCurrentMatrixIndex) { + mMatrices = Arrays.copyOf(mMatrices, mMatrices.length * 2); + } + System.arraycopy(mMatrices, currentIndex, mMatrices, mCurrentMatrixIndex, MATRIX_SIZE); + } + mSaveFlags.add(saveFlags); + } + + @Override + public void restore() { + int restoreFlags = mSaveFlags.removeLast(); + boolean restoreAlpha = (restoreFlags & SAVE_FLAG_ALPHA) == SAVE_FLAG_ALPHA; + if (restoreAlpha) { + mCurrentAlphaIndex--; + } + boolean restoreMatrix = (restoreFlags & SAVE_FLAG_MATRIX) == SAVE_FLAG_MATRIX; + if (restoreMatrix) { + mCurrentMatrixIndex -= MATRIX_SIZE; + } + } + + @Override + public void drawLine(float x1, float y1, float x2, float y2, GLPaint paint) { + draw(GLES20.GL_LINE_STRIP, OFFSET_DRAW_LINE, COUNT_LINE_VERTEX, x1, y1, x2 - x1, y2 - y1, + paint); + mCountDrawLine++; + } + + @Override + public void drawRect(float x, float y, float width, float height, GLPaint paint) { + draw(GLES20.GL_LINE_LOOP, OFFSET_DRAW_RECT, COUNT_RECT_VERTEX, x, y, width, height, paint); + mCountDrawLine++; + } + + private void draw(int type, int offset, int count, float x, float y, float width, float height, + GLPaint paint) { + draw(type, offset, count, x, y, width, height, paint.getColor(), paint.getLineWidth()); + } + + private void draw(int type, int offset, int count, float x, float y, float width, float height, + int color, float lineWidth) { + prepareDraw(offset, color, lineWidth); + draw(mDrawParameters, type, count, x, y, width, height); + } + + private void prepareDraw(int offset, int color, float lineWidth) { + GLES20.glUseProgram(mDrawProgram); + checkError(); + if (lineWidth > 0) { + GLES20.glLineWidth(lineWidth); + checkError(); + } + float[] colorArray = getColor(color); + boolean blendingEnabled = (colorArray[3] < 1f); + enableBlending(blendingEnabled); + if (blendingEnabled) { + GLES20.glBlendColor(colorArray[0], colorArray[1], colorArray[2], colorArray[3]); + checkError(); + } + + GLES20.glUniform4fv(mDrawParameters[INDEX_COLOR].handle, 1, colorArray, 0); + setPosition(mDrawParameters, offset); + checkError(); + } + + private float[] getColor(int color) { + float alpha = ((color >>> 24) & 0xFF) / 255f * getAlpha(); + float red = ((color >>> 16) & 0xFF) / 255f * alpha; + float green = ((color >>> 8) & 0xFF) / 255f * alpha; + float blue = (color & 0xFF) / 255f * alpha; + mTempColor[0] = red; + mTempColor[1] = green; + mTempColor[2] = blue; + mTempColor[3] = alpha; + return mTempColor; + } + + private void enableBlending(boolean enableBlending) { + if (enableBlending) { + GLES20.glEnable(GLES20.GL_BLEND); + checkError(); + } else { + GLES20.glDisable(GLES20.GL_BLEND); + checkError(); + } + } + + private void setPosition(ShaderParameter[] params, int offset) { + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mBoxCoordinates); + checkError(); + GLES20.glVertexAttribPointer(params[INDEX_POSITION].handle, COORDS_PER_VERTEX, + GLES20.GL_FLOAT, false, VERTEX_STRIDE, offset * VERTEX_STRIDE); + checkError(); + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); + checkError(); + } + + private void draw(ShaderParameter[] params, int type, int count, float x, float y, float width, + float height) { + setMatrix(params, x, y, width, height); + int positionHandle = params[INDEX_POSITION].handle; + GLES20.glEnableVertexAttribArray(positionHandle); + checkError(); + GLES20.glDrawArrays(type, 0, count); + checkError(); + GLES20.glDisableVertexAttribArray(positionHandle); + checkError(); + } + + private void setMatrix(ShaderParameter[] params, float x, float y, float width, float height) { + Matrix.translateM(mTempMatrix, 0, mMatrices, mCurrentMatrixIndex, x, y, 0f); + Matrix.scaleM(mTempMatrix, 0, width, height, 1f); + Matrix.multiplyMM(mTempMatrix, MATRIX_SIZE, mProjectionMatrix, 0, mTempMatrix, 0); + GLES20.glUniformMatrix4fv(params[INDEX_MATRIX].handle, 1, false, mTempMatrix, MATRIX_SIZE); + checkError(); + } + + @Override + public void fillRect(float x, float y, float width, float height, int color) { + draw(GLES20.GL_TRIANGLE_STRIP, OFFSET_FILL_RECT, COUNT_FILL_VERTEX, x, y, width, height, + color, 0f); + mCountFillRect++; + } + + @Override + public void drawTexture(BasicTexture texture, int x, int y, int width, int height) { + if (width <= 0 || height <= 0) { + return; + } + copyTextureCoordinates(texture, mTempSourceRect); + mTempTargetRect.set(x, y, x + width, y + height); + convertCoordinate(mTempSourceRect, mTempTargetRect, texture); + drawTextureRect(texture, mTempSourceRect, mTempTargetRect); + } + + private static void copyTextureCoordinates(BasicTexture texture, RectF outRect) { + int left = 0; + int top = 0; + int right = texture.getWidth(); + int bottom = texture.getHeight(); + if (texture.hasBorder()) { + left = 1; + top = 1; + right -= 1; + bottom -= 1; + } + outRect.set(left, top, right, bottom); + } + + @Override + public void drawTexture(BasicTexture texture, RectF source, RectF target) { + if (target.width() <= 0 || target.height() <= 0) { + return; + } + mTempSourceRect.set(source); + mTempTargetRect.set(target); + + convertCoordinate(mTempSourceRect, mTempTargetRect, texture); + drawTextureRect(texture, mTempSourceRect, mTempTargetRect); + } + + @Override + public void drawTexture(BasicTexture texture, float[] textureTransform, int x, int y, int w, + int h) { + if (w <= 0 || h <= 0) { + return; + } + mTempTargetRect.set(x, y, x + w, y + h); + drawTextureRect(texture, textureTransform, mTempTargetRect); + } + + private void drawTextureRect(BasicTexture texture, RectF source, RectF target) { + setTextureMatrix(source); + drawTextureRect(texture, mTempTextureMatrix, target); + } + + private void setTextureMatrix(RectF source) { + mTempTextureMatrix[0] = source.width(); + mTempTextureMatrix[5] = source.height(); + mTempTextureMatrix[12] = source.left; + mTempTextureMatrix[13] = source.top; + } + + // This function changes the source coordinate to the texture coordinates. + // It also clips the source and target coordinates if it is beyond the + // bound of the texture. + private static void convertCoordinate(RectF source, RectF target, BasicTexture texture) { + int width = texture.getWidth(); + int height = texture.getHeight(); + int texWidth = texture.getTextureWidth(); + int texHeight = texture.getTextureHeight(); + // Convert to texture coordinates + source.left /= texWidth; + source.right /= texWidth; + source.top /= texHeight; + source.bottom /= texHeight; + + // Clip if the rendering range is beyond the bound of the texture. + float xBound = (float) width / texWidth; + if (source.right > xBound) { + target.right = target.left + target.width() * (xBound - source.left) / source.width(); + source.right = xBound; + } + float yBound = (float) height / texHeight; + if (source.bottom > yBound) { + target.bottom = target.top + target.height() * (yBound - source.top) / source.height(); + source.bottom = yBound; + } + } + + private void drawTextureRect(BasicTexture texture, float[] textureMatrix, RectF target) { + ShaderParameter[] params = prepareTexture(texture); + setPosition(params, OFFSET_FILL_RECT); + GLES20.glUniformMatrix4fv(params[INDEX_TEXTURE_MATRIX].handle, 1, false, textureMatrix, 0); + checkError(); + if (texture.isFlippedVertically()) { + save(SAVE_FLAG_MATRIX); + translate(0, target.centerY()); + scale(1, -1, 1); + translate(0, -target.centerY()); + } + draw(params, GLES20.GL_TRIANGLE_STRIP, COUNT_FILL_VERTEX, target.left, target.top, + target.width(), target.height()); + if (texture.isFlippedVertically()) { + restore(); + } + mCountTextureRect++; + } + + private ShaderParameter[] prepareTexture(BasicTexture texture) { + ShaderParameter[] params; + int program; + if (texture.getTarget() == GLES20.GL_TEXTURE_2D) { + params = mTextureParameters; + program = mTextureProgram; + } else { + params = mOesTextureParameters; + program = mOesTextureProgram; + } + prepareTexture(texture, program, params); + return params; + } + + private void prepareTexture(BasicTexture texture, int program, ShaderParameter[] params) { + GLES20.glUseProgram(program); + checkError(); + enableBlending(!texture.isOpaque() || getAlpha() < OPAQUE_ALPHA); + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + checkError(); + texture.onBind(this); + GLES20.glBindTexture(texture.getTarget(), texture.getId()); + checkError(); + GLES20.glUniform1i(params[INDEX_TEXTURE_SAMPLER].handle, 0); + checkError(); + GLES20.glUniform1f(params[INDEX_ALPHA].handle, getAlpha()); + checkError(); + } + + @Override + public void drawMesh(BasicTexture texture, int x, int y, int xyBuffer, int uvBuffer, + int indexBuffer, int indexCount) { + prepareTexture(texture, mMeshProgram, mMeshParameters); + + GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, indexBuffer); + checkError(); + + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, xyBuffer); + checkError(); + int positionHandle = mMeshParameters[INDEX_POSITION].handle; + GLES20.glVertexAttribPointer(positionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, + VERTEX_STRIDE, 0); + checkError(); + + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, uvBuffer); + checkError(); + int texCoordHandle = mMeshParameters[INDEX_TEXTURE_COORD].handle; + GLES20.glVertexAttribPointer(texCoordHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, + false, VERTEX_STRIDE, 0); + checkError(); + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); + checkError(); + + GLES20.glEnableVertexAttribArray(positionHandle); + checkError(); + GLES20.glEnableVertexAttribArray(texCoordHandle); + checkError(); + + setMatrix(mMeshParameters, x, y, 1, 1); + GLES20.glDrawElements(GLES20.GL_TRIANGLE_STRIP, indexCount, GLES20.GL_UNSIGNED_BYTE, 0); + checkError(); + + GLES20.glDisableVertexAttribArray(positionHandle); + checkError(); + GLES20.glDisableVertexAttribArray(texCoordHandle); + checkError(); + GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0); + checkError(); + mCountDrawMesh++; + } + + @Override + public void drawMixed(BasicTexture texture, int toColor, float ratio, int x, int y, int w, int h) { + copyTextureCoordinates(texture, mTempSourceRect); + mTempTargetRect.set(x, y, x + w, y + h); + drawMixed(texture, toColor, ratio, mTempSourceRect, mTempTargetRect); + } + + @Override + public void drawMixed(BasicTexture texture, int toColor, float ratio, RectF source, RectF target) { + if (target.width() <= 0 || target.height() <= 0) { + return; + } + save(SAVE_FLAG_ALPHA); + + float currentAlpha = getAlpha(); + float cappedRatio = Math.min(1f, Math.max(0f, ratio)); + + float textureAlpha = (1f - cappedRatio) * currentAlpha; + setAlpha(textureAlpha); + drawTexture(texture, source, target); + + float colorAlpha = cappedRatio * currentAlpha; + setAlpha(colorAlpha); + fillRect(target.left, target.top, target.width(), target.height(), toColor); + + restore(); + } + + @Override + public boolean unloadTexture(BasicTexture texture) { + boolean unload = texture.isLoaded(); + if (unload) { + synchronized (mUnboundTextures) { + mUnboundTextures.add(texture.getId()); + } + } + return unload; + } + + @Override + public void deleteBuffer(int bufferId) { + synchronized (mUnboundTextures) { + mDeleteBuffers.add(bufferId); + } + } + + @Override + public void deleteRecycledResources() { + synchronized (mUnboundTextures) { + IntArray ids = mUnboundTextures; + if (mUnboundTextures.size() > 0) { + mGLId.glDeleteTextures(null, ids.size(), ids.getInternalArray(), 0); + ids.clear(); + } + + ids = mDeleteBuffers; + if (ids.size() > 0) { + mGLId.glDeleteBuffers(null, ids.size(), ids.getInternalArray(), 0); + ids.clear(); + } + } + } + + @Override + public void dumpStatisticsAndClear() { + String line = String.format("MESH:%d, TEX_RECT:%d, FILL_RECT:%d, LINE:%d", mCountDrawMesh, + mCountTextureRect, mCountFillRect, mCountDrawLine); + mCountDrawMesh = 0; + mCountTextureRect = 0; + mCountFillRect = 0; + mCountDrawLine = 0; + Log.d(TAG, line); + } + + @Override + public void endRenderTarget() { + RawTexture oldTexture = mTargetTextures.remove(mTargetTextures.size() - 1); + RawTexture texture = getTargetTexture(); + setRenderTarget(oldTexture, texture); + restore(); // restore matrix and alpha + } + + @Override + public void beginRenderTarget(RawTexture texture) { + save(); // save matrix and alpha and blending + RawTexture oldTexture = getTargetTexture(); + mTargetTextures.add(texture); + setRenderTarget(oldTexture, texture); + } + + private RawTexture getTargetTexture() { + return mTargetTextures.get(mTargetTextures.size() - 1); + } + + private void setRenderTarget(BasicTexture oldTexture, RawTexture texture) { + if (oldTexture == null && texture != null) { + GLES20.glGenFramebuffers(1, mFrameBuffer, 0); + checkError(); + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffer[0]); + checkError(); + } else if (oldTexture != null && texture == null) { + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0); + checkError(); + GLES20.glDeleteFramebuffers(1, mFrameBuffer, 0); + checkError(); + } + + if (texture == null) { + setSize(mScreenWidth, mScreenHeight); + } else { + setSize(texture.getWidth(), texture.getHeight()); + + if (!texture.isLoaded()) { + texture.prepare(this); + } + + GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, + texture.getTarget(), texture.getId(), 0); + checkError(); + + checkFramebufferStatus(); + } + } + + private static void checkFramebufferStatus() { + int status = GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER); + if (status != GLES20.GL_FRAMEBUFFER_COMPLETE) { + String msg = ""; + switch (status) { + case GLES20.GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: + msg = "GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT"; + break; + case GLES20.GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS: + msg = "GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS"; + break; + case GLES20.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: + msg = "GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT"; + break; + case GLES20.GL_FRAMEBUFFER_UNSUPPORTED: + msg = "GL_FRAMEBUFFER_UNSUPPORTED"; + break; + } + throw new RuntimeException(msg + ":" + Integer.toHexString(status)); + } + } + + @Override + public void setTextureParameters(BasicTexture texture) { + int target = texture.getTarget(); + GLES20.glBindTexture(target, texture.getId()); + checkError(); + GLES20.glTexParameteri(target, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameteri(target, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameterf(target, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameterf(target, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + } + + @Override + public void initializeTextureSize(BasicTexture texture, int format, int type) { + int target = texture.getTarget(); + GLES20.glBindTexture(target, texture.getId()); + checkError(); + int width = texture.getTextureWidth(); + int height = texture.getTextureHeight(); + GLES20.glTexImage2D(target, 0, format, width, height, 0, format, type, null); + } + + @Override + public void initializeTexture(BasicTexture texture, Bitmap bitmap) { + int target = texture.getTarget(); + GLES20.glBindTexture(target, texture.getId()); + checkError(); + GLUtils.texImage2D(target, 0, bitmap, 0); + } + + @Override + public void texSubImage2D(BasicTexture texture, int xOffset, int yOffset, Bitmap bitmap, + int format, int type) { + int target = texture.getTarget(); + GLES20.glBindTexture(target, texture.getId()); + checkError(); + GLUtils.texSubImage2D(target, 0, xOffset, yOffset, bitmap, format, type); + } + + @Override + public int uploadBuffer(FloatBuffer buf) { + return uploadBuffer(buf, FLOAT_SIZE); + } + + @Override + public int uploadBuffer(ByteBuffer buf) { + return uploadBuffer(buf, 1); + } + + private int uploadBuffer(Buffer buffer, int elementSize) { + mGLId.glGenBuffers(1, mTempIntArray, 0); + checkError(); + int bufferId = mTempIntArray[0]; + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, bufferId); + checkError(); + GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, buffer.capacity() * elementSize, buffer, + GLES20.GL_STATIC_DRAW); + checkError(); + return bufferId; + } + + public static void checkError() { + int error = GLES20.glGetError(); + if (error != 0) { + Throwable t = new Throwable(); + Log.e(TAG, "GL error: " + error, t); + } + } + + @SuppressWarnings("unused") + private static void printMatrix(String message, float[] m, int offset) { + StringBuilder b = new StringBuilder(message); + for (int i = 0; i < MATRIX_SIZE; i++) { + b.append(' '); + if (i % 4 == 0) { + b.append('\n'); + } + b.append(m[offset + i]); + } + Log.v(TAG, b.toString()); + } + + @Override + public void recoverFromLightCycle() { + GLES20.glViewport(0, 0, mWidth, mHeight); + GLES20.glDisable(GLES20.GL_DEPTH_TEST); + GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA); + checkError(); + } + + @Override + public void getBounds(Rect bounds, int x, int y, int width, int height) { + Matrix.translateM(mTempMatrix, 0, mMatrices, mCurrentMatrixIndex, x, y, 0f); + Matrix.scaleM(mTempMatrix, 0, width, height, 1f); + Matrix.multiplyMV(mTempMatrix, MATRIX_SIZE, mTempMatrix, 0, BOUNDS_COORDINATES, 0); + Matrix.multiplyMV(mTempMatrix, MATRIX_SIZE + 4, mTempMatrix, 0, BOUNDS_COORDINATES, 4); + bounds.left = Math.round(mTempMatrix[MATRIX_SIZE]); + bounds.right = Math.round(mTempMatrix[MATRIX_SIZE + 4]); + bounds.top = Math.round(mTempMatrix[MATRIX_SIZE + 1]); + bounds.bottom = Math.round(mTempMatrix[MATRIX_SIZE + 5]); + bounds.sort(); + } + + @Override + public GLId getGLId() { + return mGLId; + } +} diff --git a/src/com/android/gallery3d/glrenderer/GLES20IdImpl.java b/src/com/android/gallery3d/glrenderer/GLES20IdImpl.java new file mode 100644 index 000000000..6cd7149cb --- /dev/null +++ b/src/com/android/gallery3d/glrenderer/GLES20IdImpl.java @@ -0,0 +1,42 @@ +package com.android.gallery3d.glrenderer; + +import android.opengl.GLES20; + +import javax.microedition.khronos.opengles.GL11; +import javax.microedition.khronos.opengles.GL11ExtensionPack; + +public class GLES20IdImpl implements GLId { + private final int[] mTempIntArray = new int[1]; + + @Override + public int generateTexture() { + GLES20.glGenTextures(1, mTempIntArray, 0); + GLES20Canvas.checkError(); + return mTempIntArray[0]; + } + + @Override + public void glGenBuffers(int n, int[] buffers, int offset) { + GLES20.glGenBuffers(n, buffers, offset); + GLES20Canvas.checkError(); + } + + @Override + public void glDeleteTextures(GL11 gl, int n, int[] textures, int offset) { + GLES20.glDeleteTextures(n, textures, offset); + GLES20Canvas.checkError(); + } + + + @Override + public void glDeleteBuffers(GL11 gl, int n, int[] buffers, int offset) { + GLES20.glDeleteBuffers(n, buffers, offset); + GLES20Canvas.checkError(); + } + + @Override + public void glDeleteFramebuffers(GL11ExtensionPack gl11ep, int n, int[] buffers, int offset) { + GLES20.glDeleteFramebuffers(n, buffers, offset); + GLES20Canvas.checkError(); + } +} diff --git a/src/com/android/gallery3d/glrenderer/GLId.java b/src/com/android/gallery3d/glrenderer/GLId.java new file mode 100644 index 000000000..3cec558f6 --- /dev/null +++ b/src/com/android/gallery3d/glrenderer/GLId.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2012 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.glrenderer; + +import javax.microedition.khronos.opengles.GL11; +import javax.microedition.khronos.opengles.GL11ExtensionPack; + +// This mimics corresponding GL functions. +public interface GLId { + public int generateTexture(); + + public void glGenBuffers(int n, int[] buffers, int offset); + + public void glDeleteTextures(GL11 gl, int n, int[] textures, int offset); + + public void glDeleteBuffers(GL11 gl, int n, int[] buffers, int offset); + + public void glDeleteFramebuffers(GL11ExtensionPack gl11ep, int n, int[] buffers, int offset); +} diff --git a/src/com/android/gallery3d/glrenderer/GLPaint.java b/src/com/android/gallery3d/glrenderer/GLPaint.java new file mode 100644 index 000000000..16b220690 --- /dev/null +++ b/src/com/android/gallery3d/glrenderer/GLPaint.java @@ -0,0 +1,41 @@ +/* + * 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.glrenderer; + +import junit.framework.Assert; + +public class GLPaint { + private float mLineWidth = 1f; + private int mColor = 0; + + public void setColor(int color) { + mColor = color; + } + + public int getColor() { + return mColor; + } + + public void setLineWidth(float width) { + Assert.assertTrue(width >= 0); + mLineWidth = width; + } + + public float getLineWidth() { + return mLineWidth; + } +} diff --git a/src/com/android/gallery3d/glrenderer/MultiLineTexture.java b/src/com/android/gallery3d/glrenderer/MultiLineTexture.java new file mode 100644 index 000000000..82839f107 --- /dev/null +++ b/src/com/android/gallery3d/glrenderer/MultiLineTexture.java @@ -0,0 +1,52 @@ +/* + * 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.glrenderer; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.text.Layout; +import android.text.StaticLayout; +import android.text.TextPaint; + + +// MultiLineTexture is a texture shows the content of a specified String. +// +// To create a MultiLineTexture, use the newInstance() method and specify +// the String, the font size, and the color. +class MultiLineTexture extends CanvasTexture { + private final Layout mLayout; + + private MultiLineTexture(Layout layout) { + super(layout.getWidth(), layout.getHeight()); + mLayout = layout; + } + + public static MultiLineTexture newInstance( + String text, int maxWidth, float textSize, int color, + Layout.Alignment alignment) { + TextPaint paint = StringTexture.getDefaultPaint(textSize, color); + Layout layout = new StaticLayout(text, 0, text.length(), paint, + maxWidth, alignment, 1, 0, true, null, 0); + + return new MultiLineTexture(layout); + } + + @Override + protected void onDraw(Canvas canvas, Bitmap backing) { + mLayout.draw(canvas); + } +} diff --git a/src/com/android/gallery3d/glrenderer/NinePatchChunk.java b/src/com/android/gallery3d/glrenderer/NinePatchChunk.java new file mode 100644 index 000000000..9dc326622 --- /dev/null +++ b/src/com/android/gallery3d/glrenderer/NinePatchChunk.java @@ -0,0 +1,82 @@ +/* + * 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.glrenderer; + +import android.graphics.Rect; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +// See "frameworks/base/include/utils/ResourceTypes.h" for the format of +// NinePatch chunk. +class NinePatchChunk { + + public static final int NO_COLOR = 0x00000001; + public static final int TRANSPARENT_COLOR = 0x00000000; + + public Rect mPaddings = new Rect(); + + public int mDivX[]; + public int mDivY[]; + public int mColor[]; + + private static void readIntArray(int[] data, ByteBuffer buffer) { + for (int i = 0, n = data.length; i < n; ++i) { + data[i] = buffer.getInt(); + } + } + + private static void checkDivCount(int length) { + if (length == 0 || (length & 0x01) != 0) { + throw new RuntimeException("invalid nine-patch: " + length); + } + } + + public static NinePatchChunk deserialize(byte[] data) { + ByteBuffer byteBuffer = + ByteBuffer.wrap(data).order(ByteOrder.nativeOrder()); + + byte wasSerialized = byteBuffer.get(); + if (wasSerialized == 0) return null; + + NinePatchChunk chunk = new NinePatchChunk(); + chunk.mDivX = new int[byteBuffer.get()]; + chunk.mDivY = new int[byteBuffer.get()]; + chunk.mColor = new int[byteBuffer.get()]; + + checkDivCount(chunk.mDivX.length); + checkDivCount(chunk.mDivY.length); + + // skip 8 bytes + byteBuffer.getInt(); + byteBuffer.getInt(); + + chunk.mPaddings.left = byteBuffer.getInt(); + chunk.mPaddings.right = byteBuffer.getInt(); + chunk.mPaddings.top = byteBuffer.getInt(); + chunk.mPaddings.bottom = byteBuffer.getInt(); + + // skip 4 bytes + byteBuffer.getInt(); + + readIntArray(chunk.mDivX, byteBuffer); + readIntArray(chunk.mDivY, byteBuffer); + readIntArray(chunk.mColor, byteBuffer); + + return chunk; + } +}
\ No newline at end of file diff --git a/src/com/android/gallery3d/glrenderer/NinePatchTexture.java b/src/com/android/gallery3d/glrenderer/NinePatchTexture.java new file mode 100644 index 000000000..d0ddc46c3 --- /dev/null +++ b/src/com/android/gallery3d/glrenderer/NinePatchTexture.java @@ -0,0 +1,424 @@ +/* + * 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.glrenderer; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Rect; + +import com.android.gallery3d.common.Utils; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; + +// NinePatchTexture is a texture backed by a NinePatch resource. +// +// getPaddings() returns paddings specified in the NinePatch. +// getNinePatchChunk() returns the layout data specified in the NinePatch. +// +public class NinePatchTexture extends ResourceTexture { + @SuppressWarnings("unused") + private static final String TAG = "NinePatchTexture"; + private NinePatchChunk mChunk; + private SmallCache<NinePatchInstance> mInstanceCache + = new SmallCache<NinePatchInstance>(); + + public NinePatchTexture(Context context, int resId) { + super(context, resId); + } + + @Override + protected Bitmap onGetBitmap() { + if (mBitmap != null) return mBitmap; + + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + Bitmap bitmap = BitmapFactory.decodeResource( + mContext.getResources(), mResId, options); + mBitmap = bitmap; + setSize(bitmap.getWidth(), bitmap.getHeight()); + byte[] chunkData = bitmap.getNinePatchChunk(); + mChunk = chunkData == null + ? null + : NinePatchChunk.deserialize(bitmap.getNinePatchChunk()); + if (mChunk == null) { + throw new RuntimeException("invalid nine-patch image: " + mResId); + } + return bitmap; + } + + public Rect getPaddings() { + // get the paddings from nine patch + if (mChunk == null) onGetBitmap(); + return mChunk.mPaddings; + } + + public NinePatchChunk getNinePatchChunk() { + if (mChunk == null) onGetBitmap(); + return mChunk; + } + + // This is a simple cache for a small number of things. Linear search + // is used because the cache is small. It also tries to remove less used + // item when the cache is full by moving the often-used items to the front. + private static class SmallCache<V> { + private static final int CACHE_SIZE = 16; + private static final int CACHE_SIZE_START_MOVE = CACHE_SIZE / 2; + private int[] mKey = new int[CACHE_SIZE]; + private V[] mValue = (V[]) new Object[CACHE_SIZE]; + private int mCount; // number of items in this cache + + // Puts a value into the cache. If the cache is full, also returns + // a less used item, otherwise returns null. + public V put(int key, V value) { + if (mCount == CACHE_SIZE) { + V old = mValue[CACHE_SIZE - 1]; // remove the last item + mKey[CACHE_SIZE - 1] = key; + mValue[CACHE_SIZE - 1] = value; + return old; + } else { + mKey[mCount] = key; + mValue[mCount] = value; + mCount++; + return null; + } + } + + public V get(int key) { + for (int i = 0; i < mCount; i++) { + if (mKey[i] == key) { + // Move the accessed item one position to the front, so it + // will less likely to be removed when cache is full. Only + // do this if the cache is starting to get full. + if (mCount > CACHE_SIZE_START_MOVE && i > 0) { + int tmpKey = mKey[i]; + mKey[i] = mKey[i - 1]; + mKey[i - 1] = tmpKey; + + V tmpValue = mValue[i]; + mValue[i] = mValue[i - 1]; + mValue[i - 1] = tmpValue; + } + return mValue[i]; + } + } + return null; + } + + public void clear() { + for (int i = 0; i < mCount; i++) { + mValue[i] = null; // make sure it's can be garbage-collected. + } + mCount = 0; + } + + public int size() { + return mCount; + } + + public V valueAt(int i) { + return mValue[i]; + } + } + + private NinePatchInstance findInstance(GLCanvas canvas, int w, int h) { + int key = w; + key = (key << 16) | h; + NinePatchInstance instance = mInstanceCache.get(key); + + if (instance == null) { + instance = new NinePatchInstance(this, w, h); + NinePatchInstance removed = mInstanceCache.put(key, instance); + if (removed != null) { + removed.recycle(canvas); + } + } + + return instance; + } + + @Override + public void draw(GLCanvas canvas, int x, int y, int w, int h) { + if (!isLoaded()) { + mInstanceCache.clear(); + } + + if (w != 0 && h != 0) { + findInstance(canvas, w, h).draw(canvas, this, x, y); + } + } + + @Override + public void recycle() { + super.recycle(); + GLCanvas canvas = mCanvasRef; + if (canvas == null) return; + int n = mInstanceCache.size(); + for (int i = 0; i < n; i++) { + NinePatchInstance instance = mInstanceCache.valueAt(i); + instance.recycle(canvas); + } + mInstanceCache.clear(); + } +} + +// This keeps data for a specialization of NinePatchTexture with the size +// (width, height). We pre-compute the coordinates for efficiency. +class NinePatchInstance { + + @SuppressWarnings("unused") + private static final String TAG = "NinePatchInstance"; + + // We need 16 vertices for a normal nine-patch image (the 4x4 vertices) + private static final int VERTEX_BUFFER_SIZE = 16 * 2; + + // We need 22 indices for a normal nine-patch image, plus 2 for each + // transparent region. Current there are at most 1 transparent region. + private static final int INDEX_BUFFER_SIZE = 22 + 2; + + private FloatBuffer mXyBuffer; + private FloatBuffer mUvBuffer; + private ByteBuffer mIndexBuffer; + + // Names for buffer names: xy, uv, index. + private int mXyBufferName = -1; + private int mUvBufferName; + private int mIndexBufferName; + + private int mIdxCount; + + public NinePatchInstance(NinePatchTexture tex, int width, int height) { + NinePatchChunk chunk = tex.getNinePatchChunk(); + + if (width <= 0 || height <= 0) { + throw new RuntimeException("invalid dimension"); + } + + // The code should be easily extended to handle the general cases by + // allocating more space for buffers. But let's just handle the only + // use case. + if (chunk.mDivX.length != 2 || chunk.mDivY.length != 2) { + throw new RuntimeException("unsupported nine patch"); + } + + float divX[] = new float[4]; + float divY[] = new float[4]; + float divU[] = new float[4]; + float divV[] = new float[4]; + + int nx = stretch(divX, divU, chunk.mDivX, tex.getWidth(), width); + int ny = stretch(divY, divV, chunk.mDivY, tex.getHeight(), height); + + prepareVertexData(divX, divY, divU, divV, nx, ny, chunk.mColor); + } + + /** + * Stretches the texture according to the nine-patch rules. It will + * linearly distribute the strechy parts defined in the nine-patch chunk to + * the target area. + * + * <pre> + * source + * /--------------^---------------\ + * u0 u1 u2 u3 u4 u5 + * div ---> |fffff|ssssssss|fff|ssssss|ffff| ---> u + * | div0 div1 div2 div3 | + * | | / / / / + * | | / / / / + * | | / / / / + * |fffff|ssss|fff|sss|ffff| ---> x + * x0 x1 x2 x3 x4 x5 + * \----------v------------/ + * target + * + * f: fixed segment + * s: stretchy segment + * </pre> + * + * @param div the stretch parts defined in nine-patch chunk + * @param source the length of the texture + * @param target the length on the drawing plan + * @param u output, the positions of these dividers in the texture + * coordinate + * @param x output, the corresponding position of these dividers on the + * drawing plan + * @return the number of these dividers. + */ + private static int stretch( + float x[], float u[], int div[], int source, int target) { + int textureSize = Utils.nextPowerOf2(source); + float textureBound = (float) source / textureSize; + + float stretch = 0; + for (int i = 0, n = div.length; i < n; i += 2) { + stretch += div[i + 1] - div[i]; + } + + float remaining = target - source + stretch; + + float lastX = 0; + float lastU = 0; + + x[0] = 0; + u[0] = 0; + for (int i = 0, n = div.length; i < n; i += 2) { + // Make the stretchy segment a little smaller to prevent sampling + // on neighboring fixed segments. + // fixed segment + x[i + 1] = lastX + (div[i] - lastU) + 0.5f; + u[i + 1] = Math.min((div[i] + 0.5f) / textureSize, textureBound); + + // stretchy segment + float partU = div[i + 1] - div[i]; + float partX = remaining * partU / stretch; + remaining -= partX; + stretch -= partU; + + lastX = x[i + 1] + partX; + lastU = div[i + 1]; + x[i + 2] = lastX - 0.5f; + u[i + 2] = Math.min((lastU - 0.5f)/ textureSize, textureBound); + } + // the last fixed segment + x[div.length + 1] = target; + u[div.length + 1] = textureBound; + + // remove segments with length 0. + int last = 0; + for (int i = 1, n = div.length + 2; i < n; ++i) { + if ((x[i] - x[last]) < 1f) continue; + x[++last] = x[i]; + u[last] = u[i]; + } + return last + 1; + } + + private void prepareVertexData(float x[], float y[], float u[], float v[], + int nx, int ny, int[] color) { + /* + * Given a 3x3 nine-patch image, the vertex order is defined as the + * following graph: + * + * (0) (1) (2) (3) + * | /| /| /| + * | / | / | / | + * (4) (5) (6) (7) + * | \ | \ | \ | + * | \| \| \| + * (8) (9) (A) (B) + * | /| /| /| + * | / | / | / | + * (C) (D) (E) (F) + * + * And we draw the triangle strip in the following index order: + * + * index: 04152637B6A5948C9DAEBF + */ + int pntCount = 0; + float xy[] = new float[VERTEX_BUFFER_SIZE]; + float uv[] = new float[VERTEX_BUFFER_SIZE]; + for (int j = 0; j < ny; ++j) { + for (int i = 0; i < nx; ++i) { + int xIndex = (pntCount++) << 1; + int yIndex = xIndex + 1; + xy[xIndex] = x[i]; + xy[yIndex] = y[j]; + uv[xIndex] = u[i]; + uv[yIndex] = v[j]; + } + } + + int idxCount = 1; + boolean isForward = false; + byte index[] = new byte[INDEX_BUFFER_SIZE]; + for (int row = 0; row < ny - 1; row++) { + --idxCount; + isForward = !isForward; + + int start, end, inc; + if (isForward) { + start = 0; + end = nx; + inc = 1; + } else { + start = nx - 1; + end = -1; + inc = -1; + } + + for (int col = start; col != end; col += inc) { + int k = row * nx + col; + if (col != start) { + int colorIdx = row * (nx - 1) + col; + if (isForward) colorIdx--; + if (color[colorIdx] == NinePatchChunk.TRANSPARENT_COLOR) { + index[idxCount] = index[idxCount - 1]; + ++idxCount; + index[idxCount++] = (byte) k; + } + } + + index[idxCount++] = (byte) k; + index[idxCount++] = (byte) (k + nx); + } + } + + mIdxCount = idxCount; + + int size = (pntCount * 2) * (Float.SIZE / Byte.SIZE); + mXyBuffer = allocateDirectNativeOrderBuffer(size).asFloatBuffer(); + mUvBuffer = allocateDirectNativeOrderBuffer(size).asFloatBuffer(); + mIndexBuffer = allocateDirectNativeOrderBuffer(mIdxCount); + + mXyBuffer.put(xy, 0, pntCount * 2).position(0); + mUvBuffer.put(uv, 0, pntCount * 2).position(0); + mIndexBuffer.put(index, 0, idxCount).position(0); + } + + private static ByteBuffer allocateDirectNativeOrderBuffer(int size) { + return ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder()); + } + + private void prepareBuffers(GLCanvas canvas) { + mXyBufferName = canvas.uploadBuffer(mXyBuffer); + mUvBufferName = canvas.uploadBuffer(mUvBuffer); + mIndexBufferName = canvas.uploadBuffer(mIndexBuffer); + + // These buffers are never used again. + mXyBuffer = null; + mUvBuffer = null; + mIndexBuffer = null; + } + + public void draw(GLCanvas canvas, NinePatchTexture tex, int x, int y) { + if (mXyBufferName == -1) { + prepareBuffers(canvas); + } + canvas.drawMesh(tex, x, y, mXyBufferName, mUvBufferName, mIndexBufferName, mIdxCount); + } + + public void recycle(GLCanvas canvas) { + if (mXyBuffer == null) { + canvas.deleteBuffer(mXyBufferName); + canvas.deleteBuffer(mUvBufferName); + canvas.deleteBuffer(mIndexBufferName); + mXyBufferName = -1; + } + } +} diff --git a/src/com/android/gallery3d/glrenderer/RawTexture.java b/src/com/android/gallery3d/glrenderer/RawTexture.java new file mode 100644 index 000000000..93f0fdff9 --- /dev/null +++ b/src/com/android/gallery3d/glrenderer/RawTexture.java @@ -0,0 +1,73 @@ +/* + * 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.glrenderer; + +import android.util.Log; + +import javax.microedition.khronos.opengles.GL11; + +public class RawTexture extends BasicTexture { + private static final String TAG = "RawTexture"; + + private final boolean mOpaque; + private boolean mIsFlipped; + + public RawTexture(int width, int height, boolean opaque) { + mOpaque = opaque; + setSize(width, height); + } + + @Override + public boolean isOpaque() { + return mOpaque; + } + + @Override + public boolean isFlippedVertically() { + return mIsFlipped; + } + + public void setIsFlippedVertically(boolean isFlipped) { + mIsFlipped = isFlipped; + } + + protected void prepare(GLCanvas canvas) { + GLId glId = canvas.getGLId(); + mId = glId.generateTexture(); + canvas.initializeTextureSize(this, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE); + canvas.setTextureParameters(this); + mState = STATE_LOADED; + setAssociatedCanvas(canvas); + } + + @Override + protected boolean onBind(GLCanvas canvas) { + if (isLoaded()) return true; + Log.w(TAG, "lost the content due to context change"); + return false; + } + + @Override + public void yield() { + // we cannot free the texture because we have no backup. + } + + @Override + protected int getTarget() { + return GL11.GL_TEXTURE_2D; + } +} diff --git a/src/com/android/gallery3d/glrenderer/ResourceTexture.java b/src/com/android/gallery3d/glrenderer/ResourceTexture.java new file mode 100644 index 000000000..eb8e8a517 --- /dev/null +++ b/src/com/android/gallery3d/glrenderer/ResourceTexture.java @@ -0,0 +1,53 @@ +/* + * 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.glrenderer; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; + +import junit.framework.Assert; + +// ResourceTexture is a texture whose Bitmap is decoded from a resource. +// By default ResourceTexture is not opaque. +public class ResourceTexture extends UploadedTexture { + + protected final Context mContext; + protected final int mResId; + + public ResourceTexture(Context context, int resId) { + Assert.assertNotNull(context); + mContext = context; + mResId = resId; + setOpaque(false); + } + + @Override + protected Bitmap onGetBitmap() { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + return BitmapFactory.decodeResource( + mContext.getResources(), mResId, options); + } + + @Override + protected void onFreeBitmap(Bitmap bitmap) { + if (!inFinalizer()) { + bitmap.recycle(); + } + } +} diff --git a/src/com/android/gallery3d/glrenderer/StringTexture.java b/src/com/android/gallery3d/glrenderer/StringTexture.java new file mode 100644 index 000000000..56ca29753 --- /dev/null +++ b/src/com/android/gallery3d/glrenderer/StringTexture.java @@ -0,0 +1,88 @@ +/* + * 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.glrenderer; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint.FontMetricsInt; +import android.graphics.Typeface; +import android.text.TextPaint; +import android.text.TextUtils; +import android.util.FloatMath; + +// StringTexture is a texture shows the content of a specified String. +// +// To create a StringTexture, use the newInstance() method and specify +// the String, the font size, and the color. +public class StringTexture extends CanvasTexture { + private final String mText; + private final TextPaint mPaint; + private final FontMetricsInt mMetrics; + + private StringTexture(String text, TextPaint paint, + FontMetricsInt metrics, int width, int height) { + super(width, height); + mText = text; + mPaint = paint; + mMetrics = metrics; + } + + public static TextPaint getDefaultPaint(float textSize, int color) { + TextPaint paint = new TextPaint(); + paint.setTextSize(textSize); + paint.setAntiAlias(true); + paint.setColor(color); + paint.setShadowLayer(2f, 0f, 0f, Color.BLACK); + return paint; + } + + public static StringTexture newInstance( + String text, float textSize, int color) { + return newInstance(text, getDefaultPaint(textSize, color)); + } + + public static StringTexture newInstance( + String text, float textSize, int color, + float lengthLimit, boolean isBold) { + TextPaint paint = getDefaultPaint(textSize, color); + if (isBold) { + paint.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD)); + } + if (lengthLimit > 0) { + text = TextUtils.ellipsize( + text, paint, lengthLimit, TextUtils.TruncateAt.END).toString(); + } + return newInstance(text, paint); + } + + private static StringTexture newInstance(String text, TextPaint paint) { + FontMetricsInt metrics = paint.getFontMetricsInt(); + int width = (int) FloatMath.ceil(paint.measureText(text)); + int height = metrics.bottom - metrics.top; + // The texture size needs to be at least 1x1. + if (width <= 0) width = 1; + if (height <= 0) height = 1; + return new StringTexture(text, paint, metrics, width, height); + } + + @Override + protected void onDraw(Canvas canvas, Bitmap backing) { + canvas.translate(0, -mMetrics.ascent); + canvas.drawText(mText, 0, 0, mPaint); + } +} diff --git a/src/com/android/gallery3d/glrenderer/Texture.java b/src/com/android/gallery3d/glrenderer/Texture.java new file mode 100644 index 000000000..3dcae4aec --- /dev/null +++ b/src/com/android/gallery3d/glrenderer/Texture.java @@ -0,0 +1,44 @@ +/* + * 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.glrenderer; + + +// Texture is a rectangular image which can be drawn on GLCanvas. +// The isOpaque() function gives a hint about whether the texture is opaque, +// so the drawing can be done faster. +// +// This is the current texture hierarchy: +// +// Texture +// -- ColorTexture +// -- FadeInTexture +// -- BasicTexture +// -- UploadedTexture +// -- BitmapTexture +// -- Tile +// -- ResourceTexture +// -- NinePatchTexture +// -- CanvasTexture +// -- StringTexture +// +public interface Texture { + public int getWidth(); + public int getHeight(); + public void draw(GLCanvas canvas, int x, int y); + public void draw(GLCanvas canvas, int x, int y, int w, int h); + public boolean isOpaque(); +} diff --git a/src/com/android/gallery3d/glrenderer/TextureUploader.java b/src/com/android/gallery3d/glrenderer/TextureUploader.java new file mode 100644 index 000000000..f17ab845c --- /dev/null +++ b/src/com/android/gallery3d/glrenderer/TextureUploader.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2012 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.glrenderer; + +import com.android.gallery3d.ui.GLRoot; +import com.android.gallery3d.ui.GLRoot.OnGLIdleListener; + +import java.util.ArrayDeque; + +public class TextureUploader implements OnGLIdleListener { + private static final int INIT_CAPACITY = 64; + private static final int QUOTA_PER_FRAME = 1; + + private final ArrayDeque<UploadedTexture> mFgTextures = + new ArrayDeque<UploadedTexture>(INIT_CAPACITY); + private final ArrayDeque<UploadedTexture> mBgTextures = + new ArrayDeque<UploadedTexture>(INIT_CAPACITY); + private final GLRoot mGLRoot; + private volatile boolean mIsQueued = false; + + public TextureUploader(GLRoot root) { + mGLRoot = root; + } + + public synchronized void clear() { + while (!mFgTextures.isEmpty()) { + mFgTextures.pop().setIsUploading(false); + } + while (!mBgTextures.isEmpty()) { + mBgTextures.pop().setIsUploading(false); + } + } + + // caller should hold synchronized on "this" + private void queueSelfIfNeed() { + if (mIsQueued) return; + mIsQueued = true; + mGLRoot.addOnGLIdleListener(this); + } + + public synchronized void addBgTexture(UploadedTexture t) { + if (t.isContentValid()) return; + mBgTextures.addLast(t); + t.setIsUploading(true); + queueSelfIfNeed(); + } + + public synchronized void addFgTexture(UploadedTexture t) { + if (t.isContentValid()) return; + mFgTextures.addLast(t); + t.setIsUploading(true); + queueSelfIfNeed(); + } + + private int upload(GLCanvas canvas, ArrayDeque<UploadedTexture> deque, + int uploadQuota, boolean isBackground) { + while (uploadQuota > 0) { + UploadedTexture t; + synchronized (this) { + if (deque.isEmpty()) break; + t = deque.removeFirst(); + t.setIsUploading(false); + if (t.isContentValid()) continue; + + // this has to be protected by the synchronized block + // to prevent the inner bitmap get recycled + t.updateContent(canvas); + } + + // It will took some more time for a texture to be drawn for + // the first time. + // Thus, when scrolling, if a new column appears on screen, + // it may cause a UI jank even these textures are uploaded. + if (isBackground) t.draw(canvas, 0, 0); + --uploadQuota; + } + return uploadQuota; + } + + @Override + public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) { + int uploadQuota = QUOTA_PER_FRAME; + uploadQuota = upload(canvas, mFgTextures, uploadQuota, false); + if (uploadQuota < QUOTA_PER_FRAME) mGLRoot.requestRender(); + upload(canvas, mBgTextures, uploadQuota, true); + synchronized (this) { + mIsQueued = !mFgTextures.isEmpty() || !mBgTextures.isEmpty(); + return mIsQueued; + } + } +} diff --git a/src/com/android/gallery3d/glrenderer/TiledTexture.java b/src/com/android/gallery3d/glrenderer/TiledTexture.java new file mode 100644 index 000000000..6ca1de088 --- /dev/null +++ b/src/com/android/gallery3d/glrenderer/TiledTexture.java @@ -0,0 +1,349 @@ +/* + * Copyright (C) 2012 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.glrenderer; + +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PorterDuff.Mode; +import android.graphics.PorterDuffXfermode; +import android.graphics.RectF; +import android.os.SystemClock; + +import com.android.gallery3d.ui.GLRoot; +import com.android.gallery3d.ui.GLRoot.OnGLIdleListener; + +import java.util.ArrayDeque; +import java.util.ArrayList; + +// This class is similar to BitmapTexture, except the bitmap is +// split into tiles. By doing so, we may increase the time required to +// upload the whole bitmap but we reduce the time of uploading each tile +// so it make the animation more smooth and prevents jank. +public class TiledTexture implements Texture { + private static final int CONTENT_SIZE = 254; + private static final int BORDER_SIZE = 1; + private static final int TILE_SIZE = CONTENT_SIZE + 2 * BORDER_SIZE; + private static final int INIT_CAPACITY = 8; + + // We are targeting at 60fps, so we have 16ms for each frame. + // In this 16ms, we use about 4~8 ms to upload tiles. + private static final long UPLOAD_TILE_LIMIT = 4; // ms + + private static Tile sFreeTileHead = null; + private static final Object sFreeTileLock = new Object(); + + private static Bitmap sUploadBitmap; + private static Canvas sCanvas; + private static Paint sBitmapPaint; + private static Paint sPaint; + + private int mUploadIndex = 0; + + private final Tile[] mTiles; // Can be modified in different threads. + // Should be protected by "synchronized." + private final int mWidth; + private final int mHeight; + private final RectF mSrcRect = new RectF(); + private final RectF mDestRect = new RectF(); + + public static class Uploader implements OnGLIdleListener { + private final ArrayDeque<TiledTexture> mTextures = + new ArrayDeque<TiledTexture>(INIT_CAPACITY); + + private final GLRoot mGlRoot; + private boolean mIsQueued = false; + + public Uploader(GLRoot glRoot) { + mGlRoot = glRoot; + } + + public synchronized void clear() { + mTextures.clear(); + } + + public synchronized void addTexture(TiledTexture t) { + if (t.isReady()) return; + mTextures.addLast(t); + + if (mIsQueued) return; + mIsQueued = true; + mGlRoot.addOnGLIdleListener(this); + } + + @Override + public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) { + ArrayDeque<TiledTexture> deque = mTextures; + synchronized (this) { + long now = SystemClock.uptimeMillis(); + long dueTime = now + UPLOAD_TILE_LIMIT; + while (now < dueTime && !deque.isEmpty()) { + TiledTexture t = deque.peekFirst(); + if (t.uploadNextTile(canvas)) { + deque.removeFirst(); + mGlRoot.requestRender(); + } + now = SystemClock.uptimeMillis(); + } + mIsQueued = !mTextures.isEmpty(); + + // return true to keep this listener in the queue + return mIsQueued; + } + } + } + + private static class Tile extends UploadedTexture { + public int offsetX; + public int offsetY; + public Bitmap bitmap; + public Tile nextFreeTile; + public int contentWidth; + public int contentHeight; + + @Override + public void setSize(int width, int height) { + contentWidth = width; + contentHeight = height; + mWidth = width + 2 * BORDER_SIZE; + mHeight = height + 2 * BORDER_SIZE; + mTextureWidth = TILE_SIZE; + mTextureHeight = TILE_SIZE; + } + + @Override + protected Bitmap onGetBitmap() { + int x = BORDER_SIZE - offsetX; + int y = BORDER_SIZE - offsetY; + int r = bitmap.getWidth() + x; + int b = bitmap.getHeight() + y; + sCanvas.drawBitmap(bitmap, x, y, sBitmapPaint); + bitmap = null; + + // draw borders if need + if (x > 0) sCanvas.drawLine(x - 1, 0, x - 1, TILE_SIZE, sPaint); + if (y > 0) sCanvas.drawLine(0, y - 1, TILE_SIZE, y - 1, sPaint); + if (r < CONTENT_SIZE) sCanvas.drawLine(r, 0, r, TILE_SIZE, sPaint); + if (b < CONTENT_SIZE) sCanvas.drawLine(0, b, TILE_SIZE, b, sPaint); + + return sUploadBitmap; + } + + @Override + protected void onFreeBitmap(Bitmap bitmap) { + // do nothing + } + } + + private static void freeTile(Tile tile) { + tile.invalidateContent(); + tile.bitmap = null; + synchronized (sFreeTileLock) { + tile.nextFreeTile = sFreeTileHead; + sFreeTileHead = tile; + } + } + + private static Tile obtainTile() { + synchronized (sFreeTileLock) { + Tile result = sFreeTileHead; + if (result == null) return new Tile(); + sFreeTileHead = result.nextFreeTile; + result.nextFreeTile = null; + return result; + } + } + + private boolean uploadNextTile(GLCanvas canvas) { + if (mUploadIndex == mTiles.length) return true; + + synchronized (mTiles) { + Tile next = mTiles[mUploadIndex++]; + + // Make sure tile has not already been recycled by the time + // this is called (race condition in onGLIdle) + if (next.bitmap != null) { + boolean hasBeenLoad = next.isLoaded(); + next.updateContent(canvas); + + // It will take some time for a texture to be drawn for the first + // time. When scrolling, we need to draw several tiles on the screen + // at the same time. It may cause a UI jank even these textures has + // been uploaded. + if (!hasBeenLoad) next.draw(canvas, 0, 0); + } + } + return mUploadIndex == mTiles.length; + } + + public TiledTexture(Bitmap bitmap) { + mWidth = bitmap.getWidth(); + mHeight = bitmap.getHeight(); + ArrayList<Tile> list = new ArrayList<Tile>(); + + for (int x = 0, w = mWidth; x < w; x += CONTENT_SIZE) { + for (int y = 0, h = mHeight; y < h; y += CONTENT_SIZE) { + Tile tile = obtainTile(); + tile.offsetX = x; + tile.offsetY = y; + tile.bitmap = bitmap; + tile.setSize( + Math.min(CONTENT_SIZE, mWidth - x), + Math.min(CONTENT_SIZE, mHeight - y)); + list.add(tile); + } + } + mTiles = list.toArray(new Tile[list.size()]); + } + + public boolean isReady() { + return mUploadIndex == mTiles.length; + } + + // Can be called in UI thread. + public void recycle() { + synchronized (mTiles) { + for (int i = 0, n = mTiles.length; i < n; ++i) { + freeTile(mTiles[i]); + } + } + } + + public static void freeResources() { + sUploadBitmap = null; + sCanvas = null; + sBitmapPaint = null; + sPaint = null; + } + + public static void prepareResources() { + sUploadBitmap = Bitmap.createBitmap(TILE_SIZE, TILE_SIZE, Config.ARGB_8888); + sCanvas = new Canvas(sUploadBitmap); + sBitmapPaint = new Paint(Paint.FILTER_BITMAP_FLAG); + sBitmapPaint.setXfermode(new PorterDuffXfermode(Mode.SRC)); + sPaint = new Paint(); + sPaint.setXfermode(new PorterDuffXfermode(Mode.SRC)); + sPaint.setColor(Color.TRANSPARENT); + } + + // We want to draw the "source" on the "target". + // This method is to find the "output" rectangle which is + // the corresponding area of the "src". + // (x,y) target + // (x0,y0) source +---------------+ + // +----------+ | | + // | src | | output | + // | +--+ | linear map | +----+ | + // | +--+ | ----------> | | | | + // | | by (scaleX, scaleY) | +----+ | + // +----------+ | | + // Texture +---------------+ + // Canvas + private static void mapRect(RectF output, + RectF src, float x0, float y0, float x, float y, float scaleX, + float scaleY) { + output.set(x + (src.left - x0) * scaleX, + y + (src.top - y0) * scaleY, + x + (src.right - x0) * scaleX, + y + (src.bottom - y0) * scaleY); + } + + // Draws a mixed color of this texture and a specified color onto the + // a rectangle. The used color is: from * (1 - ratio) + to * ratio. + public void drawMixed(GLCanvas canvas, int color, float ratio, + int x, int y, int width, int height) { + RectF src = mSrcRect; + RectF dest = mDestRect; + float scaleX = (float) width / mWidth; + float scaleY = (float) height / mHeight; + synchronized (mTiles) { + for (int i = 0, n = mTiles.length; i < n; ++i) { + Tile t = mTiles[i]; + src.set(0, 0, t.contentWidth, t.contentHeight); + src.offset(t.offsetX, t.offsetY); + mapRect(dest, src, 0, 0, x, y, scaleX, scaleY); + src.offset(BORDER_SIZE - t.offsetX, BORDER_SIZE - t.offsetY); + canvas.drawMixed(t, color, ratio, mSrcRect, mDestRect); + } + } + } + + // Draws the texture on to the specified rectangle. + @Override + public void draw(GLCanvas canvas, int x, int y, int width, int height) { + RectF src = mSrcRect; + RectF dest = mDestRect; + float scaleX = (float) width / mWidth; + float scaleY = (float) height / mHeight; + synchronized (mTiles) { + for (int i = 0, n = mTiles.length; i < n; ++i) { + Tile t = mTiles[i]; + src.set(0, 0, t.contentWidth, t.contentHeight); + src.offset(t.offsetX, t.offsetY); + mapRect(dest, src, 0, 0, x, y, scaleX, scaleY); + src.offset(BORDER_SIZE - t.offsetX, BORDER_SIZE - t.offsetY); + canvas.drawTexture(t, mSrcRect, mDestRect); + } + } + } + + // Draws a sub region of this texture on to the specified rectangle. + public void draw(GLCanvas canvas, RectF source, RectF target) { + RectF src = mSrcRect; + RectF dest = mDestRect; + float x0 = source.left; + float y0 = source.top; + float x = target.left; + float y = target.top; + float scaleX = target.width() / source.width(); + float scaleY = target.height() / source.height(); + + synchronized (mTiles) { + for (int i = 0, n = mTiles.length; i < n; ++i) { + Tile t = mTiles[i]; + src.set(0, 0, t.contentWidth, t.contentHeight); + src.offset(t.offsetX, t.offsetY); + if (!src.intersect(source)) continue; + mapRect(dest, src, x0, y0, x, y, scaleX, scaleY); + src.offset(BORDER_SIZE - t.offsetX, BORDER_SIZE - t.offsetY); + canvas.drawTexture(t, src, dest); + } + } + } + + @Override + public int getWidth() { + return mWidth; + } + + @Override + public int getHeight() { + return mHeight; + } + + @Override + public void draw(GLCanvas canvas, int x, int y) { + draw(canvas, x, y, mWidth, mHeight); + } + + @Override + public boolean isOpaque() { + return false; + } +} diff --git a/src/com/android/gallery3d/glrenderer/UploadedTexture.java b/src/com/android/gallery3d/glrenderer/UploadedTexture.java new file mode 100644 index 000000000..f41a979b7 --- /dev/null +++ b/src/com/android/gallery3d/glrenderer/UploadedTexture.java @@ -0,0 +1,298 @@ +/* + * 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.glrenderer; + +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.opengl.GLUtils; + +import junit.framework.Assert; + +import java.util.HashMap; + +import javax.microedition.khronos.opengles.GL11; + +// UploadedTextures use a Bitmap for the content of the texture. +// +// Subclasses should implement onGetBitmap() to provide the Bitmap and +// implement onFreeBitmap(mBitmap) which will be called when the Bitmap +// is not needed anymore. +// +// isContentValid() is meaningful only when the isLoaded() returns true. +// It means whether the content needs to be updated. +// +// The user of this class should call recycle() when the texture is not +// needed anymore. +// +// By default an UploadedTexture is opaque (so it can be drawn faster without +// blending). The user or subclass can override it using setOpaque(). +public abstract class UploadedTexture extends BasicTexture { + + // To prevent keeping allocation the borders, we store those used borders here. + // Since the length will be power of two, it won't use too much memory. + private static HashMap<BorderKey, Bitmap> sBorderLines = + new HashMap<BorderKey, Bitmap>(); + private static BorderKey sBorderKey = new BorderKey(); + + @SuppressWarnings("unused") + private static final String TAG = "Texture"; + private boolean mContentValid = true; + + // indicate this textures is being uploaded in background + private boolean mIsUploading = false; + private boolean mOpaque = true; + private boolean mThrottled = false; + private static int sUploadedCount; + private static final int UPLOAD_LIMIT = 100; + + protected Bitmap mBitmap; + private int mBorder; + + protected UploadedTexture() { + this(false); + } + + protected UploadedTexture(boolean hasBorder) { + super(null, 0, STATE_UNLOADED); + if (hasBorder) { + setBorder(true); + mBorder = 1; + } + } + + protected void setIsUploading(boolean uploading) { + mIsUploading = uploading; + } + + public boolean isUploading() { + return mIsUploading; + } + + private static class BorderKey implements Cloneable { + public boolean vertical; + public Config config; + public int length; + + @Override + public int hashCode() { + int x = config.hashCode() ^ length; + return vertical ? x : -x; + } + + @Override + public boolean equals(Object object) { + if (!(object instanceof BorderKey)) return false; + BorderKey o = (BorderKey) object; + return vertical == o.vertical + && config == o.config && length == o.length; + } + + @Override + public BorderKey clone() { + try { + return (BorderKey) super.clone(); + } catch (CloneNotSupportedException e) { + throw new AssertionError(e); + } + } + } + + protected void setThrottled(boolean throttled) { + mThrottled = throttled; + } + + private static Bitmap getBorderLine( + boolean vertical, Config config, int length) { + BorderKey key = sBorderKey; + key.vertical = vertical; + key.config = config; + key.length = length; + Bitmap bitmap = sBorderLines.get(key); + if (bitmap == null) { + bitmap = vertical + ? Bitmap.createBitmap(1, length, config) + : Bitmap.createBitmap(length, 1, config); + sBorderLines.put(key.clone(), bitmap); + } + return bitmap; + } + + private Bitmap getBitmap() { + if (mBitmap == null) { + mBitmap = onGetBitmap(); + int w = mBitmap.getWidth() + mBorder * 2; + int h = mBitmap.getHeight() + mBorder * 2; + if (mWidth == UNSPECIFIED) { + setSize(w, h); + } + } + return mBitmap; + } + + private void freeBitmap() { + Assert.assertTrue(mBitmap != null); + onFreeBitmap(mBitmap); + mBitmap = null; + } + + @Override + public int getWidth() { + if (mWidth == UNSPECIFIED) getBitmap(); + return mWidth; + } + + @Override + public int getHeight() { + if (mWidth == UNSPECIFIED) getBitmap(); + return mHeight; + } + + protected abstract Bitmap onGetBitmap(); + + protected abstract void onFreeBitmap(Bitmap bitmap); + + protected void invalidateContent() { + if (mBitmap != null) freeBitmap(); + mContentValid = false; + mWidth = UNSPECIFIED; + mHeight = UNSPECIFIED; + } + + /** + * Whether the content on GPU is valid. + */ + public boolean isContentValid() { + return isLoaded() && mContentValid; + } + + /** + * Updates the content on GPU's memory. + * @param canvas + */ + public void updateContent(GLCanvas canvas) { + if (!isLoaded()) { + if (mThrottled && ++sUploadedCount > UPLOAD_LIMIT) { + return; + } + uploadToCanvas(canvas); + } else if (!mContentValid) { + Bitmap bitmap = getBitmap(); + int format = GLUtils.getInternalFormat(bitmap); + int type = GLUtils.getType(bitmap); + canvas.texSubImage2D(this, mBorder, mBorder, bitmap, format, type); + freeBitmap(); + mContentValid = true; + } + } + + public static void resetUploadLimit() { + sUploadedCount = 0; + } + + public static boolean uploadLimitReached() { + return sUploadedCount > UPLOAD_LIMIT; + } + + private void uploadToCanvas(GLCanvas canvas) { + + Bitmap bitmap = getBitmap(); + if (bitmap != null) { + try { + int bWidth = bitmap.getWidth(); + int bHeight = bitmap.getHeight(); + int width = bWidth + mBorder * 2; + int height = bHeight + mBorder * 2; + int texWidth = getTextureWidth(); + int texHeight = getTextureHeight(); + + Assert.assertTrue(bWidth <= texWidth && bHeight <= texHeight); + + // Upload the bitmap to a new texture. + mId = canvas.getGLId().generateTexture(); + canvas.setTextureParameters(this); + + if (bWidth == texWidth && bHeight == texHeight) { + canvas.initializeTexture(this, bitmap); + } else { + int format = GLUtils.getInternalFormat(bitmap); + int type = GLUtils.getType(bitmap); + Config config = bitmap.getConfig(); + + canvas.initializeTextureSize(this, format, type); + canvas.texSubImage2D(this, mBorder, mBorder, bitmap, format, type); + + if (mBorder > 0) { + // Left border + Bitmap line = getBorderLine(true, config, texHeight); + canvas.texSubImage2D(this, 0, 0, line, format, type); + + // Top border + line = getBorderLine(false, config, texWidth); + canvas.texSubImage2D(this, 0, 0, line, format, type); + } + + // Right border + if (mBorder + bWidth < texWidth) { + Bitmap line = getBorderLine(true, config, texHeight); + canvas.texSubImage2D(this, mBorder + bWidth, 0, line, format, type); + } + + // Bottom border + if (mBorder + bHeight < texHeight) { + Bitmap line = getBorderLine(false, config, texWidth); + canvas.texSubImage2D(this, 0, mBorder + bHeight, line, format, type); + } + } + } finally { + freeBitmap(); + } + // Update texture state. + setAssociatedCanvas(canvas); + mState = STATE_LOADED; + mContentValid = true; + } else { + mState = STATE_ERROR; + throw new RuntimeException("Texture load fail, no bitmap"); + } + } + + @Override + protected boolean onBind(GLCanvas canvas) { + updateContent(canvas); + return isContentValid(); + } + + @Override + protected int getTarget() { + return GL11.GL_TEXTURE_2D; + } + + public void setOpaque(boolean isOpaque) { + mOpaque = isOpaque; + } + + @Override + public boolean isOpaque() { + return mOpaque; + } + + @Override + public void recycle() { + super.recycle(); + if (mBitmap != null) freeBitmap(); + } +} diff --git a/src/com/android/gallery3d/ingest/ImportTask.java b/src/com/android/gallery3d/ingest/ImportTask.java new file mode 100644 index 000000000..7d2d641a5 --- /dev/null +++ b/src/com/android/gallery3d/ingest/ImportTask.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2013 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.ingest; + +import android.content.Context; +import android.mtp.MtpDevice; +import android.mtp.MtpObjectInfo; +import android.os.Environment; +import android.os.PowerManager; + +import com.android.gallery3d.util.GalleryUtils; + +import java.io.File; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; + +public class ImportTask implements Runnable { + + public interface Listener { + void onImportProgress(int visitedCount, int totalCount, String pathIfSuccessful); + + void onImportFinish(Collection<MtpObjectInfo> objectsNotImported, int visitedCount); + } + + static private final String WAKELOCK_LABEL = "MTP Import Task"; + + private Listener mListener; + private String mDestAlbumName; + private Collection<MtpObjectInfo> mObjectsToImport; + private MtpDevice mDevice; + private PowerManager.WakeLock mWakeLock; + + public ImportTask(MtpDevice device, Collection<MtpObjectInfo> objectsToImport, + String destAlbumName, Context context) { + mDestAlbumName = destAlbumName; + mObjectsToImport = objectsToImport; + mDevice = device; + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + mWakeLock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, WAKELOCK_LABEL); + } + + public void setListener(Listener listener) { + mListener = listener; + } + + @Override + public void run() { + mWakeLock.acquire(); + try { + List<MtpObjectInfo> objectsNotImported = new LinkedList<MtpObjectInfo>(); + int visited = 0; + int total = mObjectsToImport.size(); + mListener.onImportProgress(visited, total, null); + File dest = new File(Environment.getExternalStorageDirectory(), mDestAlbumName); + dest.mkdirs(); + for (MtpObjectInfo object : mObjectsToImport) { + visited++; + String importedPath = null; + if (GalleryUtils.hasSpaceForSize(object.getCompressedSize())) { + importedPath = new File(dest, object.getName()).getAbsolutePath(); + if (!mDevice.importFile(object.getObjectHandle(), importedPath)) { + importedPath = null; + } + } + if (importedPath == null) { + objectsNotImported.add(object); + } + if (mListener != null) { + mListener.onImportProgress(visited, total, importedPath); + } + } + if (mListener != null) { + mListener.onImportFinish(objectsNotImported, visited); + } + } finally { + mListener = null; + mWakeLock.release(); + } + } +} diff --git a/src/com/android/gallery3d/ingest/IngestActivity.java b/src/com/android/gallery3d/ingest/IngestActivity.java new file mode 100644 index 000000000..687e9fd44 --- /dev/null +++ b/src/com/android/gallery3d/ingest/IngestActivity.java @@ -0,0 +1,570 @@ +/* + * Copyright (C) 2013 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.ingest; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.res.Configuration; +import android.database.DataSetObserver; +import android.mtp.MtpObjectInfo; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.support.v4.view.ViewPager; +import android.util.SparseBooleanArray; +import android.view.ActionMode; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.AbsListView.MultiChoiceModeListener; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.TextView; + +import com.android.gallery3d.R; +import com.android.gallery3d.ingest.adapter.CheckBroker; +import com.android.gallery3d.ingest.adapter.MtpAdapter; +import com.android.gallery3d.ingest.adapter.MtpPagerAdapter; +import com.android.gallery3d.ingest.data.MtpBitmapFetch; +import com.android.gallery3d.ingest.ui.DateTileView; +import com.android.gallery3d.ingest.ui.IngestGridView; +import com.android.gallery3d.ingest.ui.IngestGridView.OnClearChoicesListener; + +import java.lang.ref.WeakReference; +import java.util.Collection; + +public class IngestActivity extends Activity implements + MtpDeviceIndex.ProgressListener, ImportTask.Listener { + + private IngestService mHelperService; + private boolean mActive = false; + private IngestGridView mGridView; + private MtpAdapter mAdapter; + private Handler mHandler; + private ProgressDialog mProgressDialog; + private ActionMode mActiveActionMode; + + private View mWarningView; + private TextView mWarningText; + private int mLastCheckedPosition = 0; + + private ViewPager mFullscreenPager; + private MtpPagerAdapter mPagerAdapter; + private boolean mFullscreenPagerVisible = false; + + private MenuItem mMenuSwitcherItem; + private MenuItem mActionMenuSwitcherItem; + + // The MTP framework components don't give us fine-grained file copy + // progress updates, so for large photos and videos, we will be stuck + // with a dialog not updating for a long time. To give the user feedback, + // we switch to the animated indeterminate progress bar after the timeout + // specified by INDETERMINATE_SWITCH_TIMEOUT_MS. On the next update from + // the framework, we switch back to the normal progress bar. + private static final int INDETERMINATE_SWITCH_TIMEOUT_MS = 3000; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + doBindHelperService(); + + setContentView(R.layout.ingest_activity_item_list); + mGridView = (IngestGridView) findViewById(R.id.ingest_gridview); + mAdapter = new MtpAdapter(this); + mAdapter.registerDataSetObserver(mMasterObserver); + mGridView.setAdapter(mAdapter); + mGridView.setMultiChoiceModeListener(mMultiChoiceModeListener); + mGridView.setOnItemClickListener(mOnItemClickListener); + mGridView.setOnClearChoicesListener(mPositionMappingCheckBroker); + + mFullscreenPager = (ViewPager) findViewById(R.id.ingest_view_pager); + + mHandler = new ItemListHandler(this); + + MtpBitmapFetch.configureForContext(this); + } + + private OnItemClickListener mOnItemClickListener = new OnItemClickListener() { + @Override + public void onItemClick(AdapterView<?> adapterView, View itemView, int position, long arg3) { + mLastCheckedPosition = position; + mGridView.setItemChecked(position, !mGridView.getCheckedItemPositions().get(position)); + } + }; + + private MultiChoiceModeListener mMultiChoiceModeListener = new MultiChoiceModeListener() { + private boolean mIgnoreItemCheckedStateChanges = false; + + private void updateSelectedTitle(ActionMode mode) { + int count = mGridView.getCheckedItemCount(); + mode.setTitle(getResources().getQuantityString( + R.plurals.number_of_items_selected, count, count)); + } + + @Override + public void onItemCheckedStateChanged(ActionMode mode, int position, long id, + boolean checked) { + if (mIgnoreItemCheckedStateChanges) return; + if (mAdapter.itemAtPositionIsBucket(position)) { + SparseBooleanArray checkedItems = mGridView.getCheckedItemPositions(); + mIgnoreItemCheckedStateChanges = true; + mGridView.setItemChecked(position, false); + + // Takes advantage of the fact that SectionIndexer imposes the + // need to clamp to the valid range + int nextSectionStart = mAdapter.getPositionForSection( + mAdapter.getSectionForPosition(position) + 1); + if (nextSectionStart == position) + nextSectionStart = mAdapter.getCount(); + + boolean rangeValue = false; // Value we want to set all of the bucket items to + + // Determine if all the items in the bucket are currently checked, so that we + // can uncheck them, otherwise we will check all items in the bucket. + for (int i = position + 1; i < nextSectionStart; i++) { + if (checkedItems.get(i) == false) { + rangeValue = true; + break; + } + } + + // Set all items in the bucket to the desired state + for (int i = position + 1; i < nextSectionStart; i++) { + if (checkedItems.get(i) != rangeValue) + mGridView.setItemChecked(i, rangeValue); + } + + mPositionMappingCheckBroker.onBulkCheckedChange(); + mIgnoreItemCheckedStateChanges = false; + } else { + mPositionMappingCheckBroker.onCheckedChange(position, checked); + } + mLastCheckedPosition = position; + updateSelectedTitle(mode); + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + return onOptionsItemSelected(item); + } + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + MenuInflater inflater = mode.getMenuInflater(); + inflater.inflate(R.menu.ingest_menu_item_list_selection, menu); + updateSelectedTitle(mode); + mActiveActionMode = mode; + mActionMenuSwitcherItem = menu.findItem(R.id.ingest_switch_view); + setSwitcherMenuState(mActionMenuSwitcherItem, mFullscreenPagerVisible); + return true; + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + mActiveActionMode = null; + mActionMenuSwitcherItem = null; + mHandler.sendEmptyMessage(ItemListHandler.MSG_BULK_CHECKED_CHANGE); + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + updateSelectedTitle(mode); + return false; + } + }; + + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.import_items: + if (mActiveActionMode != null) { + mHelperService.importSelectedItems( + mGridView.getCheckedItemPositions(), + mAdapter); + mActiveActionMode.finish(); + } + return true; + case R.id.ingest_switch_view: + setFullscreenPagerVisibility(!mFullscreenPagerVisible); + return true; + default: + return false; + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.ingest_menu_item_list_selection, menu); + mMenuSwitcherItem = menu.findItem(R.id.ingest_switch_view); + menu.findItem(R.id.import_items).setVisible(false); + setSwitcherMenuState(mMenuSwitcherItem, mFullscreenPagerVisible); + return true; + } + + @Override + protected void onDestroy() { + super.onDestroy(); + doUnbindHelperService(); + } + + @Override + protected void onResume() { + DateTileView.refreshLocale(); + mActive = true; + if (mHelperService != null) mHelperService.setClientActivity(this); + updateWarningView(); + super.onResume(); + } + + @Override + protected void onPause() { + if (mHelperService != null) mHelperService.setClientActivity(null); + mActive = false; + cleanupProgressDialog(); + super.onPause(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + MtpBitmapFetch.configureForContext(this); + } + + private void showWarningView(int textResId) { + if (mWarningView == null) { + mWarningView = findViewById(R.id.ingest_warning_view); + mWarningText = + (TextView)mWarningView.findViewById(R.id.ingest_warning_view_text); + } + mWarningText.setText(textResId); + mWarningView.setVisibility(View.VISIBLE); + setFullscreenPagerVisibility(false); + mGridView.setVisibility(View.GONE); + } + + private void hideWarningView() { + if (mWarningView != null) { + mWarningView.setVisibility(View.GONE); + setFullscreenPagerVisibility(false); + } + } + + private PositionMappingCheckBroker mPositionMappingCheckBroker = new PositionMappingCheckBroker(); + + private class PositionMappingCheckBroker extends CheckBroker + implements OnClearChoicesListener { + private int mLastMappingPager = -1; + private int mLastMappingGrid = -1; + + private int mapPagerToGridPosition(int position) { + if (position != mLastMappingPager) { + mLastMappingPager = position; + mLastMappingGrid = mAdapter.translatePositionWithoutLabels(position); + } + return mLastMappingGrid; + } + + private int mapGridToPagerPosition(int position) { + if (position != mLastMappingGrid) { + mLastMappingGrid = position; + mLastMappingPager = mPagerAdapter.translatePositionWithLabels(position); + } + return mLastMappingPager; + } + + @Override + public void setItemChecked(int position, boolean checked) { + mGridView.setItemChecked(mapPagerToGridPosition(position), checked); + } + + @Override + public void onCheckedChange(int position, boolean checked) { + if (mPagerAdapter != null) { + super.onCheckedChange(mapGridToPagerPosition(position), checked); + } + } + + @Override + public boolean isItemChecked(int position) { + return mGridView.getCheckedItemPositions().get(mapPagerToGridPosition(position)); + } + + @Override + public void onClearChoices() { + onBulkCheckedChange(); + } + }; + + private DataSetObserver mMasterObserver = new DataSetObserver() { + @Override + public void onChanged() { + if (mPagerAdapter != null) mPagerAdapter.notifyDataSetChanged(); + } + + @Override + public void onInvalidated() { + if (mPagerAdapter != null) mPagerAdapter.notifyDataSetChanged(); + } + }; + + private int pickFullscreenStartingPosition() { + int firstVisiblePosition = mGridView.getFirstVisiblePosition(); + if (mLastCheckedPosition <= firstVisiblePosition + || mLastCheckedPosition > mGridView.getLastVisiblePosition()) { + return firstVisiblePosition; + } else { + return mLastCheckedPosition; + } + } + + private void setSwitcherMenuState(MenuItem menuItem, boolean inFullscreenMode) { + if (menuItem == null) return; + if (!inFullscreenMode) { + menuItem.setIcon(android.R.drawable.ic_menu_zoom); + menuItem.setTitle(R.string.switch_photo_fullscreen); + } else { + menuItem.setIcon(android.R.drawable.ic_dialog_dialer); + menuItem.setTitle(R.string.switch_photo_grid); + } + } + + private void setFullscreenPagerVisibility(boolean visible) { + mFullscreenPagerVisible = visible; + if (visible) { + if (mPagerAdapter == null) { + mPagerAdapter = new MtpPagerAdapter(this, mPositionMappingCheckBroker); + mPagerAdapter.setMtpDeviceIndex(mAdapter.getMtpDeviceIndex()); + } + mFullscreenPager.setAdapter(mPagerAdapter); + mFullscreenPager.setCurrentItem(mPagerAdapter.translatePositionWithLabels( + pickFullscreenStartingPosition()), false); + } else if (mPagerAdapter != null) { + mGridView.setSelection(mAdapter.translatePositionWithoutLabels( + mFullscreenPager.getCurrentItem())); + mFullscreenPager.setAdapter(null); + } + mGridView.setVisibility(visible ? View.INVISIBLE : View.VISIBLE); + mFullscreenPager.setVisibility(visible ? View.VISIBLE : View.INVISIBLE); + if (mActionMenuSwitcherItem != null) { + setSwitcherMenuState(mActionMenuSwitcherItem, visible); + } + setSwitcherMenuState(mMenuSwitcherItem, visible); + } + + private void updateWarningView() { + if (!mAdapter.deviceConnected()) { + showWarningView(R.string.ingest_no_device); + } else if (mAdapter.indexReady() && mAdapter.getCount() == 0) { + showWarningView(R.string.ingest_empty_device); + } else { + hideWarningView(); + } + } + + private void UiThreadNotifyIndexChanged() { + mAdapter.notifyDataSetChanged(); + if (mActiveActionMode != null) { + mActiveActionMode.finish(); + mActiveActionMode = null; + } + updateWarningView(); + } + + protected void notifyIndexChanged() { + mHandler.sendEmptyMessage(ItemListHandler.MSG_NOTIFY_CHANGED); + } + + private static class ProgressState { + String message; + String title; + int current; + int max; + + public void reset() { + title = null; + message = null; + current = 0; + max = 0; + } + } + + private ProgressState mProgressState = new ProgressState(); + + @Override + public void onObjectIndexed(MtpObjectInfo object, int numVisited) { + // Not guaranteed to be called on the UI thread + mProgressState.reset(); + mProgressState.max = 0; + mProgressState.message = getResources().getQuantityString( + R.plurals.ingest_number_of_items_scanned, numVisited, numVisited); + mHandler.sendEmptyMessage(ItemListHandler.MSG_PROGRESS_UPDATE); + } + + @Override + public void onSorting() { + // Not guaranteed to be called on the UI thread + mProgressState.reset(); + mProgressState.max = 0; + mProgressState.message = getResources().getString(R.string.ingest_sorting); + mHandler.sendEmptyMessage(ItemListHandler.MSG_PROGRESS_UPDATE); + } + + @Override + public void onIndexFinish() { + // Not guaranteed to be called on the UI thread + mHandler.sendEmptyMessage(ItemListHandler.MSG_PROGRESS_HIDE); + mHandler.sendEmptyMessage(ItemListHandler.MSG_NOTIFY_CHANGED); + } + + @Override + public void onImportProgress(final int visitedCount, final int totalCount, + String pathIfSuccessful) { + // Not guaranteed to be called on the UI thread + mProgressState.reset(); + mProgressState.max = totalCount; + mProgressState.current = visitedCount; + mProgressState.title = getResources().getString(R.string.ingest_importing); + mHandler.sendEmptyMessage(ItemListHandler.MSG_PROGRESS_UPDATE); + mHandler.removeMessages(ItemListHandler.MSG_PROGRESS_INDETERMINATE); + mHandler.sendEmptyMessageDelayed(ItemListHandler.MSG_PROGRESS_INDETERMINATE, + INDETERMINATE_SWITCH_TIMEOUT_MS); + } + + @Override + public void onImportFinish(Collection<MtpObjectInfo> objectsNotImported, + int numVisited) { + // Not guaranteed to be called on the UI thread + mHandler.sendEmptyMessage(ItemListHandler.MSG_PROGRESS_HIDE); + mHandler.removeMessages(ItemListHandler.MSG_PROGRESS_INDETERMINATE); + // TODO: maybe show an extra dialog listing the ones that failed + // importing, if any? + } + + private ProgressDialog getProgressDialog() { + if (mProgressDialog == null || !mProgressDialog.isShowing()) { + mProgressDialog = new ProgressDialog(this); + mProgressDialog.setCancelable(false); + } + return mProgressDialog; + } + + private void updateProgressDialog() { + ProgressDialog dialog = getProgressDialog(); + boolean indeterminate = (mProgressState.max == 0); + dialog.setIndeterminate(indeterminate); + dialog.setProgressStyle(indeterminate ? ProgressDialog.STYLE_SPINNER + : ProgressDialog.STYLE_HORIZONTAL); + if (mProgressState.title != null) { + dialog.setTitle(mProgressState.title); + } + if (mProgressState.message != null) { + dialog.setMessage(mProgressState.message); + } + if (!indeterminate) { + dialog.setProgress(mProgressState.current); + dialog.setMax(mProgressState.max); + } + if (!dialog.isShowing()) { + dialog.show(); + } + } + + private void makeProgressDialogIndeterminate() { + ProgressDialog dialog = getProgressDialog(); + dialog.setIndeterminate(true); + } + + private void cleanupProgressDialog() { + if (mProgressDialog != null) { + mProgressDialog.hide(); + mProgressDialog = null; + } + } + + // This is static and uses a WeakReference in order to avoid leaking the Activity + private static class ItemListHandler extends Handler { + public static final int MSG_PROGRESS_UPDATE = 0; + public static final int MSG_PROGRESS_HIDE = 1; + public static final int MSG_NOTIFY_CHANGED = 2; + public static final int MSG_BULK_CHECKED_CHANGE = 3; + public static final int MSG_PROGRESS_INDETERMINATE = 4; + + WeakReference<IngestActivity> mParentReference; + + public ItemListHandler(IngestActivity parent) { + super(); + mParentReference = new WeakReference<IngestActivity>(parent); + } + + public void handleMessage(Message message) { + IngestActivity parent = mParentReference.get(); + if (parent == null || !parent.mActive) + return; + switch (message.what) { + case MSG_PROGRESS_HIDE: + parent.cleanupProgressDialog(); + break; + case MSG_PROGRESS_UPDATE: + parent.updateProgressDialog(); + break; + case MSG_NOTIFY_CHANGED: + parent.UiThreadNotifyIndexChanged(); + break; + case MSG_BULK_CHECKED_CHANGE: + parent.mPositionMappingCheckBroker.onBulkCheckedChange(); + break; + case MSG_PROGRESS_INDETERMINATE: + parent.makeProgressDialogIndeterminate(); + break; + default: + break; + } + } + } + + private ServiceConnection mHelperServiceConnection = new ServiceConnection() { + public void onServiceConnected(ComponentName className, IBinder service) { + mHelperService = ((IngestService.LocalBinder) service).getService(); + mHelperService.setClientActivity(IngestActivity.this); + MtpDeviceIndex index = mHelperService.getIndex(); + mAdapter.setMtpDeviceIndex(index); + if (mPagerAdapter != null) mPagerAdapter.setMtpDeviceIndex(index); + } + + public void onServiceDisconnected(ComponentName className) { + mHelperService = null; + } + }; + + private void doBindHelperService() { + bindService(new Intent(getApplicationContext(), IngestService.class), + mHelperServiceConnection, Context.BIND_AUTO_CREATE); + } + + private void doUnbindHelperService() { + if (mHelperService != null) { + mHelperService.setClientActivity(null); + unbindService(mHelperServiceConnection); + } + } +} diff --git a/src/com/android/gallery3d/ingest/IngestService.java b/src/com/android/gallery3d/ingest/IngestService.java new file mode 100644 index 000000000..0ce3ab6a9 --- /dev/null +++ b/src/com/android/gallery3d/ingest/IngestService.java @@ -0,0 +1,320 @@ +/* + * Copyright (C) 2013 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.ingest; + +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.media.MediaScannerConnection; +import android.media.MediaScannerConnection.MediaScannerConnectionClient; +import android.mtp.MtpDevice; +import android.mtp.MtpDeviceInfo; +import android.mtp.MtpObjectInfo; +import android.net.Uri; +import android.os.Binder; +import android.os.IBinder; +import android.os.SystemClock; +import android.support.v4.app.NotificationCompat; +import android.util.SparseBooleanArray; +import android.widget.Adapter; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.NotificationIds; +import com.android.gallery3d.data.MtpClient; +import com.android.gallery3d.util.BucketNames; +import com.android.gallery3d.util.UsageStatistics; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class IngestService extends Service implements ImportTask.Listener, + MtpDeviceIndex.ProgressListener, MtpClient.Listener { + + public class LocalBinder extends Binder { + IngestService getService() { + return IngestService.this; + } + } + + private static final int PROGRESS_UPDATE_INTERVAL_MS = 180; + + private static MtpClient sClient; + + private final IBinder mBinder = new LocalBinder(); + private ScannerClient mScannerClient; + private MtpDevice mDevice; + private String mDevicePrettyName; + private MtpDeviceIndex mIndex; + private IngestActivity mClientActivity; + private boolean mRedeliverImportFinish = false; + private int mRedeliverImportFinishCount = 0; + private Collection<MtpObjectInfo> mRedeliverObjectsNotImported; + private boolean mRedeliverNotifyIndexChanged = false; + private boolean mRedeliverIndexFinish = false; + private NotificationManager mNotificationManager; + private NotificationCompat.Builder mNotificationBuilder; + private long mLastProgressIndexTime = 0; + private boolean mNeedRelaunchNotification = false; + + @Override + public void onCreate() { + super.onCreate(); + mScannerClient = new ScannerClient(this); + mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + mNotificationBuilder = new NotificationCompat.Builder(this); + mNotificationBuilder.setSmallIcon(android.R.drawable.stat_notify_sync) // TODO drawable + .setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, IngestActivity.class), 0)); + mIndex = MtpDeviceIndex.getInstance(); + mIndex.setProgressListener(this); + + if (sClient == null) { + sClient = new MtpClient(getApplicationContext()); + } + List<MtpDevice> devices = sClient.getDeviceList(); + if (devices.size() > 0) { + setDevice(devices.get(0)); + } + sClient.addListener(this); + } + + @Override + public void onDestroy() { + sClient.removeListener(this); + mIndex.unsetProgressListener(this); + super.onDestroy(); + } + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + private void setDevice(MtpDevice device) { + if (mDevice == device) return; + mRedeliverImportFinish = false; + mRedeliverObjectsNotImported = null; + mRedeliverNotifyIndexChanged = false; + mRedeliverIndexFinish = false; + mDevice = device; + mIndex.setDevice(mDevice); + if (mDevice != null) { + MtpDeviceInfo deviceInfo = mDevice.getDeviceInfo(); + if (deviceInfo == null) { + setDevice(null); + return; + } else { + mDevicePrettyName = deviceInfo.getModel(); + mNotificationBuilder.setContentTitle(mDevicePrettyName); + new Thread(mIndex.getIndexRunnable()).start(); + } + } else { + mDevicePrettyName = null; + } + if (mClientActivity != null) { + mClientActivity.notifyIndexChanged(); + } else { + mRedeliverNotifyIndexChanged = true; + } + } + + protected MtpDeviceIndex getIndex() { + return mIndex; + } + + protected void setClientActivity(IngestActivity activity) { + if (mClientActivity == activity) return; + mClientActivity = activity; + if (mClientActivity == null) { + if (mNeedRelaunchNotification) { + mNotificationBuilder.setProgress(0, 0, false) + .setContentText(getResources().getText(R.string.ingest_scanning_done)); + mNotificationManager.notify(NotificationIds.INGEST_NOTIFICATION_SCANNING, + mNotificationBuilder.build()); + } + return; + } + mNotificationManager.cancel(NotificationIds.INGEST_NOTIFICATION_IMPORTING); + mNotificationManager.cancel(NotificationIds.INGEST_NOTIFICATION_SCANNING); + if (mRedeliverImportFinish) { + mClientActivity.onImportFinish(mRedeliverObjectsNotImported, + mRedeliverImportFinishCount); + mRedeliverImportFinish = false; + mRedeliverObjectsNotImported = null; + } + if (mRedeliverNotifyIndexChanged) { + mClientActivity.notifyIndexChanged(); + mRedeliverNotifyIndexChanged = false; + } + if (mRedeliverIndexFinish) { + mClientActivity.onIndexFinish(); + mRedeliverIndexFinish = false; + } + } + + protected void importSelectedItems(SparseBooleanArray selected, Adapter adapter) { + List<MtpObjectInfo> importHandles = new ArrayList<MtpObjectInfo>(); + for (int i = 0; i < selected.size(); i++) { + if (selected.valueAt(i)) { + Object item = adapter.getItem(selected.keyAt(i)); + if (item instanceof MtpObjectInfo) { + importHandles.add(((MtpObjectInfo) item)); + } + } + } + ImportTask task = new ImportTask(mDevice, importHandles, BucketNames.IMPORTED, this); + task.setListener(this); + mNotificationBuilder.setProgress(0, 0, true) + .setContentText(getResources().getText(R.string.ingest_importing)); + startForeground(NotificationIds.INGEST_NOTIFICATION_IMPORTING, + mNotificationBuilder.build()); + new Thread(task).start(); + } + + @Override + public void deviceAdded(MtpDevice device) { + if (mDevice == null) { + setDevice(device); + UsageStatistics.onEvent(UsageStatistics.COMPONENT_IMPORTER, + "DeviceConnected", null); + } + } + + @Override + public void deviceRemoved(MtpDevice device) { + if (device == mDevice) { + setDevice(null); + mNeedRelaunchNotification = false; + mNotificationManager.cancel(NotificationIds.INGEST_NOTIFICATION_SCANNING); + } + } + + @Override + public void onImportProgress(int visitedCount, int totalCount, + String pathIfSuccessful) { + if (pathIfSuccessful != null) { + mScannerClient.scanPath(pathIfSuccessful); + } + mNeedRelaunchNotification = false; + if (mClientActivity != null) { + mClientActivity.onImportProgress(visitedCount, totalCount, pathIfSuccessful); + } + mNotificationBuilder.setProgress(totalCount, visitedCount, false) + .setContentText(getResources().getText(R.string.ingest_importing)); + mNotificationManager.notify(NotificationIds.INGEST_NOTIFICATION_IMPORTING, + mNotificationBuilder.build()); + } + + @Override + public void onImportFinish(Collection<MtpObjectInfo> objectsNotImported, + int visitedCount) { + stopForeground(true); + mNeedRelaunchNotification = true; + if (mClientActivity != null) { + mClientActivity.onImportFinish(objectsNotImported, visitedCount); + } else { + mRedeliverImportFinish = true; + mRedeliverObjectsNotImported = objectsNotImported; + mRedeliverImportFinishCount = visitedCount; + mNotificationBuilder.setProgress(0, 0, false) + .setContentText(getResources().getText(R.string.import_complete)); + mNotificationManager.notify(NotificationIds.INGEST_NOTIFICATION_IMPORTING, + mNotificationBuilder.build()); + } + UsageStatistics.onEvent(UsageStatistics.COMPONENT_IMPORTER, + "ImportFinished", null, visitedCount); + } + + @Override + public void onObjectIndexed(MtpObjectInfo object, int numVisited) { + mNeedRelaunchNotification = false; + if (mClientActivity != null) { + mClientActivity.onObjectIndexed(object, numVisited); + } else { + // Throttle the updates to one every PROGRESS_UPDATE_INTERVAL_MS milliseconds + long currentTime = SystemClock.uptimeMillis(); + if (currentTime > mLastProgressIndexTime + PROGRESS_UPDATE_INTERVAL_MS) { + mLastProgressIndexTime = currentTime; + mNotificationBuilder.setProgress(0, numVisited, true) + .setContentText(getResources().getText(R.string.ingest_scanning)); + mNotificationManager.notify(NotificationIds.INGEST_NOTIFICATION_SCANNING, + mNotificationBuilder.build()); + } + } + } + + @Override + public void onSorting() { + if (mClientActivity != null) mClientActivity.onSorting(); + } + + @Override + public void onIndexFinish() { + mNeedRelaunchNotification = true; + if (mClientActivity != null) { + mClientActivity.onIndexFinish(); + } else { + mNotificationBuilder.setProgress(0, 0, false) + .setContentText(getResources().getText(R.string.ingest_scanning_done)); + mNotificationManager.notify(NotificationIds.INGEST_NOTIFICATION_SCANNING, + mNotificationBuilder.build()); + mRedeliverIndexFinish = true; + } + } + + // Copied from old Gallery3d code + private static final class ScannerClient implements MediaScannerConnectionClient { + ArrayList<String> mPaths = new ArrayList<String>(); + MediaScannerConnection mScannerConnection; + boolean mConnected; + Object mLock = new Object(); + + public ScannerClient(Context context) { + mScannerConnection = new MediaScannerConnection(context, this); + } + + public void scanPath(String path) { + synchronized (mLock) { + if (mConnected) { + mScannerConnection.scanFile(path, null); + } else { + mPaths.add(path); + mScannerConnection.connect(); + } + } + } + + @Override + public void onMediaScannerConnected() { + synchronized (mLock) { + mConnected = true; + if (!mPaths.isEmpty()) { + for (String path : mPaths) { + mScannerConnection.scanFile(path, null); + } + mPaths.clear(); + } + } + } + + @Override + public void onScanCompleted(String path, Uri uri) { + } + } +} diff --git a/src/com/android/gallery3d/ingest/MtpDeviceIndex.java b/src/com/android/gallery3d/ingest/MtpDeviceIndex.java new file mode 100644 index 000000000..d30f94a87 --- /dev/null +++ b/src/com/android/gallery3d/ingest/MtpDeviceIndex.java @@ -0,0 +1,596 @@ +/* + * Copyright (C) 2013 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.ingest; + +import android.mtp.MtpConstants; +import android.mtp.MtpDevice; +import android.mtp.MtpObjectInfo; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Stack; + +/** + * MTP objects in the index are organized into "buckets," or groupings. + * At present, these buckets are based on the date an item was created. + * + * When the index is created, the buckets are sorted in their natural + * order, and the items within the buckets sorted by the date they are taken. + * + * The index enables the access of items and bucket labels as one unified list. + * For example, let's say we have the following data in the index: + * [Bucket A]: [photo 1], [photo 2] + * [Bucket B]: [photo 3] + * + * Then the items can be thought of as being organized as a 5 element list: + * [Bucket A], [photo 1], [photo 2], [Bucket B], [photo 3] + * + * The data can also be accessed in descending order, in which case the list + * would be a bit different from simply reversing the ascending list, since the + * bucket labels need to always be at the beginning: + * [Bucket B], [photo 3], [Bucket A], [photo 2], [photo 1] + * + * The index enables all the following operations in constant time, both for + * ascending and descending views of the data: + * - get/getAscending/getDescending: get an item at a specified list position + * - size: get the total number of items (bucket labels and MTP objects) + * - getFirstPositionForBucketNumber + * - getBucketNumberForPosition + * - isFirstInBucket + * + * See the comments in buildLookupIndex for implementation notes. + */ +public class MtpDeviceIndex { + + public static final int FORMAT_MOV = 0x300D; // For some reason this is not in MtpConstants + + public static final Set<Integer> SUPPORTED_IMAGE_FORMATS; + public static final Set<Integer> SUPPORTED_VIDEO_FORMATS; + + static { + SUPPORTED_IMAGE_FORMATS = new HashSet<Integer>(); + SUPPORTED_IMAGE_FORMATS.add(MtpConstants.FORMAT_JFIF); + SUPPORTED_IMAGE_FORMATS.add(MtpConstants.FORMAT_EXIF_JPEG); + SUPPORTED_IMAGE_FORMATS.add(MtpConstants.FORMAT_PNG); + SUPPORTED_IMAGE_FORMATS.add(MtpConstants.FORMAT_GIF); + SUPPORTED_IMAGE_FORMATS.add(MtpConstants.FORMAT_BMP); + + SUPPORTED_VIDEO_FORMATS = new HashSet<Integer>(); + SUPPORTED_VIDEO_FORMATS.add(MtpConstants.FORMAT_3GP_CONTAINER); + SUPPORTED_VIDEO_FORMATS.add(MtpConstants.FORMAT_AVI); + SUPPORTED_VIDEO_FORMATS.add(MtpConstants.FORMAT_MP4_CONTAINER); + SUPPORTED_VIDEO_FORMATS.add(MtpConstants.FORMAT_MPEG); + // TODO: add FORMAT_MOV once Media Scanner supports .mov files + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((mDevice == null) ? 0 : mDevice.getDeviceId()); + result = prime * result + mGeneration; + return result; + } + + public interface ProgressListener { + public void onObjectIndexed(MtpObjectInfo object, int numVisited); + + public void onSorting(); + + public void onIndexFinish(); + } + + public enum SortOrder { + Ascending, Descending + } + + private MtpDevice mDevice; + private int[] mUnifiedLookupIndex; + private MtpObjectInfo[] mMtpObjects; + private DateBucket[] mBuckets; + private int mGeneration = 0; + + public enum Progress { + Uninitialized, Initialized, Pending, Started, Sorting, Finished + } + + private Progress mProgress = Progress.Uninitialized; + private ProgressListener mProgressListener; + + private static final MtpDeviceIndex sInstance = new MtpDeviceIndex(); + private static final MtpObjectTimestampComparator sMtpObjectComparator = + new MtpObjectTimestampComparator(); + + public static MtpDeviceIndex getInstance() { + return sInstance; + } + + private MtpDeviceIndex() { + } + + synchronized public MtpDevice getDevice() { + return mDevice; + } + + /** + * Sets the MtpDevice that should be indexed and initializes state, but does + * not kick off the actual indexing task, which is instead done by using + * {@link #getIndexRunnable()} + * + * @param device The MtpDevice that should be indexed + */ + synchronized public void setDevice(MtpDevice device) { + if (device == mDevice) return; + mDevice = device; + resetState(); + } + + /** + * Provides a Runnable for the indexing task assuming the state has already + * been correctly initialized (by calling {@link #setDevice(MtpDevice)}) and + * has not already been run. + * + * @return Runnable for the main indexing task + */ + synchronized public Runnable getIndexRunnable() { + if (mProgress != Progress.Initialized) return null; + mProgress = Progress.Pending; + return new IndexRunnable(mDevice); + } + + synchronized public boolean indexReady() { + return mProgress == Progress.Finished; + } + + synchronized public Progress getProgress() { + return mProgress; + } + + /** + * @param listener Listener to change to + * @return Progress at the time the listener was added (useful for + * configuring initial UI state) + */ + synchronized public Progress setProgressListener(ProgressListener listener) { + mProgressListener = listener; + return mProgress; + } + + /** + * Make the listener null if it matches the argument + * + * @param listener Listener to unset, if currently registered + */ + synchronized public void unsetProgressListener(ProgressListener listener) { + if (mProgressListener == listener) + mProgressListener = null; + } + + /** + * @return The total number of elements in the index (labels and items) + */ + public int size() { + return mProgress == Progress.Finished ? mUnifiedLookupIndex.length : 0; + } + + /** + * @param position Index of item to fetch, where 0 is the first item in the + * specified order + * @param order + * @return the bucket label or MtpObjectInfo at the specified position and + * order + */ + public Object get(int position, SortOrder order) { + if (mProgress != Progress.Finished) return null; + if(order == SortOrder.Ascending) { + DateBucket bucket = mBuckets[mUnifiedLookupIndex[position]]; + if (bucket.unifiedStartIndex == position) { + return bucket.bucket; + } else { + return mMtpObjects[bucket.itemsStartIndex + position - 1 + - bucket.unifiedStartIndex]; + } + } else { + int zeroIndex = mUnifiedLookupIndex.length - 1 - position; + DateBucket bucket = mBuckets[mUnifiedLookupIndex[zeroIndex]]; + if (bucket.unifiedEndIndex == zeroIndex) { + return bucket.bucket; + } else { + return mMtpObjects[bucket.itemsStartIndex + zeroIndex + - bucket.unifiedStartIndex]; + } + } + } + + /** + * @param position Index of item to fetch from a view of the data that doesn't + * include labels and is in the specified order + * @return position-th item in specified order, when not including labels + */ + public MtpObjectInfo getWithoutLabels(int position, SortOrder order) { + if (mProgress != Progress.Finished) return null; + if (order == SortOrder.Ascending) { + return mMtpObjects[position]; + } else { + return mMtpObjects[mMtpObjects.length - 1 - position]; + } + } + + /** + * Although this is O(log(number of buckets)), and thus should not be used + * in hotspots, even if the attached device has items for every day for + * a five-year timeframe, it would still only take 11 iterations at most, + * so shouldn't be a huge issue. + * @param position Index of item to map from a view of the data that doesn't + * include labels and is in the specified order + * @param order + * @return position in a view of the data that does include labels + */ + public int getPositionFromPositionWithoutLabels(int position, SortOrder order) { + if (mProgress != Progress.Finished) return -1; + if (order == SortOrder.Descending) { + position = mMtpObjects.length - 1 - position; + } + int bucketNumber = 0; + int iMin = 0; + int iMax = mBuckets.length - 1; + while (iMax >= iMin) { + int iMid = (iMax + iMin) / 2; + if (mBuckets[iMid].itemsStartIndex + mBuckets[iMid].numItems <= position) { + iMin = iMid + 1; + } else if (mBuckets[iMid].itemsStartIndex > position) { + iMax = iMid - 1; + } else { + bucketNumber = iMid; + break; + } + } + int mappedPos = mBuckets[bucketNumber].unifiedStartIndex + + position - mBuckets[bucketNumber].itemsStartIndex; + if (order == SortOrder.Descending) { + mappedPos = mUnifiedLookupIndex.length - 1 - mappedPos; + } + return mappedPos; + } + + public int getPositionWithoutLabelsFromPosition(int position, SortOrder order) { + if (mProgress != Progress.Finished) return -1; + if(order == SortOrder.Ascending) { + DateBucket bucket = mBuckets[mUnifiedLookupIndex[position]]; + if (bucket.unifiedStartIndex == position) position++; + return bucket.itemsStartIndex + position - 1 - bucket.unifiedStartIndex; + } else { + int zeroIndex = mUnifiedLookupIndex.length - 1 - position; + DateBucket bucket = mBuckets[mUnifiedLookupIndex[zeroIndex]]; + if (bucket.unifiedEndIndex == zeroIndex) zeroIndex--; + return mMtpObjects.length - 1 - bucket.itemsStartIndex + - zeroIndex + bucket.unifiedStartIndex; + } + } + + /** + * @return The number of MTP items in the index (without labels) + */ + public int sizeWithoutLabels() { + return mProgress == Progress.Finished ? mMtpObjects.length : 0; + } + + public int getFirstPositionForBucketNumber(int bucketNumber, SortOrder order) { + if (order == SortOrder.Ascending) { + return mBuckets[bucketNumber].unifiedStartIndex; + } else { + return mUnifiedLookupIndex.length - mBuckets[mBuckets.length - 1 - bucketNumber].unifiedEndIndex - 1; + } + } + + public int getBucketNumberForPosition(int position, SortOrder order) { + if (order == SortOrder.Ascending) { + return mUnifiedLookupIndex[position]; + } else { + return mBuckets.length - 1 - mUnifiedLookupIndex[mUnifiedLookupIndex.length - 1 - position]; + } + } + + public boolean isFirstInBucket(int position, SortOrder order) { + if (order == SortOrder.Ascending) { + return mBuckets[mUnifiedLookupIndex[position]].unifiedStartIndex == position; + } else { + position = mUnifiedLookupIndex.length - 1 - position; + return mBuckets[mUnifiedLookupIndex[position]].unifiedEndIndex == position; + } + } + + private Object[] mCachedReverseBuckets; + + public Object[] getBuckets(SortOrder order) { + if (mBuckets == null) return null; + if (order == SortOrder.Ascending) { + return mBuckets; + } else { + if (mCachedReverseBuckets == null) { + computeReversedBuckets(); + } + return mCachedReverseBuckets; + } + } + + /* + * See the comments for buildLookupIndex for notes on the specific fields of + * this class. + */ + private class DateBucket implements Comparable<DateBucket> { + SimpleDate bucket; + List<MtpObjectInfo> tempElementsList = new ArrayList<MtpObjectInfo>(); + int unifiedStartIndex; + int unifiedEndIndex; + int itemsStartIndex; + int numItems; + + public DateBucket(SimpleDate bucket) { + this.bucket = bucket; + } + + public DateBucket(SimpleDate bucket, MtpObjectInfo firstElement) { + this(bucket); + tempElementsList.add(firstElement); + } + + void sortElements(Comparator<MtpObjectInfo> comparator) { + Collections.sort(tempElementsList, comparator); + } + + @Override + public String toString() { + return bucket.toString(); + } + + @Override + public int hashCode() { + return bucket.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (!(obj instanceof DateBucket)) return false; + DateBucket other = (DateBucket) obj; + if (bucket == null) { + if (other.bucket != null) return false; + } else if (!bucket.equals(other.bucket)) { + return false; + } + return true; + } + + @Override + public int compareTo(DateBucket another) { + return this.bucket.compareTo(another.bucket); + } + } + + /** + * Comparator to sort MtpObjectInfo objects by date created. + */ + private static class MtpObjectTimestampComparator implements Comparator<MtpObjectInfo> { + @Override + public int compare(MtpObjectInfo o1, MtpObjectInfo o2) { + long diff = o1.getDateCreated() - o2.getDateCreated(); + if (diff < 0) { + return -1; + } else if (diff == 0) { + return 0; + } else { + return 1; + } + } + } + + private void resetState() { + mGeneration++; + mUnifiedLookupIndex = null; + mMtpObjects = null; + mBuckets = null; + mCachedReverseBuckets = null; + mProgress = (mDevice == null) ? Progress.Uninitialized : Progress.Initialized; + } + + + private class IndexRunnable implements Runnable { + private int[] mUnifiedLookupIndex; + private MtpObjectInfo[] mMtpObjects; + private DateBucket[] mBuckets; + private Map<SimpleDate, DateBucket> mBucketsTemp; + private MtpDevice mDevice; + private int mNumObjects = 0; + + private class IndexingException extends Exception {}; + + public IndexRunnable(MtpDevice device) { + mDevice = device; + } + + /* + * Implementation note: this is the way the index supports a lot of its operations in + * constant time and respecting the need to have bucket names always come before items + * in that bucket when accessing the list sequentially, both in ascending and descending + * orders. + * + * Let's say the data we have in the index is the following: + * [Bucket A]: [photo 1], [photo 2] + * [Bucket B]: [photo 3] + * + * In this case, the lookup index array would be + * [0, 0, 0, 1, 1] + * + * Now, whether we access the list in ascending or descending order, we know which bucket + * to look in (0 corresponds to A and 1 to B), and can return the bucket label as the first + * item in a bucket as needed. The individual IndexBUckets have a startIndex and endIndex + * that correspond to indices in this lookup index array, allowing us to calculate the + * offset of the specific item we want from within a specific bucket. + */ + private void buildLookupIndex() { + int numBuckets = mBuckets.length; + mUnifiedLookupIndex = new int[mNumObjects + numBuckets]; + int currentUnifiedIndexEntry = 0; + int nextUnifiedEntry; + + mMtpObjects = new MtpObjectInfo[mNumObjects]; + int currentItemsEntry = 0; + for (int i = 0; i < numBuckets; i++) { + DateBucket bucket = mBuckets[i]; + nextUnifiedEntry = currentUnifiedIndexEntry + bucket.tempElementsList.size() + 1; + Arrays.fill(mUnifiedLookupIndex, currentUnifiedIndexEntry, nextUnifiedEntry, i); + bucket.unifiedStartIndex = currentUnifiedIndexEntry; + bucket.unifiedEndIndex = nextUnifiedEntry - 1; + currentUnifiedIndexEntry = nextUnifiedEntry; + + bucket.itemsStartIndex = currentItemsEntry; + bucket.numItems = bucket.tempElementsList.size(); + for (int j = 0; j < bucket.numItems; j++) { + mMtpObjects[currentItemsEntry] = bucket.tempElementsList.get(j); + currentItemsEntry++; + } + bucket.tempElementsList = null; + } + } + + private void copyResults() { + MtpDeviceIndex.this.mUnifiedLookupIndex = mUnifiedLookupIndex; + MtpDeviceIndex.this.mMtpObjects = mMtpObjects; + MtpDeviceIndex.this.mBuckets = mBuckets; + mUnifiedLookupIndex = null; + mMtpObjects = null; + mBuckets = null; + } + + @Override + public void run() { + try { + indexDevice(); + } catch (IndexingException e) { + synchronized (MtpDeviceIndex.this) { + resetState(); + if (mProgressListener != null) { + mProgressListener.onIndexFinish(); + } + } + } + } + + private void indexDevice() throws IndexingException { + synchronized (MtpDeviceIndex.this) { + mProgress = Progress.Started; + } + mBucketsTemp = new HashMap<SimpleDate, DateBucket>(); + for (int storageId : mDevice.getStorageIds()) { + if (mDevice != getDevice()) throw new IndexingException(); + Stack<Integer> pendingDirectories = new Stack<Integer>(); + pendingDirectories.add(0xFFFFFFFF); // start at the root of the device + while (!pendingDirectories.isEmpty()) { + if (mDevice != getDevice()) throw new IndexingException(); + int dirHandle = pendingDirectories.pop(); + for (int objectHandle : mDevice.getObjectHandles(storageId, 0, dirHandle)) { + MtpObjectInfo objectInfo = mDevice.getObjectInfo(objectHandle); + if (objectInfo == null) throw new IndexingException(); + int format = objectInfo.getFormat(); + if (format == MtpConstants.FORMAT_ASSOCIATION) { + pendingDirectories.add(objectHandle); + } else if (SUPPORTED_IMAGE_FORMATS.contains(format) + || SUPPORTED_VIDEO_FORMATS.contains(format)) { + addObject(objectInfo); + } + } + } + } + Collection<DateBucket> values = mBucketsTemp.values(); + mBucketsTemp = null; + mBuckets = values.toArray(new DateBucket[values.size()]); + values = null; + synchronized (MtpDeviceIndex.this) { + mProgress = Progress.Sorting; + if (mProgressListener != null) { + mProgressListener.onSorting(); + } + } + sortAll(); + buildLookupIndex(); + synchronized (MtpDeviceIndex.this) { + if (mDevice != getDevice()) throw new IndexingException(); + copyResults(); + + /* + * In order for getBuckets to operate in constant time for descending + * order, we must precompute a reversed array of the buckets, mainly + * because the android.widget.SectionIndexer interface which adapters + * that call getBuckets implement depends on section numbers to be + * ascending relative to the scroll position, so we must have this for + * descending order or the scrollbar goes crazy. + */ + computeReversedBuckets(); + + mProgress = Progress.Finished; + if (mProgressListener != null) { + mProgressListener.onIndexFinish(); + } + } + } + + private SimpleDate mDateInstance = new SimpleDate(); + + private void addObject(MtpObjectInfo objectInfo) { + mNumObjects++; + mDateInstance.setTimestamp(objectInfo.getDateCreated()); + DateBucket bucket = mBucketsTemp.get(mDateInstance); + if (bucket == null) { + bucket = new DateBucket(mDateInstance, objectInfo); + mBucketsTemp.put(mDateInstance, bucket); + mDateInstance = new SimpleDate(); // only create new date + // objects when they are used + return; + } else { + bucket.tempElementsList.add(objectInfo); + } + if (mProgressListener != null) { + mProgressListener.onObjectIndexed(objectInfo, mNumObjects); + } + } + + private void sortAll() { + Arrays.sort(mBuckets); + for (DateBucket bucket : mBuckets) { + bucket.sortElements(sMtpObjectComparator); + } + } + + } + + private void computeReversedBuckets() { + mCachedReverseBuckets = new Object[mBuckets.length]; + for (int i = 0; i < mCachedReverseBuckets.length; i++) { + mCachedReverseBuckets[i] = mBuckets[mBuckets.length - 1 - i]; + } + } +} diff --git a/src/com/android/gallery3d/ingest/SimpleDate.java b/src/com/android/gallery3d/ingest/SimpleDate.java new file mode 100644 index 000000000..05db2cde2 --- /dev/null +++ b/src/com/android/gallery3d/ingest/SimpleDate.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2013 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.ingest; + +import java.text.DateFormat; +import java.util.Calendar; + +/** + * Represents a date (year, month, day) + */ +public class SimpleDate implements Comparable<SimpleDate> { + public int month; // MM + public int day; // DD + public int year; // YYYY + private long timestamp; + private String mCachedStringRepresentation; + + public SimpleDate() { + } + + public SimpleDate(long timestamp) { + setTimestamp(timestamp); + } + + private static Calendar sCalendarInstance = Calendar.getInstance(); + + public void setTimestamp(long timestamp) { + synchronized (sCalendarInstance) { + // TODO find a more efficient way to convert a timestamp to a date? + sCalendarInstance.setTimeInMillis(timestamp); + this.day = sCalendarInstance.get(Calendar.DATE); + this.month = sCalendarInstance.get(Calendar.MONTH); + this.year = sCalendarInstance.get(Calendar.YEAR); + this.timestamp = timestamp; + mCachedStringRepresentation = DateFormat.getDateInstance(DateFormat.SHORT).format(timestamp); + } + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + day; + result = prime * result + month; + result = prime * result + year; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (!(obj instanceof SimpleDate)) + return false; + SimpleDate other = (SimpleDate) obj; + if (year != other.year) + return false; + if (month != other.month) + return false; + if (day != other.day) + return false; + return true; + } + + @Override + public int compareTo(SimpleDate other) { + int yearDiff = this.year - other.getYear(); + if (yearDiff != 0) + return yearDiff; + else { + int monthDiff = this.month - other.getMonth(); + if (monthDiff != 0) + return monthDiff; + else + return this.day - other.getDay(); + } + } + + public int getDay() { + return day; + } + + public int getMonth() { + return month; + } + + public int getYear() { + return year; + } + + @Override + public String toString() { + if (mCachedStringRepresentation == null) { + mCachedStringRepresentation = DateFormat.getDateInstance(DateFormat.SHORT).format(timestamp); + } + return mCachedStringRepresentation; + } +} diff --git a/src/com/android/gallery3d/ingest/adapter/CheckBroker.java b/src/com/android/gallery3d/ingest/adapter/CheckBroker.java new file mode 100644 index 000000000..6783f23c5 --- /dev/null +++ b/src/com/android/gallery3d/ingest/adapter/CheckBroker.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2013 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.ingest.adapter; + +import java.util.ArrayList; +import java.util.Collection; + +public abstract class CheckBroker { + private Collection<OnCheckedChangedListener> mListeners = + new ArrayList<OnCheckedChangedListener>(); + + public interface OnCheckedChangedListener { + public void onCheckedChanged(int position, boolean isChecked); + public void onBulkCheckedChanged(); + } + + public abstract void setItemChecked(int position, boolean checked); + + public void onCheckedChange(int position, boolean checked) { + if (isItemChecked(position) != checked) { + for (OnCheckedChangedListener l : mListeners) { + l.onCheckedChanged(position, checked); + } + } + } + + public void onBulkCheckedChange() { + for (OnCheckedChangedListener l : mListeners) { + l.onBulkCheckedChanged(); + } + } + + public abstract boolean isItemChecked(int position); + + public void registerOnCheckedChangeListener(OnCheckedChangedListener l) { + mListeners.add(l); + } + + public void unregisterOnCheckedChangeListener(OnCheckedChangedListener l) { + mListeners.remove(l); + } +} diff --git a/src/com/android/gallery3d/ingest/adapter/MtpAdapter.java b/src/com/android/gallery3d/ingest/adapter/MtpAdapter.java new file mode 100644 index 000000000..e8dd69f8c --- /dev/null +++ b/src/com/android/gallery3d/ingest/adapter/MtpAdapter.java @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2013 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.ingest.adapter; + +import android.app.Activity; +import android.content.Context; +import android.mtp.MtpObjectInfo; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.SectionIndexer; + +import com.android.gallery3d.R; +import com.android.gallery3d.ingest.MtpDeviceIndex; +import com.android.gallery3d.ingest.MtpDeviceIndex.SortOrder; +import com.android.gallery3d.ingest.SimpleDate; +import com.android.gallery3d.ingest.ui.DateTileView; +import com.android.gallery3d.ingest.ui.MtpThumbnailTileView; + +public class MtpAdapter extends BaseAdapter implements SectionIndexer { + public static final int ITEM_TYPE_MEDIA = 0; + public static final int ITEM_TYPE_BUCKET = 1; + + private Context mContext; + private MtpDeviceIndex mModel; + private SortOrder mSortOrder = SortOrder.Descending; + private LayoutInflater mInflater; + private int mGeneration = 0; + + public MtpAdapter(Activity context) { + super(); + mContext = context; + mInflater = LayoutInflater.from(context); + } + + public void setMtpDeviceIndex(MtpDeviceIndex index) { + mModel = index; + notifyDataSetChanged(); + } + + public MtpDeviceIndex getMtpDeviceIndex() { + return mModel; + } + + @Override + public void notifyDataSetChanged() { + mGeneration++; + super.notifyDataSetChanged(); + } + + @Override + public void notifyDataSetInvalidated() { + mGeneration++; + super.notifyDataSetInvalidated(); + } + + public boolean deviceConnected() { + return (mModel != null) && (mModel.getDevice() != null); + } + + public boolean indexReady() { + return (mModel != null) && mModel.indexReady(); + } + + @Override + public int getCount() { + return mModel != null ? mModel.size() : 0; + } + + @Override + public Object getItem(int position) { + return mModel.get(position, mSortOrder); + } + + @Override + public boolean areAllItemsEnabled() { + return true; + } + + @Override + public boolean isEnabled(int position) { + return true; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public int getViewTypeCount() { + return 2; + } + + @Override + public int getItemViewType(int position) { + // If the position is the first in its section, then it corresponds to + // a title tile, if not it's a media tile + if (position == getPositionForSection(getSectionForPosition(position))) { + return ITEM_TYPE_BUCKET; + } else { + return ITEM_TYPE_MEDIA; + } + } + + public boolean itemAtPositionIsBucket(int position) { + return getItemViewType(position) == ITEM_TYPE_BUCKET; + } + + public boolean itemAtPositionIsMedia(int position) { + return getItemViewType(position) == ITEM_TYPE_MEDIA; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + int type = getItemViewType(position); + if (type == ITEM_TYPE_MEDIA) { + MtpThumbnailTileView imageView; + if (convertView == null) { + imageView = (MtpThumbnailTileView) mInflater.inflate( + R.layout.ingest_thumbnail, parent, false); + } else { + imageView = (MtpThumbnailTileView) convertView; + } + imageView.setMtpDeviceAndObjectInfo(mModel.getDevice(), (MtpObjectInfo)getItem(position), mGeneration); + return imageView; + } else { + DateTileView dateTile; + if (convertView == null) { + dateTile = (DateTileView) mInflater.inflate( + R.layout.ingest_date_tile, parent, false); + } else { + dateTile = (DateTileView) convertView; + } + dateTile.setDate((SimpleDate)getItem(position)); + return dateTile; + } + } + + @Override + public int getPositionForSection(int section) { + if (getCount() == 0) { + return 0; + } + int numSections = getSections().length; + if (section >= numSections) { + section = numSections - 1; + } + return mModel.getFirstPositionForBucketNumber(section, mSortOrder); + } + + @Override + public int getSectionForPosition(int position) { + int count = getCount(); + if (count == 0) { + return 0; + } + if (position >= count) { + position = count - 1; + } + return mModel.getBucketNumberForPosition(position, mSortOrder); + } + + @Override + public Object[] getSections() { + return getCount() > 0 ? mModel.getBuckets(mSortOrder) : null; + } + + public SortOrder getSortOrder() { + return mSortOrder; + } + + public int translatePositionWithoutLabels(int position) { + if (mModel == null) return -1; + return mModel.getPositionFromPositionWithoutLabels(position, mSortOrder); + } +} diff --git a/src/com/android/gallery3d/ingest/adapter/MtpPagerAdapter.java b/src/com/android/gallery3d/ingest/adapter/MtpPagerAdapter.java new file mode 100644 index 000000000..9e7abc01d --- /dev/null +++ b/src/com/android/gallery3d/ingest/adapter/MtpPagerAdapter.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2013 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.ingest.adapter; + +import android.content.Context; +import android.mtp.MtpObjectInfo; +import android.support.v4.view.PagerAdapter; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.android.gallery3d.R; +import com.android.gallery3d.ingest.MtpDeviceIndex; +import com.android.gallery3d.ingest.MtpDeviceIndex.SortOrder; +import com.android.gallery3d.ingest.ui.MtpFullscreenView; + +public class MtpPagerAdapter extends PagerAdapter { + + private LayoutInflater mInflater; + private int mGeneration = 0; + private CheckBroker mBroker; + private MtpDeviceIndex mModel; + private SortOrder mSortOrder = SortOrder.Descending; + + private MtpFullscreenView mReusableView = null; + + public MtpPagerAdapter(Context context, CheckBroker broker) { + super(); + mInflater = LayoutInflater.from(context); + mBroker = broker; + } + + public void setMtpDeviceIndex(MtpDeviceIndex index) { + mModel = index; + notifyDataSetChanged(); + } + + @Override + public int getCount() { + return mModel != null ? mModel.sizeWithoutLabels() : 0; + } + + @Override + public void notifyDataSetChanged() { + mGeneration++; + super.notifyDataSetChanged(); + } + + public int translatePositionWithLabels(int position) { + if (mModel == null) return -1; + return mModel.getPositionWithoutLabelsFromPosition(position, mSortOrder); + } + + @Override + public void finishUpdate(ViewGroup container) { + mReusableView = null; + super.finishUpdate(container); + } + + @Override + public boolean isViewFromObject(View view, Object object) { + return view == object; + } + + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + MtpFullscreenView v = (MtpFullscreenView)object; + container.removeView(v); + mBroker.unregisterOnCheckedChangeListener(v); + mReusableView = v; + } + + @Override + public Object instantiateItem(ViewGroup container, int position) { + MtpFullscreenView v; + if (mReusableView != null) { + v = mReusableView; + mReusableView = null; + } else { + v = (MtpFullscreenView) mInflater.inflate(R.layout.ingest_fullsize, container, false); + } + MtpObjectInfo i = mModel.getWithoutLabels(position, mSortOrder); + v.getImageView().setMtpDeviceAndObjectInfo(mModel.getDevice(), i, mGeneration); + v.setPositionAndBroker(position, mBroker); + container.addView(v); + return v; + } +} diff --git a/src/com/android/gallery3d/ingest/data/BitmapWithMetadata.java b/src/com/android/gallery3d/ingest/data/BitmapWithMetadata.java new file mode 100644 index 000000000..bbc90f670 --- /dev/null +++ b/src/com/android/gallery3d/ingest/data/BitmapWithMetadata.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2013 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.ingest.data; + +import android.graphics.Bitmap; + +public class BitmapWithMetadata { + public Bitmap bitmap; + public int rotationDegrees; + + public BitmapWithMetadata(Bitmap bitmap, int rotationDegrees) { + this.bitmap = bitmap; + this.rotationDegrees = rotationDegrees; + } +} diff --git a/src/com/android/gallery3d/ingest/data/MtpBitmapFetch.java b/src/com/android/gallery3d/ingest/data/MtpBitmapFetch.java new file mode 100644 index 000000000..30868c22b --- /dev/null +++ b/src/com/android/gallery3d/ingest/data/MtpBitmapFetch.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2013 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.ingest.data; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.mtp.MtpDevice; +import android.mtp.MtpObjectInfo; +import android.util.DisplayMetrics; +import android.view.WindowManager; + +import com.android.camera.Exif; +import com.android.photos.data.GalleryBitmapPool; + +public class MtpBitmapFetch { + private static int sMaxSize = 0; + + public static void recycleThumbnail(Bitmap b) { + if (b != null) { + GalleryBitmapPool.getInstance().put(b); + } + } + + public static Bitmap getThumbnail(MtpDevice device, MtpObjectInfo info) { + byte[] imageBytes = device.getThumbnail(info.getObjectHandle()); + if (imageBytes == null) { + return null; + } + BitmapFactory.Options o = new BitmapFactory.Options(); + o.inJustDecodeBounds = true; + BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, o); + if (o.outWidth == 0 || o.outHeight == 0) { + return null; + } + o.inBitmap = GalleryBitmapPool.getInstance().get(o.outWidth, o.outHeight); + o.inMutable = true; + o.inJustDecodeBounds = false; + o.inSampleSize = 1; + try { + return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, o); + } catch (IllegalArgumentException e) { + // BitmapFactory throws an exception rather than returning null + // when image decoding fails and an existing bitmap was supplied + // for recycling, even if the failure was not caused by the use + // of that bitmap. + return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length); + } + } + + public static BitmapWithMetadata getFullsize(MtpDevice device, MtpObjectInfo info) { + return getFullsize(device, info, sMaxSize); + } + + public static BitmapWithMetadata getFullsize(MtpDevice device, MtpObjectInfo info, int maxSide) { + byte[] imageBytes = device.getObject(info.getObjectHandle(), info.getCompressedSize()); + if (imageBytes == null) { + return null; + } + Bitmap created; + if (maxSide > 0) { + BitmapFactory.Options o = new BitmapFactory.Options(); + o.inJustDecodeBounds = true; + BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, o); + int w = o.outWidth; + int h = o.outHeight; + int comp = Math.max(h, w); + int sampleSize = 1; + while ((comp >> 1) >= maxSide) { + comp = comp >> 1; + sampleSize++; + } + o.inSampleSize = sampleSize; + o.inJustDecodeBounds = false; + created = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, o); + } else { + created = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length); + } + if (created == null) { + return null; + } + + return new BitmapWithMetadata(created, Exif.getOrientation(imageBytes)); + } + + public static void configureForContext(Context context) { + DisplayMetrics metrics = new DisplayMetrics(); + WindowManager wm = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); + wm.getDefaultDisplay().getMetrics(metrics); + sMaxSize = Math.max(metrics.heightPixels, metrics.widthPixels); + } +} diff --git a/src/com/android/gallery3d/ingest/ui/DateTileView.java b/src/com/android/gallery3d/ingest/ui/DateTileView.java new file mode 100644 index 000000000..52fe9b85b --- /dev/null +++ b/src/com/android/gallery3d/ingest/ui/DateTileView.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2013 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.ingest.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.FrameLayout; +import android.widget.TextView; + +import com.android.gallery3d.R; +import com.android.gallery3d.ingest.SimpleDate; + +import java.text.DateFormatSymbols; +import java.util.Locale; + +public class DateTileView extends FrameLayout { + private static String[] sMonthNames = DateFormatSymbols.getInstance().getShortMonths(); + private static Locale sLocale; + + static { + refreshLocale(); + } + + public static boolean refreshLocale() { + Locale currentLocale = Locale.getDefault(); + if (!currentLocale.equals(sLocale)) { + sLocale = currentLocale; + sMonthNames = DateFormatSymbols.getInstance(sLocale).getShortMonths(); + return true; + } else { + return false; + } + } + + private TextView mDateTextView; + private TextView mMonthTextView; + private TextView mYearTextView; + private int mMonth = -1; + private int mYear = -1; + private int mDate = -1; + private String[] mMonthNames = sMonthNames; + + public DateTileView(Context context) { + super(context); + } + + public DateTileView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public DateTileView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // Force this to be square + super.onMeasure(widthMeasureSpec, widthMeasureSpec); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mDateTextView = (TextView) findViewById(R.id.date_tile_day); + mMonthTextView = (TextView) findViewById(R.id.date_tile_month); + mYearTextView = (TextView) findViewById(R.id.date_tile_year); + } + + public void setDate(SimpleDate date) { + setDate(date.getDay(), date.getMonth(), date.getYear()); + } + + public void setDate(int date, int month, int year) { + if (date != mDate) { + mDate = date; + mDateTextView.setText(mDate > 9 ? Integer.toString(mDate) : "0" + mDate); + } + if (mMonthNames != sMonthNames) { + mMonthNames = sMonthNames; + if (month == mMonth) { + mMonthTextView.setText(mMonthNames[mMonth]); + } + } + if (month != mMonth) { + mMonth = month; + mMonthTextView.setText(mMonthNames[mMonth]); + } + if (year != mYear) { + mYear = year; + mYearTextView.setText(Integer.toString(mYear)); + } + } +} diff --git a/src/com/android/gallery3d/ingest/ui/IngestGridView.java b/src/com/android/gallery3d/ingest/ui/IngestGridView.java new file mode 100644 index 000000000..c821259fe --- /dev/null +++ b/src/com/android/gallery3d/ingest/ui/IngestGridView.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2013 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.ingest.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.GridView; + +/** + * This just extends GridView with the ability to listen for calls + * to clearChoices() + */ +public class IngestGridView extends GridView { + + public interface OnClearChoicesListener { + public void onClearChoices(); + } + + private OnClearChoicesListener mOnClearChoicesListener = null; + + public IngestGridView(Context context) { + super(context); + } + + public IngestGridView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public IngestGridView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public void setOnClearChoicesListener(OnClearChoicesListener l) { + mOnClearChoicesListener = l; + } + + @Override + public void clearChoices() { + super.clearChoices(); + if (mOnClearChoicesListener != null) { + mOnClearChoicesListener.onClearChoices(); + } + } +} diff --git a/src/com/android/gallery3d/ingest/ui/MtpFullscreenView.java b/src/com/android/gallery3d/ingest/ui/MtpFullscreenView.java new file mode 100644 index 000000000..8d3884dc6 --- /dev/null +++ b/src/com/android/gallery3d/ingest/ui/MtpFullscreenView.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2013 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.ingest.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.CheckBox; +import android.widget.Checkable; +import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.RelativeLayout; + +import com.android.gallery3d.R; +import com.android.gallery3d.ingest.adapter.CheckBroker; + +public class MtpFullscreenView extends RelativeLayout implements Checkable, + CompoundButton.OnCheckedChangeListener, CheckBroker.OnCheckedChangedListener { + + private MtpImageView mImageView; + private CheckBox mCheckbox; + private int mPosition = -1; + private CheckBroker mBroker; + + public MtpFullscreenView(Context context) { + super(context); + } + + public MtpFullscreenView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public MtpFullscreenView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mImageView = (MtpImageView) findViewById(R.id.ingest_fullsize_image); + mCheckbox = (CheckBox) findViewById(R.id.ingest_fullsize_image_checkbox); + mCheckbox.setOnCheckedChangeListener(this); + } + + @Override + public boolean isChecked() { + return mCheckbox.isChecked(); + } + + @Override + public void setChecked(boolean checked) { + mCheckbox.setChecked(checked); + } + + @Override + public void toggle() { + mCheckbox.toggle(); + } + + @Override + public void onDetachedFromWindow() { + setPositionAndBroker(-1, null); + super.onDetachedFromWindow(); + } + + public MtpImageView getImageView() { + return mImageView; + } + + public int getPosition() { + return mPosition; + } + + public void setPositionAndBroker(int position, CheckBroker b) { + if (mBroker != null) { + mBroker.unregisterOnCheckedChangeListener(this); + } + mPosition = position; + mBroker = b; + if (mBroker != null) { + setChecked(mBroker.isItemChecked(position)); + mBroker.registerOnCheckedChangeListener(this); + } + } + + @Override + public void onCheckedChanged(CompoundButton arg0, boolean isChecked) { + if (mBroker != null) mBroker.setItemChecked(mPosition, isChecked); + } + + @Override + public void onCheckedChanged(int position, boolean isChecked) { + if (position == mPosition) { + setChecked(isChecked); + } + } + + @Override + public void onBulkCheckedChanged() { + if(mBroker != null) setChecked(mBroker.isItemChecked(mPosition)); + } +} diff --git a/src/com/android/gallery3d/ingest/ui/MtpImageView.java b/src/com/android/gallery3d/ingest/ui/MtpImageView.java new file mode 100644 index 000000000..80c105126 --- /dev/null +++ b/src/com/android/gallery3d/ingest/ui/MtpImageView.java @@ -0,0 +1,280 @@ +/* + * Copyright (C) 2013 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.ingest.ui; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.drawable.Drawable; +import android.mtp.MtpDevice; +import android.mtp.MtpObjectInfo; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.util.AttributeSet; +import android.widget.ImageView; + +import com.android.gallery3d.R; +import com.android.gallery3d.ingest.MtpDeviceIndex; +import com.android.gallery3d.ingest.data.BitmapWithMetadata; +import com.android.gallery3d.ingest.data.MtpBitmapFetch; + +import java.lang.ref.WeakReference; + +public class MtpImageView extends ImageView { + // We will use the thumbnail for images larger than this threshold + private static final int MAX_FULLSIZE_PREVIEW_SIZE = 8388608; // 8 megabytes + + private int mObjectHandle; + private int mGeneration; + + private WeakReference<MtpImageView> mWeakReference = new WeakReference<MtpImageView>(this); + private Object mFetchLock = new Object(); + private boolean mFetchPending = false; + private MtpObjectInfo mFetchObjectInfo; + private MtpDevice mFetchDevice; + private Object mFetchResult; + private Drawable mOverlayIcon; + private boolean mShowOverlayIcon; + + private static final FetchImageHandler sFetchHandler = FetchImageHandler.createOnNewThread(); + private static final ShowImageHandler sFetchCompleteHandler = new ShowImageHandler(); + + private void init() { + showPlaceholder(); + } + + public MtpImageView(Context context) { + super(context); + init(); + } + + public MtpImageView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public MtpImageView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(); + } + + private void showPlaceholder() { + setImageResource(android.R.color.transparent); + } + + public void setMtpDeviceAndObjectInfo(MtpDevice device, MtpObjectInfo object, int gen) { + int handle = object.getObjectHandle(); + if (handle == mObjectHandle && gen == mGeneration) { + return; + } + cancelLoadingAndClear(); + showPlaceholder(); + mGeneration = gen; + mObjectHandle = handle; + mShowOverlayIcon = MtpDeviceIndex.SUPPORTED_VIDEO_FORMATS.contains(object.getFormat()); + if (mShowOverlayIcon && mOverlayIcon == null) { + mOverlayIcon = getResources().getDrawable(R.drawable.ic_control_play); + updateOverlayIconBounds(); + } + synchronized (mFetchLock) { + mFetchObjectInfo = object; + mFetchDevice = device; + if (mFetchPending) return; + mFetchPending = true; + sFetchHandler.sendMessage( + sFetchHandler.obtainMessage(0, mWeakReference)); + } + } + + protected Object fetchMtpImageDataFromDevice(MtpDevice device, MtpObjectInfo info) { + if (info.getCompressedSize() <= MAX_FULLSIZE_PREVIEW_SIZE + && MtpDeviceIndex.SUPPORTED_IMAGE_FORMATS.contains(info.getFormat())) { + return MtpBitmapFetch.getFullsize(device, info); + } else { + return new BitmapWithMetadata(MtpBitmapFetch.getThumbnail(device, info), 0); + } + } + + private float mLastBitmapWidth; + private float mLastBitmapHeight; + private int mLastRotationDegrees; + private Matrix mDrawMatrix = new Matrix(); + + private void updateDrawMatrix() { + mDrawMatrix.reset(); + float dwidth; + float dheight; + float vheight = getHeight(); + float vwidth = getWidth(); + float scale; + boolean rotated90 = (mLastRotationDegrees % 180 != 0); + if (rotated90) { + dwidth = mLastBitmapHeight; + dheight = mLastBitmapWidth; + } else { + dwidth = mLastBitmapWidth; + dheight = mLastBitmapHeight; + } + if (dwidth <= vwidth && dheight <= vheight) { + scale = 1.0f; + } else { + scale = Math.min(vwidth / dwidth, vheight / dheight); + } + mDrawMatrix.setScale(scale, scale); + if (rotated90) { + mDrawMatrix.postTranslate(-dheight * scale * 0.5f, + -dwidth * scale * 0.5f); + mDrawMatrix.postRotate(mLastRotationDegrees); + mDrawMatrix.postTranslate(dwidth * scale * 0.5f, + dheight * scale * 0.5f); + } + mDrawMatrix.postTranslate((vwidth - dwidth * scale) * 0.5f, + (vheight - dheight * scale) * 0.5f); + if (!rotated90 && mLastRotationDegrees > 0) { + // rotated by a multiple of 180 + mDrawMatrix.postRotate(mLastRotationDegrees, vwidth / 2, vheight / 2); + } + setImageMatrix(mDrawMatrix); + } + + private static final int OVERLAY_ICON_SIZE_DENOMINATOR = 4; + + private void updateOverlayIconBounds() { + int iheight = mOverlayIcon.getIntrinsicHeight(); + int iwidth = mOverlayIcon.getIntrinsicWidth(); + int vheight = getHeight(); + int vwidth = getWidth(); + float scale_height = ((float) vheight) / (iheight * OVERLAY_ICON_SIZE_DENOMINATOR); + float scale_width = ((float) vwidth) / (iwidth * OVERLAY_ICON_SIZE_DENOMINATOR); + if (scale_height >= 1f && scale_width >= 1f) { + mOverlayIcon.setBounds((vwidth - iwidth) / 2, + (vheight - iheight) / 2, + (vwidth + iwidth) / 2, + (vheight + iheight) / 2); + } else { + float scale = Math.min(scale_height, scale_width); + mOverlayIcon.setBounds((int) (vwidth - scale * iwidth) / 2, + (int) (vheight - scale * iheight) / 2, + (int) (vwidth + scale * iwidth) / 2, + (int) (vheight + scale * iheight) / 2); + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + if (changed && getScaleType() == ScaleType.MATRIX) { + updateDrawMatrix(); + } + if (mShowOverlayIcon && changed && mOverlayIcon != null) { + updateOverlayIconBounds(); + } + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + if (mShowOverlayIcon && mOverlayIcon != null) { + mOverlayIcon.draw(canvas); + } + } + + protected void onMtpImageDataFetchedFromDevice(Object result) { + BitmapWithMetadata bitmapWithMetadata = (BitmapWithMetadata)result; + if (getScaleType() == ScaleType.MATRIX) { + mLastBitmapHeight = bitmapWithMetadata.bitmap.getHeight(); + mLastBitmapWidth = bitmapWithMetadata.bitmap.getWidth(); + mLastRotationDegrees = bitmapWithMetadata.rotationDegrees; + updateDrawMatrix(); + } else { + setRotation(bitmapWithMetadata.rotationDegrees); + } + setAlpha(0f); + setImageBitmap(bitmapWithMetadata.bitmap); + animate().alpha(1f); + } + + protected void cancelLoadingAndClear() { + synchronized (mFetchLock) { + mFetchDevice = null; + mFetchObjectInfo = null; + mFetchResult = null; + } + animate().cancel(); + setImageResource(android.R.color.transparent); + } + + @Override + public void onDetachedFromWindow() { + cancelLoadingAndClear(); + super.onDetachedFromWindow(); + } + + private static class FetchImageHandler extends Handler { + public FetchImageHandler(Looper l) { + super(l); + } + + public static FetchImageHandler createOnNewThread() { + HandlerThread t = new HandlerThread("MtpImageView Fetch"); + t.start(); + return new FetchImageHandler(t.getLooper()); + } + + @Override + public void handleMessage(Message msg) { + @SuppressWarnings("unchecked") + MtpImageView parent = ((WeakReference<MtpImageView>) msg.obj).get(); + if (parent == null) return; + MtpObjectInfo objectInfo; + MtpDevice device; + synchronized (parent.mFetchLock) { + parent.mFetchPending = false; + device = parent.mFetchDevice; + objectInfo = parent.mFetchObjectInfo; + } + if (device == null) return; + Object result = parent.fetchMtpImageDataFromDevice(device, objectInfo); + if (result == null) return; + synchronized (parent.mFetchLock) { + if (parent.mFetchObjectInfo != objectInfo) return; + parent.mFetchResult = result; + parent.mFetchDevice = null; + parent.mFetchObjectInfo = null; + sFetchCompleteHandler.sendMessage( + sFetchCompleteHandler.obtainMessage(0, parent.mWeakReference)); + } + } + } + + private static class ShowImageHandler extends Handler { + @Override + public void handleMessage(Message msg) { + @SuppressWarnings("unchecked") + MtpImageView parent = ((WeakReference<MtpImageView>) msg.obj).get(); + if (parent == null) return; + Object result; + synchronized (parent.mFetchLock) { + result = parent.mFetchResult; + } + if (result == null) return; + parent.onMtpImageDataFetchedFromDevice(result); + } + } +} diff --git a/src/com/android/gallery3d/ingest/ui/MtpThumbnailTileView.java b/src/com/android/gallery3d/ingest/ui/MtpThumbnailTileView.java new file mode 100644 index 000000000..3307e78aa --- /dev/null +++ b/src/com/android/gallery3d/ingest/ui/MtpThumbnailTileView.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2013 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.ingest.ui; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.mtp.MtpDevice; +import android.mtp.MtpObjectInfo; +import android.util.AttributeSet; +import android.widget.Checkable; + +import com.android.gallery3d.R; +import com.android.gallery3d.ingest.data.MtpBitmapFetch; + + +public class MtpThumbnailTileView extends MtpImageView implements Checkable { + + private Paint mForegroundPaint; + private boolean mIsChecked; + private Bitmap mBitmap; + + private void init() { + mForegroundPaint = new Paint(); + mForegroundPaint.setColor(getResources().getColor(R.color.ingest_highlight_semitransparent)); + } + + public MtpThumbnailTileView(Context context) { + super(context); + init(); + } + + public MtpThumbnailTileView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public MtpThumbnailTileView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(); + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // Force this to be square + super.onMeasure(widthMeasureSpec, widthMeasureSpec); + } + + @Override + protected Object fetchMtpImageDataFromDevice(MtpDevice device, MtpObjectInfo info) { + return MtpBitmapFetch.getThumbnail(device, info); + } + + @Override + protected void onMtpImageDataFetchedFromDevice(Object result) { + mBitmap = (Bitmap)result; + setImageBitmap(mBitmap); + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + if (isChecked()) { + canvas.drawRect(canvas.getClipBounds(), mForegroundPaint); + } + } + + @Override + public boolean isChecked() { + return mIsChecked; + } + + @Override + public void setChecked(boolean checked) { + mIsChecked = checked; + } + + @Override + public void toggle() { + setChecked(!mIsChecked); + } + + @Override + protected void cancelLoadingAndClear() { + super.cancelLoadingAndClear(); + if (mBitmap != null) { + MtpBitmapFetch.recycleThumbnail(mBitmap); + mBitmap = null; + } + } +} diff --git a/src/com/android/gallery3d/onetimeinitializer/GalleryWidgetMigrator.java b/src/com/android/gallery3d/onetimeinitializer/GalleryWidgetMigrator.java new file mode 100644 index 000000000..ef26b1b97 --- /dev/null +++ b/src/com/android/gallery3d/onetimeinitializer/GalleryWidgetMigrator.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2012 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.onetimeinitializer; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Build; +import android.os.Environment; +import android.preference.PreferenceManager; +import android.util.Log; + +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.LocalAlbum; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.gadget.WidgetDatabaseHelper; +import com.android.gallery3d.gadget.WidgetDatabaseHelper.Entry; +import com.android.gallery3d.util.GalleryUtils; + +import java.io.File; +import java.util.HashMap; +import java.util.List; + +/** + * This one-timer migrates local-album gallery app widgets from old paths from prior releases + * to updated paths in the current build version. This migration is needed because of + * bucket ID (i.e., directory hash) change in JB and JB MR1 (The external storage path has changed + * from /mnt/sdcard in pre-JB releases, to /storage/sdcard0 in JB, then again + * to /external/storage/sdcard/0 in JB MR1). + */ +public class GalleryWidgetMigrator { + private static final String TAG = "GalleryWidgetMigrator"; + private static final String PRE_JB_EXT_PATH = "/mnt/sdcard"; + private static final String JB_EXT_PATH = "/storage/sdcard0"; + private static final String NEW_EXT_PATH = + Environment.getExternalStorageDirectory().getAbsolutePath(); + private static final int RELATIVE_PATH_START = NEW_EXT_PATH.length(); + private static final String KEY_EXT_PATH = "external_storage_path"; + + /** + * Migrates local-album gallery widgets from prior releases to current release + * due to bucket ID (i.e., directory hash) change. + */ + public static void migrateGalleryWidgets(Context context) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + // Migration is only needed when external storage path has changed + String extPath = prefs.getString(KEY_EXT_PATH, null); + boolean isDone = NEW_EXT_PATH.equals(extPath); + if (isDone) return; + + try { + migrateGalleryWidgetsInternal(context); + prefs.edit().putString(KEY_EXT_PATH, NEW_EXT_PATH).commit(); + } catch (Throwable t) { + // exception may be thrown if external storage is not available(?) + Log.w(TAG, "migrateGalleryWidgets", t); + } + } + + private static void migrateGalleryWidgetsInternal(Context context) { + GalleryApp galleryApp = (GalleryApp) context.getApplicationContext(); + DataManager manager = galleryApp.getDataManager(); + WidgetDatabaseHelper dbHelper = new WidgetDatabaseHelper(context); + + // only need to migrate local-album entries of type TYPE_ALBUM + List<Entry> entries = dbHelper.getEntries(WidgetDatabaseHelper.TYPE_ALBUM); + if (entries == null) return; + + // Check each entry's relativePath. If exists, update bucket id using relative + // path combined with external storage path. Otherwise, iterate through old external + // storage paths to find the relative path that matches the old bucket id, and then update + // bucket id and relative path + HashMap<Integer, Entry> localEntries = new HashMap<Integer, Entry>(entries.size()); + for (Entry entry : entries) { + Path path = Path.fromString(entry.albumPath); + MediaSet mediaSet = (MediaSet) manager.getMediaObject(path); + if (mediaSet instanceof LocalAlbum) { + if (entry.relativePath != null && entry.relativePath.length() > 0) { + // update entry using relative path + external storage path + updateEntryUsingRelativePath(entry, dbHelper); + } else { + int bucketId = Integer.parseInt(path.getSuffix()); + localEntries.put(bucketId, entry); + } + } + } + if (!localEntries.isEmpty()) migrateLocalEntries(context, localEntries, dbHelper); + } + + private static void migrateLocalEntries(Context context, + HashMap<Integer, Entry> entries, WidgetDatabaseHelper dbHelper) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + String oldExtPath = prefs.getString(KEY_EXT_PATH, null); + if (oldExtPath != null) { + migrateLocalEntries(entries, dbHelper, oldExtPath); + return; + } + // If old external storage path is unknown, it could be either Pre-JB or JB version + // we need to try both. + migrateLocalEntries(entries, dbHelper, PRE_JB_EXT_PATH); + if (!entries.isEmpty() && + Build.VERSION.SDK_INT > ApiHelper.VERSION_CODES.JELLY_BEAN) { + migrateLocalEntries(entries, dbHelper, JB_EXT_PATH); + } + } + + private static void migrateLocalEntries(HashMap<Integer, Entry> entries, + WidgetDatabaseHelper dbHelper, String oldExtPath) { + File root = Environment.getExternalStorageDirectory(); + // check the DCIM directory first; this should take care of 99% use cases + updatePath(new File(root, "DCIM"), entries, dbHelper, oldExtPath); + // check other directories if DCIM doesn't cut it + if (!entries.isEmpty()) updatePath(root, entries, dbHelper, oldExtPath); + } + private static void updatePath(File root, HashMap<Integer, Entry> entries, + WidgetDatabaseHelper dbHelper, String oldExtStorage) { + File[] files = root.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory() && !entries.isEmpty()) { + String path = file.getAbsolutePath(); + String oldPath = oldExtStorage + path.substring(RELATIVE_PATH_START); + int oldBucketId = GalleryUtils.getBucketId(oldPath); + Entry entry = entries.remove(oldBucketId); + if (entry != null) { + int newBucketId = GalleryUtils.getBucketId(path); + String newAlbumPath = Path.fromString(entry.albumPath) + .getParent() + .getChild(newBucketId) + .toString(); + Log.d(TAG, "migrate from " + entry.albumPath + " to " + newAlbumPath); + entry.albumPath = newAlbumPath; + // update entry's relative path + entry.relativePath = path.substring(RELATIVE_PATH_START); + dbHelper.updateEntry(entry); + } + updatePath(file, entries, dbHelper, oldExtStorage); // recursion + } + } + } + } + + private static void updateEntryUsingRelativePath(Entry entry, WidgetDatabaseHelper dbHelper) { + String newPath = NEW_EXT_PATH + entry.relativePath; + int newBucketId = GalleryUtils.getBucketId(newPath); + String newAlbumPath = Path.fromString(entry.albumPath) + .getParent() + .getChild(newBucketId) + .toString(); + entry.albumPath = newAlbumPath; + dbHelper.updateEntry(entry); + } +} diff --git a/src/com/android/gallery3d/provider/GalleryProvider.java b/src/com/android/gallery3d/provider/GalleryProvider.java new file mode 100644 index 000000000..d6c7ccd4d --- /dev/null +++ b/src/com/android/gallery3d/provider/GalleryProvider.java @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2009 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.provider; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Binder; +import android.os.ParcelFileDescriptor; +import android.provider.MediaStore.Images.ImageColumns; +import android.util.Log; + +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.common.AsyncTaskUtil; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.picasasource.PicasaSource; +import com.android.gallery3d.util.GalleryUtils; + +import java.io.FileNotFoundException; +import java.io.IOException; + +public class GalleryProvider extends ContentProvider { + private static final String TAG = "GalleryProvider"; + + public static final String AUTHORITY = "com.android.gallery3d.provider"; + public static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY); + + public static interface PicasaColumns { + public static final String USER_ACCOUNT = "user_account"; + public static final String PICASA_ID = "picasa_id"; + } + + private static final String[] SUPPORTED_PICASA_COLUMNS = { + PicasaColumns.USER_ACCOUNT, + PicasaColumns.PICASA_ID, + ImageColumns.DISPLAY_NAME, + ImageColumns.SIZE, + ImageColumns.MIME_TYPE, + ImageColumns.DATE_TAKEN, + ImageColumns.LATITUDE, + ImageColumns.LONGITUDE, + ImageColumns.ORIENTATION}; + + private DataManager mDataManager; + private static Uri sBaseUri; + + public static String getAuthority(Context context) { + return context.getPackageName() + ".provider"; + } + + public static Uri getUriFor(Context context, Path path) { + if (sBaseUri == null) { + sBaseUri = Uri.parse("content://" + context.getPackageName() + ".provider"); + } + return sBaseUri.buildUpon() + .appendEncodedPath(path.toString().substring(1)) // ignore the leading '/' + .build(); + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException(); + } + + // TODO: consider concurrent access + @Override + public String getType(Uri uri) { + long token = Binder.clearCallingIdentity(); + try { + Path path = Path.fromString(uri.getPath()); + MediaItem item = (MediaItem) mDataManager.getMediaObject(path); + return item != null ? item.getMimeType() : null; + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean onCreate() { + GalleryApp app = (GalleryApp) getContext().getApplicationContext(); + mDataManager = app.getDataManager(); + return true; + } + + // TODO: consider concurrent access + @Override + public Cursor query(Uri uri, String[] projection, + String selection, String[] selectionArgs, String sortOrder) { + long token = Binder.clearCallingIdentity(); + try { + Path path = Path.fromString(uri.getPath()); + MediaObject object = mDataManager.getMediaObject(path); + if (object == null) { + Log.w(TAG, "cannot find: " + uri); + return null; + } + if (PicasaSource.isPicasaImage(object)) { + return queryPicasaItem(object, + projection, selection, selectionArgs, sortOrder); + } else { + return null; + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + + private Cursor queryPicasaItem(MediaObject image, String[] projection, + String selection, String[] selectionArgs, String sortOrder) { + if (projection == null) projection = SUPPORTED_PICASA_COLUMNS; + Object[] columnValues = new Object[projection.length]; + double latitude = PicasaSource.getLatitude(image); + double longitude = PicasaSource.getLongitude(image); + boolean isValidLatlong = GalleryUtils.isValidLocation(latitude, longitude); + + for (int i = 0, n = projection.length; i < n; ++i) { + String column = projection[i]; + if (PicasaColumns.USER_ACCOUNT.equals(column)) { + columnValues[i] = PicasaSource.getUserAccount(getContext(), image); + } else if (PicasaColumns.PICASA_ID.equals(column)) { + columnValues[i] = PicasaSource.getPicasaId(image); + } else if (ImageColumns.DISPLAY_NAME.equals(column)) { + columnValues[i] = PicasaSource.getImageTitle(image); + } else if (ImageColumns.SIZE.equals(column)){ + columnValues[i] = PicasaSource.getImageSize(image); + } else if (ImageColumns.MIME_TYPE.equals(column)) { + columnValues[i] = PicasaSource.getContentType(image); + } else if (ImageColumns.DATE_TAKEN.equals(column)) { + columnValues[i] = PicasaSource.getDateTaken(image); + } else if (ImageColumns.LATITUDE.equals(column)) { + columnValues[i] = isValidLatlong ? latitude : null; + } else if (ImageColumns.LONGITUDE.equals(column)) { + columnValues[i] = isValidLatlong ? longitude : null; + } else if (ImageColumns.ORIENTATION.equals(column)) { + columnValues[i] = PicasaSource.getRotation(image); + } else { + Log.w(TAG, "unsupported column: " + column); + } + } + MatrixCursor cursor = new MatrixCursor(projection); + cursor.addRow(columnValues); + return cursor; + } + + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode) + throws FileNotFoundException { + long token = Binder.clearCallingIdentity(); + try { + if (mode.contains("w")) { + throw new FileNotFoundException("cannot open file for write"); + } + Path path = Path.fromString(uri.getPath()); + MediaObject object = mDataManager.getMediaObject(path); + if (object == null) { + throw new FileNotFoundException(uri.toString()); + } + if (PicasaSource.isPicasaImage(object)) { + return PicasaSource.openFile(getContext(), object, mode); + } else { + throw new FileNotFoundException("unspported type: " + object); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException(); + } + + private static interface PipeDataWriter<T> { + void writeDataToPipe(ParcelFileDescriptor output, T args); + } + + // Modified from ContentProvider.openPipeHelper. We are target at API LEVEL 10. + // But openPipeHelper is available in API LEVEL 11. + private static <T> ParcelFileDescriptor openPipeHelper( + final T args, final PipeDataWriter<T> func) throws FileNotFoundException { + try { + final ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); + AsyncTask<Object, Object, Object> task = new AsyncTask<Object, Object, Object>() { + @Override + protected Object doInBackground(Object... params) { + try { + func.writeDataToPipe(pipe[1], args); + return null; + } finally { + Utils.closeSilently(pipe[1]); + } + } + }; + AsyncTaskUtil.executeInParallel(task, (Object[]) null); + return pipe[0]; + } catch (IOException e) { + throw new FileNotFoundException("failure making pipe"); + } + } + +} diff --git a/src/com/android/gallery3d/ui/AbstractSlotRenderer.java b/src/com/android/gallery3d/ui/AbstractSlotRenderer.java new file mode 100644 index 000000000..729439dc3 --- /dev/null +++ b/src/com/android/gallery3d/ui/AbstractSlotRenderer.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2012 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.ui; + +import android.content.Context; +import android.graphics.Rect; + +import com.android.gallery3d.R; +import com.android.gallery3d.glrenderer.FadeOutTexture; +import com.android.gallery3d.glrenderer.GLCanvas; +import com.android.gallery3d.glrenderer.NinePatchTexture; +import com.android.gallery3d.glrenderer.ResourceTexture; +import com.android.gallery3d.glrenderer.Texture; + +public abstract class AbstractSlotRenderer implements SlotView.SlotRenderer { + + private final ResourceTexture mVideoOverlay; + private final ResourceTexture mVideoPlayIcon; + private final ResourceTexture mPanoramaIcon; + private final NinePatchTexture mFramePressed; + private final NinePatchTexture mFrameSelected; + private FadeOutTexture mFramePressedUp; + + protected AbstractSlotRenderer(Context context) { + mVideoOverlay = new ResourceTexture(context, R.drawable.ic_video_thumb); + mVideoPlayIcon = new ResourceTexture(context, R.drawable.ic_gallery_play); + mPanoramaIcon = new ResourceTexture(context, R.drawable.ic_360pano_holo_light); + mFramePressed = new NinePatchTexture(context, R.drawable.grid_pressed); + mFrameSelected = new NinePatchTexture(context, R.drawable.grid_selected); + } + + protected void drawContent(GLCanvas canvas, + Texture content, int width, int height, int rotation) { + canvas.save(GLCanvas.SAVE_FLAG_MATRIX); + + // The content is always rendered in to the largest square that fits + // inside the slot, aligned to the top of the slot. + width = height = Math.min(width, height); + if (rotation != 0) { + canvas.translate(width / 2, height / 2); + canvas.rotate(rotation, 0, 0, 1); + canvas.translate(-width / 2, -height / 2); + } + + // Fit the content into the box + float scale = Math.min( + (float) width / content.getWidth(), + (float) height / content.getHeight()); + canvas.scale(scale, scale, 1); + content.draw(canvas, 0, 0); + + canvas.restore(); + } + + protected void drawVideoOverlay(GLCanvas canvas, int width, int height) { + // Scale the video overlay to the height of the thumbnail and put it + // on the left side. + ResourceTexture v = mVideoOverlay; + float scale = (float) height / v.getHeight(); + int w = Math.round(scale * v.getWidth()); + int h = Math.round(scale * v.getHeight()); + v.draw(canvas, 0, 0, w, h); + + int s = Math.min(width, height) / 6; + mVideoPlayIcon.draw(canvas, (width - s) / 2, (height - s) / 2, s, s); + } + + protected void drawPanoramaIcon(GLCanvas canvas, int width, int height) { + int iconSize = Math.min(width, height) / 6; + mPanoramaIcon.draw(canvas, (width - iconSize) / 2, (height - iconSize) / 2, + iconSize, iconSize); + } + + protected boolean isPressedUpFrameFinished() { + if (mFramePressedUp != null) { + if (mFramePressedUp.isAnimating()) { + return false; + } else { + mFramePressedUp = null; + } + } + return true; + } + + protected void drawPressedUpFrame(GLCanvas canvas, int width, int height) { + if (mFramePressedUp == null) { + mFramePressedUp = new FadeOutTexture(mFramePressed); + } + drawFrame(canvas, mFramePressed.getPaddings(), mFramePressedUp, 0, 0, width, height); + } + + protected void drawPressedFrame(GLCanvas canvas, int width, int height) { + drawFrame(canvas, mFramePressed.getPaddings(), mFramePressed, 0, 0, width, height); + } + + protected void drawSelectedFrame(GLCanvas canvas, int width, int height) { + drawFrame(canvas, mFrameSelected.getPaddings(), mFrameSelected, 0, 0, width, height); + } + + protected static void drawFrame(GLCanvas canvas, Rect padding, Texture frame, + int x, int y, int width, int height) { + frame.draw(canvas, x - padding.left, y - padding.top, width + padding.left + padding.right, + height + padding.top + padding.bottom); + } +} diff --git a/src/com/android/gallery3d/ui/ActionModeHandler.java b/src/com/android/gallery3d/ui/ActionModeHandler.java new file mode 100644 index 000000000..6b4f10312 --- /dev/null +++ b/src/com/android/gallery3d/ui/ActionModeHandler.java @@ -0,0 +1,501 @@ +/* + * 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.ui; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.nfc.NfcAdapter; +import android.os.Handler; +import android.view.ActionMode; +import android.view.ActionMode.Callback; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +import android.widget.ShareActionProvider; +import android.widget.ShareActionProvider.OnShareTargetSelectedListener; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.AbstractGalleryActivity; +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.MediaObject.PanoramaSupportCallback; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.ui.MenuExecutor.ProgressListener; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.GalleryUtils; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import java.util.ArrayList; + +public class ActionModeHandler implements Callback, PopupList.OnPopupItemClickListener { + + @SuppressWarnings("unused") + private static final String TAG = "ActionModeHandler"; + + private static final int MAX_SELECTED_ITEMS_FOR_SHARE_INTENT = 300; + private static final int MAX_SELECTED_ITEMS_FOR_PANORAMA_SHARE_INTENT = 10; + + private static final int SUPPORT_MULTIPLE_MASK = MediaObject.SUPPORT_DELETE + | MediaObject.SUPPORT_ROTATE | MediaObject.SUPPORT_SHARE + | MediaObject.SUPPORT_CACHE; + + public interface ActionModeListener { + public boolean onActionItemClicked(MenuItem item); + } + + private final AbstractGalleryActivity mActivity; + private final MenuExecutor mMenuExecutor; + private final SelectionManager mSelectionManager; + private final NfcAdapter mNfcAdapter; + private Menu mMenu; + private MenuItem mSharePanoramaMenuItem; + private MenuItem mShareMenuItem; + private ShareActionProvider mSharePanoramaActionProvider; + private ShareActionProvider mShareActionProvider; + private SelectionMenu mSelectionMenu; + private ActionModeListener mListener; + private Future<?> mMenuTask; + private final Handler mMainHandler; + private ActionMode mActionMode; + + private static class GetAllPanoramaSupports implements PanoramaSupportCallback { + private int mNumInfoRequired; + private JobContext mJobContext; + public boolean mAllPanoramas = true; + public boolean mAllPanorama360 = true; + public boolean mHasPanorama360 = false; + private Object mLock = new Object(); + + public GetAllPanoramaSupports(ArrayList<MediaObject> mediaObjects, JobContext jc) { + mJobContext = jc; + mNumInfoRequired = mediaObjects.size(); + for (MediaObject mediaObject : mediaObjects) { + mediaObject.getPanoramaSupport(this); + } + } + + @Override + public void panoramaInfoAvailable(MediaObject mediaObject, boolean isPanorama, + boolean isPanorama360) { + synchronized (mLock) { + mNumInfoRequired--; + mAllPanoramas = isPanorama && mAllPanoramas; + mAllPanorama360 = isPanorama360 && mAllPanorama360; + mHasPanorama360 = mHasPanorama360 || isPanorama360; + if (mNumInfoRequired == 0 || mJobContext.isCancelled()) { + mLock.notifyAll(); + } + } + } + + public void waitForPanoramaSupport() { + synchronized (mLock) { + while (mNumInfoRequired != 0 && !mJobContext.isCancelled()) { + try { + mLock.wait(); + } catch (InterruptedException e) { + // May be a cancelled job context + } + } + } + } + } + + public ActionModeHandler( + AbstractGalleryActivity activity, SelectionManager selectionManager) { + mActivity = Utils.checkNotNull(activity); + mSelectionManager = Utils.checkNotNull(selectionManager); + mMenuExecutor = new MenuExecutor(activity, selectionManager); + mMainHandler = new Handler(activity.getMainLooper()); + mNfcAdapter = NfcAdapter.getDefaultAdapter(mActivity.getAndroidContext()); + } + + public void startActionMode() { + Activity a = mActivity; + mActionMode = a.startActionMode(this); + View customView = LayoutInflater.from(a).inflate( + R.layout.action_mode, null); + mActionMode.setCustomView(customView); + mSelectionMenu = new SelectionMenu(a, + (Button) customView.findViewById(R.id.selection_menu), this); + updateSelectionMenu(); + } + + public void finishActionMode() { + mActionMode.finish(); + } + + public void setTitle(String title) { + mSelectionMenu.setTitle(title); + } + + public void setActionModeListener(ActionModeListener listener) { + mListener = listener; + } + + private WakeLockHoldingProgressListener mDeleteProgressListener; + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + GLRoot root = mActivity.getGLRoot(); + root.lockRenderThread(); + try { + boolean result; + // Give listener a chance to process this command before it's routed to + // ActionModeHandler, which handles command only based on the action id. + // Sometimes the listener may have more background information to handle + // an action command. + if (mListener != null) { + result = mListener.onActionItemClicked(item); + if (result) { + mSelectionManager.leaveSelectionMode(); + return result; + } + } + ProgressListener listener = null; + String confirmMsg = null; + int action = item.getItemId(); + if (action == R.id.action_delete) { + confirmMsg = mActivity.getResources().getQuantityString( + R.plurals.delete_selection, mSelectionManager.getSelectedCount()); + if (mDeleteProgressListener == null) { + mDeleteProgressListener = new WakeLockHoldingProgressListener(mActivity, + "Gallery Delete Progress Listener"); + } + listener = mDeleteProgressListener; + } + mMenuExecutor.onMenuClicked(item, confirmMsg, listener); + } finally { + root.unlockRenderThread(); + } + return true; + } + + @Override + public boolean onPopupItemClick(int itemId) { + GLRoot root = mActivity.getGLRoot(); + root.lockRenderThread(); + try { + if (itemId == R.id.action_select_all) { + updateSupportedOperation(); + mMenuExecutor.onMenuClicked(itemId, null, false, true); + } + return true; + } finally { + root.unlockRenderThread(); + } + } + + private void updateSelectionMenu() { + // update title + int count = mSelectionManager.getSelectedCount(); + String format = mActivity.getResources().getQuantityString( + R.plurals.number_of_items_selected, count); + setTitle(String.format(format, count)); + + // For clients who call SelectionManager.selectAll() directly, we need to ensure the + // menu status is consistent with selection manager. + mSelectionMenu.updateSelectAllMode(mSelectionManager.inSelectAllMode()); + } + + private final OnShareTargetSelectedListener mShareTargetSelectedListener = + new OnShareTargetSelectedListener() { + @Override + public boolean onShareTargetSelected(ShareActionProvider source, Intent intent) { + mSelectionManager.leaveSelectionMode(); + return false; + } + }; + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + mode.getMenuInflater().inflate(R.menu.operation, menu); + + mMenu = menu; + mSharePanoramaMenuItem = menu.findItem(R.id.action_share_panorama); + if (mSharePanoramaMenuItem != null) { + mSharePanoramaActionProvider = (ShareActionProvider) mSharePanoramaMenuItem + .getActionProvider(); + mSharePanoramaActionProvider.setOnShareTargetSelectedListener( + mShareTargetSelectedListener); + mSharePanoramaActionProvider.setShareHistoryFileName("panorama_share_history.xml"); + } + mShareMenuItem = menu.findItem(R.id.action_share); + if (mShareMenuItem != null) { + mShareActionProvider = (ShareActionProvider) mShareMenuItem + .getActionProvider(); + mShareActionProvider.setOnShareTargetSelectedListener( + mShareTargetSelectedListener); + mShareActionProvider.setShareHistoryFileName("share_history.xml"); + } + return true; + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + mSelectionManager.leaveSelectionMode(); + } + + private ArrayList<MediaObject> getSelectedMediaObjects(JobContext jc) { + ArrayList<Path> unexpandedPaths = mSelectionManager.getSelected(false); + if (unexpandedPaths.isEmpty()) { + // This happens when starting selection mode from overflow menu + // (instead of long press a media object) + return null; + } + ArrayList<MediaObject> selected = new ArrayList<MediaObject>(); + DataManager manager = mActivity.getDataManager(); + for (Path path : unexpandedPaths) { + if (jc.isCancelled()) { + return null; + } + selected.add(manager.getMediaObject(path)); + } + + return selected; + } + // Menu options are determined by selection set itself. + // We cannot expand it because MenuExecuter executes it based on + // the selection set instead of the expanded result. + // e.g. LocalImage can be rotated but collections of them (LocalAlbum) can't. + private int computeMenuOptions(ArrayList<MediaObject> selected) { + int operation = MediaObject.SUPPORT_ALL; + int type = 0; + for (MediaObject mediaObject: selected) { + int support = mediaObject.getSupportedOperations(); + type |= mediaObject.getMediaType(); + operation &= support; + } + + switch (selected.size()) { + case 1: + final String mimeType = MenuExecutor.getMimeType(type); + if (!GalleryUtils.isEditorAvailable(mActivity, mimeType)) { + operation &= ~MediaObject.SUPPORT_EDIT; + } + break; + default: + operation &= SUPPORT_MULTIPLE_MASK; + } + + return operation; + } + + @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN) + private void setNfcBeamPushUris(Uri[] uris) { + if (mNfcAdapter != null && ApiHelper.HAS_SET_BEAM_PUSH_URIS) { + mNfcAdapter.setBeamPushUrisCallback(null, mActivity); + mNfcAdapter.setBeamPushUris(uris, mActivity); + } + } + + // Share intent needs to expand the selection set so we can get URI of + // each media item + private Intent computePanoramaSharingIntent(JobContext jc, int maxItems) { + ArrayList<Path> expandedPaths = mSelectionManager.getSelected(true, maxItems); + if (expandedPaths == null || expandedPaths.size() == 0) { + return new Intent(); + } + final ArrayList<Uri> uris = new ArrayList<Uri>(); + DataManager manager = mActivity.getDataManager(); + final Intent intent = new Intent(); + for (Path path : expandedPaths) { + if (jc.isCancelled()) return null; + uris.add(manager.getContentUri(path)); + } + + final int size = uris.size(); + if (size > 0) { + if (size > 1) { + intent.setAction(Intent.ACTION_SEND_MULTIPLE); + intent.setType(GalleryUtils.MIME_TYPE_PANORAMA360); + intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); + } else { + intent.setAction(Intent.ACTION_SEND); + intent.setType(GalleryUtils.MIME_TYPE_PANORAMA360); + intent.putExtra(Intent.EXTRA_STREAM, uris.get(0)); + } + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + + return intent; + } + + private Intent computeSharingIntent(JobContext jc, int maxItems) { + ArrayList<Path> expandedPaths = mSelectionManager.getSelected(true, maxItems); + if (expandedPaths == null || expandedPaths.size() == 0) { + setNfcBeamPushUris(null); + return new Intent(); + } + final ArrayList<Uri> uris = new ArrayList<Uri>(); + DataManager manager = mActivity.getDataManager(); + int type = 0; + final Intent intent = new Intent(); + for (Path path : expandedPaths) { + if (jc.isCancelled()) return null; + int support = manager.getSupportedOperations(path); + type |= manager.getMediaType(path); + + if ((support & MediaObject.SUPPORT_SHARE) != 0) { + uris.add(manager.getContentUri(path)); + } + } + + final int size = uris.size(); + if (size > 0) { + final String mimeType = MenuExecutor.getMimeType(type); + if (size > 1) { + intent.setAction(Intent.ACTION_SEND_MULTIPLE).setType(mimeType); + intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); + } else { + intent.setAction(Intent.ACTION_SEND).setType(mimeType); + intent.putExtra(Intent.EXTRA_STREAM, uris.get(0)); + } + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + setNfcBeamPushUris(uris.toArray(new Uri[uris.size()])); + } else { + setNfcBeamPushUris(null); + } + + return intent; + } + + public void updateSupportedOperation(Path path, boolean selected) { + // TODO: We need to improve the performance + updateSupportedOperation(); + } + + public void updateSupportedOperation() { + // Interrupt previous unfinished task, mMenuTask is only accessed in main thread + if (mMenuTask != null) mMenuTask.cancel(); + + updateSelectionMenu(); + + // Disable share actions until share intent is in good shape + if (mSharePanoramaMenuItem != null) mSharePanoramaMenuItem.setEnabled(false); + if (mShareMenuItem != null) mShareMenuItem.setEnabled(false); + + // Generate sharing intent and update supported operations in the background + // The task can take a long time and be canceled in the mean time. + mMenuTask = mActivity.getThreadPool().submit(new Job<Void>() { + @Override + public Void run(final JobContext jc) { + // Pass1: Deal with unexpanded media object list for menu operation. + ArrayList<MediaObject> selected = getSelectedMediaObjects(jc); + if (selected == null) { + mMainHandler.post(new Runnable() { + @Override + public void run() { + mMenuTask = null; + if (jc.isCancelled()) return; + // Disable all the operations when no item is selected + MenuExecutor.updateMenuOperation(mMenu, 0); + } + }); + return null; + } + final int operation = computeMenuOptions(selected); + if (jc.isCancelled()) { + return null; + } + int numSelected = selected.size(); + final boolean canSharePanoramas = + numSelected < MAX_SELECTED_ITEMS_FOR_PANORAMA_SHARE_INTENT; + final boolean canShare = + numSelected < MAX_SELECTED_ITEMS_FOR_SHARE_INTENT; + + final GetAllPanoramaSupports supportCallback = canSharePanoramas ? + new GetAllPanoramaSupports(selected, jc) + : null; + + // Pass2: Deal with expanded media object list for sharing operation. + final Intent share_panorama_intent = canSharePanoramas ? + computePanoramaSharingIntent(jc, MAX_SELECTED_ITEMS_FOR_PANORAMA_SHARE_INTENT) + : new Intent(); + final Intent share_intent = canShare ? + computeSharingIntent(jc, MAX_SELECTED_ITEMS_FOR_SHARE_INTENT) + : new Intent(); + + if (canSharePanoramas) { + supportCallback.waitForPanoramaSupport(); + } + if (jc.isCancelled()) { + return null; + } + mMainHandler.post(new Runnable() { + @Override + public void run() { + mMenuTask = null; + if (jc.isCancelled()) return; + MenuExecutor.updateMenuOperation(mMenu, operation); + MenuExecutor.updateMenuForPanorama(mMenu, + canSharePanoramas && supportCallback.mAllPanorama360, + canSharePanoramas && supportCallback.mHasPanorama360); + if (mSharePanoramaMenuItem != null) { + mSharePanoramaMenuItem.setEnabled(true); + if (canSharePanoramas && supportCallback.mAllPanorama360) { + mShareMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); + mShareMenuItem.setTitle( + mActivity.getResources().getString(R.string.share_as_photo)); + } else { + mSharePanoramaMenuItem.setVisible(false); + mShareMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); + mShareMenuItem.setTitle( + mActivity.getResources().getString(R.string.share)); + } + mSharePanoramaActionProvider.setShareIntent(share_panorama_intent); + } + if (mShareMenuItem != null) { + mShareMenuItem.setEnabled(canShare); + mShareActionProvider.setShareIntent(share_intent); + } + } + }); + return null; + } + }); + } + + public void pause() { + if (mMenuTask != null) { + mMenuTask.cancel(); + mMenuTask = null; + } + mMenuExecutor.pause(); + } + + public void destroy() { + mMenuExecutor.destroy(); + } + + public void resume() { + if (mSelectionManager.inSelectionMode()) updateSupportedOperation(); + mMenuExecutor.resume(); + } +} diff --git a/src/com/android/gallery3d/ui/AlbumLabelMaker.java b/src/com/android/gallery3d/ui/AlbumLabelMaker.java new file mode 100644 index 000000000..da1cac0bd --- /dev/null +++ b/src/com/android/gallery3d/ui/AlbumLabelMaker.java @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2012 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.ui; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.PorterDuff; +import android.graphics.Typeface; +import android.text.TextPaint; +import android.text.TextUtils; + +import com.android.gallery3d.R; +import com.android.gallery3d.data.DataSourceType; +import com.android.photos.data.GalleryBitmapPool; +import com.android.gallery3d.util.ThreadPool; +import com.android.gallery3d.util.ThreadPool.JobContext; + +public class AlbumLabelMaker { + private static final int BORDER_SIZE = 0; + + private final AlbumSetSlotRenderer.LabelSpec mSpec; + private final TextPaint mTitlePaint; + private final TextPaint mCountPaint; + private final Context mContext; + + private int mLabelWidth; + private int mBitmapWidth; + private int mBitmapHeight; + + private final LazyLoadedBitmap mLocalSetIcon; + private final LazyLoadedBitmap mPicasaIcon; + private final LazyLoadedBitmap mCameraIcon; + + public AlbumLabelMaker(Context context, AlbumSetSlotRenderer.LabelSpec spec) { + mContext = context; + mSpec = spec; + mTitlePaint = getTextPaint(spec.titleFontSize, spec.titleColor, false); + mCountPaint = getTextPaint(spec.countFontSize, spec.countColor, false); + + mLocalSetIcon = new LazyLoadedBitmap(R.drawable.frame_overlay_gallery_folder); + mPicasaIcon = new LazyLoadedBitmap(R.drawable.frame_overlay_gallery_picasa); + mCameraIcon = new LazyLoadedBitmap(R.drawable.frame_overlay_gallery_camera); + } + + public static int getBorderSize() { + return BORDER_SIZE; + } + + private Bitmap getOverlayAlbumIcon(int sourceType) { + switch (sourceType) { + case DataSourceType.TYPE_CAMERA: + return mCameraIcon.get(); + case DataSourceType.TYPE_LOCAL: + return mLocalSetIcon.get(); + case DataSourceType.TYPE_PICASA: + return mPicasaIcon.get(); + } + return null; + } + + private static TextPaint getTextPaint(int textSize, int color, boolean isBold) { + TextPaint paint = new TextPaint(); + paint.setTextSize(textSize); + paint.setAntiAlias(true); + paint.setColor(color); + //paint.setShadowLayer(2f, 0f, 0f, Color.LTGRAY); + if (isBold) { + paint.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD)); + } + return paint; + } + + private class LazyLoadedBitmap { + private Bitmap mBitmap; + private int mResId; + + public LazyLoadedBitmap(int resId) { + mResId = resId; + } + + public synchronized Bitmap get() { + if (mBitmap == null) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + mBitmap = BitmapFactory.decodeResource( + mContext.getResources(), mResId, options); + } + return mBitmap; + } + } + + public synchronized void setLabelWidth(int width) { + if (mLabelWidth == width) return; + mLabelWidth = width; + int borders = 2 * BORDER_SIZE; + mBitmapWidth = width + borders; + mBitmapHeight = mSpec.labelBackgroundHeight + borders; + } + + public ThreadPool.Job<Bitmap> requestLabel( + String title, String count, int sourceType) { + return new AlbumLabelJob(title, count, sourceType); + } + + static void drawText(Canvas canvas, + int x, int y, String text, int lengthLimit, TextPaint p) { + // The TextPaint cannot be used concurrently + synchronized (p) { + text = TextUtils.ellipsize( + text, p, lengthLimit, TextUtils.TruncateAt.END).toString(); + canvas.drawText(text, x, y - p.getFontMetricsInt().ascent, p); + } + } + + private class AlbumLabelJob implements ThreadPool.Job<Bitmap> { + private final String mTitle; + private final String mCount; + private final int mSourceType; + + public AlbumLabelJob(String title, String count, int sourceType) { + mTitle = title; + mCount = count; + mSourceType = sourceType; + } + + @Override + public Bitmap run(JobContext jc) { + AlbumSetSlotRenderer.LabelSpec s = mSpec; + + String title = mTitle; + String count = mCount; + Bitmap icon = getOverlayAlbumIcon(mSourceType); + + Bitmap bitmap; + int labelWidth; + + synchronized (this) { + labelWidth = mLabelWidth; + bitmap = GalleryBitmapPool.getInstance().get(mBitmapWidth, mBitmapHeight); + } + + if (bitmap == null) { + int borders = 2 * BORDER_SIZE; + bitmap = Bitmap.createBitmap(labelWidth + borders, + s.labelBackgroundHeight + borders, Config.ARGB_8888); + } + + Canvas canvas = new Canvas(bitmap); + canvas.clipRect(BORDER_SIZE, BORDER_SIZE, + bitmap.getWidth() - BORDER_SIZE, + bitmap.getHeight() - BORDER_SIZE); + canvas.drawColor(mSpec.backgroundColor, PorterDuff.Mode.SRC); + + canvas.translate(BORDER_SIZE, BORDER_SIZE); + + // draw title + if (jc.isCancelled()) return null; + int x = s.leftMargin + s.iconSize; + // TODO: is the offset relevant in new reskin? + // int y = s.titleOffset; + int y = (s.labelBackgroundHeight - s.titleFontSize) / 2; + drawText(canvas, x, y, title, labelWidth - s.leftMargin - x - + s.titleRightMargin, mTitlePaint); + + // draw count + if (jc.isCancelled()) return null; + x = labelWidth - s.titleRightMargin; + y = (s.labelBackgroundHeight - s.countFontSize) / 2; + drawText(canvas, x, y, count, + labelWidth - x , mCountPaint); + + // draw the icon + if (icon != null) { + if (jc.isCancelled()) return null; + float scale = (float) s.iconSize / icon.getWidth(); + canvas.translate(s.leftMargin, (s.labelBackgroundHeight - + Math.round(scale * icon.getHeight()))/2f); + canvas.scale(scale, scale); + canvas.drawBitmap(icon, 0, 0, null); + } + + return bitmap; + } + } + + public void recycleLabel(Bitmap label) { + GalleryBitmapPool.getInstance().put(label); + } +} diff --git a/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java b/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java new file mode 100644 index 000000000..8149df4b3 --- /dev/null +++ b/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java @@ -0,0 +1,549 @@ +/*T + * 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.ui; + +import android.graphics.Bitmap; +import android.os.Message; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.AbstractGalleryActivity; +import com.android.gallery3d.app.AlbumSetDataLoader; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.DataSourceType; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.glrenderer.BitmapTexture; +import com.android.gallery3d.glrenderer.Texture; +import com.android.gallery3d.glrenderer.TextureUploader; +import com.android.gallery3d.glrenderer.TiledTexture; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.FutureListener; +import com.android.gallery3d.util.ThreadPool; + +public class AlbumSetSlidingWindow implements AlbumSetDataLoader.DataListener { + private static final String TAG = "AlbumSetSlidingWindow"; + private static final int MSG_UPDATE_ALBUM_ENTRY = 1; + + public static interface Listener { + public void onSizeChanged(int size); + public void onContentChanged(); + } + + private final AlbumSetDataLoader mSource; + private int mSize; + + private int mContentStart = 0; + private int mContentEnd = 0; + + private int mActiveStart = 0; + private int mActiveEnd = 0; + + private Listener mListener; + + private final AlbumSetEntry mData[]; + private final SynchronizedHandler mHandler; + private final ThreadPool mThreadPool; + private final AlbumLabelMaker mLabelMaker; + private final String mLoadingText; + + private final TiledTexture.Uploader mContentUploader; + private final TextureUploader mLabelUploader; + + private int mActiveRequestCount = 0; + private boolean mIsActive = false; + private BitmapTexture mLoadingLabel; + + private int mSlotWidth; + + public static class AlbumSetEntry { + public MediaSet album; + public MediaItem coverItem; + public Texture content; + public BitmapTexture labelTexture; + public TiledTexture bitmapTexture; + public Path setPath; + public String title; + public int totalCount; + public int sourceType; + public int cacheFlag; + public int cacheStatus; + public int rotation; + public boolean isWaitLoadingDisplayed; + public long setDataVersion; + public long coverDataVersion; + private BitmapLoader labelLoader; + private BitmapLoader coverLoader; + } + + public AlbumSetSlidingWindow(AbstractGalleryActivity activity, + AlbumSetDataLoader source, AlbumSetSlotRenderer.LabelSpec labelSpec, int cacheSize) { + source.setModelListener(this); + mSource = source; + mData = new AlbumSetEntry[cacheSize]; + mSize = source.size(); + mThreadPool = activity.getThreadPool(); + + mLabelMaker = new AlbumLabelMaker(activity.getAndroidContext(), labelSpec); + mLoadingText = activity.getAndroidContext().getString(R.string.loading); + mContentUploader = new TiledTexture.Uploader(activity.getGLRoot()); + mLabelUploader = new TextureUploader(activity.getGLRoot()); + + mHandler = new SynchronizedHandler(activity.getGLRoot()) { + @Override + public void handleMessage(Message message) { + Utils.assertTrue(message.what == MSG_UPDATE_ALBUM_ENTRY); + ((EntryUpdater) message.obj).updateEntry(); + } + }; + } + + public void setListener(Listener listener) { + mListener = listener; + } + + public AlbumSetEntry get(int slotIndex) { + if (!isActiveSlot(slotIndex)) { + Utils.fail("invalid slot: %s outsides (%s, %s)", + slotIndex, mActiveStart, mActiveEnd); + } + return mData[slotIndex % mData.length]; + } + + public int size() { + return mSize; + } + + public boolean isActiveSlot(int slotIndex) { + return slotIndex >= mActiveStart && slotIndex < mActiveEnd; + } + + private void setContentWindow(int contentStart, int contentEnd) { + if (contentStart == mContentStart && contentEnd == mContentEnd) return; + + if (contentStart >= mContentEnd || mContentStart >= contentEnd) { + for (int i = mContentStart, n = mContentEnd; i < n; ++i) { + freeSlotContent(i); + } + mSource.setActiveWindow(contentStart, contentEnd); + for (int i = contentStart; i < contentEnd; ++i) { + prepareSlotContent(i); + } + } else { + for (int i = mContentStart; i < contentStart; ++i) { + freeSlotContent(i); + } + for (int i = contentEnd, n = mContentEnd; i < n; ++i) { + freeSlotContent(i); + } + mSource.setActiveWindow(contentStart, contentEnd); + for (int i = contentStart, n = mContentStart; i < n; ++i) { + prepareSlotContent(i); + } + for (int i = mContentEnd; i < contentEnd; ++i) { + prepareSlotContent(i); + } + } + + mContentStart = contentStart; + mContentEnd = contentEnd; + } + + public void setActiveWindow(int start, int end) { + if (!(start <= end && end - start <= mData.length && end <= mSize)) { + Utils.fail("start = %s, end = %s, length = %s, size = %s", + start, end, mData.length, mSize); + } + + AlbumSetEntry data[] = mData; + mActiveStart = start; + mActiveEnd = end; + int contentStart = Utils.clamp((start + end) / 2 - data.length / 2, + 0, Math.max(0, mSize - data.length)); + int contentEnd = Math.min(contentStart + data.length, mSize); + setContentWindow(contentStart, contentEnd); + + if (mIsActive) { + updateTextureUploadQueue(); + updateAllImageRequests(); + } + } + + // We would like to request non active slots in the following order: + // Order: 8 6 4 2 1 3 5 7 + // |---------|---------------|---------| + // |<- active ->| + // |<-------- cached range ----------->| + private void requestNonactiveImages() { + int range = Math.max( + mContentEnd - mActiveEnd, mActiveStart - mContentStart); + for (int i = 0 ;i < range; ++i) { + requestImagesInSlot(mActiveEnd + i); + requestImagesInSlot(mActiveStart - 1 - i); + } + } + + private void cancelNonactiveImages() { + int range = Math.max( + mContentEnd - mActiveEnd, mActiveStart - mContentStart); + for (int i = 0 ;i < range; ++i) { + cancelImagesInSlot(mActiveEnd + i); + cancelImagesInSlot(mActiveStart - 1 - i); + } + } + + private void requestImagesInSlot(int slotIndex) { + if (slotIndex < mContentStart || slotIndex >= mContentEnd) return; + AlbumSetEntry entry = mData[slotIndex % mData.length]; + if (entry.coverLoader != null) entry.coverLoader.startLoad(); + if (entry.labelLoader != null) entry.labelLoader.startLoad(); + } + + private void cancelImagesInSlot(int slotIndex) { + if (slotIndex < mContentStart || slotIndex >= mContentEnd) return; + AlbumSetEntry entry = mData[slotIndex % mData.length]; + if (entry.coverLoader != null) entry.coverLoader.cancelLoad(); + if (entry.labelLoader != null) entry.labelLoader.cancelLoad(); + } + + private static long getDataVersion(MediaObject object) { + return object == null + ? MediaSet.INVALID_DATA_VERSION + : object.getDataVersion(); + } + + private void freeSlotContent(int slotIndex) { + AlbumSetEntry entry = mData[slotIndex % mData.length]; + if (entry.coverLoader != null) entry.coverLoader.recycle(); + if (entry.labelLoader != null) entry.labelLoader.recycle(); + if (entry.labelTexture != null) entry.labelTexture.recycle(); + if (entry.bitmapTexture != null) entry.bitmapTexture.recycle(); + mData[slotIndex % mData.length] = null; + } + + private boolean isLabelChanged( + AlbumSetEntry entry, String title, int totalCount, int sourceType) { + return !Utils.equals(entry.title, title) + || entry.totalCount != totalCount + || entry.sourceType != sourceType; + } + + private void updateAlbumSetEntry(AlbumSetEntry entry, int slotIndex) { + MediaSet album = mSource.getMediaSet(slotIndex); + MediaItem cover = mSource.getCoverItem(slotIndex); + int totalCount = mSource.getTotalCount(slotIndex); + + entry.album = album; + entry.setDataVersion = getDataVersion(album); + entry.cacheFlag = identifyCacheFlag(album); + entry.cacheStatus = identifyCacheStatus(album); + entry.setPath = (album == null) ? null : album.getPath(); + + String title = (album == null) ? "" : Utils.ensureNotNull(album.getName()); + int sourceType = DataSourceType.identifySourceType(album); + if (isLabelChanged(entry, title, totalCount, sourceType)) { + entry.title = title; + entry.totalCount = totalCount; + entry.sourceType = sourceType; + if (entry.labelLoader != null) { + entry.labelLoader.recycle(); + entry.labelLoader = null; + entry.labelTexture = null; + } + if (album != null) { + entry.labelLoader = new AlbumLabelLoader( + slotIndex, title, totalCount, sourceType); + } + } + + entry.coverItem = cover; + if (getDataVersion(cover) != entry.coverDataVersion) { + entry.coverDataVersion = getDataVersion(cover); + entry.rotation = (cover == null) ? 0 : cover.getRotation(); + if (entry.coverLoader != null) { + entry.coverLoader.recycle(); + entry.coverLoader = null; + entry.bitmapTexture = null; + entry.content = null; + } + if (cover != null) { + entry.coverLoader = new AlbumCoverLoader(slotIndex, cover); + } + } + } + + private void prepareSlotContent(int slotIndex) { + AlbumSetEntry entry = new AlbumSetEntry(); + updateAlbumSetEntry(entry, slotIndex); + mData[slotIndex % mData.length] = entry; + } + + private static boolean startLoadBitmap(BitmapLoader loader) { + if (loader == null) return false; + loader.startLoad(); + return loader.isRequestInProgress(); + } + + private void uploadBackgroundTextureInSlot(int index) { + if (index < mContentStart || index >= mContentEnd) return; + AlbumSetEntry entry = mData[index % mData.length]; + if (entry.bitmapTexture != null) { + mContentUploader.addTexture(entry.bitmapTexture); + } + if (entry.labelTexture != null) { + mLabelUploader.addBgTexture(entry.labelTexture); + } + } + + private void updateTextureUploadQueue() { + if (!mIsActive) return; + mContentUploader.clear(); + mLabelUploader.clear(); + + // Upload foreground texture + for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) { + AlbumSetEntry entry = mData[i % mData.length]; + if (entry.bitmapTexture != null) { + mContentUploader.addTexture(entry.bitmapTexture); + } + if (entry.labelTexture != null) { + mLabelUploader.addFgTexture(entry.labelTexture); + } + } + + // add background textures + int range = Math.max( + (mContentEnd - mActiveEnd), (mActiveStart - mContentStart)); + for (int i = 0; i < range; ++i) { + uploadBackgroundTextureInSlot(mActiveEnd + i); + uploadBackgroundTextureInSlot(mActiveStart - i - 1); + } + } + + private void updateAllImageRequests() { + mActiveRequestCount = 0; + for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) { + AlbumSetEntry entry = mData[i % mData.length]; + if (startLoadBitmap(entry.coverLoader)) ++mActiveRequestCount; + if (startLoadBitmap(entry.labelLoader)) ++mActiveRequestCount; + } + if (mActiveRequestCount == 0) { + requestNonactiveImages(); + } else { + cancelNonactiveImages(); + } + } + + @Override + public void onSizeChanged(int size) { + if (mIsActive && mSize != size) { + mSize = size; + if (mListener != null) mListener.onSizeChanged(mSize); + if (mContentEnd > mSize) mContentEnd = mSize; + if (mActiveEnd > mSize) mActiveEnd = mSize; + } + } + + @Override + public void onContentChanged(int index) { + if (!mIsActive) { + // paused, ignore slot changed event + return; + } + + // If the updated content is not cached, ignore it + if (index < mContentStart || index >= mContentEnd) { + Log.w(TAG, String.format( + "invalid update: %s is outside (%s, %s)", + index, mContentStart, mContentEnd) ); + return; + } + + AlbumSetEntry entry = mData[index % mData.length]; + updateAlbumSetEntry(entry, index); + updateAllImageRequests(); + updateTextureUploadQueue(); + if (mListener != null && isActiveSlot(index)) { + mListener.onContentChanged(); + } + } + + public BitmapTexture getLoadingTexture() { + if (mLoadingLabel == null) { + Bitmap bitmap = mLabelMaker.requestLabel( + mLoadingText, "", DataSourceType.TYPE_NOT_CATEGORIZED) + .run(ThreadPool.JOB_CONTEXT_STUB); + mLoadingLabel = new BitmapTexture(bitmap); + mLoadingLabel.setOpaque(false); + } + return mLoadingLabel; + } + + public void pause() { + mIsActive = false; + mLabelUploader.clear(); + mContentUploader.clear(); + TiledTexture.freeResources(); + for (int i = mContentStart, n = mContentEnd; i < n; ++i) { + freeSlotContent(i); + } + } + + public void resume() { + mIsActive = true; + TiledTexture.prepareResources(); + for (int i = mContentStart, n = mContentEnd; i < n; ++i) { + prepareSlotContent(i); + } + updateAllImageRequests(); + } + + private static interface EntryUpdater { + public void updateEntry(); + } + + private class AlbumCoverLoader extends BitmapLoader implements EntryUpdater { + private MediaItem mMediaItem; + private final int mSlotIndex; + + public AlbumCoverLoader(int slotIndex, MediaItem item) { + mSlotIndex = slotIndex; + mMediaItem = item; + } + + @Override + protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l) { + return mThreadPool.submit(mMediaItem.requestImage( + MediaItem.TYPE_MICROTHUMBNAIL), l); + } + + @Override + protected void onLoadComplete(Bitmap bitmap) { + mHandler.obtainMessage(MSG_UPDATE_ALBUM_ENTRY, this).sendToTarget(); + } + + @Override + public void updateEntry() { + Bitmap bitmap = getBitmap(); + if (bitmap == null) return; // error or recycled + + AlbumSetEntry entry = mData[mSlotIndex % mData.length]; + TiledTexture texture = new TiledTexture(bitmap); + entry.bitmapTexture = texture; + entry.content = texture; + + if (isActiveSlot(mSlotIndex)) { + mContentUploader.addTexture(texture); + --mActiveRequestCount; + if (mActiveRequestCount == 0) requestNonactiveImages(); + if (mListener != null) mListener.onContentChanged(); + } else { + mContentUploader.addTexture(texture); + } + } + } + + private static int identifyCacheFlag(MediaSet set) { + if (set == null || (set.getSupportedOperations() + & MediaSet.SUPPORT_CACHE) == 0) { + return MediaSet.CACHE_FLAG_NO; + } + + return set.getCacheFlag(); + } + + private static int identifyCacheStatus(MediaSet set) { + if (set == null || (set.getSupportedOperations() + & MediaSet.SUPPORT_CACHE) == 0) { + return MediaSet.CACHE_STATUS_NOT_CACHED; + } + + return set.getCacheStatus(); + } + + private class AlbumLabelLoader extends BitmapLoader implements EntryUpdater { + private final int mSlotIndex; + private final String mTitle; + private final int mTotalCount; + private final int mSourceType; + + public AlbumLabelLoader( + int slotIndex, String title, int totalCount, int sourceType) { + mSlotIndex = slotIndex; + mTitle = title; + mTotalCount = totalCount; + mSourceType = sourceType; + } + + @Override + protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l) { + return mThreadPool.submit(mLabelMaker.requestLabel( + mTitle, String.valueOf(mTotalCount), mSourceType), l); + } + + @Override + protected void onLoadComplete(Bitmap bitmap) { + mHandler.obtainMessage(MSG_UPDATE_ALBUM_ENTRY, this).sendToTarget(); + } + + @Override + public void updateEntry() { + Bitmap bitmap = getBitmap(); + if (bitmap == null) return; // Error or recycled + + AlbumSetEntry entry = mData[mSlotIndex % mData.length]; + BitmapTexture texture = new BitmapTexture(bitmap); + texture.setOpaque(false); + entry.labelTexture = texture; + + if (isActiveSlot(mSlotIndex)) { + mLabelUploader.addFgTexture(texture); + --mActiveRequestCount; + if (mActiveRequestCount == 0) requestNonactiveImages(); + if (mListener != null) mListener.onContentChanged(); + } else { + mLabelUploader.addBgTexture(texture); + } + } + } + + public void onSlotSizeChanged(int width, int height) { + if (mSlotWidth == width) return; + + mSlotWidth = width; + mLoadingLabel = null; + mLabelMaker.setLabelWidth(mSlotWidth); + + if (!mIsActive) return; + + for (int i = mContentStart, n = mContentEnd; i < n; ++i) { + AlbumSetEntry entry = mData[i % mData.length]; + if (entry.labelLoader != null) { + entry.labelLoader.recycle(); + entry.labelLoader = null; + entry.labelTexture = null; + } + if (entry.album != null) { + entry.labelLoader = new AlbumLabelLoader(i, + entry.title, entry.totalCount, entry.sourceType); + } + } + updateAllImageRequests(); + updateTextureUploadQueue(); + } +} diff --git a/src/com/android/gallery3d/ui/AlbumSetSlotRenderer.java b/src/com/android/gallery3d/ui/AlbumSetSlotRenderer.java new file mode 100644 index 000000000..5332ef89a --- /dev/null +++ b/src/com/android/gallery3d/ui/AlbumSetSlotRenderer.java @@ -0,0 +1,242 @@ +/* + * 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.ui; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.AbstractGalleryActivity; +import com.android.gallery3d.app.AlbumSetDataLoader; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.glrenderer.ColorTexture; +import com.android.gallery3d.glrenderer.FadeInTexture; +import com.android.gallery3d.glrenderer.GLCanvas; +import com.android.gallery3d.glrenderer.ResourceTexture; +import com.android.gallery3d.glrenderer.Texture; +import com.android.gallery3d.glrenderer.TiledTexture; +import com.android.gallery3d.glrenderer.UploadedTexture; +import com.android.gallery3d.ui.AlbumSetSlidingWindow.AlbumSetEntry; + +public class AlbumSetSlotRenderer extends AbstractSlotRenderer { + @SuppressWarnings("unused") + private static final String TAG = "AlbumSetView"; + private static final int CACHE_SIZE = 96; + private final int mPlaceholderColor; + + private final ColorTexture mWaitLoadingTexture; + private final ResourceTexture mCameraOverlay; + private final AbstractGalleryActivity mActivity; + private final SelectionManager mSelectionManager; + protected final LabelSpec mLabelSpec; + + protected AlbumSetSlidingWindow mDataWindow; + private SlotView mSlotView; + + private int mPressedIndex = -1; + private boolean mAnimatePressedUp; + private Path mHighlightItemPath = null; + private boolean mInSelectionMode; + + public static class LabelSpec { + public int labelBackgroundHeight; + public int titleOffset; + public int countOffset; + public int titleFontSize; + public int countFontSize; + public int leftMargin; + public int iconSize; + public int titleRightMargin; + public int backgroundColor; + public int titleColor; + public int countColor; + public int borderSize; + } + + public AlbumSetSlotRenderer(AbstractGalleryActivity activity, + SelectionManager selectionManager, + SlotView slotView, LabelSpec labelSpec, int placeholderColor) { + super (activity); + mActivity = activity; + mSelectionManager = selectionManager; + mSlotView = slotView; + mLabelSpec = labelSpec; + mPlaceholderColor = placeholderColor; + + mWaitLoadingTexture = new ColorTexture(mPlaceholderColor); + mWaitLoadingTexture.setSize(1, 1); + mCameraOverlay = new ResourceTexture(activity, + R.drawable.ic_cameraalbum_overlay); + } + + public void setPressedIndex(int index) { + if (mPressedIndex == index) return; + mPressedIndex = index; + mSlotView.invalidate(); + } + + public void setPressedUp() { + if (mPressedIndex == -1) return; + mAnimatePressedUp = true; + mSlotView.invalidate(); + } + + public void setHighlightItemPath(Path path) { + if (mHighlightItemPath == path) return; + mHighlightItemPath = path; + mSlotView.invalidate(); + } + + public void setModel(AlbumSetDataLoader model) { + if (mDataWindow != null) { + mDataWindow.setListener(null); + mDataWindow = null; + mSlotView.setSlotCount(0); + } + if (model != null) { + mDataWindow = new AlbumSetSlidingWindow( + mActivity, model, mLabelSpec, CACHE_SIZE); + mDataWindow.setListener(new MyCacheListener()); + mSlotView.setSlotCount(mDataWindow.size()); + } + } + + private static Texture checkLabelTexture(Texture texture) { + return ((texture instanceof UploadedTexture) + && ((UploadedTexture) texture).isUploading()) + ? null + : texture; + } + + private static Texture checkContentTexture(Texture texture) { + return ((texture instanceof TiledTexture) + && !((TiledTexture) texture).isReady()) + ? null + : texture; + } + + @Override + public int renderSlot(GLCanvas canvas, int index, int pass, int width, int height) { + AlbumSetEntry entry = mDataWindow.get(index); + int renderRequestFlags = 0; + renderRequestFlags |= renderContent(canvas, entry, width, height); + renderRequestFlags |= renderLabel(canvas, entry, width, height); + renderRequestFlags |= renderOverlay(canvas, index, entry, width, height); + return renderRequestFlags; + } + + protected int renderOverlay( + GLCanvas canvas, int index, AlbumSetEntry entry, int width, int height) { + int renderRequestFlags = 0; + if (entry.album != null && entry.album.isCameraRoll()) { + int uncoveredHeight = height - mLabelSpec.labelBackgroundHeight; + int dim = uncoveredHeight / 2; + mCameraOverlay.draw(canvas, (width - dim) / 2, + (uncoveredHeight - dim) / 2, dim, dim); + } + if (mPressedIndex == index) { + if (mAnimatePressedUp) { + drawPressedUpFrame(canvas, width, height); + renderRequestFlags |= SlotView.RENDER_MORE_FRAME; + if (isPressedUpFrameFinished()) { + mAnimatePressedUp = false; + mPressedIndex = -1; + } + } else { + drawPressedFrame(canvas, width, height); + } + } else if ((mHighlightItemPath != null) && (mHighlightItemPath == entry.setPath)) { + drawSelectedFrame(canvas, width, height); + } else if (mInSelectionMode && mSelectionManager.isItemSelected(entry.setPath)) { + drawSelectedFrame(canvas, width, height); + } + return renderRequestFlags; + } + + protected int renderContent( + GLCanvas canvas, AlbumSetEntry entry, int width, int height) { + int renderRequestFlags = 0; + + Texture content = checkContentTexture(entry.content); + if (content == null) { + content = mWaitLoadingTexture; + entry.isWaitLoadingDisplayed = true; + } else if (entry.isWaitLoadingDisplayed) { + entry.isWaitLoadingDisplayed = false; + content = new FadeInTexture(mPlaceholderColor, entry.bitmapTexture); + entry.content = content; + } + drawContent(canvas, content, width, height, entry.rotation); + if ((content instanceof FadeInTexture) && + ((FadeInTexture) content).isAnimating()) { + renderRequestFlags |= SlotView.RENDER_MORE_FRAME; + } + + return renderRequestFlags; + } + + protected int renderLabel( + GLCanvas canvas, AlbumSetEntry entry, int width, int height) { + Texture content = checkLabelTexture(entry.labelTexture); + if (content == null) { + content = mWaitLoadingTexture; + } + int b = AlbumLabelMaker.getBorderSize(); + int h = mLabelSpec.labelBackgroundHeight; + content.draw(canvas, -b, height - h + b, width + b + b, h); + + return 0; + } + + @Override + public void prepareDrawing() { + mInSelectionMode = mSelectionManager.inSelectionMode(); + } + + private class MyCacheListener implements AlbumSetSlidingWindow.Listener { + + @Override + public void onSizeChanged(int size) { + mSlotView.setSlotCount(size); + } + + @Override + public void onContentChanged() { + mSlotView.invalidate(); + } + } + + public void pause() { + mDataWindow.pause(); + } + + public void resume() { + mDataWindow.resume(); + } + + @Override + public void onVisibleRangeChanged(int visibleStart, int visibleEnd) { + if (mDataWindow != null) { + mDataWindow.setActiveWindow(visibleStart, visibleEnd); + } + } + + @Override + public void onSlotSizeChanged(int width, int height) { + if (mDataWindow != null) { + mDataWindow.onSlotSizeChanged(width, height); + } + } +} diff --git a/src/com/android/gallery3d/ui/AlbumSlidingWindow.java b/src/com/android/gallery3d/ui/AlbumSlidingWindow.java new file mode 100644 index 000000000..fec7d1e92 --- /dev/null +++ b/src/com/android/gallery3d/ui/AlbumSlidingWindow.java @@ -0,0 +1,365 @@ +/* + * 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.ui; + +import android.graphics.Bitmap; +import android.os.Message; + +import com.android.gallery3d.app.AbstractGalleryActivity; +import com.android.gallery3d.app.AlbumDataLoader; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.MediaObject.PanoramaSupportCallback; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.glrenderer.Texture; +import com.android.gallery3d.glrenderer.TiledTexture; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.FutureListener; +import com.android.gallery3d.util.JobLimiter; + +public class AlbumSlidingWindow implements AlbumDataLoader.DataListener { + @SuppressWarnings("unused") + private static final String TAG = "AlbumSlidingWindow"; + + private static final int MSG_UPDATE_ENTRY = 0; + private static final int JOB_LIMIT = 2; + + public static interface Listener { + public void onSizeChanged(int size); + public void onContentChanged(); + } + + public static class AlbumEntry { + public MediaItem item; + public Path path; + public boolean isPanorama; + public int rotation; + public int mediaType; + public boolean isWaitDisplayed; + public TiledTexture bitmapTexture; + public Texture content; + private BitmapLoader contentLoader; + private PanoSupportListener mPanoSupportListener; + } + + private final AlbumDataLoader mSource; + private final AlbumEntry mData[]; + private final SynchronizedHandler mHandler; + private final JobLimiter mThreadPool; + private final TiledTexture.Uploader mTileUploader; + + private int mSize; + + private int mContentStart = 0; + private int mContentEnd = 0; + + private int mActiveStart = 0; + private int mActiveEnd = 0; + + private Listener mListener; + + private int mActiveRequestCount = 0; + private boolean mIsActive = false; + + private class PanoSupportListener implements PanoramaSupportCallback { + public final AlbumEntry mEntry; + public PanoSupportListener (AlbumEntry entry) { + mEntry = entry; + } + @Override + public void panoramaInfoAvailable(MediaObject mediaObject, boolean isPanorama, + boolean isPanorama360) { + if (mEntry != null) mEntry.isPanorama = isPanorama; + } + } + + public AlbumSlidingWindow(AbstractGalleryActivity activity, + AlbumDataLoader source, int cacheSize) { + source.setDataListener(this); + mSource = source; + mData = new AlbumEntry[cacheSize]; + mSize = source.size(); + + mHandler = new SynchronizedHandler(activity.getGLRoot()) { + @Override + public void handleMessage(Message message) { + Utils.assertTrue(message.what == MSG_UPDATE_ENTRY); + ((ThumbnailLoader) message.obj).updateEntry(); + } + }; + + mThreadPool = new JobLimiter(activity.getThreadPool(), JOB_LIMIT); + mTileUploader = new TiledTexture.Uploader(activity.getGLRoot()); + } + + public void setListener(Listener listener) { + mListener = listener; + } + + public AlbumEntry get(int slotIndex) { + if (!isActiveSlot(slotIndex)) { + Utils.fail("invalid slot: %s outsides (%s, %s)", + slotIndex, mActiveStart, mActiveEnd); + } + return mData[slotIndex % mData.length]; + } + + public boolean isActiveSlot(int slotIndex) { + return slotIndex >= mActiveStart && slotIndex < mActiveEnd; + } + + private void setContentWindow(int contentStart, int contentEnd) { + if (contentStart == mContentStart && contentEnd == mContentEnd) return; + + if (!mIsActive) { + mContentStart = contentStart; + mContentEnd = contentEnd; + mSource.setActiveWindow(contentStart, contentEnd); + return; + } + + if (contentStart >= mContentEnd || mContentStart >= contentEnd) { + for (int i = mContentStart, n = mContentEnd; i < n; ++i) { + freeSlotContent(i); + } + mSource.setActiveWindow(contentStart, contentEnd); + for (int i = contentStart; i < contentEnd; ++i) { + prepareSlotContent(i); + } + } else { + for (int i = mContentStart; i < contentStart; ++i) { + freeSlotContent(i); + } + for (int i = contentEnd, n = mContentEnd; i < n; ++i) { + freeSlotContent(i); + } + mSource.setActiveWindow(contentStart, contentEnd); + for (int i = contentStart, n = mContentStart; i < n; ++i) { + prepareSlotContent(i); + } + for (int i = mContentEnd; i < contentEnd; ++i) { + prepareSlotContent(i); + } + } + + mContentStart = contentStart; + mContentEnd = contentEnd; + } + + public void setActiveWindow(int start, int end) { + if (!(start <= end && end - start <= mData.length && end <= mSize)) { + Utils.fail("%s, %s, %s, %s", start, end, mData.length, mSize); + } + AlbumEntry data[] = mData; + + mActiveStart = start; + mActiveEnd = end; + + int contentStart = Utils.clamp((start + end) / 2 - data.length / 2, + 0, Math.max(0, mSize - data.length)); + int contentEnd = Math.min(contentStart + data.length, mSize); + setContentWindow(contentStart, contentEnd); + updateTextureUploadQueue(); + if (mIsActive) updateAllImageRequests(); + } + + private void uploadBgTextureInSlot(int index) { + if (index < mContentEnd && index >= mContentStart) { + AlbumEntry entry = mData[index % mData.length]; + if (entry.bitmapTexture != null) { + mTileUploader.addTexture(entry.bitmapTexture); + } + } + } + + private void updateTextureUploadQueue() { + if (!mIsActive) return; + mTileUploader.clear(); + + // add foreground textures + for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) { + AlbumEntry entry = mData[i % mData.length]; + if (entry.bitmapTexture != null) { + mTileUploader.addTexture(entry.bitmapTexture); + } + } + + // add background textures + int range = Math.max( + (mContentEnd - mActiveEnd), (mActiveStart - mContentStart)); + for (int i = 0; i < range; ++i) { + uploadBgTextureInSlot(mActiveEnd + i); + uploadBgTextureInSlot(mActiveStart - i - 1); + } + } + + // We would like to request non active slots in the following order: + // Order: 8 6 4 2 1 3 5 7 + // |---------|---------------|---------| + // |<- active ->| + // |<-------- cached range ----------->| + private void requestNonactiveImages() { + int range = Math.max( + (mContentEnd - mActiveEnd), (mActiveStart - mContentStart)); + for (int i = 0 ;i < range; ++i) { + requestSlotImage(mActiveEnd + i); + requestSlotImage(mActiveStart - 1 - i); + } + } + + // return whether the request is in progress or not + private boolean requestSlotImage(int slotIndex) { + if (slotIndex < mContentStart || slotIndex >= mContentEnd) return false; + AlbumEntry entry = mData[slotIndex % mData.length]; + if (entry.content != null || entry.item == null) return false; + + // Set up the panorama callback + entry.mPanoSupportListener = new PanoSupportListener(entry); + entry.item.getPanoramaSupport(entry.mPanoSupportListener); + + entry.contentLoader.startLoad(); + return entry.contentLoader.isRequestInProgress(); + } + + private void cancelNonactiveImages() { + int range = Math.max( + (mContentEnd - mActiveEnd), (mActiveStart - mContentStart)); + for (int i = 0 ;i < range; ++i) { + cancelSlotImage(mActiveEnd + i); + cancelSlotImage(mActiveStart - 1 - i); + } + } + + private void cancelSlotImage(int slotIndex) { + if (slotIndex < mContentStart || slotIndex >= mContentEnd) return; + AlbumEntry item = mData[slotIndex % mData.length]; + if (item.contentLoader != null) item.contentLoader.cancelLoad(); + } + + private void freeSlotContent(int slotIndex) { + AlbumEntry data[] = mData; + int index = slotIndex % data.length; + AlbumEntry entry = data[index]; + if (entry.contentLoader != null) entry.contentLoader.recycle(); + if (entry.bitmapTexture != null) entry.bitmapTexture.recycle(); + data[index] = null; + } + + private void prepareSlotContent(int slotIndex) { + AlbumEntry entry = new AlbumEntry(); + MediaItem item = mSource.get(slotIndex); // item could be null; + entry.item = item; + entry.mediaType = (item == null) + ? MediaItem.MEDIA_TYPE_UNKNOWN + : entry.item.getMediaType(); + entry.path = (item == null) ? null : item.getPath(); + entry.rotation = (item == null) ? 0 : item.getRotation(); + entry.contentLoader = new ThumbnailLoader(slotIndex, entry.item); + mData[slotIndex % mData.length] = entry; + } + + private void updateAllImageRequests() { + mActiveRequestCount = 0; + for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) { + if (requestSlotImage(i)) ++mActiveRequestCount; + } + if (mActiveRequestCount == 0) { + requestNonactiveImages(); + } else { + cancelNonactiveImages(); + } + } + + private class ThumbnailLoader extends BitmapLoader { + private final int mSlotIndex; + private final MediaItem mItem; + + public ThumbnailLoader(int slotIndex, MediaItem item) { + mSlotIndex = slotIndex; + mItem = item; + } + + @Override + protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l) { + return mThreadPool.submit( + mItem.requestImage(MediaItem.TYPE_MICROTHUMBNAIL), this); + } + + @Override + protected void onLoadComplete(Bitmap bitmap) { + mHandler.obtainMessage(MSG_UPDATE_ENTRY, this).sendToTarget(); + } + + public void updateEntry() { + Bitmap bitmap = getBitmap(); + if (bitmap == null) return; // error or recycled + AlbumEntry entry = mData[mSlotIndex % mData.length]; + entry.bitmapTexture = new TiledTexture(bitmap); + entry.content = entry.bitmapTexture; + + if (isActiveSlot(mSlotIndex)) { + mTileUploader.addTexture(entry.bitmapTexture); + --mActiveRequestCount; + if (mActiveRequestCount == 0) requestNonactiveImages(); + if (mListener != null) mListener.onContentChanged(); + } else { + mTileUploader.addTexture(entry.bitmapTexture); + } + } + } + + @Override + public void onSizeChanged(int size) { + if (mSize != size) { + mSize = size; + if (mListener != null) mListener.onSizeChanged(mSize); + if (mContentEnd > mSize) mContentEnd = mSize; + if (mActiveEnd > mSize) mActiveEnd = mSize; + } + } + + @Override + public void onContentChanged(int index) { + if (index >= mContentStart && index < mContentEnd && mIsActive) { + freeSlotContent(index); + prepareSlotContent(index); + updateAllImageRequests(); + if (mListener != null && isActiveSlot(index)) { + mListener.onContentChanged(); + } + } + } + + public void resume() { + mIsActive = true; + TiledTexture.prepareResources(); + for (int i = mContentStart, n = mContentEnd; i < n; ++i) { + prepareSlotContent(i); + } + updateAllImageRequests(); + } + + public void pause() { + mIsActive = false; + mTileUploader.clear(); + TiledTexture.freeResources(); + for (int i = mContentStart, n = mContentEnd; i < n; ++i) { + freeSlotContent(i); + } + } +} diff --git a/src/com/android/gallery3d/ui/AlbumSlotRenderer.java b/src/com/android/gallery3d/ui/AlbumSlotRenderer.java new file mode 100644 index 000000000..dc6c89b0e --- /dev/null +++ b/src/com/android/gallery3d/ui/AlbumSlotRenderer.java @@ -0,0 +1,201 @@ +/* + * 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.ui; + +import com.android.gallery3d.app.AbstractGalleryActivity; +import com.android.gallery3d.app.AlbumDataLoader; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.glrenderer.ColorTexture; +import com.android.gallery3d.glrenderer.FadeInTexture; +import com.android.gallery3d.glrenderer.GLCanvas; +import com.android.gallery3d.glrenderer.Texture; +import com.android.gallery3d.glrenderer.TiledTexture; + +public class AlbumSlotRenderer extends AbstractSlotRenderer { + @SuppressWarnings("unused") + private static final String TAG = "AlbumView"; + + public interface SlotFilter { + public boolean acceptSlot(int index); + } + + private final int mPlaceholderColor; + private static final int CACHE_SIZE = 96; + + private AlbumSlidingWindow mDataWindow; + private final AbstractGalleryActivity mActivity; + private final ColorTexture mWaitLoadingTexture; + private final SlotView mSlotView; + private final SelectionManager mSelectionManager; + + private int mPressedIndex = -1; + private boolean mAnimatePressedUp; + private Path mHighlightItemPath = null; + private boolean mInSelectionMode; + + private SlotFilter mSlotFilter; + + public AlbumSlotRenderer(AbstractGalleryActivity activity, SlotView slotView, + SelectionManager selectionManager, int placeholderColor) { + super(activity); + mActivity = activity; + mSlotView = slotView; + mSelectionManager = selectionManager; + mPlaceholderColor = placeholderColor; + + mWaitLoadingTexture = new ColorTexture(mPlaceholderColor); + mWaitLoadingTexture.setSize(1, 1); + } + + public void setPressedIndex(int index) { + if (mPressedIndex == index) return; + mPressedIndex = index; + mSlotView.invalidate(); + } + + public void setPressedUp() { + if (mPressedIndex == -1) return; + mAnimatePressedUp = true; + mSlotView.invalidate(); + } + + public void setHighlightItemPath(Path path) { + if (mHighlightItemPath == path) return; + mHighlightItemPath = path; + mSlotView.invalidate(); + } + + public void setModel(AlbumDataLoader model) { + if (mDataWindow != null) { + mDataWindow.setListener(null); + mSlotView.setSlotCount(0); + mDataWindow = null; + } + if (model != null) { + mDataWindow = new AlbumSlidingWindow(mActivity, model, CACHE_SIZE); + mDataWindow.setListener(new MyDataModelListener()); + mSlotView.setSlotCount(model.size()); + } + } + + private static Texture checkTexture(Texture texture) { + return (texture instanceof TiledTexture) + && !((TiledTexture) texture).isReady() + ? null + : texture; + } + + @Override + public int renderSlot(GLCanvas canvas, int index, int pass, int width, int height) { + if (mSlotFilter != null && !mSlotFilter.acceptSlot(index)) return 0; + + AlbumSlidingWindow.AlbumEntry entry = mDataWindow.get(index); + + int renderRequestFlags = 0; + + Texture content = checkTexture(entry.content); + if (content == null) { + content = mWaitLoadingTexture; + entry.isWaitDisplayed = true; + } else if (entry.isWaitDisplayed) { + entry.isWaitDisplayed = false; + content = new FadeInTexture(mPlaceholderColor, entry.bitmapTexture); + entry.content = content; + } + drawContent(canvas, content, width, height, entry.rotation); + if ((content instanceof FadeInTexture) && + ((FadeInTexture) content).isAnimating()) { + renderRequestFlags |= SlotView.RENDER_MORE_FRAME; + } + + if (entry.mediaType == MediaObject.MEDIA_TYPE_VIDEO) { + drawVideoOverlay(canvas, width, height); + } + + if (entry.isPanorama) { + drawPanoramaIcon(canvas, width, height); + } + + renderRequestFlags |= renderOverlay(canvas, index, entry, width, height); + + return renderRequestFlags; + } + + private int renderOverlay(GLCanvas canvas, int index, + AlbumSlidingWindow.AlbumEntry entry, int width, int height) { + int renderRequestFlags = 0; + if (mPressedIndex == index) { + if (mAnimatePressedUp) { + drawPressedUpFrame(canvas, width, height); + renderRequestFlags |= SlotView.RENDER_MORE_FRAME; + if (isPressedUpFrameFinished()) { + mAnimatePressedUp = false; + mPressedIndex = -1; + } + } else { + drawPressedFrame(canvas, width, height); + } + } else if ((entry.path != null) && (mHighlightItemPath == entry.path)) { + drawSelectedFrame(canvas, width, height); + } else if (mInSelectionMode && mSelectionManager.isItemSelected(entry.path)) { + drawSelectedFrame(canvas, width, height); + } + return renderRequestFlags; + } + + private class MyDataModelListener implements AlbumSlidingWindow.Listener { + @Override + public void onContentChanged() { + mSlotView.invalidate(); + } + + @Override + public void onSizeChanged(int size) { + mSlotView.setSlotCount(size); + } + } + + public void resume() { + mDataWindow.resume(); + } + + public void pause() { + mDataWindow.pause(); + } + + @Override + public void prepareDrawing() { + mInSelectionMode = mSelectionManager.inSelectionMode(); + } + + @Override + public void onVisibleRangeChanged(int visibleStart, int visibleEnd) { + if (mDataWindow != null) { + mDataWindow.setActiveWindow(visibleStart, visibleEnd); + } + } + + @Override + public void onSlotSizeChanged(int width, int height) { + // Do nothing + } + + public void setSlotFilter(SlotFilter slotFilter) { + mSlotFilter = slotFilter; + } +} diff --git a/src/com/android/gallery3d/ui/AnimationTime.java b/src/com/android/gallery3d/ui/AnimationTime.java new file mode 100644 index 000000000..063677423 --- /dev/null +++ b/src/com/android/gallery3d/ui/AnimationTime.java @@ -0,0 +1,45 @@ + +/* + * Copyright (C) 2012 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.ui; + +import android.os.SystemClock; + +// +// The animation time should ideally be the vsync time the frame will be +// displayed, but that is an unknown time in the future. So we use the system +// time just after eglSwapBuffers (when GLSurfaceView.onDrawFrame is called) +// as a approximation. +// +public class AnimationTime { + private static volatile long sTime; + + // Sets current time as the animation time. + public static void update() { + sTime = SystemClock.uptimeMillis(); + } + + // Returns the animation time. + public static long get() { + return sTime; + } + + public static long startTime() { + sTime = SystemClock.uptimeMillis(); + return sTime; + } +} diff --git a/src/com/android/gallery3d/ui/BitmapLoader.java b/src/com/android/gallery3d/ui/BitmapLoader.java new file mode 100644 index 000000000..a708a90f3 --- /dev/null +++ b/src/com/android/gallery3d/ui/BitmapLoader.java @@ -0,0 +1,108 @@ +/* + * 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.ui; + +import android.graphics.Bitmap; + +import com.android.photos.data.GalleryBitmapPool; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.FutureListener; + +// We use this class to +// 1.) load bitmaps in background. +// 2.) as a place holder for the loaded bitmap +public abstract class BitmapLoader implements FutureListener<Bitmap> { + @SuppressWarnings("unused") + private static final String TAG = "BitmapLoader"; + + /* Transition Map: + * INIT -> REQUESTED, RECYCLED + * REQUESTED -> INIT (cancel), LOADED, ERROR, RECYCLED + * LOADED, ERROR -> RECYCLED + */ + private static final int STATE_INIT = 0; + private static final int STATE_REQUESTED = 1; + private static final int STATE_LOADED = 2; + private static final int STATE_ERROR = 3; + private static final int STATE_RECYCLED = 4; + + private int mState = STATE_INIT; + // mTask is not null only when a task is on the way + private Future<Bitmap> mTask; + private Bitmap mBitmap; + + @Override + public void onFutureDone(Future<Bitmap> future) { + synchronized (this) { + mTask = null; + mBitmap = future.get(); + if (mState == STATE_RECYCLED) { + if (mBitmap != null) { + GalleryBitmapPool.getInstance().put(mBitmap); + mBitmap = null; + } + return; // don't call callback + } + if (future.isCancelled() && mBitmap == null) { + if (mState == STATE_REQUESTED) mTask = submitBitmapTask(this); + return; // don't call callback + } else { + mState = mBitmap == null ? STATE_ERROR : STATE_LOADED; + } + } + onLoadComplete(mBitmap); + } + + public synchronized void startLoad() { + if (mState == STATE_INIT) { + mState = STATE_REQUESTED; + if (mTask == null) mTask = submitBitmapTask(this); + } + } + + public synchronized void cancelLoad() { + if (mState == STATE_REQUESTED) { + mState = STATE_INIT; + if (mTask != null) mTask.cancel(); + } + } + + // Recycle the loader and the bitmap + public synchronized void recycle() { + mState = STATE_RECYCLED; + if (mBitmap != null) { + GalleryBitmapPool.getInstance().put(mBitmap); + mBitmap = null; + } + if (mTask != null) mTask.cancel(); + } + + public synchronized boolean isRequestInProgress() { + return mState == STATE_REQUESTED; + } + + public synchronized boolean isRecycled() { + return mState == STATE_RECYCLED; + } + + public synchronized Bitmap getBitmap() { + return mBitmap; + } + + abstract protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l); + abstract protected void onLoadComplete(Bitmap bitmap); +} diff --git a/src/com/android/gallery3d/ui/BitmapScreenNail.java b/src/com/android/gallery3d/ui/BitmapScreenNail.java new file mode 100644 index 000000000..a3d403946 --- /dev/null +++ b/src/com/android/gallery3d/ui/BitmapScreenNail.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2012 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.ui; + +import android.graphics.Bitmap; +import android.graphics.RectF; + +import com.android.gallery3d.glrenderer.BitmapTexture; +import com.android.gallery3d.glrenderer.GLCanvas; + +public class BitmapScreenNail implements ScreenNail { + private final BitmapTexture mBitmapTexture; + + public BitmapScreenNail(Bitmap bitmap) { + mBitmapTexture = new BitmapTexture(bitmap); + } + + @Override + public int getWidth() { + return mBitmapTexture.getWidth(); + } + + @Override + public int getHeight() { + return mBitmapTexture.getHeight(); + } + + @Override + public void draw(GLCanvas canvas, int x, int y, int width, int height) { + mBitmapTexture.draw(canvas, x, y, width, height); + } + + @Override + public void noDraw() { + // do nothing + } + + @Override + public void recycle() { + mBitmapTexture.recycle(); + } + + @Override + public void draw(GLCanvas canvas, RectF source, RectF dest) { + canvas.drawTexture(mBitmapTexture, source, dest); + } +} diff --git a/src/com/android/gallery3d/ui/BitmapTileProvider.java b/src/com/android/gallery3d/ui/BitmapTileProvider.java new file mode 100644 index 000000000..e1a8b7644 --- /dev/null +++ b/src/com/android/gallery3d/ui/BitmapTileProvider.java @@ -0,0 +1,103 @@ +/* + * 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.ui; + +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.Canvas; + +import com.android.gallery3d.common.BitmapUtils; +import com.android.photos.data.GalleryBitmapPool; + +import java.util.ArrayList; + +public class BitmapTileProvider implements TileImageView.TileSource { + private final ScreenNail mScreenNail; + private final Bitmap[] mMipmaps; + private final Config mConfig; + private final int mImageWidth; + private final int mImageHeight; + + private boolean mRecycled = false; + + public BitmapTileProvider(Bitmap bitmap, int maxBackupSize) { + mImageWidth = bitmap.getWidth(); + mImageHeight = bitmap.getHeight(); + ArrayList<Bitmap> list = new ArrayList<Bitmap>(); + list.add(bitmap); + while (bitmap.getWidth() > maxBackupSize + || bitmap.getHeight() > maxBackupSize) { + bitmap = BitmapUtils.resizeBitmapByScale(bitmap, 0.5f, false); + list.add(bitmap); + } + + mScreenNail = new BitmapScreenNail(list.remove(list.size() - 1)); + mMipmaps = list.toArray(new Bitmap[list.size()]); + mConfig = Config.ARGB_8888; + } + + @Override + public ScreenNail getScreenNail() { + return mScreenNail; + } + + @Override + public int getImageHeight() { + return mImageHeight; + } + + @Override + public int getImageWidth() { + return mImageWidth; + } + + @Override + public int getLevelCount() { + return mMipmaps.length; + } + + @Override + public Bitmap getTile(int level, int x, int y, int tileSize) { + x >>= level; + y >>= level; + + Bitmap result = GalleryBitmapPool.getInstance().get(tileSize, tileSize); + if (result == null) { + result = Bitmap.createBitmap(tileSize, tileSize, mConfig); + } else { + result.eraseColor(0); + } + + Bitmap mipmap = mMipmaps[level]; + Canvas canvas = new Canvas(result); + int offsetX = -x; + int offsetY = -y; + canvas.drawBitmap(mipmap, offsetX, offsetY, null); + return result; + } + + public void recycle() { + if (mRecycled) return; + mRecycled = true; + for (Bitmap bitmap : mMipmaps) { + BitmapUtils.recycleSilently(bitmap); + } + if (mScreenNail != null) { + mScreenNail.recycle(); + } + } +} diff --git a/src/com/android/gallery3d/ui/CacheStorageUsageInfo.java b/src/com/android/gallery3d/ui/CacheStorageUsageInfo.java new file mode 100644 index 000000000..46f7a2433 --- /dev/null +++ b/src/com/android/gallery3d/ui/CacheStorageUsageInfo.java @@ -0,0 +1,90 @@ +/* + * 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.ui; + +import android.content.Context; +import android.os.StatFs; + +import com.android.gallery3d.app.AbstractGalleryActivity; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import java.io.File; + +public class CacheStorageUsageInfo { + @SuppressWarnings("unused") + private static final String TAG = "CacheStorageUsageInfo"; + + // number of bytes the storage has. + private long mTotalBytes; + + // number of bytes already used. + private long mUsedBytes; + + // number of bytes used for the cache (should be less then usedBytes). + private long mUsedCacheBytes; + + // number of bytes used for the cache if all pending downloads (and removals) are completed. + private long mTargetCacheBytes; + + private AbstractGalleryActivity mActivity; + private Context mContext; + private long mUserChangeDelta; + + public CacheStorageUsageInfo(AbstractGalleryActivity activity) { + mActivity = activity; + mContext = activity.getAndroidContext(); + } + + public void increaseTargetCacheSize(long delta) { + mUserChangeDelta += delta; + } + + public void loadStorageInfo(JobContext jc) { + File cacheDir = mContext.getExternalCacheDir(); + if (cacheDir == null) { + cacheDir = mContext.getCacheDir(); + } + + String path = cacheDir.getAbsolutePath(); + StatFs stat = new StatFs(path); + long blockSize = stat.getBlockSize(); + long availableBlocks = stat.getAvailableBlocks(); + long totalBlocks = stat.getBlockCount(); + + mTotalBytes = blockSize * totalBlocks; + mUsedBytes = blockSize * (totalBlocks - availableBlocks); + mUsedCacheBytes = mActivity.getDataManager().getTotalUsedCacheSize(); + mTargetCacheBytes = mActivity.getDataManager().getTotalTargetCacheSize(); + } + + public long getTotalBytes() { + return mTotalBytes; + } + + public long getExpectedUsedBytes() { + return mUsedBytes - mUsedCacheBytes + mTargetCacheBytes + mUserChangeDelta; + } + + public long getUsedBytes() { + // Should it be usedBytes - usedCacheBytes + targetCacheBytes ? + return mUsedBytes; + } + + public long getFreeBytes() { + return mTotalBytes - mUsedBytes; + } +} diff --git a/src/com/android/gallery3d/ui/CaptureAnimation.java b/src/com/android/gallery3d/ui/CaptureAnimation.java new file mode 100644 index 000000000..87c054ab3 --- /dev/null +++ b/src/com/android/gallery3d/ui/CaptureAnimation.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2012 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.ui; + +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; + +public class CaptureAnimation { + // The amount of change for zooming out. + private static final float ZOOM_DELTA = 0.2f; + // Pre-calculated value for convenience. + private static final float ZOOM_IN_BEGIN = 1f - ZOOM_DELTA; + + private static final Interpolator sZoomOutInterpolator = + new DecelerateInterpolator(); + private static final Interpolator sZoomInInterpolator = + new AccelerateInterpolator(); + private static final Interpolator sSlideInterpolator = + new AccelerateDecelerateInterpolator(); + + // Calculate the slide factor based on the give time fraction. + public static float calculateSlide(float fraction) { + return sSlideInterpolator.getInterpolation(fraction); + } + + // Calculate the scale factor based on the given time fraction. + public static float calculateScale(float fraction) { + float value; + if (fraction <= 0.5f) { + // Zoom in for the beginning. + value = 1f - ZOOM_DELTA * + sZoomOutInterpolator.getInterpolation(fraction * 2); + } else { + // Zoom out for the ending. + value = ZOOM_IN_BEGIN + ZOOM_DELTA * + sZoomInInterpolator.getInterpolation((fraction - 0.5f) * 2f); + } + return value; + } +} diff --git a/src/com/android/gallery3d/ui/DetailsAddressResolver.java b/src/com/android/gallery3d/ui/DetailsAddressResolver.java new file mode 100644 index 000000000..8de667745 --- /dev/null +++ b/src/com/android/gallery3d/ui/DetailsAddressResolver.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2011 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.ui; + +import android.content.Context; +import android.location.Address; +import android.os.Handler; +import android.os.Looper; + +import com.android.gallery3d.app.AbstractGalleryActivity; +import com.android.gallery3d.data.MediaDetails; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.FutureListener; +import com.android.gallery3d.util.GalleryUtils; +import com.android.gallery3d.util.ReverseGeocoder; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +public class DetailsAddressResolver { + private AddressResolvingListener mListener; + private final AbstractGalleryActivity mContext; + private Future<Address> mAddressLookupJob; + private final Handler mHandler; + + private class AddressLookupJob implements Job<Address> { + private double[] mLatlng; + + protected AddressLookupJob(double[] latlng) { + mLatlng = latlng; + } + + @Override + public Address run(JobContext jc) { + ReverseGeocoder geocoder = new ReverseGeocoder(mContext.getAndroidContext()); + return geocoder.lookupAddress(mLatlng[0], mLatlng[1], true); + } + } + + public interface AddressResolvingListener { + public void onAddressAvailable(String address); + } + + public DetailsAddressResolver(AbstractGalleryActivity context) { + mContext = context; + mHandler = new Handler(Looper.getMainLooper()); + } + + public String resolveAddress(double[] latlng, AddressResolvingListener listener) { + mListener = listener; + mAddressLookupJob = mContext.getThreadPool().submit( + new AddressLookupJob(latlng), + new FutureListener<Address>() { + @Override + public void onFutureDone(final Future<Address> future) { + mAddressLookupJob = null; + if (!future.isCancelled()) { + mHandler.post(new Runnable() { + @Override + public void run() { + updateLocation(future.get()); + } + }); + } + } + }); + return GalleryUtils.formatLatitudeLongitude("(%f,%f)", latlng[0], latlng[1]); + } + + private void updateLocation(Address address) { + if (address != null) { + Context context = mContext.getAndroidContext(); + String parts[] = { + address.getAdminArea(), + address.getSubAdminArea(), + address.getLocality(), + address.getSubLocality(), + address.getThoroughfare(), + address.getSubThoroughfare(), + address.getPremises(), + address.getPostalCode(), + address.getCountryName() + }; + + String addressText = ""; + for (int i = 0; i < parts.length; i++) { + if (parts[i] == null || parts[i].isEmpty()) continue; + if (!addressText.isEmpty()) { + addressText += ", "; + } + addressText += parts[i]; + } + String text = String.format("%s : %s", DetailsHelper.getDetailsName( + context, MediaDetails.INDEX_LOCATION), addressText); + mListener.onAddressAvailable(text); + } + } + + public void cancel() { + if (mAddressLookupJob != null) { + mAddressLookupJob.cancel(); + mAddressLookupJob = null; + } + } +} diff --git a/src/com/android/gallery3d/ui/DetailsHelper.java b/src/com/android/gallery3d/ui/DetailsHelper.java new file mode 100644 index 000000000..47296f655 --- /dev/null +++ b/src/com/android/gallery3d/ui/DetailsHelper.java @@ -0,0 +1,148 @@ +/* + * 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.ui; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.view.View.MeasureSpec; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.AbstractGalleryActivity; +import com.android.gallery3d.data.MediaDetails; +import com.android.gallery3d.ui.DetailsAddressResolver.AddressResolvingListener; + +public class DetailsHelper { + private static DetailsAddressResolver sAddressResolver; + private DetailsViewContainer mContainer; + + public interface DetailsSource { + public int size(); + public int setIndex(); + public MediaDetails getDetails(); + } + + public interface CloseListener { + public void onClose(); + } + + public interface DetailsViewContainer { + public void reloadDetails(); + public void setCloseListener(CloseListener listener); + public void show(); + public void hide(); + } + + public interface ResolutionResolvingListener { + public void onResolutionAvailable(int width, int height); + } + + public DetailsHelper(AbstractGalleryActivity activity, GLView rootPane, DetailsSource source) { + mContainer = new DialogDetailsView(activity, source); + } + + public void layout(int left, int top, int right, int bottom) { + if (mContainer instanceof GLView) { + GLView view = (GLView) mContainer; + view.measure(MeasureSpec.UNSPECIFIED, + MeasureSpec.makeMeasureSpec(bottom - top, MeasureSpec.AT_MOST)); + view.layout(0, top, view.getMeasuredWidth(), top + view.getMeasuredHeight()); + } + } + + public void reloadDetails() { + mContainer.reloadDetails(); + } + + public void setCloseListener(CloseListener listener) { + mContainer.setCloseListener(listener); + } + + public static String resolveAddress(AbstractGalleryActivity activity, double[] latlng, + AddressResolvingListener listener) { + if (sAddressResolver == null) { + sAddressResolver = new DetailsAddressResolver(activity); + } else { + sAddressResolver.cancel(); + } + return sAddressResolver.resolveAddress(latlng, listener); + } + + public static void resolveResolution(String path, ResolutionResolvingListener listener) { + Bitmap bitmap = BitmapFactory.decodeFile(path); + if (bitmap == null) return; + listener.onResolutionAvailable(bitmap.getWidth(), bitmap.getHeight()); + } + + public static void pause() { + if (sAddressResolver != null) sAddressResolver.cancel(); + } + + public void show() { + mContainer.show(); + } + + public void hide() { + mContainer.hide(); + } + + public static String getDetailsName(Context context, int key) { + switch (key) { + case MediaDetails.INDEX_TITLE: + return context.getString(R.string.title); + case MediaDetails.INDEX_DESCRIPTION: + return context.getString(R.string.description); + case MediaDetails.INDEX_DATETIME: + return context.getString(R.string.time); + case MediaDetails.INDEX_LOCATION: + return context.getString(R.string.location); + case MediaDetails.INDEX_PATH: + return context.getString(R.string.path); + case MediaDetails.INDEX_WIDTH: + return context.getString(R.string.width); + case MediaDetails.INDEX_HEIGHT: + return context.getString(R.string.height); + case MediaDetails.INDEX_ORIENTATION: + return context.getString(R.string.orientation); + case MediaDetails.INDEX_DURATION: + return context.getString(R.string.duration); + case MediaDetails.INDEX_MIMETYPE: + return context.getString(R.string.mimetype); + case MediaDetails.INDEX_SIZE: + return context.getString(R.string.file_size); + case MediaDetails.INDEX_MAKE: + return context.getString(R.string.maker); + case MediaDetails.INDEX_MODEL: + return context.getString(R.string.model); + case MediaDetails.INDEX_FLASH: + return context.getString(R.string.flash); + case MediaDetails.INDEX_APERTURE: + return context.getString(R.string.aperture); + case MediaDetails.INDEX_FOCAL_LENGTH: + return context.getString(R.string.focal_length); + case MediaDetails.INDEX_WHITE_BALANCE: + return context.getString(R.string.white_balance); + case MediaDetails.INDEX_EXPOSURE_TIME: + return context.getString(R.string.exposure_time); + case MediaDetails.INDEX_ISO: + return context.getString(R.string.iso); + default: + return "Unknown key" + key; + } + } +} + + diff --git a/src/com/android/gallery3d/ui/DialogDetailsView.java b/src/com/android/gallery3d/ui/DialogDetailsView.java new file mode 100644 index 000000000..058c03654 --- /dev/null +++ b/src/com/android/gallery3d/ui/DialogDetailsView.java @@ -0,0 +1,288 @@ +/* + * Copyright (C) 2011 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.ui; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnDismissListener; +import android.text.format.Formatter; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ListView; +import android.widget.TextView; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.AbstractGalleryActivity; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.MediaDetails; +import com.android.gallery3d.ui.DetailsAddressResolver.AddressResolvingListener; +import com.android.gallery3d.ui.DetailsHelper.CloseListener; +import com.android.gallery3d.ui.DetailsHelper.DetailsSource; +import com.android.gallery3d.ui.DetailsHelper.DetailsViewContainer; +import com.android.gallery3d.ui.DetailsHelper.ResolutionResolvingListener; + +import java.util.ArrayList; +import java.util.Map.Entry; + +public class DialogDetailsView implements DetailsViewContainer { + @SuppressWarnings("unused") + private static final String TAG = "DialogDetailsView"; + + private final AbstractGalleryActivity mActivity; + private DetailsAdapter mAdapter; + private MediaDetails mDetails; + private final DetailsSource mSource; + private int mIndex; + private Dialog mDialog; + private CloseListener mListener; + + public DialogDetailsView(AbstractGalleryActivity activity, DetailsSource source) { + mActivity = activity; + mSource = source; + } + + @Override + public void show() { + reloadDetails(); + mDialog.show(); + } + + @Override + public void hide() { + mDialog.hide(); + } + + @Override + public void reloadDetails() { + int index = mSource.setIndex(); + if (index == -1) return; + MediaDetails details = mSource.getDetails(); + if (details != null) { + if (mIndex == index && mDetails == details) return; + mIndex = index; + mDetails = details; + setDetails(details); + } + } + + private void setDetails(MediaDetails details) { + mAdapter = new DetailsAdapter(details); + String title = String.format( + mActivity.getAndroidContext().getString(R.string.details_title), + mIndex + 1, mSource.size()); + ListView detailsList = (ListView) LayoutInflater.from(mActivity.getAndroidContext()).inflate( + R.layout.details_list, null, false); + detailsList.setAdapter(mAdapter); + mDialog = new AlertDialog.Builder(mActivity) + .setView(detailsList) + .setTitle(title) + .setPositiveButton(R.string.close, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int whichButton) { + mDialog.dismiss(); + } + }) + .create(); + + mDialog.setOnDismissListener(new OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + if (mListener != null) { + mListener.onClose(); + } + } + }); + } + + + private class DetailsAdapter extends BaseAdapter + implements AddressResolvingListener, ResolutionResolvingListener { + private final ArrayList<String> mItems; + private int mLocationIndex; + private int mWidthIndex = -1; + private int mHeightIndex = -1; + + public DetailsAdapter(MediaDetails details) { + Context context = mActivity.getAndroidContext(); + mItems = new ArrayList<String>(details.size()); + mLocationIndex = -1; + setDetails(context, details); + } + + private void setDetails(Context context, MediaDetails details) { + boolean resolutionIsValid = true; + String path = null; + for (Entry<Integer, Object> detail : details) { + String value; + switch (detail.getKey()) { + case MediaDetails.INDEX_LOCATION: { + double[] latlng = (double[]) detail.getValue(); + mLocationIndex = mItems.size(); + value = DetailsHelper.resolveAddress(mActivity, latlng, this); + break; + } + case MediaDetails.INDEX_SIZE: { + value = Formatter.formatFileSize( + context, (Long) detail.getValue()); + break; + } + case MediaDetails.INDEX_WHITE_BALANCE: { + value = "1".equals(detail.getValue()) + ? context.getString(R.string.manual) + : context.getString(R.string.auto); + break; + } + case MediaDetails.INDEX_FLASH: { + MediaDetails.FlashState flash = + (MediaDetails.FlashState) detail.getValue(); + // TODO: camera doesn't fill in the complete values, show more information + // when it is fixed. + if (flash.isFlashFired()) { + value = context.getString(R.string.flash_on); + } else { + value = context.getString(R.string.flash_off); + } + break; + } + case MediaDetails.INDEX_EXPOSURE_TIME: { + value = (String) detail.getValue(); + double time = Double.valueOf(value); + if (time < 1.0f) { + value = String.format("1/%d", (int) (0.5f + 1 / time)); + } else { + int integer = (int) time; + time -= integer; + value = String.valueOf(integer) + "''"; + if (time > 0.0001) { + value += String.format(" 1/%d", (int) (0.5f + 1 / time)); + } + } + break; + } + case MediaDetails.INDEX_WIDTH: + mWidthIndex = mItems.size(); + value = detail.getValue().toString(); + if (value.equalsIgnoreCase("0")) { + value = context.getString(R.string.unknown); + resolutionIsValid = false; + } + break; + case MediaDetails.INDEX_HEIGHT: { + mHeightIndex = mItems.size(); + value = detail.getValue().toString(); + if (value.equalsIgnoreCase("0")) { + value = context.getString(R.string.unknown); + resolutionIsValid = false; + } + break; + } + case MediaDetails.INDEX_PATH: + // Get the path and then fall through to the default case + path = detail.getValue().toString(); + default: { + Object valueObj = detail.getValue(); + // This shouldn't happen, log its key to help us diagnose the problem. + if (valueObj == null) { + Utils.fail("%s's value is Null", + DetailsHelper.getDetailsName(context, detail.getKey())); + } + value = valueObj.toString(); + } + } + int key = detail.getKey(); + if (details.hasUnit(key)) { + value = String.format("%s: %s %s", DetailsHelper.getDetailsName( + context, key), value, context.getString(details.getUnit(key))); + } else { + value = String.format("%s: %s", DetailsHelper.getDetailsName( + context, key), value); + } + mItems.add(value); + if (!resolutionIsValid) { + DetailsHelper.resolveResolution(path, this); + } + } + } + + @Override + public boolean areAllItemsEnabled() { + return false; + } + + @Override + public boolean isEnabled(int position) { + return false; + } + + @Override + public int getCount() { + return mItems.size(); + } + + @Override + public Object getItem(int position) { + return mDetails.getDetail(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + TextView tv; + if (convertView == null) { + tv = (TextView) LayoutInflater.from(mActivity.getAndroidContext()).inflate( + R.layout.details, parent, false); + } else { + tv = (TextView) convertView; + } + tv.setText(mItems.get(position)); + return tv; + } + + @Override + public void onAddressAvailable(String address) { + mItems.set(mLocationIndex, address); + notifyDataSetChanged(); + } + + @Override + public void onResolutionAvailable(int width, int height) { + if (width == 0 || height == 0) return; + // Update the resolution with the new width and height + Context context = mActivity.getAndroidContext(); + String widthString = String.format("%s: %d", DetailsHelper.getDetailsName( + context, MediaDetails.INDEX_WIDTH), width); + String heightString = String.format("%s: %d", DetailsHelper.getDetailsName( + context, MediaDetails.INDEX_HEIGHT), height); + mItems.set(mWidthIndex, String.valueOf(widthString)); + mItems.set(mHeightIndex, String.valueOf(heightString)); + notifyDataSetChanged(); + } + } + + @Override + public void setCloseListener(CloseListener listener) { + mListener = listener; + } +} diff --git a/src/com/android/gallery3d/ui/DownUpDetector.java b/src/com/android/gallery3d/ui/DownUpDetector.java new file mode 100644 index 000000000..19db77262 --- /dev/null +++ b/src/com/android/gallery3d/ui/DownUpDetector.java @@ -0,0 +1,61 @@ +/* + * 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.ui; + +import android.view.MotionEvent; + +public class DownUpDetector { + public interface DownUpListener { + void onDown(MotionEvent e); + void onUp(MotionEvent e); + } + + private boolean mStillDown; + private DownUpListener mListener; + + public DownUpDetector(DownUpListener listener) { + mListener = listener; + } + + private void setState(boolean down, MotionEvent e) { + if (down == mStillDown) return; + mStillDown = down; + if (down) { + mListener.onDown(e); + } else { + mListener.onUp(e); + } + } + + public void onTouchEvent(MotionEvent ev) { + switch (ev.getAction() & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: + setState(true, ev); + break; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_POINTER_DOWN: // Multitouch event - abort. + setState(false, ev); + break; + } + } + + public boolean isDown() { + return mStillDown; + } +} diff --git a/src/com/android/gallery3d/ui/EdgeEffect.java b/src/com/android/gallery3d/ui/EdgeEffect.java new file mode 100644 index 000000000..87ff0c5d3 --- /dev/null +++ b/src/com/android/gallery3d/ui/EdgeEffect.java @@ -0,0 +1,443 @@ +/* + * Copyright (C) 2011 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.ui; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; + +import com.android.gallery3d.R; +import com.android.gallery3d.glrenderer.GLCanvas; +import com.android.gallery3d.glrenderer.ResourceTexture; + +// This is copied from android.widget.EdgeEffect with some small modifications: +// (1) Copy the images (overscroll_{edge|glow}.png) to local resources. +// (2) Use "GLCanvas" instead of "Canvas" for draw()'s parameter. +// (3) Use a private Drawable class (which inherits from ResourceTexture) +// instead of android.graphics.drawable.Drawable to hold the images. +// The private Drawable class is used to translate original Canvas calls to +// corresponding GLCanvas calls. + +/** + * This class performs the graphical effect used at the edges of scrollable widgets + * when the user scrolls beyond the content bounds in 2D space. + * + * <p>EdgeEffect is stateful. Custom widgets using EdgeEffect should create an + * instance for each edge that should show the effect, feed it input data using + * the methods {@link #onAbsorb(int)}, {@link #onPull(float)}, and {@link #onRelease()}, + * and draw the effect using {@link #draw(Canvas)} in the widget's overridden + * {@link android.view.View#draw(Canvas)} method. If {@link #isFinished()} returns + * false after drawing, the edge effect's animation is not yet complete and the widget + * should schedule another drawing pass to continue the animation.</p> + * + * <p>When drawing, widgets should draw their main content and child views first, + * usually by invoking <code>super.draw(canvas)</code> from an overridden <code>draw</code> + * method. (This will invoke onDraw and dispatch drawing to child views as needed.) + * The edge effect may then be drawn on top of the view's content using the + * {@link #draw(Canvas)} method.</p> + */ +public class EdgeEffect { + @SuppressWarnings("unused") + private static final String TAG = "EdgeEffect"; + + // Time it will take the effect to fully recede in ms + private static final int RECEDE_TIME = 1000; + + // Time it will take before a pulled glow begins receding in ms + private static final int PULL_TIME = 167; + + // Time it will take in ms for a pulled glow to decay to partial strength before release + private static final int PULL_DECAY_TIME = 1000; + + private static final float MAX_ALPHA = 0.8f; + private static final float HELD_EDGE_ALPHA = 0.7f; + private static final float HELD_EDGE_SCALE_Y = 0.5f; + private static final float HELD_GLOW_ALPHA = 0.5f; + private static final float HELD_GLOW_SCALE_Y = 0.5f; + + private static final float MAX_GLOW_HEIGHT = 4.f; + + private static final float PULL_GLOW_BEGIN = 1.f; + private static final float PULL_EDGE_BEGIN = 0.6f; + + // Minimum velocity that will be absorbed + private static final int MIN_VELOCITY = 100; + + private static final float EPSILON = 0.001f; + + private final Drawable mEdge; + private final Drawable mGlow; + private int mWidth; + private int mHeight; + private final int MIN_WIDTH = 300; + private final int mMinWidth; + + private float mEdgeAlpha; + private float mEdgeScaleY; + private float mGlowAlpha; + private float mGlowScaleY; + + private float mEdgeAlphaStart; + private float mEdgeAlphaFinish; + private float mEdgeScaleYStart; + private float mEdgeScaleYFinish; + private float mGlowAlphaStart; + private float mGlowAlphaFinish; + private float mGlowScaleYStart; + private float mGlowScaleYFinish; + + private long mStartTime; + private float mDuration; + + private final Interpolator mInterpolator; + + private static final int STATE_IDLE = 0; + private static final int STATE_PULL = 1; + private static final int STATE_ABSORB = 2; + private static final int STATE_RECEDE = 3; + private static final int STATE_PULL_DECAY = 4; + + // How much dragging should effect the height of the edge image. + // Number determined by user testing. + private static final int PULL_DISTANCE_EDGE_FACTOR = 7; + + // How much dragging should effect the height of the glow image. + // Number determined by user testing. + private static final int PULL_DISTANCE_GLOW_FACTOR = 7; + private static final float PULL_DISTANCE_ALPHA_GLOW_FACTOR = 1.1f; + + private static final int VELOCITY_EDGE_FACTOR = 8; + private static final int VELOCITY_GLOW_FACTOR = 16; + + private int mState = STATE_IDLE; + + private float mPullDistance; + + /** + * Construct a new EdgeEffect with a theme appropriate for the provided context. + * @param context Context used to provide theming and resource information for the EdgeEffect + */ + public EdgeEffect(Context context) { + mEdge = new Drawable(context, R.drawable.overscroll_edge); + mGlow = new Drawable(context, R.drawable.overscroll_glow); + + mMinWidth = (int) (context.getResources().getDisplayMetrics().density * MIN_WIDTH + 0.5f); + mInterpolator = new DecelerateInterpolator(); + } + + /** + * Set the size of this edge effect in pixels. + * + * @param width Effect width in pixels + * @param height Effect height in pixels + */ + public void setSize(int width, int height) { + mWidth = width; + mHeight = height; + } + + /** + * Reports if this EdgeEffect's animation is finished. If this method returns false + * after a call to {@link #draw(Canvas)} the host widget should schedule another + * drawing pass to continue the animation. + * + * @return true if animation is finished, false if drawing should continue on the next frame. + */ + public boolean isFinished() { + return mState == STATE_IDLE; + } + + /** + * Immediately finish the current animation. + * After this call {@link #isFinished()} will return true. + */ + public void finish() { + mState = STATE_IDLE; + } + + /** + * A view should call this when content is pulled away from an edge by the user. + * This will update the state of the current visual effect and its associated animation. + * The host view should always {@link android.view.View#invalidate()} after this + * and draw the results accordingly. + * + * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to + * 1.f (full length of the view) or negative values to express change + * back toward the edge reached to initiate the effect. + */ + public void onPull(float deltaDistance) { + final long now = AnimationTime.get(); + if (mState == STATE_PULL_DECAY && now - mStartTime < mDuration) { + return; + } + if (mState != STATE_PULL) { + mGlowScaleY = PULL_GLOW_BEGIN; + } + mState = STATE_PULL; + + mStartTime = now; + mDuration = PULL_TIME; + + mPullDistance += deltaDistance; + float distance = Math.abs(mPullDistance); + + mEdgeAlpha = mEdgeAlphaStart = Math.max(PULL_EDGE_BEGIN, Math.min(distance, MAX_ALPHA)); + mEdgeScaleY = mEdgeScaleYStart = Math.max( + HELD_EDGE_SCALE_Y, Math.min(distance * PULL_DISTANCE_EDGE_FACTOR, 1.f)); + + mGlowAlpha = mGlowAlphaStart = Math.min(MAX_ALPHA, + mGlowAlpha + + (Math.abs(deltaDistance) * PULL_DISTANCE_ALPHA_GLOW_FACTOR)); + + float glowChange = Math.abs(deltaDistance); + if (deltaDistance > 0 && mPullDistance < 0) { + glowChange = -glowChange; + } + if (mPullDistance == 0) { + mGlowScaleY = 0; + } + + // Do not allow glow to get larger than MAX_GLOW_HEIGHT. + mGlowScaleY = mGlowScaleYStart = Math.min(MAX_GLOW_HEIGHT, Math.max( + 0, mGlowScaleY + glowChange * PULL_DISTANCE_GLOW_FACTOR)); + + mEdgeAlphaFinish = mEdgeAlpha; + mEdgeScaleYFinish = mEdgeScaleY; + mGlowAlphaFinish = mGlowAlpha; + mGlowScaleYFinish = mGlowScaleY; + } + + /** + * Call when the object is released after being pulled. + * This will begin the "decay" phase of the effect. After calling this method + * the host view should {@link android.view.View#invalidate()} and thereby + * draw the results accordingly. + */ + public void onRelease() { + mPullDistance = 0; + + if (mState != STATE_PULL && mState != STATE_PULL_DECAY) { + return; + } + + mState = STATE_RECEDE; + mEdgeAlphaStart = mEdgeAlpha; + mEdgeScaleYStart = mEdgeScaleY; + mGlowAlphaStart = mGlowAlpha; + mGlowScaleYStart = mGlowScaleY; + + mEdgeAlphaFinish = 0.f; + mEdgeScaleYFinish = 0.f; + mGlowAlphaFinish = 0.f; + mGlowScaleYFinish = 0.f; + + mStartTime = AnimationTime.get(); + mDuration = RECEDE_TIME; + } + + /** + * Call when the effect absorbs an impact at the given velocity. + * Used when a fling reaches the scroll boundary. + * + * <p>When using a {@link android.widget.Scroller} or {@link android.widget.OverScroller}, + * the method <code>getCurrVelocity</code> will provide a reasonable approximation + * to use here.</p> + * + * @param velocity Velocity at impact in pixels per second. + */ + public void onAbsorb(int velocity) { + mState = STATE_ABSORB; + velocity = Math.max(MIN_VELOCITY, Math.abs(velocity)); + + mStartTime = AnimationTime.get(); + mDuration = 0.1f + (velocity * 0.03f); + + // The edge should always be at least partially visible, regardless + // of velocity. + mEdgeAlphaStart = 0.f; + mEdgeScaleY = mEdgeScaleYStart = 0.f; + // The glow depends more on the velocity, and therefore starts out + // nearly invisible. + mGlowAlphaStart = 0.5f; + mGlowScaleYStart = 0.f; + + // Factor the velocity by 8. Testing on device shows this works best to + // reflect the strength of the user's scrolling. + mEdgeAlphaFinish = Math.max(0, Math.min(velocity * VELOCITY_EDGE_FACTOR, 1)); + // Edge should never get larger than the size of its asset. + mEdgeScaleYFinish = Math.max( + HELD_EDGE_SCALE_Y, Math.min(velocity * VELOCITY_EDGE_FACTOR, 1.f)); + + // Growth for the size of the glow should be quadratic to properly + // respond + // to a user's scrolling speed. The faster the scrolling speed, the more + // intense the effect should be for both the size and the saturation. + mGlowScaleYFinish = Math.min(0.025f + (velocity * (velocity / 100) * 0.00015f), 1.75f); + // Alpha should change for the glow as well as size. + mGlowAlphaFinish = Math.max( + mGlowAlphaStart, Math.min(velocity * VELOCITY_GLOW_FACTOR * .00001f, MAX_ALPHA)); + } + + + /** + * Draw into the provided canvas. Assumes that the canvas has been rotated + * accordingly and the size has been set. The effect will be drawn the full + * width of X=0 to X=width, beginning from Y=0 and extending to some factor < + * 1.f of height. + * + * @param canvas Canvas to draw into + * @return true if drawing should continue beyond this frame to continue the + * animation + */ + public boolean draw(GLCanvas canvas) { + update(); + + final int edgeHeight = mEdge.getIntrinsicHeight(); + final int edgeWidth = mEdge.getIntrinsicWidth(); + final int glowHeight = mGlow.getIntrinsicHeight(); + final int glowWidth = mGlow.getIntrinsicWidth(); + + mGlow.setAlpha((int) (Math.max(0, Math.min(mGlowAlpha, 1)) * 255)); + + int glowBottom = (int) Math.min( + glowHeight * mGlowScaleY * glowHeight/ glowWidth * 0.6f, + glowHeight * MAX_GLOW_HEIGHT); + if (mWidth < mMinWidth) { + // Center the glow and clip it. + int glowLeft = (mWidth - mMinWidth)/2; + mGlow.setBounds(glowLeft, 0, mWidth - glowLeft, glowBottom); + } else { + // Stretch the glow to fit. + mGlow.setBounds(0, 0, mWidth, glowBottom); + } + + mGlow.draw(canvas); + + mEdge.setAlpha((int) (Math.max(0, Math.min(mEdgeAlpha, 1)) * 255)); + + int edgeBottom = (int) (edgeHeight * mEdgeScaleY); + if (mWidth < mMinWidth) { + // Center the edge and clip it. + int edgeLeft = (mWidth - mMinWidth)/2; + mEdge.setBounds(edgeLeft, 0, mWidth - edgeLeft, edgeBottom); + } else { + // Stretch the edge to fit. + mEdge.setBounds(0, 0, mWidth, edgeBottom); + } + mEdge.draw(canvas); + + return mState != STATE_IDLE; + } + + private void update() { + final long time = AnimationTime.get(); + final float t = Math.min((time - mStartTime) / mDuration, 1.f); + + final float interp = mInterpolator.getInterpolation(t); + + mEdgeAlpha = mEdgeAlphaStart + (mEdgeAlphaFinish - mEdgeAlphaStart) * interp; + mEdgeScaleY = mEdgeScaleYStart + (mEdgeScaleYFinish - mEdgeScaleYStart) * interp; + mGlowAlpha = mGlowAlphaStart + (mGlowAlphaFinish - mGlowAlphaStart) * interp; + mGlowScaleY = mGlowScaleYStart + (mGlowScaleYFinish - mGlowScaleYStart) * interp; + + if (t >= 1.f - EPSILON) { + switch (mState) { + case STATE_ABSORB: + mState = STATE_RECEDE; + mStartTime = AnimationTime.get(); + mDuration = RECEDE_TIME; + + mEdgeAlphaStart = mEdgeAlpha; + mEdgeScaleYStart = mEdgeScaleY; + mGlowAlphaStart = mGlowAlpha; + mGlowScaleYStart = mGlowScaleY; + + // After absorb, the glow and edge should fade to nothing. + mEdgeAlphaFinish = 0.f; + mEdgeScaleYFinish = 0.f; + mGlowAlphaFinish = 0.f; + mGlowScaleYFinish = 0.f; + break; + case STATE_PULL: + mState = STATE_PULL_DECAY; + mStartTime = AnimationTime.get(); + mDuration = PULL_DECAY_TIME; + + mEdgeAlphaStart = mEdgeAlpha; + mEdgeScaleYStart = mEdgeScaleY; + mGlowAlphaStart = mGlowAlpha; + mGlowScaleYStart = mGlowScaleY; + + // After pull, the glow and edge should fade to nothing. + mEdgeAlphaFinish = 0.f; + mEdgeScaleYFinish = 0.f; + mGlowAlphaFinish = 0.f; + mGlowScaleYFinish = 0.f; + break; + case STATE_PULL_DECAY: + // When receding, we want edge to decrease more slowly + // than the glow. + float factor = mGlowScaleYFinish != 0 ? 1 + / (mGlowScaleYFinish * mGlowScaleYFinish) + : Float.MAX_VALUE; + mEdgeScaleY = mEdgeScaleYStart + + (mEdgeScaleYFinish - mEdgeScaleYStart) * + interp * factor; + mState = STATE_RECEDE; + break; + case STATE_RECEDE: + mState = STATE_IDLE; + break; + } + } + } + + private static class Drawable extends ResourceTexture { + private Rect mBounds = new Rect(); + private int mAlpha = 255; + + public Drawable(Context context, int resId) { + super(context, resId); + } + + public int getIntrinsicWidth() { + return getWidth(); + } + + public int getIntrinsicHeight() { + return getHeight(); + } + + public void setBounds(int left, int top, int right, int bottom) { + mBounds.set(left, top, right, bottom); + } + + public void setAlpha(int alpha) { + mAlpha = alpha; + } + + public void draw(GLCanvas canvas) { + canvas.save(GLCanvas.SAVE_FLAG_ALPHA); + canvas.multiplyAlpha(mAlpha / 255.0f); + Rect b = mBounds; + draw(canvas, b.left, b.top, b.width(), b.height()); + canvas.restore(); + } + } +} diff --git a/src/com/android/gallery3d/ui/EdgeView.java b/src/com/android/gallery3d/ui/EdgeView.java new file mode 100644 index 000000000..051de18fa --- /dev/null +++ b/src/com/android/gallery3d/ui/EdgeView.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2011 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.ui; + +import android.content.Context; +import android.opengl.Matrix; + +import com.android.gallery3d.glrenderer.GLCanvas; + +// EdgeView draws EdgeEffect (blue glow) at four sides of the view. +public class EdgeView extends GLView { + @SuppressWarnings("unused") + private static final String TAG = "EdgeView"; + + public static final int INVALID_DIRECTION = -1; + public static final int TOP = 0; + public static final int LEFT = 1; + public static final int BOTTOM = 2; + public static final int RIGHT = 3; + + // Each edge effect has a transform matrix, and each matrix has 16 elements. + // We put all the elements in one array. These constants specify the + // starting index of each matrix. + private static final int TOP_M = TOP * 16; + private static final int LEFT_M = LEFT * 16; + private static final int BOTTOM_M = BOTTOM * 16; + private static final int RIGHT_M = RIGHT * 16; + + private EdgeEffect[] mEffect = new EdgeEffect[4]; + private float[] mMatrix = new float[4 * 16]; + + public EdgeView(Context context) { + for (int i = 0; i < 4; i++) { + mEffect[i] = new EdgeEffect(context); + } + } + + @Override + protected void onLayout( + boolean changeSize, int left, int top, int right, int bottom) { + if (!changeSize) return; + + int w = right - left; + int h = bottom - top; + for (int i = 0; i < 4; i++) { + if ((i & 1) == 0) { // top or bottom + mEffect[i].setSize(w, h); + } else { // left or right + mEffect[i].setSize(h, w); + } + } + + // Set up transforms for the four edges. Without transforms an + // EdgeEffect draws the TOP edge from (0, 0) to (w, Y * h) where Y + // is some factor < 1. For other edges we need to move, rotate, and + // flip the effects into proper places. + Matrix.setIdentityM(mMatrix, TOP_M); + Matrix.setIdentityM(mMatrix, LEFT_M); + Matrix.setIdentityM(mMatrix, BOTTOM_M); + Matrix.setIdentityM(mMatrix, RIGHT_M); + + Matrix.rotateM(mMatrix, LEFT_M, 90, 0, 0, 1); + Matrix.scaleM(mMatrix, LEFT_M, 1, -1, 1); + + Matrix.translateM(mMatrix, BOTTOM_M, 0, h, 0); + Matrix.scaleM(mMatrix, BOTTOM_M, 1, -1, 1); + + Matrix.translateM(mMatrix, RIGHT_M, w, 0, 0); + Matrix.rotateM(mMatrix, RIGHT_M, 90, 0, 0, 1); + } + + @Override + protected void render(GLCanvas canvas) { + super.render(canvas); + boolean more = false; + for (int i = 0; i < 4; i++) { + canvas.save(GLCanvas.SAVE_FLAG_MATRIX); + canvas.multiplyMatrix(mMatrix, i * 16); + more |= mEffect[i].draw(canvas); + canvas.restore(); + } + if (more) { + invalidate(); + } + } + + // Called when the content is pulled away from the edge. + // offset is in pixels. direction is one of {TOP, LEFT, BOTTOM, RIGHT}. + public void onPull(int offset, int direction) { + int fullLength = ((direction & 1) == 0) ? getWidth() : getHeight(); + mEffect[direction].onPull((float)offset / fullLength); + if (!mEffect[direction].isFinished()) { + invalidate(); + } + } + + // Call when the object is released after being pulled. + public void onRelease() { + boolean more = false; + for (int i = 0; i < 4; i++) { + mEffect[i].onRelease(); + more |= !mEffect[i].isFinished(); + } + if (more) { + invalidate(); + } + } + + // Call when the effect absorbs an impact at the given velocity. + // Used when a fling reaches the scroll boundary. velocity is in pixels + // per second. direction is one of {TOP, LEFT, BOTTOM, RIGHT}. + public void onAbsorb(int velocity, int direction) { + mEffect[direction].onAbsorb(velocity); + if (!mEffect[direction].isFinished()) { + invalidate(); + } + } +} diff --git a/src/com/android/gallery3d/ui/FlingScroller.java b/src/com/android/gallery3d/ui/FlingScroller.java new file mode 100644 index 000000000..6f98c64f9 --- /dev/null +++ b/src/com/android/gallery3d/ui/FlingScroller.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2011 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.ui; + + +// This is a customized version of Scroller, with a interface similar to +// android.widget.Scroller. It does fling only, not scroll. +// +// The differences between the this Scroller and the system one are: +// +// (1) The velocity does not change because of min/max limit. +// (2) The duration is different. +// (3) The deceleration curve is different. +class FlingScroller { + @SuppressWarnings("unused") + private static final String TAG = "FlingController"; + + // The fling duration (in milliseconds) when velocity is 1 pixel/second + private static final float FLING_DURATION_PARAM = 50f; + private static final int DECELERATED_FACTOR = 4; + + private int mStartX, mStartY; + private int mMinX, mMinY, mMaxX, mMaxY; + private double mSinAngle; + private double mCosAngle; + private int mDuration; + private int mDistance; + private int mFinalX, mFinalY; + + private int mCurrX, mCurrY; + private double mCurrV; + + public int getFinalX() { + return mFinalX; + } + + public int getFinalY() { + return mFinalY; + } + + public int getDuration() { + return mDuration; + } + + public int getCurrX() { + return mCurrX; + + } + + public int getCurrY() { + return mCurrY; + } + + public int getCurrVelocityX() { + return (int)Math.round(mCurrV * mCosAngle); + } + + public int getCurrVelocityY() { + return (int)Math.round(mCurrV * mSinAngle); + } + + public void fling(int startX, int startY, int velocityX, int velocityY, + int minX, int maxX, int minY, int maxY) { + mStartX = startX; + mStartY = startY; + mMinX = minX; + mMinY = minY; + mMaxX = maxX; + mMaxY = maxY; + + double velocity = Math.hypot(velocityX, velocityY); + mSinAngle = velocityY / velocity; + mCosAngle = velocityX / velocity; + // + // The position formula: x(t) = s + (e - s) * (1 - (1 - t / T) ^ d) + // velocity formula: v(t) = d * (e - s) * (1 - t / T) ^ (d - 1) / T + // Thus, + // v0 = d * (e - s) / T => (e - s) = v0 * T / d + // + + // Ta = T_ref * (Va / V_ref) ^ (1 / (d - 1)); V_ref = 1 pixel/second; + mDuration = (int)Math.round(FLING_DURATION_PARAM + * Math.pow(Math.abs(velocity), 1.0 / (DECELERATED_FACTOR - 1))); + + // (e - s) = v0 * T / d + mDistance = (int)Math.round( + velocity * mDuration / DECELERATED_FACTOR / 1000); + + mFinalX = getX(1.0f); + mFinalY = getY(1.0f); + } + + public void computeScrollOffset(float progress) { + progress = Math.min(progress, 1); + float f = 1 - progress; + f = 1 - (float) Math.pow(f, DECELERATED_FACTOR); + mCurrX = getX(f); + mCurrY = getY(f); + mCurrV = getV(progress); + } + + private int getX(float f) { + int r = (int) Math.round(mStartX + f * mDistance * mCosAngle); + if (mCosAngle > 0 && mStartX <= mMaxX) { + r = Math.min(r, mMaxX); + } else if (mCosAngle < 0 && mStartX >= mMinX) { + r = Math.max(r, mMinX); + } + return r; + } + + private int getY(float f) { + int r = (int) Math.round(mStartY + f * mDistance * mSinAngle); + if (mSinAngle > 0 && mStartY <= mMaxY) { + r = Math.min(r, mMaxY); + } else if (mSinAngle < 0 && mStartY >= mMinY) { + r = Math.max(r, mMinY); + } + return r; + } + + private double getV(float progress) { + // velocity formula: v(t) = d * (e - s) * (1 - t / T) ^ (d - 1) / T + return DECELERATED_FACTOR * mDistance * 1000 * + Math.pow(1 - progress, DECELERATED_FACTOR - 1) / mDuration; + } +} diff --git a/src/com/android/gallery3d/ui/GLRoot.java b/src/com/android/gallery3d/ui/GLRoot.java new file mode 100644 index 000000000..33a82eaf7 --- /dev/null +++ b/src/com/android/gallery3d/ui/GLRoot.java @@ -0,0 +1,53 @@ +/* + * 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.ui; + +import android.content.Context; +import android.graphics.Matrix; + +import com.android.gallery3d.anim.CanvasAnimation; +import com.android.gallery3d.glrenderer.GLCanvas; + +public interface GLRoot { + + // Listener will be called when GL is idle AND before each frame. + // Mainly used for uploading textures. + public static interface OnGLIdleListener { + public boolean onGLIdle( + GLCanvas canvas, boolean renderRequested); + } + + public void addOnGLIdleListener(OnGLIdleListener listener); + public void registerLaunchedAnimation(CanvasAnimation animation); + public void requestRenderForced(); + public void requestRender(); + public void requestLayoutContentPane(); + + public void lockRenderThread(); + public void unlockRenderThread(); + + public void setContentPane(GLView content); + public void setOrientationSource(OrientationSource source); + public int getDisplayRotation(); + public int getCompensation(); + public Matrix getCompensationMatrix(); + public void freeze(); + public void unfreeze(); + public void setLightsOutMode(boolean enabled); + + public Context getContext(); +} diff --git a/src/com/android/gallery3d/ui/GLRootView.java b/src/com/android/gallery3d/ui/GLRootView.java new file mode 100644 index 000000000..dc898d83d --- /dev/null +++ b/src/com/android/gallery3d/ui/GLRootView.java @@ -0,0 +1,630 @@ +/* + * 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.ui; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Matrix; +import android.graphics.PixelFormat; +import android.opengl.GLSurfaceView; +import android.os.Build; +import android.os.Process; +import android.os.SystemClock; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.SurfaceHolder; +import android.view.View; + +import com.android.gallery3d.R; +import com.android.gallery3d.anim.CanvasAnimation; +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.glrenderer.BasicTexture; +import com.android.gallery3d.glrenderer.GLCanvas; +import com.android.gallery3d.glrenderer.GLES11Canvas; +import com.android.gallery3d.glrenderer.GLES20Canvas; +import com.android.gallery3d.glrenderer.UploadedTexture; +import com.android.gallery3d.util.GalleryUtils; +import com.android.gallery3d.util.MotionEventHelper; +import com.android.gallery3d.util.Profile; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; +import javax.microedition.khronos.opengles.GL11; + +// The root component of all <code>GLView</code>s. The rendering is done in GL +// thread while the event handling is done in the main thread. To synchronize +// the two threads, the entry points of this package need to synchronize on the +// <code>GLRootView</code> instance unless it can be proved that the rendering +// thread won't access the same thing as the method. The entry points include: +// (1) The public methods of HeadUpDisplay +// (2) The public methods of CameraHeadUpDisplay +// (3) The overridden methods in GLRootView. +public class GLRootView extends GLSurfaceView + implements GLSurfaceView.Renderer, GLRoot { + private static final String TAG = "GLRootView"; + + private static final boolean DEBUG_FPS = false; + private int mFrameCount = 0; + private long mFrameCountingStart = 0; + + private static final boolean DEBUG_INVALIDATE = false; + private int mInvalidateColor = 0; + + private static final boolean DEBUG_DRAWING_STAT = false; + + private static final boolean DEBUG_PROFILE = false; + private static final boolean DEBUG_PROFILE_SLOW_ONLY = false; + + private static final int FLAG_INITIALIZED = 1; + private static final int FLAG_NEED_LAYOUT = 2; + + private GL11 mGL; + private GLCanvas mCanvas; + private GLView mContentView; + + private OrientationSource mOrientationSource; + // mCompensation is the difference between the UI orientation on GLCanvas + // and the framework orientation. See OrientationManager for details. + private int mCompensation; + // mCompensationMatrix maps the coordinates of touch events. It is kept sync + // with mCompensation. + private Matrix mCompensationMatrix = new Matrix(); + private int mDisplayRotation; + + private int mFlags = FLAG_NEED_LAYOUT; + private volatile boolean mRenderRequested = false; + + private final ArrayList<CanvasAnimation> mAnimations = + new ArrayList<CanvasAnimation>(); + + private final ArrayDeque<OnGLIdleListener> mIdleListeners = + new ArrayDeque<OnGLIdleListener>(); + + private final IdleRunner mIdleRunner = new IdleRunner(); + + private final ReentrantLock mRenderLock = new ReentrantLock(); + private final Condition mFreezeCondition = + mRenderLock.newCondition(); + private boolean mFreeze; + + private long mLastDrawFinishTime; + private boolean mInDownState = false; + private boolean mFirstDraw = true; + + public GLRootView(Context context) { + this(context, null); + } + + public GLRootView(Context context, AttributeSet attrs) { + super(context, attrs); + mFlags |= FLAG_INITIALIZED; + setBackgroundDrawable(null); + setEGLContextClientVersion(ApiHelper.HAS_GLES20_REQUIRED ? 2 : 1); + if (ApiHelper.USE_888_PIXEL_FORMAT) { + setEGLConfigChooser(8, 8, 8, 0, 0, 0); + } else { + setEGLConfigChooser(5, 6, 5, 0, 0, 0); + } + setRenderer(this); + if (ApiHelper.USE_888_PIXEL_FORMAT) { + getHolder().setFormat(PixelFormat.RGB_888); + } else { + getHolder().setFormat(PixelFormat.RGB_565); + } + + // Uncomment this to enable gl error check. + // setDebugFlags(DEBUG_CHECK_GL_ERROR); + } + + @Override + public void registerLaunchedAnimation(CanvasAnimation animation) { + // Register the newly launched animation so that we can set the start + // time more precisely. (Usually, it takes much longer for first + // rendering, so we set the animation start time as the time we + // complete rendering) + mAnimations.add(animation); + } + + @Override + public void addOnGLIdleListener(OnGLIdleListener listener) { + synchronized (mIdleListeners) { + mIdleListeners.addLast(listener); + mIdleRunner.enable(); + } + } + + @Override + public void setContentPane(GLView content) { + if (mContentView == content) return; + if (mContentView != null) { + if (mInDownState) { + long now = SystemClock.uptimeMillis(); + MotionEvent cancelEvent = MotionEvent.obtain( + now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0); + mContentView.dispatchTouchEvent(cancelEvent); + cancelEvent.recycle(); + mInDownState = false; + } + mContentView.detachFromRoot(); + BasicTexture.yieldAllTextures(); + } + mContentView = content; + if (content != null) { + content.attachToRoot(this); + requestLayoutContentPane(); + } + } + + @Override + public void requestRenderForced() { + superRequestRender(); + } + + @Override + public void requestRender() { + if (DEBUG_INVALIDATE) { + StackTraceElement e = Thread.currentThread().getStackTrace()[4]; + String caller = e.getFileName() + ":" + e.getLineNumber() + " "; + Log.d(TAG, "invalidate: " + caller); + } + if (mRenderRequested) return; + mRenderRequested = true; + if (ApiHelper.HAS_POST_ON_ANIMATION) { + postOnAnimation(mRequestRenderOnAnimationFrame); + } else { + super.requestRender(); + } + } + + private Runnable mRequestRenderOnAnimationFrame = new Runnable() { + @Override + public void run() { + superRequestRender(); + } + }; + + private void superRequestRender() { + super.requestRender(); + } + + @Override + public void requestLayoutContentPane() { + mRenderLock.lock(); + try { + if (mContentView == null || (mFlags & FLAG_NEED_LAYOUT) != 0) return; + + // "View" system will invoke onLayout() for initialization(bug ?), we + // have to ignore it since the GLThread is not ready yet. + if ((mFlags & FLAG_INITIALIZED) == 0) return; + + mFlags |= FLAG_NEED_LAYOUT; + requestRender(); + } finally { + mRenderLock.unlock(); + } + } + + private void layoutContentPane() { + mFlags &= ~FLAG_NEED_LAYOUT; + + int w = getWidth(); + int h = getHeight(); + int displayRotation = 0; + int compensation = 0; + + // Get the new orientation values + if (mOrientationSource != null) { + displayRotation = mOrientationSource.getDisplayRotation(); + compensation = mOrientationSource.getCompensation(); + } else { + displayRotation = 0; + compensation = 0; + } + + if (mCompensation != compensation) { + mCompensation = compensation; + if (mCompensation % 180 != 0) { + mCompensationMatrix.setRotate(mCompensation); + // move center to origin before rotation + mCompensationMatrix.preTranslate(-w / 2, -h / 2); + // align with the new origin after rotation + mCompensationMatrix.postTranslate(h / 2, w / 2); + } else { + mCompensationMatrix.setRotate(mCompensation, w / 2, h / 2); + } + } + mDisplayRotation = displayRotation; + + // Do the actual layout. + if (mCompensation % 180 != 0) { + int tmp = w; + w = h; + h = tmp; + } + Log.i(TAG, "layout content pane " + w + "x" + h + + " (compensation " + mCompensation + ")"); + if (mContentView != null && w != 0 && h != 0) { + mContentView.layout(0, 0, w, h); + } + // Uncomment this to dump the view hierarchy. + //mContentView.dumpTree(""); + } + + @Override + protected void onLayout( + boolean changed, int left, int top, int right, int bottom) { + if (changed) requestLayoutContentPane(); + } + + /** + * Called when the context is created, possibly after automatic destruction. + */ + // This is a GLSurfaceView.Renderer callback + @Override + public void onSurfaceCreated(GL10 gl1, EGLConfig config) { + GL11 gl = (GL11) gl1; + if (mGL != null) { + // The GL Object has changed + Log.i(TAG, "GLObject has changed from " + mGL + " to " + gl); + } + mRenderLock.lock(); + try { + mGL = gl; + mCanvas = ApiHelper.HAS_GLES20_REQUIRED ? new GLES20Canvas() : new GLES11Canvas(gl); + BasicTexture.invalidateAllTextures(); + } finally { + mRenderLock.unlock(); + } + + if (DEBUG_FPS || DEBUG_PROFILE) { + setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY); + } else { + setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); + } + } + + /** + * Called when the OpenGL surface is recreated without destroying the + * context. + */ + // This is a GLSurfaceView.Renderer callback + @Override + public void onSurfaceChanged(GL10 gl1, int width, int height) { + Log.i(TAG, "onSurfaceChanged: " + width + "x" + height + + ", gl10: " + gl1.toString()); + Process.setThreadPriority(Process.THREAD_PRIORITY_DISPLAY); + GalleryUtils.setRenderThread(); + if (DEBUG_PROFILE) { + Log.d(TAG, "Start profiling"); + Profile.enable(20); // take a sample every 20ms + } + GL11 gl = (GL11) gl1; + Utils.assertTrue(mGL == gl); + + mCanvas.setSize(width, height); + } + + private void outputFps() { + long now = System.nanoTime(); + if (mFrameCountingStart == 0) { + mFrameCountingStart = now; + } else if ((now - mFrameCountingStart) > 1000000000) { + Log.d(TAG, "fps: " + (double) mFrameCount + * 1000000000 / (now - mFrameCountingStart)); + mFrameCountingStart = now; + mFrameCount = 0; + } + ++mFrameCount; + } + + @Override + public void onDrawFrame(GL10 gl) { + AnimationTime.update(); + long t0; + if (DEBUG_PROFILE_SLOW_ONLY) { + Profile.hold(); + t0 = System.nanoTime(); + } + mRenderLock.lock(); + + while (mFreeze) { + mFreezeCondition.awaitUninterruptibly(); + } + + try { + onDrawFrameLocked(gl); + } finally { + mRenderLock.unlock(); + } + + // We put a black cover View in front of the SurfaceView and hide it + // after the first draw. This prevents the SurfaceView being transparent + // before the first draw. + if (mFirstDraw) { + mFirstDraw = false; + post(new Runnable() { + @Override + public void run() { + View root = getRootView(); + View cover = root.findViewById(R.id.gl_root_cover); + cover.setVisibility(GONE); + } + }); + } + + if (DEBUG_PROFILE_SLOW_ONLY) { + long t = System.nanoTime(); + long durationInMs = (t - mLastDrawFinishTime) / 1000000; + long durationDrawInMs = (t - t0) / 1000000; + mLastDrawFinishTime = t; + + if (durationInMs > 34) { // 34ms -> we skipped at least 2 frames + Log.v(TAG, "----- SLOW (" + durationDrawInMs + "/" + + durationInMs + ") -----"); + Profile.commit(); + } else { + Profile.drop(); + } + } + } + + private void onDrawFrameLocked(GL10 gl) { + if (DEBUG_FPS) outputFps(); + + // release the unbound textures and deleted buffers. + mCanvas.deleteRecycledResources(); + + // reset texture upload limit + UploadedTexture.resetUploadLimit(); + + mRenderRequested = false; + + if ((mOrientationSource != null + && mDisplayRotation != mOrientationSource.getDisplayRotation()) + || (mFlags & FLAG_NEED_LAYOUT) != 0) { + layoutContentPane(); + } + + mCanvas.save(GLCanvas.SAVE_FLAG_ALL); + rotateCanvas(-mCompensation); + if (mContentView != null) { + mContentView.render(mCanvas); + } else { + // Make sure we always draw something to prevent displaying garbage + mCanvas.clearBuffer(); + } + mCanvas.restore(); + + if (!mAnimations.isEmpty()) { + long now = AnimationTime.get(); + for (int i = 0, n = mAnimations.size(); i < n; i++) { + mAnimations.get(i).setStartTime(now); + } + mAnimations.clear(); + } + + if (UploadedTexture.uploadLimitReached()) { + requestRender(); + } + + synchronized (mIdleListeners) { + if (!mIdleListeners.isEmpty()) mIdleRunner.enable(); + } + + if (DEBUG_INVALIDATE) { + mCanvas.fillRect(10, 10, 5, 5, mInvalidateColor); + mInvalidateColor = ~mInvalidateColor; + } + + if (DEBUG_DRAWING_STAT) { + mCanvas.dumpStatisticsAndClear(); + } + } + + private void rotateCanvas(int degrees) { + if (degrees == 0) return; + int w = getWidth(); + int h = getHeight(); + int cx = w / 2; + int cy = h / 2; + mCanvas.translate(cx, cy); + mCanvas.rotate(degrees, 0, 0, 1); + if (degrees % 180 != 0) { + mCanvas.translate(-cy, -cx); + } else { + mCanvas.translate(-cx, -cy); + } + } + + @Override + public boolean dispatchTouchEvent(MotionEvent event) { + if (!isEnabled()) return false; + + int action = event.getAction(); + if (action == MotionEvent.ACTION_CANCEL + || action == MotionEvent.ACTION_UP) { + mInDownState = false; + } else if (!mInDownState && action != MotionEvent.ACTION_DOWN) { + return false; + } + + if (mCompensation != 0) { + event = MotionEventHelper.transformEvent(event, mCompensationMatrix); + } + + mRenderLock.lock(); + try { + // If this has been detached from root, we don't need to handle event + boolean handled = mContentView != null + && mContentView.dispatchTouchEvent(event); + if (action == MotionEvent.ACTION_DOWN && handled) { + mInDownState = true; + } + return handled; + } finally { + mRenderLock.unlock(); + } + } + + private class IdleRunner implements Runnable { + // true if the idle runner is in the queue + private boolean mActive = false; + + @Override + public void run() { + OnGLIdleListener listener; + synchronized (mIdleListeners) { + mActive = false; + if (mIdleListeners.isEmpty()) return; + listener = mIdleListeners.removeFirst(); + } + mRenderLock.lock(); + boolean keepInQueue; + try { + keepInQueue = listener.onGLIdle(mCanvas, mRenderRequested); + } finally { + mRenderLock.unlock(); + } + synchronized (mIdleListeners) { + if (keepInQueue) mIdleListeners.addLast(listener); + if (!mRenderRequested && !mIdleListeners.isEmpty()) enable(); + } + } + + public void enable() { + // Who gets the flag can add it to the queue + if (mActive) return; + mActive = true; + queueEvent(this); + } + } + + @Override + public void lockRenderThread() { + mRenderLock.lock(); + } + + @Override + public void unlockRenderThread() { + mRenderLock.unlock(); + } + + @Override + public void onPause() { + unfreeze(); + super.onPause(); + if (DEBUG_PROFILE) { + Log.d(TAG, "Stop profiling"); + Profile.disableAll(); + Profile.dumpToFile("/sdcard/gallery.prof"); + Profile.reset(); + } + } + + @Override + public void setOrientationSource(OrientationSource source) { + mOrientationSource = source; + } + + @Override + public int getDisplayRotation() { + return mDisplayRotation; + } + + @Override + public int getCompensation() { + return mCompensation; + } + + @Override + public Matrix getCompensationMatrix() { + return mCompensationMatrix; + } + + @Override + public void freeze() { + mRenderLock.lock(); + mFreeze = true; + mRenderLock.unlock(); + } + + @Override + public void unfreeze() { + mRenderLock.lock(); + mFreeze = false; + mFreezeCondition.signalAll(); + mRenderLock.unlock(); + } + + @Override + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public void setLightsOutMode(boolean enabled) { + if (!ApiHelper.HAS_SET_SYSTEM_UI_VISIBILITY) return; + + int flags = 0; + if (enabled) { + flags = STATUS_BAR_HIDDEN; + if (ApiHelper.HAS_VIEW_SYSTEM_UI_FLAG_LAYOUT_STABLE) { + flags |= (SYSTEM_UI_FLAG_FULLSCREEN | SYSTEM_UI_FLAG_LAYOUT_STABLE); + } + } + setSystemUiVisibility(flags); + } + + // We need to unfreeze in the following methods and in onPause(). + // These methods will wait on GLThread. If we have freezed the GLRootView, + // the GLThread will wait on main thread to call unfreeze and cause dead + // lock. + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { + unfreeze(); + super.surfaceChanged(holder, format, w, h); + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + unfreeze(); + super.surfaceCreated(holder); + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + unfreeze(); + super.surfaceDestroyed(holder); + } + + @Override + protected void onDetachedFromWindow() { + unfreeze(); + super.onDetachedFromWindow(); + } + + @Override + protected void finalize() throws Throwable { + try { + unfreeze(); + } finally { + super.finalize(); + } + } +} diff --git a/src/com/android/gallery3d/ui/GLView.java b/src/com/android/gallery3d/ui/GLView.java new file mode 100644 index 000000000..83de19fe4 --- /dev/null +++ b/src/com/android/gallery3d/ui/GLView.java @@ -0,0 +1,465 @@ +/* + * 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.ui; + +import android.graphics.Rect; +import android.os.SystemClock; +import android.view.MotionEvent; + +import com.android.gallery3d.anim.CanvasAnimation; +import com.android.gallery3d.anim.StateTransitionAnimation; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.glrenderer.GLCanvas; + +import java.util.ArrayList; + +// GLView is a UI component. It can render to a GLCanvas and accept touch +// events. A GLView may have zero or more child GLView and they form a tree +// structure. The rendering and event handling will pass through the tree +// structure. +// +// A GLView tree should be attached to a GLRoot before event dispatching and +// rendering happens. GLView asks GLRoot to re-render or re-layout the +// GLView hierarchy using requestRender() and requestLayoutContentPane(). +// +// The render() method is called in a separate thread. Before calling +// dispatchTouchEvent() and layout(), GLRoot acquires a lock to avoid the +// rendering thread running at the same time. If there are other entry points +// from main thread (like a Handler) in your GLView, you need to call +// lockRendering() if the rendering thread should not run at the same time. +// +public class GLView { + private static final String TAG = "GLView"; + + public static final int VISIBLE = 0; + public static final int INVISIBLE = 1; + + private static final int FLAG_INVISIBLE = 1; + private static final int FLAG_SET_MEASURED_SIZE = 2; + private static final int FLAG_LAYOUT_REQUESTED = 4; + + public interface OnClickListener { + void onClick(GLView v); + } + + protected final Rect mBounds = new Rect(); + protected final Rect mPaddings = new Rect(); + + private GLRoot mRoot; + protected GLView mParent; + private ArrayList<GLView> mComponents; + private GLView mMotionTarget; + + private CanvasAnimation mAnimation; + + private int mViewFlags = 0; + + protected int mMeasuredWidth = 0; + protected int mMeasuredHeight = 0; + + private int mLastWidthSpec = -1; + private int mLastHeightSpec = -1; + + protected int mScrollY = 0; + protected int mScrollX = 0; + protected int mScrollHeight = 0; + protected int mScrollWidth = 0; + + private float [] mBackgroundColor; + private StateTransitionAnimation mTransition; + + public void startAnimation(CanvasAnimation animation) { + GLRoot root = getGLRoot(); + if (root == null) throw new IllegalStateException(); + mAnimation = animation; + if (mAnimation != null) { + mAnimation.start(); + root.registerLaunchedAnimation(mAnimation); + } + invalidate(); + } + + // Sets the visiblity of this GLView (either GLView.VISIBLE or + // GLView.INVISIBLE). + public void setVisibility(int visibility) { + if (visibility == getVisibility()) return; + if (visibility == VISIBLE) { + mViewFlags &= ~FLAG_INVISIBLE; + } else { + mViewFlags |= FLAG_INVISIBLE; + } + onVisibilityChanged(visibility); + invalidate(); + } + + // Returns GLView.VISIBLE or GLView.INVISIBLE + public int getVisibility() { + return (mViewFlags & FLAG_INVISIBLE) == 0 ? VISIBLE : INVISIBLE; + } + + // This should only be called on the content pane (the topmost GLView). + public void attachToRoot(GLRoot root) { + Utils.assertTrue(mParent == null && mRoot == null); + onAttachToRoot(root); + } + + // This should only be called on the content pane (the topmost GLView). + public void detachFromRoot() { + Utils.assertTrue(mParent == null && mRoot != null); + onDetachFromRoot(); + } + + // Returns the number of children of the GLView. + public int getComponentCount() { + return mComponents == null ? 0 : mComponents.size(); + } + + // Returns the children for the given index. + public GLView getComponent(int index) { + if (mComponents == null) { + throw new ArrayIndexOutOfBoundsException(index); + } + return mComponents.get(index); + } + + // Adds a child to this GLView. + public void addComponent(GLView component) { + // Make sure the component doesn't have a parent currently. + if (component.mParent != null) throw new IllegalStateException(); + + // Build parent-child links + if (mComponents == null) { + mComponents = new ArrayList<GLView>(); + } + mComponents.add(component); + component.mParent = this; + + // If this is added after we have a root, tell the component. + if (mRoot != null) { + component.onAttachToRoot(mRoot); + } + } + + // Removes a child from this GLView. + public boolean removeComponent(GLView component) { + if (mComponents == null) return false; + if (mComponents.remove(component)) { + removeOneComponent(component); + return true; + } + return false; + } + + // Removes all children of this GLView. + public void removeAllComponents() { + for (int i = 0, n = mComponents.size(); i < n; ++i) { + removeOneComponent(mComponents.get(i)); + } + mComponents.clear(); + } + + private void removeOneComponent(GLView component) { + if (mMotionTarget == component) { + long now = SystemClock.uptimeMillis(); + MotionEvent cancelEvent = MotionEvent.obtain( + now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0); + dispatchTouchEvent(cancelEvent); + cancelEvent.recycle(); + } + component.onDetachFromRoot(); + component.mParent = null; + } + + public Rect bounds() { + return mBounds; + } + + public int getWidth() { + return mBounds.right - mBounds.left; + } + + public int getHeight() { + return mBounds.bottom - mBounds.top; + } + + public GLRoot getGLRoot() { + return mRoot; + } + + // Request re-rendering of the view hierarchy. + // This is used for animation or when the contents changed. + public void invalidate() { + GLRoot root = getGLRoot(); + if (root != null) root.requestRender(); + } + + // Request re-layout of the view hierarchy. + public void requestLayout() { + mViewFlags |= FLAG_LAYOUT_REQUESTED; + mLastHeightSpec = -1; + mLastWidthSpec = -1; + if (mParent != null) { + mParent.requestLayout(); + } else { + // Is this a content pane ? + GLRoot root = getGLRoot(); + if (root != null) root.requestLayoutContentPane(); + } + } + + protected void render(GLCanvas canvas) { + boolean transitionActive = false; + if (mTransition != null && mTransition.calculate(AnimationTime.get())) { + invalidate(); + transitionActive = mTransition.isActive(); + } + renderBackground(canvas); + canvas.save(); + if (transitionActive) { + mTransition.applyContentTransform(this, canvas); + } + for (int i = 0, n = getComponentCount(); i < n; ++i) { + renderChild(canvas, getComponent(i)); + } + canvas.restore(); + if (transitionActive) { + mTransition.applyOverlay(this, canvas); + } + } + + public void setIntroAnimation(StateTransitionAnimation intro) { + mTransition = intro; + if (mTransition != null) mTransition.start(); + } + + public float [] getBackgroundColor() { + return mBackgroundColor; + } + + public void setBackgroundColor(float [] color) { + mBackgroundColor = color; + } + + protected void renderBackground(GLCanvas view) { + if (mBackgroundColor != null) { + view.clearBuffer(mBackgroundColor); + } + if (mTransition != null && mTransition.isActive()) { + mTransition.applyBackground(this, view); + return; + } + } + + protected void renderChild(GLCanvas canvas, GLView component) { + if (component.getVisibility() != GLView.VISIBLE + && component.mAnimation == null) return; + + int xoffset = component.mBounds.left - mScrollX; + int yoffset = component.mBounds.top - mScrollY; + + canvas.translate(xoffset, yoffset); + + CanvasAnimation anim = component.mAnimation; + if (anim != null) { + canvas.save(anim.getCanvasSaveFlags()); + if (anim.calculate(AnimationTime.get())) { + invalidate(); + } else { + component.mAnimation = null; + } + anim.apply(canvas); + } + component.render(canvas); + if (anim != null) canvas.restore(); + canvas.translate(-xoffset, -yoffset); + } + + protected boolean onTouch(MotionEvent event) { + return false; + } + + protected boolean dispatchTouchEvent(MotionEvent event, + int x, int y, GLView component, boolean checkBounds) { + Rect rect = component.mBounds; + int left = rect.left; + int top = rect.top; + if (!checkBounds || rect.contains(x, y)) { + event.offsetLocation(-left, -top); + if (component.dispatchTouchEvent(event)) { + event.offsetLocation(left, top); + return true; + } + event.offsetLocation(left, top); + } + return false; + } + + protected boolean dispatchTouchEvent(MotionEvent event) { + int x = (int) event.getX(); + int y = (int) event.getY(); + int action = event.getAction(); + if (mMotionTarget != null) { + if (action == MotionEvent.ACTION_DOWN) { + MotionEvent cancel = MotionEvent.obtain(event); + cancel.setAction(MotionEvent.ACTION_CANCEL); + dispatchTouchEvent(cancel, x, y, mMotionTarget, false); + mMotionTarget = null; + } else { + dispatchTouchEvent(event, x, y, mMotionTarget, false); + if (action == MotionEvent.ACTION_CANCEL + || action == MotionEvent.ACTION_UP) { + mMotionTarget = null; + } + return true; + } + } + if (action == MotionEvent.ACTION_DOWN) { + // in the reverse rendering order + for (int i = getComponentCount() - 1; i >= 0; --i) { + GLView component = getComponent(i); + if (component.getVisibility() != GLView.VISIBLE) continue; + if (dispatchTouchEvent(event, x, y, component, true)) { + mMotionTarget = component; + return true; + } + } + } + return onTouch(event); + } + + public Rect getPaddings() { + return mPaddings; + } + + public void layout(int left, int top, int right, int bottom) { + boolean sizeChanged = setBounds(left, top, right, bottom); + mViewFlags &= ~FLAG_LAYOUT_REQUESTED; + // We call onLayout no matter sizeChanged is true or not because the + // orientation may change without changing the size of the View (for + // example, rotate the device by 180 degrees), and we want to handle + // orientation change in onLayout. + onLayout(sizeChanged, left, top, right, bottom); + } + + private boolean setBounds(int left, int top, int right, int bottom) { + boolean sizeChanged = (right - left) != (mBounds.right - mBounds.left) + || (bottom - top) != (mBounds.bottom - mBounds.top); + mBounds.set(left, top, right, bottom); + return sizeChanged; + } + + public void measure(int widthSpec, int heightSpec) { + if (widthSpec == mLastWidthSpec && heightSpec == mLastHeightSpec + && (mViewFlags & FLAG_LAYOUT_REQUESTED) == 0) { + return; + } + + mLastWidthSpec = widthSpec; + mLastHeightSpec = heightSpec; + + mViewFlags &= ~FLAG_SET_MEASURED_SIZE; + onMeasure(widthSpec, heightSpec); + if ((mViewFlags & FLAG_SET_MEASURED_SIZE) == 0) { + throw new IllegalStateException(getClass().getName() + + " should call setMeasuredSize() in onMeasure()"); + } + } + + protected void onMeasure(int widthSpec, int heightSpec) { + } + + protected void setMeasuredSize(int width, int height) { + mViewFlags |= FLAG_SET_MEASURED_SIZE; + mMeasuredWidth = width; + mMeasuredHeight = height; + } + + public int getMeasuredWidth() { + return mMeasuredWidth; + } + + public int getMeasuredHeight() { + return mMeasuredHeight; + } + + protected void onLayout( + boolean changeSize, int left, int top, int right, int bottom) { + } + + /** + * Gets the bounds of the given descendant that relative to this view. + */ + public boolean getBoundsOf(GLView descendant, Rect out) { + int xoffset = 0; + int yoffset = 0; + GLView view = descendant; + while (view != this) { + if (view == null) return false; + Rect bounds = view.mBounds; + xoffset += bounds.left; + yoffset += bounds.top; + view = view.mParent; + } + out.set(xoffset, yoffset, xoffset + descendant.getWidth(), + yoffset + descendant.getHeight()); + return true; + } + + protected void onVisibilityChanged(int visibility) { + for (int i = 0, n = getComponentCount(); i < n; ++i) { + GLView child = getComponent(i); + if (child.getVisibility() == GLView.VISIBLE) { + child.onVisibilityChanged(visibility); + } + } + } + + protected void onAttachToRoot(GLRoot root) { + mRoot = root; + for (int i = 0, n = getComponentCount(); i < n; ++i) { + getComponent(i).onAttachToRoot(root); + } + } + + protected void onDetachFromRoot() { + for (int i = 0, n = getComponentCount(); i < n; ++i) { + getComponent(i).onDetachFromRoot(); + } + mRoot = null; + } + + public void lockRendering() { + if (mRoot != null) { + mRoot.lockRenderThread(); + } + } + + public void unlockRendering() { + if (mRoot != null) { + mRoot.unlockRenderThread(); + } + } + + // This is for debugging only. + // Dump the view hierarchy into log. + void dumpTree(String prefix) { + Log.d(TAG, prefix + getClass().getSimpleName()); + for (int i = 0, n = getComponentCount(); i < n; ++i) { + getComponent(i).dumpTree(prefix + "...."); + } + } +} diff --git a/src/com/android/gallery3d/ui/GestureRecognizer.java b/src/com/android/gallery3d/ui/GestureRecognizer.java new file mode 100644 index 000000000..1e5250b9b --- /dev/null +++ b/src/com/android/gallery3d/ui/GestureRecognizer.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2012 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.ui; + +import android.content.Context; +import android.os.SystemClock; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; + +// This class aggregates three gesture detectors: GestureDetector, +// ScaleGestureDetector, and DownUpDetector. +public class GestureRecognizer { + @SuppressWarnings("unused") + private static final String TAG = "GestureRecognizer"; + + public interface Listener { + boolean onSingleTapUp(float x, float y); + boolean onDoubleTap(float x, float y); + boolean onScroll(float dx, float dy, float totalX, float totalY); + boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY); + boolean onScaleBegin(float focusX, float focusY); + boolean onScale(float focusX, float focusY, float scale); + void onScaleEnd(); + void onDown(float x, float y); + void onUp(); + } + + private final GestureDetector mGestureDetector; + private final ScaleGestureDetector mScaleDetector; + private final DownUpDetector mDownUpDetector; + private final Listener mListener; + + public GestureRecognizer(Context context, Listener listener) { + mListener = listener; + mGestureDetector = new GestureDetector(context, new MyGestureListener(), + null, true /* ignoreMultitouch */); + mScaleDetector = new ScaleGestureDetector( + context, new MyScaleListener()); + mDownUpDetector = new DownUpDetector(new MyDownUpListener()); + } + + public void onTouchEvent(MotionEvent event) { + mGestureDetector.onTouchEvent(event); + mScaleDetector.onTouchEvent(event); + mDownUpDetector.onTouchEvent(event); + } + + public boolean isDown() { + return mDownUpDetector.isDown(); + } + + public void cancelScale() { + long now = SystemClock.uptimeMillis(); + MotionEvent cancelEvent = MotionEvent.obtain( + now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0); + mScaleDetector.onTouchEvent(cancelEvent); + cancelEvent.recycle(); + } + + private class MyGestureListener + extends GestureDetector.SimpleOnGestureListener { + @Override + public boolean onSingleTapUp(MotionEvent e) { + return mListener.onSingleTapUp(e.getX(), e.getY()); + } + + @Override + public boolean onDoubleTap(MotionEvent e) { + return mListener.onDoubleTap(e.getX(), e.getY()); + } + + @Override + public boolean onScroll( + MotionEvent e1, MotionEvent e2, float dx, float dy) { + return mListener.onScroll( + dx, dy, e2.getX() - e1.getX(), e2.getY() - e1.getY()); + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, + float velocityY) { + return mListener.onFling(e1, e2, velocityX, velocityY); + } + } + + private class MyScaleListener + extends ScaleGestureDetector.SimpleOnScaleGestureListener { + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + return mListener.onScaleBegin( + detector.getFocusX(), detector.getFocusY()); + } + + @Override + public boolean onScale(ScaleGestureDetector detector) { + return mListener.onScale(detector.getFocusX(), + detector.getFocusY(), detector.getScaleFactor()); + } + + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + mListener.onScaleEnd(); + } + } + + private class MyDownUpListener implements DownUpDetector.DownUpListener { + @Override + public void onDown(MotionEvent e) { + mListener.onDown(e.getX(), e.getY()); + } + + @Override + public void onUp(MotionEvent e) { + mListener.onUp(); + } + } +} diff --git a/src/com/android/gallery3d/ui/Log.java b/src/com/android/gallery3d/ui/Log.java new file mode 100644 index 000000000..5570763bb --- /dev/null +++ b/src/com/android/gallery3d/ui/Log.java @@ -0,0 +1,54 @@ +/* + * 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.ui; + +// TODO: Delete this +public class Log { + public static int v(String tag, String msg) { + return android.util.Log.v(tag, msg); + } + public static int v(String tag, String msg, Throwable tr) { + return android.util.Log.v(tag, msg, tr); + } + public static int d(String tag, String msg) { + return android.util.Log.d(tag, msg); + } + public static int d(String tag, String msg, Throwable tr) { + return android.util.Log.d(tag, msg, tr); + } + public static int i(String tag, String msg) { + return android.util.Log.i(tag, msg); + } + public static int i(String tag, String msg, Throwable tr) { + return android.util.Log.i(tag, msg, tr); + } + public static int w(String tag, String msg) { + return android.util.Log.w(tag, msg); + } + public static int w(String tag, String msg, Throwable tr) { + return android.util.Log.w(tag, msg, tr); + } + public static int w(String tag, Throwable tr) { + return android.util.Log.w(tag, tr); + } + public static int e(String tag, String msg) { + return android.util.Log.e(tag, msg); + } + public static int e(String tag, String msg, Throwable tr) { + return android.util.Log.e(tag, msg, tr); + } +} diff --git a/src/com/android/gallery3d/ui/ManageCacheDrawer.java b/src/com/android/gallery3d/ui/ManageCacheDrawer.java new file mode 100644 index 000000000..d210bd1f1 --- /dev/null +++ b/src/com/android/gallery3d/ui/ManageCacheDrawer.java @@ -0,0 +1,116 @@ +/* + * 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.ui; + +import android.content.Context; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.AbstractGalleryActivity; +import com.android.gallery3d.data.DataSourceType; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.glrenderer.GLCanvas; +import com.android.gallery3d.glrenderer.ResourceTexture; +import com.android.gallery3d.glrenderer.StringTexture; +import com.android.gallery3d.ui.AlbumSetSlidingWindow.AlbumSetEntry; + +public class ManageCacheDrawer extends AlbumSetSlotRenderer { + private final ResourceTexture mCheckedItem; + private final ResourceTexture mUnCheckedItem; + private final SelectionManager mSelectionManager; + + private final ResourceTexture mLocalAlbumIcon; + private final StringTexture mCachingText; + + private final int mCachePinSize; + private final int mCachePinMargin; + + public ManageCacheDrawer(AbstractGalleryActivity activity, SelectionManager selectionManager, + SlotView slotView, LabelSpec labelSpec, int cachePinSize, int cachePinMargin) { + super(activity, selectionManager, slotView, labelSpec, + activity.getResources().getColor(R.color.cache_placeholder)); + Context context = activity; + mCheckedItem = new ResourceTexture( + context, R.drawable.btn_make_offline_normal_on_holo_dark); + mUnCheckedItem = new ResourceTexture( + context, R.drawable.btn_make_offline_normal_off_holo_dark); + mLocalAlbumIcon = new ResourceTexture( + context, R.drawable.btn_make_offline_disabled_on_holo_dark); + String cachingLabel = context.getString(R.string.caching_label); + mCachingText = StringTexture.newInstance(cachingLabel, 12, 0xffffffff); + mSelectionManager = selectionManager; + mCachePinSize = cachePinSize; + mCachePinMargin = cachePinMargin; + } + + private static boolean isLocal(int dataSourceType) { + return dataSourceType != DataSourceType.TYPE_PICASA; + } + + @Override + public int renderSlot(GLCanvas canvas, int index, int pass, int width, int height) { + AlbumSetEntry entry = mDataWindow.get(index); + + boolean wantCache = entry.cacheFlag == MediaSet.CACHE_FLAG_FULL; + boolean isCaching = wantCache && ( + entry.cacheStatus != MediaSet.CACHE_STATUS_CACHED_FULL); + boolean selected = mSelectionManager.isItemSelected(entry.setPath); + boolean chooseToCache = wantCache ^ selected; + boolean available = isLocal(entry.sourceType) || chooseToCache; + + int renderRequestFlags = 0; + + if (!available) { + canvas.save(GLCanvas.SAVE_FLAG_ALPHA); + canvas.multiplyAlpha(0.6f); + } + renderRequestFlags |= renderContent(canvas, entry, width, height); + if (!available) canvas.restore(); + + renderRequestFlags |= renderLabel(canvas, entry, width, height); + + drawCachingPin(canvas, entry.setPath, + entry.sourceType, isCaching, chooseToCache, width, height); + + renderRequestFlags |= renderOverlay(canvas, index, entry, width, height); + return renderRequestFlags; + } + + private void drawCachingPin(GLCanvas canvas, Path path, int dataSourceType, + boolean isCaching, boolean chooseToCache, int width, int height) { + ResourceTexture icon; + if (isLocal(dataSourceType)) { + icon = mLocalAlbumIcon; + } else if (chooseToCache) { + icon = mCheckedItem; + } else { + icon = mUnCheckedItem; + } + + // show the icon in right bottom + int s = mCachePinSize; + int m = mCachePinMargin; + icon.draw(canvas, width - m - s, height - s, s, s); + + if (isCaching) { + int w = mCachingText.getWidth(); + int h = mCachingText.getHeight(); + // Show the caching text in bottom center + mCachingText.draw(canvas, (width - w) / 2, height - h); + } + } +} diff --git a/src/com/android/gallery3d/ui/MeasureHelper.java b/src/com/android/gallery3d/ui/MeasureHelper.java new file mode 100644 index 000000000..f65dc10b3 --- /dev/null +++ b/src/com/android/gallery3d/ui/MeasureHelper.java @@ -0,0 +1,65 @@ +/* + * 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.ui; + +import android.graphics.Rect; +import android.view.View.MeasureSpec; + +class MeasureHelper { + + private static MeasureHelper sInstance = new MeasureHelper(null); + + private GLView mComponent; + private int mPreferredWidth; + private int mPreferredHeight; + + private MeasureHelper(GLView component) { + mComponent = component; + } + + public static MeasureHelper getInstance(GLView component) { + sInstance.mComponent = component; + return sInstance; + } + + public MeasureHelper setPreferredContentSize(int width, int height) { + mPreferredWidth = width; + mPreferredHeight = height; + return this; + } + + public void measure(int widthSpec, int heightSpec) { + Rect p = mComponent.getPaddings(); + setMeasuredSize( + getLength(widthSpec, mPreferredWidth + p.left + p.right), + getLength(heightSpec, mPreferredHeight + p.top + p.bottom)); + } + + private static int getLength(int measureSpec, int prefered) { + int specLength = MeasureSpec.getSize(measureSpec); + switch(MeasureSpec.getMode(measureSpec)) { + case MeasureSpec.EXACTLY: return specLength; + case MeasureSpec.AT_MOST: return Math.min(prefered, specLength); + default: return prefered; + } + } + + protected void setMeasuredSize(int width, int height) { + mComponent.setMeasuredSize(width, height); + } + +} diff --git a/src/com/android/gallery3d/ui/MenuExecutor.java b/src/com/android/gallery3d/ui/MenuExecutor.java new file mode 100644 index 000000000..29def0527 --- /dev/null +++ b/src/com/android/gallery3d/ui/MenuExecutor.java @@ -0,0 +1,448 @@ +/* + * 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.ui; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnCancelListener; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.os.Handler; +import android.os.Message; +import android.view.Menu; +import android.view.MenuItem; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.AbstractGalleryActivity; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.filtershow.crop.CropActivity; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.GalleryUtils; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import java.util.ArrayList; + +public class MenuExecutor { + @SuppressWarnings("unused") + private static final String TAG = "MenuExecutor"; + + private static final int MSG_TASK_COMPLETE = 1; + private static final int MSG_TASK_UPDATE = 2; + private static final int MSG_TASK_START = 3; + private static final int MSG_DO_SHARE = 4; + + public static final int EXECUTION_RESULT_SUCCESS = 1; + public static final int EXECUTION_RESULT_FAIL = 2; + public static final int EXECUTION_RESULT_CANCEL = 3; + + private ProgressDialog mDialog; + private Future<?> mTask; + // wait the operation to finish when we want to stop it. + private boolean mWaitOnStop; + private boolean mPaused; + + private final AbstractGalleryActivity mActivity; + private final SelectionManager mSelectionManager; + private final Handler mHandler; + + private static ProgressDialog createProgressDialog( + Context context, int titleId, int progressMax) { + ProgressDialog dialog = new ProgressDialog(context); + dialog.setTitle(titleId); + dialog.setMax(progressMax); + dialog.setCancelable(false); + dialog.setIndeterminate(false); + if (progressMax > 1) { + dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); + } + return dialog; + } + + public interface ProgressListener { + public void onConfirmDialogShown(); + public void onConfirmDialogDismissed(boolean confirmed); + public void onProgressStart(); + public void onProgressUpdate(int index); + public void onProgressComplete(int result); + } + + public MenuExecutor( + AbstractGalleryActivity activity, SelectionManager selectionManager) { + mActivity = Utils.checkNotNull(activity); + mSelectionManager = Utils.checkNotNull(selectionManager); + mHandler = new SynchronizedHandler(mActivity.getGLRoot()) { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_TASK_START: { + if (message.obj != null) { + ProgressListener listener = (ProgressListener) message.obj; + listener.onProgressStart(); + } + break; + } + case MSG_TASK_COMPLETE: { + stopTaskAndDismissDialog(); + if (message.obj != null) { + ProgressListener listener = (ProgressListener) message.obj; + listener.onProgressComplete(message.arg1); + } + mSelectionManager.leaveSelectionMode(); + break; + } + case MSG_TASK_UPDATE: { + if (mDialog != null && !mPaused) mDialog.setProgress(message.arg1); + if (message.obj != null) { + ProgressListener listener = (ProgressListener) message.obj; + listener.onProgressUpdate(message.arg1); + } + break; + } + case MSG_DO_SHARE: { + ((Activity) mActivity).startActivity((Intent) message.obj); + break; + } + } + } + }; + } + + private void stopTaskAndDismissDialog() { + if (mTask != null) { + if (!mWaitOnStop) mTask.cancel(); + if (mDialog != null && mDialog.isShowing()) mDialog.dismiss(); + mDialog = null; + mTask = null; + } + } + + public void resume() { + mPaused = false; + if (mDialog != null) mDialog.show(); + } + + public void pause() { + mPaused = true; + if (mDialog != null && mDialog.isShowing()) mDialog.hide(); + } + + public void destroy() { + stopTaskAndDismissDialog(); + } + + private void onProgressUpdate(int index, ProgressListener listener) { + mHandler.sendMessage( + mHandler.obtainMessage(MSG_TASK_UPDATE, index, 0, listener)); + } + + private void onProgressStart(ProgressListener listener) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_TASK_START, listener)); + } + + private void onProgressComplete(int result, ProgressListener listener) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_TASK_COMPLETE, result, 0, listener)); + } + + public static void updateMenuOperation(Menu menu, int supported) { + boolean supportDelete = (supported & MediaObject.SUPPORT_DELETE) != 0; + boolean supportRotate = (supported & MediaObject.SUPPORT_ROTATE) != 0; + boolean supportCrop = (supported & MediaObject.SUPPORT_CROP) != 0; + boolean supportTrim = (supported & MediaObject.SUPPORT_TRIM) != 0; + boolean supportMute = (supported & MediaObject.SUPPORT_MUTE) != 0; + boolean supportShare = (supported & MediaObject.SUPPORT_SHARE) != 0; + boolean supportSetAs = (supported & MediaObject.SUPPORT_SETAS) != 0; + boolean supportShowOnMap = (supported & MediaObject.SUPPORT_SHOW_ON_MAP) != 0; + boolean supportCache = (supported & MediaObject.SUPPORT_CACHE) != 0; + boolean supportEdit = (supported & MediaObject.SUPPORT_EDIT) != 0; + boolean supportInfo = (supported & MediaObject.SUPPORT_INFO) != 0; + + setMenuItemVisible(menu, R.id.action_delete, supportDelete); + setMenuItemVisible(menu, R.id.action_rotate_ccw, supportRotate); + setMenuItemVisible(menu, R.id.action_rotate_cw, supportRotate); + setMenuItemVisible(menu, R.id.action_crop, supportCrop); + setMenuItemVisible(menu, R.id.action_trim, supportTrim); + setMenuItemVisible(menu, R.id.action_mute, supportMute); + // Hide panorama until call to updateMenuForPanorama corrects it + setMenuItemVisible(menu, R.id.action_share_panorama, false); + setMenuItemVisible(menu, R.id.action_share, supportShare); + setMenuItemVisible(menu, R.id.action_setas, supportSetAs); + setMenuItemVisible(menu, R.id.action_show_on_map, supportShowOnMap); + setMenuItemVisible(menu, R.id.action_edit, supportEdit); + setMenuItemVisible(menu, R.id.action_simple_edit, supportEdit); + setMenuItemVisible(menu, R.id.action_details, supportInfo); + } + + public static void updateMenuForPanorama(Menu menu, boolean shareAsPanorama360, + boolean disablePanorama360Options) { + setMenuItemVisible(menu, R.id.action_share_panorama, shareAsPanorama360); + if (disablePanorama360Options) { + setMenuItemVisible(menu, R.id.action_rotate_ccw, false); + setMenuItemVisible(menu, R.id.action_rotate_cw, false); + } + } + + private static void setMenuItemVisible(Menu menu, int itemId, boolean visible) { + MenuItem item = menu.findItem(itemId); + if (item != null) item.setVisible(visible); + } + + private Path getSingleSelectedPath() { + ArrayList<Path> ids = mSelectionManager.getSelected(true); + Utils.assertTrue(ids.size() == 1); + return ids.get(0); + } + + private Intent getIntentBySingleSelectedPath(String action) { + DataManager manager = mActivity.getDataManager(); + Path path = getSingleSelectedPath(); + String mimeType = getMimeType(manager.getMediaType(path)); + return new Intent(action).setDataAndType(manager.getContentUri(path), mimeType); + } + + private void onMenuClicked(int action, ProgressListener listener) { + onMenuClicked(action, listener, false, true); + } + + public void onMenuClicked(int action, ProgressListener listener, + boolean waitOnStop, boolean showDialog) { + int title; + switch (action) { + case R.id.action_select_all: + if (mSelectionManager.inSelectAllMode()) { + mSelectionManager.deSelectAll(); + } else { + mSelectionManager.selectAll(); + } + return; + case R.id.action_crop: { + Intent intent = getIntentBySingleSelectedPath(CropActivity.CROP_ACTION); + ((Activity) mActivity).startActivity(intent); + return; + } + case R.id.action_edit: { + Intent intent = getIntentBySingleSelectedPath(Intent.ACTION_EDIT) + .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + ((Activity) mActivity).startActivity(Intent.createChooser(intent, null)); + return; + } + case R.id.action_setas: { + Intent intent = getIntentBySingleSelectedPath(Intent.ACTION_ATTACH_DATA) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.putExtra("mimeType", intent.getType()); + Activity activity = mActivity; + activity.startActivity(Intent.createChooser( + intent, activity.getString(R.string.set_as))); + return; + } + case R.id.action_delete: + title = R.string.delete; + break; + case R.id.action_rotate_cw: + title = R.string.rotate_right; + break; + case R.id.action_rotate_ccw: + title = R.string.rotate_left; + break; + case R.id.action_show_on_map: + title = R.string.show_on_map; + break; + default: + return; + } + startAction(action, title, listener, waitOnStop, showDialog); + } + + private class ConfirmDialogListener implements OnClickListener, OnCancelListener { + private final int mActionId; + private final ProgressListener mListener; + + public ConfirmDialogListener(int actionId, ProgressListener listener) { + mActionId = actionId; + mListener = listener; + } + + @Override + public void onClick(DialogInterface dialog, int which) { + if (which == DialogInterface.BUTTON_POSITIVE) { + if (mListener != null) { + mListener.onConfirmDialogDismissed(true); + } + onMenuClicked(mActionId, mListener); + } else { + if (mListener != null) { + mListener.onConfirmDialogDismissed(false); + } + } + } + + @Override + public void onCancel(DialogInterface dialog) { + if (mListener != null) { + mListener.onConfirmDialogDismissed(false); + } + } + } + + public void onMenuClicked(MenuItem menuItem, String confirmMsg, + final ProgressListener listener) { + final int action = menuItem.getItemId(); + + if (confirmMsg != null) { + if (listener != null) listener.onConfirmDialogShown(); + ConfirmDialogListener cdl = new ConfirmDialogListener(action, listener); + new AlertDialog.Builder(mActivity.getAndroidContext()) + .setMessage(confirmMsg) + .setOnCancelListener(cdl) + .setPositiveButton(R.string.ok, cdl) + .setNegativeButton(R.string.cancel, cdl) + .create().show(); + } else { + onMenuClicked(action, listener); + } + } + + public void startAction(int action, int title, ProgressListener listener) { + startAction(action, title, listener, false, true); + } + + public void startAction(int action, int title, ProgressListener listener, + boolean waitOnStop, boolean showDialog) { + ArrayList<Path> ids = mSelectionManager.getSelected(false); + stopTaskAndDismissDialog(); + + Activity activity = mActivity; + if (showDialog) { + mDialog = createProgressDialog(activity, title, ids.size()); + mDialog.show(); + } else { + mDialog = null; + } + MediaOperation operation = new MediaOperation(action, ids, listener); + mTask = mActivity.getBatchServiceThreadPoolIfAvailable().submit(operation, null); + mWaitOnStop = waitOnStop; + } + + public void startSingleItemAction(int action, Path targetPath) { + ArrayList<Path> ids = new ArrayList<Path>(1); + ids.add(targetPath); + mDialog = null; + MediaOperation operation = new MediaOperation(action, ids, null); + mTask = mActivity.getBatchServiceThreadPoolIfAvailable().submit(operation, null); + mWaitOnStop = false; + } + + public static String getMimeType(int type) { + switch (type) { + case MediaObject.MEDIA_TYPE_IMAGE : + return GalleryUtils.MIME_TYPE_IMAGE; + case MediaObject.MEDIA_TYPE_VIDEO : + return GalleryUtils.MIME_TYPE_VIDEO; + default: return GalleryUtils.MIME_TYPE_ALL; + } + } + + private boolean execute( + DataManager manager, JobContext jc, int cmd, Path path) { + boolean result = true; + Log.v(TAG, "Execute cmd: " + cmd + " for " + path); + long startTime = System.currentTimeMillis(); + + switch (cmd) { + case R.id.action_delete: + manager.delete(path); + break; + case R.id.action_rotate_cw: + manager.rotate(path, 90); + break; + case R.id.action_rotate_ccw: + manager.rotate(path, -90); + break; + case R.id.action_toggle_full_caching: { + MediaObject obj = manager.getMediaObject(path); + int cacheFlag = obj.getCacheFlag(); + if (cacheFlag == MediaObject.CACHE_FLAG_FULL) { + cacheFlag = MediaObject.CACHE_FLAG_SCREENNAIL; + } else { + cacheFlag = MediaObject.CACHE_FLAG_FULL; + } + obj.cache(cacheFlag); + break; + } + case R.id.action_show_on_map: { + MediaItem item = (MediaItem) manager.getMediaObject(path); + double latlng[] = new double[2]; + item.getLatLong(latlng); + if (GalleryUtils.isValidLocation(latlng[0], latlng[1])) { + GalleryUtils.showOnMap(mActivity, latlng[0], latlng[1]); + } + break; + } + default: + throw new AssertionError(); + } + Log.v(TAG, "It takes " + (System.currentTimeMillis() - startTime) + + " ms to execute cmd for " + path); + return result; + } + + private class MediaOperation implements Job<Void> { + private final ArrayList<Path> mItems; + private final int mOperation; + private final ProgressListener mListener; + + public MediaOperation(int operation, ArrayList<Path> items, + ProgressListener listener) { + mOperation = operation; + mItems = items; + mListener = listener; + } + + @Override + public Void run(JobContext jc) { + int index = 0; + DataManager manager = mActivity.getDataManager(); + int result = EXECUTION_RESULT_SUCCESS; + try { + onProgressStart(mListener); + for (Path id : mItems) { + if (jc.isCancelled()) { + result = EXECUTION_RESULT_CANCEL; + break; + } + if (!execute(manager, jc, mOperation, id)) { + result = EXECUTION_RESULT_FAIL; + } + onProgressUpdate(index++, mListener); + } + } catch (Throwable th) { + Log.e(TAG, "failed to execute operation " + mOperation + + " : " + th); + } finally { + onProgressComplete(result, mListener); + } + return null; + } + } +} diff --git a/src/com/android/gallery3d/ui/OrientationSource.java b/src/com/android/gallery3d/ui/OrientationSource.java new file mode 100644 index 000000000..e13ce1cec --- /dev/null +++ b/src/com/android/gallery3d/ui/OrientationSource.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2012 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.ui; + +public interface OrientationSource { + public int getDisplayRotation(); + public int getCompensation(); +} diff --git a/src/com/android/gallery3d/ui/Paper.java b/src/com/android/gallery3d/ui/Paper.java new file mode 100644 index 000000000..b36f5c3a2 --- /dev/null +++ b/src/com/android/gallery3d/ui/Paper.java @@ -0,0 +1,183 @@ +/* + * 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.ui; + +import android.graphics.Rect; +import android.opengl.Matrix; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; + +import com.android.gallery3d.common.Utils; + +// This class does the overscroll effect. +class Paper { + @SuppressWarnings("unused") + private static final String TAG = "Paper"; + private static final int ROTATE_FACTOR = 4; + private EdgeAnimation mAnimationLeft = new EdgeAnimation(); + private EdgeAnimation mAnimationRight = new EdgeAnimation(); + private int mWidth; + private float[] mMatrix = new float[16]; + + public void overScroll(float distance) { + distance /= mWidth; // make it relative to width + if (distance < 0) { + mAnimationLeft.onPull(-distance); + } else { + mAnimationRight.onPull(distance); + } + } + + public void edgeReached(float velocity) { + velocity /= mWidth; // make it relative to width + if (velocity < 0) { + mAnimationRight.onAbsorb(-velocity); + } else { + mAnimationLeft.onAbsorb(velocity); + } + } + + public void onRelease() { + mAnimationLeft.onRelease(); + mAnimationRight.onRelease(); + } + + public boolean advanceAnimation() { + // Note that we use "|" because we want both animations get updated. + return mAnimationLeft.update() | mAnimationRight.update(); + } + + public void setSize(int width, int height) { + mWidth = width; + } + + public float[] getTransform(Rect rect, float scrollX) { + float left = mAnimationLeft.getValue(); + float right = mAnimationRight.getValue(); + float screenX = rect.centerX() - scrollX; + // We linearly interpolate the value [left, right] for the screenX + // range int [-1/4, 5/4]*mWidth. So if part of the thumbnail is outside + // the screen, we still get some transform. + float x = screenX + mWidth / 4; + int range = 3 * mWidth / 2; + float t = ((range - x) * left - x * right) / range; + // compress t to the range (-1, 1) by the function + // f(t) = (1 / (1 + e^-t) - 0.5) * 2 + // then multiply by 90 to make the range (-45, 45) + float degrees = + (1 / (1 + (float) Math.exp(-t * ROTATE_FACTOR)) - 0.5f) * 2 * -45; + Matrix.setIdentityM(mMatrix, 0); + Matrix.translateM(mMatrix, 0, mMatrix, 0, rect.centerX(), rect.centerY(), 0); + Matrix.rotateM(mMatrix, 0, degrees, 0, 1, 0); + Matrix.translateM(mMatrix, 0, mMatrix, 0, -rect.width() / 2, -rect.height() / 2, 0); + return mMatrix; + } +} + +// This class follows the structure of frameworks's EdgeEffect class. +class EdgeAnimation { + @SuppressWarnings("unused") + private static final String TAG = "EdgeAnimation"; + + private static final int STATE_IDLE = 0; + private static final int STATE_PULL = 1; + private static final int STATE_ABSORB = 2; + private static final int STATE_RELEASE = 3; + + // Time it will take the effect to fully done in ms + private static final int ABSORB_TIME = 200; + private static final int RELEASE_TIME = 500; + + private static final float VELOCITY_FACTOR = 0.1f; + + private final Interpolator mInterpolator; + + private int mState; + private float mValue; + + private float mValueStart; + private float mValueFinish; + private long mStartTime; + private long mDuration; + + public EdgeAnimation() { + mInterpolator = new DecelerateInterpolator(); + mState = STATE_IDLE; + } + + private void startAnimation(float start, float finish, long duration, + int newState) { + mValueStart = start; + mValueFinish = finish; + mDuration = duration; + mStartTime = now(); + mState = newState; + } + + // The deltaDistance's magnitude is in the range of -1 (no change) to 1. + // The value 1 is the full length of the view. Negative values means the + // movement is in the opposite direction. + public void onPull(float deltaDistance) { + if (mState == STATE_ABSORB) return; + mValue = Utils.clamp(mValue + deltaDistance, -1.0f, 1.0f); + mState = STATE_PULL; + } + + public void onRelease() { + if (mState == STATE_IDLE || mState == STATE_ABSORB) return; + startAnimation(mValue, 0, RELEASE_TIME, STATE_RELEASE); + } + + public void onAbsorb(float velocity) { + float finish = Utils.clamp(mValue + velocity * VELOCITY_FACTOR, + -1.0f, 1.0f); + startAnimation(mValue, finish, ABSORB_TIME, STATE_ABSORB); + } + + public boolean update() { + if (mState == STATE_IDLE) return false; + if (mState == STATE_PULL) return true; + + float t = Utils.clamp((float)(now() - mStartTime) / mDuration, 0.0f, 1.0f); + /* Use linear interpolation for absorb, quadratic for others */ + float interp = (mState == STATE_ABSORB) + ? t : mInterpolator.getInterpolation(t); + + mValue = mValueStart + (mValueFinish - mValueStart) * interp; + + if (t >= 1.0f) { + switch (mState) { + case STATE_ABSORB: + startAnimation(mValue, 0, RELEASE_TIME, STATE_RELEASE); + break; + case STATE_RELEASE: + mState = STATE_IDLE; + break; + } + } + + return true; + } + + public float getValue() { + return mValue; + } + + private long now() { + return AnimationTime.get(); + } +} diff --git a/src/com/android/gallery3d/ui/PhotoFallbackEffect.java b/src/com/android/gallery3d/ui/PhotoFallbackEffect.java new file mode 100644 index 000000000..4603285a4 --- /dev/null +++ b/src/com/android/gallery3d/ui/PhotoFallbackEffect.java @@ -0,0 +1,179 @@ +/* + * 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.ui; + +import android.graphics.Rect; +import android.graphics.RectF; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; + +import com.android.gallery3d.anim.Animation; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.glrenderer.GLCanvas; +import com.android.gallery3d.glrenderer.RawTexture; +import com.android.gallery3d.ui.AlbumSlotRenderer.SlotFilter; + +import java.util.ArrayList; + +public class PhotoFallbackEffect extends Animation implements SlotFilter { + + private static final int ANIM_DURATION = 300; + private static final Interpolator ANIM_INTERPOLATE = new DecelerateInterpolator(1.5f); + + public static class Entry { + public int index; + public Path path; + public Rect source; + public Rect dest; + public RawTexture texture; + + public Entry(Path path, Rect source, RawTexture texture) { + this.path = path; + this.source = source; + this.texture = texture; + } + } + + public interface PositionProvider { + public Rect getPosition(int index); + public int getItemIndex(Path path); + } + + private RectF mSource = new RectF(); + private RectF mTarget = new RectF(); + private float mProgress; + private PositionProvider mPositionProvider; + + private ArrayList<Entry> mList = new ArrayList<Entry>(); + + public PhotoFallbackEffect() { + setDuration(ANIM_DURATION); + setInterpolator(ANIM_INTERPOLATE); + } + + public void addEntry(Path path, Rect rect, RawTexture texture) { + mList.add(new Entry(path, rect, texture)); + } + + public Entry getEntry(Path path) { + for (int i = 0, n = mList.size(); i < n; ++i) { + Entry entry = mList.get(i); + if (entry.path == path) return entry; + } + return null; + } + + public boolean draw(GLCanvas canvas) { + boolean more = calculate(AnimationTime.get()); + for (int i = 0, n = mList.size(); i < n; ++i) { + Entry entry = mList.get(i); + if (entry.index < 0) continue; + entry.dest = mPositionProvider.getPosition(entry.index); + drawEntry(canvas, entry); + } + return more; + } + + private void drawEntry(GLCanvas canvas, Entry entry) { + if (!entry.texture.isLoaded()) return; + + int w = entry.texture.getWidth(); + int h = entry.texture.getHeight(); + + Rect s = entry.source; + Rect d = entry.dest; + + // the following calculation is based on d.width() == d.height() + + float p = mProgress; + + float fullScale = (float) d.height() / Math.min(s.width(), s.height()); + float scale = fullScale * p + 1 * (1 - p); + + float cx = d.centerX() * p + s.centerX() * (1 - p); + float cy = d.centerY() * p + s.centerY() * (1 - p); + + float ch = s.height() * scale; + float cw = s.width() * scale; + + if (w > h) { + // draw the center part + mTarget.set(cx - ch / 2, cy - ch / 2, cx + ch / 2, cy + ch / 2); + mSource.set((w - h) / 2, 0, (w + h) / 2, h); + canvas.drawTexture(entry.texture, mSource, mTarget); + + canvas.save(GLCanvas.SAVE_FLAG_ALPHA); + canvas.multiplyAlpha(1 - p); + + // draw the left part + mTarget.set(cx - cw / 2, cy - ch / 2, cx - ch / 2, cy + ch / 2); + mSource.set(0, 0, (w - h) / 2, h); + canvas.drawTexture(entry.texture, mSource, mTarget); + + // draw the right part + mTarget.set(cx + ch / 2, cy - ch / 2, cx + cw / 2, cy + ch / 2); + mSource.set((w + h) / 2, 0, w, h); + canvas.drawTexture(entry.texture, mSource, mTarget); + + canvas.restore(); + } else { + // draw the center part + mTarget.set(cx - cw / 2, cy - cw / 2, cx + cw / 2, cy + cw / 2); + mSource.set(0, (h - w) / 2, w, (h + w) / 2); + canvas.drawTexture(entry.texture, mSource, mTarget); + + canvas.save(GLCanvas.SAVE_FLAG_ALPHA); + canvas.multiplyAlpha(1 - p); + + // draw the upper part + mTarget.set(cx - cw / 2, cy - ch / 2, cx + cw / 2, cy - cw / 2); + mSource.set(0, 0, w, (h - w) / 2); + canvas.drawTexture(entry.texture, mSource, mTarget); + + // draw the bottom part + mTarget.set(cx - cw / 2, cy + cw / 2, cx + cw / 2, cy + ch / 2); + mSource.set(0, (w + h) / 2, w, h); + canvas.drawTexture(entry.texture, mSource, mTarget); + + canvas.restore(); + } + } + + @Override + protected void onCalculate(float progress) { + mProgress = progress; + } + + public void setPositionProvider(PositionProvider provider) { + mPositionProvider = provider; + if (mPositionProvider != null) { + for (int i = 0, n = mList.size(); i < n; ++i) { + Entry entry = mList.get(i); + entry.index = mPositionProvider.getItemIndex(entry.path); + } + } + } + + @Override + public boolean acceptSlot(int index) { + for (int i = 0, n = mList.size(); i < n; ++i) { + Entry entry = mList.get(i); + if (entry.index == index) return false; + } + return true; + } +} diff --git a/src/com/android/gallery3d/ui/PhotoView.java b/src/com/android/gallery3d/ui/PhotoView.java new file mode 100644 index 000000000..7afa20348 --- /dev/null +++ b/src/com/android/gallery3d/ui/PhotoView.java @@ -0,0 +1,1858 @@ +/* + * 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.ui; + +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.os.Build; +import android.os.Message; +import android.util.FloatMath; +import android.view.MotionEvent; +import android.view.View.MeasureSpec; +import android.view.animation.AccelerateInterpolator; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.AbstractGalleryActivity; +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.glrenderer.GLCanvas; +import com.android.gallery3d.glrenderer.RawTexture; +import com.android.gallery3d.glrenderer.ResourceTexture; +import com.android.gallery3d.glrenderer.StringTexture; +import com.android.gallery3d.glrenderer.Texture; +import com.android.gallery3d.util.GalleryUtils; +import com.android.gallery3d.util.RangeArray; +import com.android.gallery3d.util.UsageStatistics; + +public class PhotoView extends GLView { + @SuppressWarnings("unused") + private static final String TAG = "PhotoView"; + private final int mPlaceholderColor; + + public static final int INVALID_SIZE = -1; + public static final long INVALID_DATA_VERSION = + MediaObject.INVALID_DATA_VERSION; + + public static class Size { + public int width; + public int height; + } + + public interface Model extends TileImageView.TileSource { + public int getCurrentIndex(); + public void moveTo(int index); + + // Returns the size for the specified picture. If the size information is + // not avaiable, width = height = 0. + public void getImageSize(int offset, Size size); + + // Returns the media item for the specified picture. + public MediaItem getMediaItem(int offset); + + // Returns the rotation for the specified picture. + public int getImageRotation(int offset); + + // This amends the getScreenNail() method of TileImageView.Model to get + // ScreenNail at previous (negative offset) or next (positive offset) + // positions. Returns null if the specified ScreenNail is unavailable. + public ScreenNail getScreenNail(int offset); + + // Set this to true if we need the model to provide full images. + public void setNeedFullImage(boolean enabled); + + // Returns true if the item is the Camera preview. + public boolean isCamera(int offset); + + // Returns true if the item is the Panorama. + public boolean isPanorama(int offset); + + // Returns true if the item is a static image that represents camera + // preview. + public boolean isStaticCamera(int offset); + + // Returns true if the item is a Video. + public boolean isVideo(int offset); + + // Returns true if the item can be deleted. + public boolean isDeletable(int offset); + + public static final int LOADING_INIT = 0; + public static final int LOADING_COMPLETE = 1; + public static final int LOADING_FAIL = 2; + + public int getLoadingState(int offset); + + // When data change happens, we need to decide which MediaItem to focus + // on. + // + // 1. If focus hint path != null, we try to focus on it if we can find + // it. This is used for undo a deletion, so we can focus on the + // undeleted item. + // + // 2. Otherwise try to focus on the MediaItem that is currently focused, + // if we can find it. + // + // 3. Otherwise try to focus on the previous MediaItem or the next + // MediaItem, depending on the value of focus hint direction. + public static final int FOCUS_HINT_NEXT = 0; + public static final int FOCUS_HINT_PREVIOUS = 1; + public void setFocusHintDirection(int direction); + public void setFocusHintPath(Path path); + } + + public interface Listener { + public void onSingleTapUp(int x, int y); + public void onFullScreenChanged(boolean full); + public void onActionBarAllowed(boolean allowed); + public void onActionBarWanted(); + public void onCurrentImageUpdated(); + public void onDeleteImage(Path path, int offset); + public void onUndoDeleteImage(); + public void onCommitDeleteImage(); + public void onFilmModeChanged(boolean enabled); + public void onPictureCenter(boolean isCamera); + public void onUndoBarVisibilityChanged(boolean visible); + } + + // The rules about orientation locking: + // + // (1) We need to lock the orientation if we are in page mode camera + // preview, so there is no (unwanted) rotation animation when the user + // rotates the device. + // + // (2) We need to unlock the orientation if we want to show the action bar + // because the action bar follows the system orientation. + // + // The rules about action bar: + // + // (1) If we are in film mode, we don't show action bar. + // + // (2) If we go from camera to gallery with capture animation, we show + // action bar. + private static final int MSG_CANCEL_EXTRA_SCALING = 2; + private static final int MSG_SWITCH_FOCUS = 3; + private static final int MSG_CAPTURE_ANIMATION_DONE = 4; + private static final int MSG_DELETE_ANIMATION_DONE = 5; + private static final int MSG_DELETE_DONE = 6; + private static final int MSG_UNDO_BAR_TIMEOUT = 7; + private static final int MSG_UNDO_BAR_FULL_CAMERA = 8; + + private static final float SWIPE_THRESHOLD = 300f; + + private static final float DEFAULT_TEXT_SIZE = 20; + private static float TRANSITION_SCALE_FACTOR = 0.74f; + private static final int ICON_RATIO = 6; + + // whether we want to apply card deck effect in page mode. + private static final boolean CARD_EFFECT = true; + + // whether we want to apply offset effect in film mode. + private static final boolean OFFSET_EFFECT = true; + + // Used to calculate the scaling factor for the card deck effect. + private ZInterpolator mScaleInterpolator = new ZInterpolator(0.5f); + + // Used to calculate the alpha factor for the fading animation. + private AccelerateInterpolator mAlphaInterpolator = + new AccelerateInterpolator(0.9f); + + // We keep this many previous ScreenNails. (also this many next ScreenNails) + public static final int SCREEN_NAIL_MAX = 3; + + // These are constants for the delete gesture. + private static final int SWIPE_ESCAPE_VELOCITY = 500; // dp/sec + private static final int MAX_DISMISS_VELOCITY = 2500; // dp/sec + private static final int SWIPE_ESCAPE_DISTANCE = 150; // dp + + // The picture entries, the valid index is from -SCREEN_NAIL_MAX to + // SCREEN_NAIL_MAX. + private final RangeArray<Picture> mPictures = + new RangeArray<Picture>(-SCREEN_NAIL_MAX, SCREEN_NAIL_MAX); + private Size[] mSizes = new Size[2 * SCREEN_NAIL_MAX + 1]; + + private final MyGestureListener mGestureListener; + private final GestureRecognizer mGestureRecognizer; + private final PositionController mPositionController; + + private Listener mListener; + private Model mModel; + private StringTexture mNoThumbnailText; + private TileImageView mTileView; + private EdgeView mEdgeView; + private UndoBarView mUndoBar; + private Texture mVideoPlayIcon; + + private SynchronizedHandler mHandler; + + private boolean mCancelExtraScalingPending; + private boolean mFilmMode = false; + private boolean mWantPictureCenterCallbacks = false; + private int mDisplayRotation = 0; + private int mCompensation = 0; + private boolean mFullScreenCamera; + private Rect mCameraRelativeFrame = new Rect(); + private Rect mCameraRect = new Rect(); + private boolean mFirst = true; + + // [mPrevBound, mNextBound] is the range of index for all pictures in the + // model, if we assume the index of current focused picture is 0. So if + // there are some previous pictures, mPrevBound < 0, and if there are some + // next pictures, mNextBound > 0. + private int mPrevBound; + private int mNextBound; + + // This variable prevents us doing snapback until its values goes to 0. This + // happens if the user gesture is still in progress or we are in a capture + // animation. + private int mHolding; + private static final int HOLD_TOUCH_DOWN = 1; + private static final int HOLD_CAPTURE_ANIMATION = 2; + private static final int HOLD_DELETE = 4; + + // mTouchBoxIndex is the index of the box that is touched by the down + // gesture in film mode. The value Integer.MAX_VALUE means no box was + // touched. + private int mTouchBoxIndex = Integer.MAX_VALUE; + // Whether the box indicated by mTouchBoxIndex is deletable. Only meaningful + // if mTouchBoxIndex is not Integer.MAX_VALUE. + private boolean mTouchBoxDeletable; + // This is the index of the last deleted item. This is only used as a hint + // to hide the undo button when we are too far away from the deleted + // item. The value Integer.MAX_VALUE means there is no such hint. + private int mUndoIndexHint = Integer.MAX_VALUE; + + private Context mContext; + + public PhotoView(AbstractGalleryActivity activity) { + mTileView = new TileImageView(activity); + addComponent(mTileView); + mContext = activity.getAndroidContext(); + mPlaceholderColor = mContext.getResources().getColor( + R.color.photo_placeholder); + mEdgeView = new EdgeView(mContext); + addComponent(mEdgeView); + mUndoBar = new UndoBarView(mContext); + addComponent(mUndoBar); + mUndoBar.setVisibility(GLView.INVISIBLE); + mUndoBar.setOnClickListener(new OnClickListener() { + @Override + public void onClick(GLView v) { + mListener.onUndoDeleteImage(); + hideUndoBar(); + } + }); + mNoThumbnailText = StringTexture.newInstance( + mContext.getString(R.string.no_thumbnail), + DEFAULT_TEXT_SIZE, Color.WHITE); + + mHandler = new MyHandler(activity.getGLRoot()); + + mGestureListener = new MyGestureListener(); + mGestureRecognizer = new GestureRecognizer(mContext, mGestureListener); + + mPositionController = new PositionController(mContext, + new PositionController.Listener() { + + @Override + public void invalidate() { + PhotoView.this.invalidate(); + } + + @Override + public boolean isHoldingDown() { + return (mHolding & HOLD_TOUCH_DOWN) != 0; + } + + @Override + public boolean isHoldingDelete() { + return (mHolding & HOLD_DELETE) != 0; + } + + @Override + public void onPull(int offset, int direction) { + mEdgeView.onPull(offset, direction); + } + + @Override + public void onRelease() { + mEdgeView.onRelease(); + } + + @Override + public void onAbsorb(int velocity, int direction) { + mEdgeView.onAbsorb(velocity, direction); + } + }); + mVideoPlayIcon = new ResourceTexture(mContext, R.drawable.ic_control_play); + for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) { + if (i == 0) { + mPictures.put(i, new FullPicture()); + } else { + mPictures.put(i, new ScreenNailPicture(i)); + } + } + } + + public void stopScrolling() { + mPositionController.stopScrolling(); + } + + public void setModel(Model model) { + mModel = model; + mTileView.setModel(mModel); + } + + class MyHandler extends SynchronizedHandler { + public MyHandler(GLRoot root) { + super(root); + } + + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_CANCEL_EXTRA_SCALING: { + mGestureRecognizer.cancelScale(); + mPositionController.setExtraScalingRange(false); + mCancelExtraScalingPending = false; + break; + } + case MSG_SWITCH_FOCUS: { + switchFocus(); + break; + } + case MSG_CAPTURE_ANIMATION_DONE: { + // message.arg1 is the offset parameter passed to + // switchWithCaptureAnimation(). + captureAnimationDone(message.arg1); + break; + } + case MSG_DELETE_ANIMATION_DONE: { + // message.obj is the Path of the MediaItem which should be + // deleted. message.arg1 is the offset of the image. + mListener.onDeleteImage((Path) message.obj, message.arg1); + // Normally a box which finishes delete animation will hold + // position until the underlying MediaItem is actually + // deleted, and HOLD_DELETE will be cancelled that time. In + // case the MediaItem didn't actually get deleted in 2 + // seconds, we will cancel HOLD_DELETE and make it bounce + // back. + + // We make sure there is at most one MSG_DELETE_DONE + // in the handler. + mHandler.removeMessages(MSG_DELETE_DONE); + Message m = mHandler.obtainMessage(MSG_DELETE_DONE); + mHandler.sendMessageDelayed(m, 2000); + + int numberOfPictures = mNextBound - mPrevBound + 1; + if (numberOfPictures == 2) { + if (mModel.isCamera(mNextBound) + || mModel.isCamera(mPrevBound)) { + numberOfPictures--; + } + } + showUndoBar(numberOfPictures <= 1); + break; + } + case MSG_DELETE_DONE: { + if (!mHandler.hasMessages(MSG_DELETE_ANIMATION_DONE)) { + mHolding &= ~HOLD_DELETE; + snapback(); + } + break; + } + case MSG_UNDO_BAR_TIMEOUT: { + checkHideUndoBar(UNDO_BAR_TIMEOUT); + break; + } + case MSG_UNDO_BAR_FULL_CAMERA: { + checkHideUndoBar(UNDO_BAR_FULL_CAMERA); + break; + } + default: throw new AssertionError(message.what); + } + } + } + + public void setWantPictureCenterCallbacks(boolean wanted) { + mWantPictureCenterCallbacks = wanted; + } + + //////////////////////////////////////////////////////////////////////////// + // Data/Image change notifications + //////////////////////////////////////////////////////////////////////////// + + public void notifyDataChange(int[] fromIndex, int prevBound, int nextBound) { + mPrevBound = prevBound; + mNextBound = nextBound; + + // Update mTouchBoxIndex + if (mTouchBoxIndex != Integer.MAX_VALUE) { + int k = mTouchBoxIndex; + mTouchBoxIndex = Integer.MAX_VALUE; + for (int i = 0; i < 2 * SCREEN_NAIL_MAX + 1; i++) { + if (fromIndex[i] == k) { + mTouchBoxIndex = i - SCREEN_NAIL_MAX; + break; + } + } + } + + // Hide undo button if we are too far away + if (mUndoIndexHint != Integer.MAX_VALUE) { + if (Math.abs(mUndoIndexHint - mModel.getCurrentIndex()) >= 3) { + hideUndoBar(); + } + } + + // Update the ScreenNails. + for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) { + Picture p = mPictures.get(i); + p.reload(); + mSizes[i + SCREEN_NAIL_MAX] = p.getSize(); + } + + boolean wasDeleting = mPositionController.hasDeletingBox(); + + // Move the boxes + mPositionController.moveBox(fromIndex, mPrevBound < 0, mNextBound > 0, + mModel.isCamera(0), mSizes); + + for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) { + setPictureSize(i); + } + + boolean isDeleting = mPositionController.hasDeletingBox(); + + // If the deletion is done, make HOLD_DELETE persist for only the time + // needed for a snapback animation. + if (wasDeleting && !isDeleting) { + mHandler.removeMessages(MSG_DELETE_DONE); + Message m = mHandler.obtainMessage(MSG_DELETE_DONE); + mHandler.sendMessageDelayed( + m, PositionController.SNAPBACK_ANIMATION_TIME); + } + + invalidate(); + } + + public boolean isDeleting() { + return (mHolding & HOLD_DELETE) != 0 + && mPositionController.hasDeletingBox(); + } + + public void notifyImageChange(int index) { + if (index == 0) { + mListener.onCurrentImageUpdated(); + } + mPictures.get(index).reload(); + setPictureSize(index); + invalidate(); + } + + private void setPictureSize(int index) { + Picture p = mPictures.get(index); + mPositionController.setImageSize(index, p.getSize(), + index == 0 && p.isCamera() ? mCameraRect : null); + } + + @Override + protected void onLayout( + boolean changeSize, int left, int top, int right, int bottom) { + int w = right - left; + int h = bottom - top; + mTileView.layout(0, 0, w, h); + mEdgeView.layout(0, 0, w, h); + mUndoBar.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + mUndoBar.layout(0, h - mUndoBar.getMeasuredHeight(), w, h); + + GLRoot root = getGLRoot(); + int displayRotation = root.getDisplayRotation(); + int compensation = root.getCompensation(); + if (mDisplayRotation != displayRotation + || mCompensation != compensation) { + mDisplayRotation = displayRotation; + mCompensation = compensation; + + // We need to change the size and rotation of the Camera ScreenNail, + // but we don't want it to animate because the size doen't actually + // change in the eye of the user. + for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) { + Picture p = mPictures.get(i); + if (p.isCamera()) { + p.forceSize(); + } + } + } + + updateCameraRect(); + mPositionController.setConstrainedFrame(mCameraRect); + if (changeSize) { + mPositionController.setViewSize(getWidth(), getHeight()); + } + } + + // Update the camera rectangle due to layout change or camera relative frame + // change. + private void updateCameraRect() { + // Get the width and height in framework orientation because the given + // mCameraRelativeFrame is in that coordinates. + int w = getWidth(); + int h = getHeight(); + if (mCompensation % 180 != 0) { + int tmp = w; + w = h; + h = tmp; + } + int l = mCameraRelativeFrame.left; + int t = mCameraRelativeFrame.top; + int r = mCameraRelativeFrame.right; + int b = mCameraRelativeFrame.bottom; + + // Now convert it to the coordinates we are using. + switch (mCompensation) { + case 0: mCameraRect.set(l, t, r, b); break; + case 90: mCameraRect.set(h - b, l, h - t, r); break; + case 180: mCameraRect.set(w - r, h - b, w - l, h - t); break; + case 270: mCameraRect.set(t, w - r, b, w - l); break; + } + + Log.d(TAG, "compensation = " + mCompensation + + ", CameraRelativeFrame = " + mCameraRelativeFrame + + ", mCameraRect = " + mCameraRect); + } + + public void setCameraRelativeFrame(Rect frame) { + mCameraRelativeFrame.set(frame); + updateCameraRect(); + // Originally we do + // mPositionController.setConstrainedFrame(mCameraRect); + // here, but it is moved to a parameter of the setImageSize() call, so + // it can be updated atomically with the CameraScreenNail's size change. + } + + // Returns the rotation we need to do to the camera texture before drawing + // it to the canvas, assuming the camera texture is correct when the device + // is in its natural orientation. + private int getCameraRotation() { + return (mCompensation - mDisplayRotation + 360) % 360; + } + + private int getPanoramaRotation() { + // This function is magic + // The issue here is that Pano makes bad assumptions about rotation and + // orientation. The first is it assumes only two rotations are possible, + // 0 and 90. Thus, if display rotation is >= 180, we invert the output. + // The second is that it assumes landscape is a 90 rotation from portrait, + // however on landscape devices this is not true. Thus, if we are in portrait + // on a landscape device, we need to invert the output + int orientation = mContext.getResources().getConfiguration().orientation; + boolean invertPortrait = (orientation == Configuration.ORIENTATION_PORTRAIT + && (mDisplayRotation == 90 || mDisplayRotation == 270)); + boolean invert = (mDisplayRotation >= 180); + if (invert != invertPortrait) { + return (mCompensation + 180) % 360; + } + return mCompensation; + } + + //////////////////////////////////////////////////////////////////////////// + // Pictures + //////////////////////////////////////////////////////////////////////////// + + private interface Picture { + void reload(); + void draw(GLCanvas canvas, Rect r); + void setScreenNail(ScreenNail s); + boolean isCamera(); // whether the picture is a camera preview + boolean isDeletable(); // whether the picture can be deleted + void forceSize(); // called when mCompensation changes + Size getSize(); + } + + class FullPicture implements Picture { + private int mRotation; + private boolean mIsCamera; + private boolean mIsPanorama; + private boolean mIsStaticCamera; + private boolean mIsVideo; + private boolean mIsDeletable; + private int mLoadingState = Model.LOADING_INIT; + private Size mSize = new Size(); + + @Override + public void reload() { + // mImageWidth and mImageHeight will get updated + mTileView.notifyModelInvalidated(); + + mIsCamera = mModel.isCamera(0); + mIsPanorama = mModel.isPanorama(0); + mIsStaticCamera = mModel.isStaticCamera(0); + mIsVideo = mModel.isVideo(0); + mIsDeletable = mModel.isDeletable(0); + mLoadingState = mModel.getLoadingState(0); + setScreenNail(mModel.getScreenNail(0)); + updateSize(); + } + + @Override + public Size getSize() { + return mSize; + } + + @Override + public void forceSize() { + updateSize(); + mPositionController.forceImageSize(0, mSize); + } + + private void updateSize() { + if (mIsPanorama) { + mRotation = getPanoramaRotation(); + } else if (mIsCamera && !mIsStaticCamera) { + mRotation = getCameraRotation(); + } else { + mRotation = mModel.getImageRotation(0); + } + + int w = mTileView.mImageWidth; + int h = mTileView.mImageHeight; + mSize.width = getRotated(mRotation, w, h); + mSize.height = getRotated(mRotation, h, w); + } + + @Override + public void draw(GLCanvas canvas, Rect r) { + drawTileView(canvas, r); + + // We want to have the following transitions: + // (1) Move camera preview out of its place: switch to film mode + // (2) Move camera preview into its place: switch to page mode + // The extra mWasCenter check makes sure (1) does not apply if in + // page mode, we move _to_ the camera preview from another picture. + + // Holdings except touch-down prevent the transitions. + if ((mHolding & ~HOLD_TOUCH_DOWN) != 0) return; + + if (mWantPictureCenterCallbacks && mPositionController.isCenter()) { + mListener.onPictureCenter(mIsCamera); + } + } + + @Override + public void setScreenNail(ScreenNail s) { + mTileView.setScreenNail(s); + } + + @Override + public boolean isCamera() { + return mIsCamera; + } + + @Override + public boolean isDeletable() { + return mIsDeletable; + } + + private void drawTileView(GLCanvas canvas, Rect r) { + float imageScale = mPositionController.getImageScale(); + int viewW = getWidth(); + int viewH = getHeight(); + float cx = r.exactCenterX(); + float cy = r.exactCenterY(); + float scale = 1f; // the scaling factor due to card effect + + canvas.save(GLCanvas.SAVE_FLAG_MATRIX | GLCanvas.SAVE_FLAG_ALPHA); + float filmRatio = mPositionController.getFilmRatio(); + boolean wantsCardEffect = CARD_EFFECT && !mIsCamera + && filmRatio != 1f && !mPictures.get(-1).isCamera() + && !mPositionController.inOpeningAnimation(); + boolean wantsOffsetEffect = OFFSET_EFFECT && mIsDeletable + && filmRatio == 1f && r.centerY() != viewH / 2; + if (wantsCardEffect) { + // Calculate the move-out progress value. + int left = r.left; + int right = r.right; + float progress = calculateMoveOutProgress(left, right, viewW); + progress = Utils.clamp(progress, -1f, 1f); + + // We only want to apply the fading animation if the scrolling + // movement is to the right. + if (progress < 0) { + scale = getScrollScale(progress); + float alpha = getScrollAlpha(progress); + scale = interpolate(filmRatio, scale, 1f); + alpha = interpolate(filmRatio, alpha, 1f); + + imageScale *= scale; + canvas.multiplyAlpha(alpha); + + float cxPage; // the cx value in page mode + if (right - left <= viewW) { + // If the picture is narrower than the view, keep it at + // the center of the view. + cxPage = viewW / 2f; + } else { + // If the picture is wider than the view (it's + // zoomed-in), keep the left edge of the object align + // the the left edge of the view. + cxPage = (right - left) * scale / 2f; + } + cx = interpolate(filmRatio, cxPage, cx); + } + } else if (wantsOffsetEffect) { + float offset = (float) (r.centerY() - viewH / 2) / viewH; + float alpha = getOffsetAlpha(offset); + canvas.multiplyAlpha(alpha); + } + + // Draw the tile view. + setTileViewPosition(cx, cy, viewW, viewH, imageScale); + renderChild(canvas, mTileView); + + // Draw the play video icon and the message. + canvas.translate((int) (cx + 0.5f), (int) (cy + 0.5f)); + int s = (int) (scale * Math.min(r.width(), r.height()) + 0.5f); + if (mIsVideo) drawVideoPlayIcon(canvas, s); + if (mLoadingState == Model.LOADING_FAIL) { + drawLoadingFailMessage(canvas); + } + + // Draw a debug indicator showing which picture has focus (index == + // 0). + //canvas.fillRect(-10, -10, 20, 20, 0x80FF00FF); + + canvas.restore(); + } + + // Set the position of the tile view + private void setTileViewPosition(float cx, float cy, + int viewW, int viewH, float scale) { + // Find out the bitmap coordinates of the center of the view + int imageW = mPositionController.getImageWidth(); + int imageH = mPositionController.getImageHeight(); + int centerX = (int) (imageW / 2f + (viewW / 2f - cx) / scale + 0.5f); + int centerY = (int) (imageH / 2f + (viewH / 2f - cy) / scale + 0.5f); + + int inverseX = imageW - centerX; + int inverseY = imageH - centerY; + int x, y; + switch (mRotation) { + case 0: x = centerX; y = centerY; break; + case 90: x = centerY; y = inverseX; break; + case 180: x = inverseX; y = inverseY; break; + case 270: x = inverseY; y = centerX; break; + default: + throw new RuntimeException(String.valueOf(mRotation)); + } + mTileView.setPosition(x, y, scale, mRotation); + } + } + + private class ScreenNailPicture implements Picture { + private int mIndex; + private int mRotation; + private ScreenNail mScreenNail; + private boolean mIsCamera; + private boolean mIsPanorama; + private boolean mIsStaticCamera; + private boolean mIsVideo; + private boolean mIsDeletable; + private int mLoadingState = Model.LOADING_INIT; + private Size mSize = new Size(); + + public ScreenNailPicture(int index) { + mIndex = index; + } + + @Override + public void reload() { + mIsCamera = mModel.isCamera(mIndex); + mIsPanorama = mModel.isPanorama(mIndex); + mIsStaticCamera = mModel.isStaticCamera(mIndex); + mIsVideo = mModel.isVideo(mIndex); + mIsDeletable = mModel.isDeletable(mIndex); + mLoadingState = mModel.getLoadingState(mIndex); + setScreenNail(mModel.getScreenNail(mIndex)); + updateSize(); + } + + @Override + public Size getSize() { + return mSize; + } + + @Override + public void draw(GLCanvas canvas, Rect r) { + if (mScreenNail == null) { + // Draw a placeholder rectange if there should be a picture in + // this position (but somehow there isn't). + if (mIndex >= mPrevBound && mIndex <= mNextBound) { + drawPlaceHolder(canvas, r); + } + return; + } + int w = getWidth(); + int h = getHeight(); + if (r.left >= w || r.right <= 0 || r.top >= h || r.bottom <= 0) { + mScreenNail.noDraw(); + return; + } + + float filmRatio = mPositionController.getFilmRatio(); + boolean wantsCardEffect = CARD_EFFECT && mIndex > 0 + && filmRatio != 1f && !mPictures.get(0).isCamera(); + boolean wantsOffsetEffect = OFFSET_EFFECT && mIsDeletable + && filmRatio == 1f && r.centerY() != h / 2; + int cx = wantsCardEffect + ? (int) (interpolate(filmRatio, w / 2, r.centerX()) + 0.5f) + : r.centerX(); + int cy = r.centerY(); + canvas.save(GLCanvas.SAVE_FLAG_MATRIX | GLCanvas.SAVE_FLAG_ALPHA); + canvas.translate(cx, cy); + if (wantsCardEffect) { + float progress = (float) (w / 2 - r.centerX()) / w; + progress = Utils.clamp(progress, -1, 1); + float alpha = getScrollAlpha(progress); + float scale = getScrollScale(progress); + alpha = interpolate(filmRatio, alpha, 1f); + scale = interpolate(filmRatio, scale, 1f); + canvas.multiplyAlpha(alpha); + canvas.scale(scale, scale, 1); + } else if (wantsOffsetEffect) { + float offset = (float) (r.centerY() - h / 2) / h; + float alpha = getOffsetAlpha(offset); + canvas.multiplyAlpha(alpha); + } + if (mRotation != 0) { + canvas.rotate(mRotation, 0, 0, 1); + } + int drawW = getRotated(mRotation, r.width(), r.height()); + int drawH = getRotated(mRotation, r.height(), r.width()); + mScreenNail.draw(canvas, -drawW / 2, -drawH / 2, drawW, drawH); + if (isScreenNailAnimating()) { + invalidate(); + } + int s = Math.min(drawW, drawH); + if (mIsVideo) drawVideoPlayIcon(canvas, s); + if (mLoadingState == Model.LOADING_FAIL) { + drawLoadingFailMessage(canvas); + } + canvas.restore(); + } + + private boolean isScreenNailAnimating() { + return (mScreenNail instanceof TiledScreenNail) + && ((TiledScreenNail) mScreenNail).isAnimating(); + } + + @Override + public void setScreenNail(ScreenNail s) { + mScreenNail = s; + } + + @Override + public void forceSize() { + updateSize(); + mPositionController.forceImageSize(mIndex, mSize); + } + + private void updateSize() { + if (mIsPanorama) { + mRotation = getPanoramaRotation(); + } else if (mIsCamera && !mIsStaticCamera) { + mRotation = getCameraRotation(); + } else { + mRotation = mModel.getImageRotation(mIndex); + } + + if (mScreenNail != null) { + mSize.width = mScreenNail.getWidth(); + mSize.height = mScreenNail.getHeight(); + } else { + // If we don't have ScreenNail available, we can still try to + // get the size information of it. + mModel.getImageSize(mIndex, mSize); + } + + int w = mSize.width; + int h = mSize.height; + mSize.width = getRotated(mRotation, w, h); + mSize.height = getRotated(mRotation, h, w); + } + + @Override + public boolean isCamera() { + return mIsCamera; + } + + @Override + public boolean isDeletable() { + return mIsDeletable; + } + } + + // Draw a gray placeholder in the specified rectangle. + private void drawPlaceHolder(GLCanvas canvas, Rect r) { + canvas.fillRect(r.left, r.top, r.width(), r.height(), mPlaceholderColor); + } + + // Draw the video play icon (in the place where the spinner was) + private void drawVideoPlayIcon(GLCanvas canvas, int side) { + int s = side / ICON_RATIO; + // Draw the video play icon at the center + mVideoPlayIcon.draw(canvas, -s / 2, -s / 2, s, s); + } + + // Draw the "no thumbnail" message + private void drawLoadingFailMessage(GLCanvas canvas) { + StringTexture m = mNoThumbnailText; + m.draw(canvas, -m.getWidth() / 2, -m.getHeight() / 2); + } + + private static int getRotated(int degree, int original, int theother) { + return (degree % 180 == 0) ? original : theother; + } + + //////////////////////////////////////////////////////////////////////////// + // Gestures Handling + //////////////////////////////////////////////////////////////////////////// + + @Override + protected boolean onTouch(MotionEvent event) { + mGestureRecognizer.onTouchEvent(event); + return true; + } + + private class MyGestureListener implements GestureRecognizer.Listener { + private boolean mIgnoreUpEvent = false; + // If we can change mode for this scale gesture. + private boolean mCanChangeMode; + // If we have changed the film mode in this scaling gesture. + private boolean mModeChanged; + // If this scaling gesture should be ignored. + private boolean mIgnoreScalingGesture; + // whether the down action happened while the view is scrolling. + private boolean mDownInScrolling; + // If we should ignore all gestures other than onSingleTapUp. + private boolean mIgnoreSwipingGesture; + // If a scrolling has happened after a down gesture. + private boolean mScrolledAfterDown; + // If the first scrolling move is in X direction. In the film mode, X + // direction scrolling is normal scrolling. but Y direction scrolling is + // a delete gesture. + private boolean mFirstScrollX; + // The accumulated Y delta that has been sent to mPositionController. + private int mDeltaY; + // The accumulated scaling change from a scaling gesture. + private float mAccScale; + // If an onFling happened after the last onDown + private boolean mHadFling; + + @Override + public boolean onSingleTapUp(float x, float y) { + // On crespo running Android 2.3.6 (gingerbread), a pinch out gesture results in the + // following call sequence: onDown(), onUp() and then onSingleTapUp(). The correct + // sequence for a single-tap-up gesture should be: onDown(), onSingleTapUp() and onUp(). + // The call sequence for a pinch out gesture in JB is: onDown(), then onUp() and there's + // no onSingleTapUp(). Base on these observations, the following condition is added to + // filter out the false alarm where onSingleTapUp() is called within a pinch out + // gesture. The framework fix went into ICS. Refer to b/4588114. + if (Build.VERSION.SDK_INT < ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) { + if ((mHolding & HOLD_TOUCH_DOWN) == 0) { + return true; + } + } + + // We do this in addition to onUp() because we want the snapback of + // setFilmMode to happen. + mHolding &= ~HOLD_TOUCH_DOWN; + + if (mFilmMode && !mDownInScrolling) { + switchToHitPicture((int) (x + 0.5f), (int) (y + 0.5f)); + + // If this is a lock screen photo, let the listener handle the + // event. Tapping on lock screen photo should take the user + // directly to the lock screen. + MediaItem item = mModel.getMediaItem(0); + int supported = 0; + if (item != null) supported = item.getSupportedOperations(); + if ((supported & MediaItem.SUPPORT_ACTION) == 0) { + setFilmMode(false); + mIgnoreUpEvent = true; + return true; + } + } + + if (mListener != null) { + // Do the inverse transform of the touch coordinates. + Matrix m = getGLRoot().getCompensationMatrix(); + Matrix inv = new Matrix(); + m.invert(inv); + float[] pts = new float[] {x, y}; + inv.mapPoints(pts); + mListener.onSingleTapUp((int) (pts[0] + 0.5f), (int) (pts[1] + 0.5f)); + } + return true; + } + + @Override + public boolean onDoubleTap(float x, float y) { + if (mIgnoreSwipingGesture) return true; + if (mPictures.get(0).isCamera()) return false; + PositionController controller = mPositionController; + float scale = controller.getImageScale(); + // onDoubleTap happened on the second ACTION_DOWN. + // We need to ignore the next UP event. + mIgnoreUpEvent = true; + if (scale <= .75f || controller.isAtMinimalScale()) { + controller.zoomIn(x, y, Math.max(1.0f, scale * 1.5f)); + } else { + controller.resetToFullView(); + } + return true; + } + + @Override + public boolean onScroll(float dx, float dy, float totalX, float totalY) { + if (mIgnoreSwipingGesture) return true; + if (!mScrolledAfterDown) { + mScrolledAfterDown = true; + mFirstScrollX = (Math.abs(dx) > Math.abs(dy)); + } + + int dxi = (int) (-dx + 0.5f); + int dyi = (int) (-dy + 0.5f); + if (mFilmMode) { + if (mFirstScrollX) { + mPositionController.scrollFilmX(dxi); + } else { + if (mTouchBoxIndex == Integer.MAX_VALUE) return true; + int newDeltaY = calculateDeltaY(totalY); + int d = newDeltaY - mDeltaY; + if (d != 0) { + mPositionController.scrollFilmY(mTouchBoxIndex, d); + mDeltaY = newDeltaY; + } + } + } else { + mPositionController.scrollPage(dxi, dyi); + } + return true; + } + + private int calculateDeltaY(float delta) { + if (mTouchBoxDeletable) return (int) (delta + 0.5f); + + // don't let items that can't be deleted be dragged more than + // maxScrollDistance, and make it harder and harder to drag. + int size = getHeight(); + float maxScrollDistance = 0.15f * size; + if (Math.abs(delta) >= size) { + delta = delta > 0 ? maxScrollDistance : -maxScrollDistance; + } else { + delta = maxScrollDistance * + FloatMath.sin((delta / size) * (float) (Math.PI / 2)); + } + return (int) (delta + 0.5f); + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + if (mIgnoreSwipingGesture) return true; + if (mModeChanged) return true; + if (swipeImages(velocityX, velocityY)) { + mIgnoreUpEvent = true; + } else { + flingImages(velocityX, velocityY, Math.abs(e2.getY() - e1.getY())); + } + mHadFling = true; + return true; + } + + private boolean flingImages(float velocityX, float velocityY, float dY) { + int vx = (int) (velocityX + 0.5f); + int vy = (int) (velocityY + 0.5f); + if (!mFilmMode) { + return mPositionController.flingPage(vx, vy); + } + if (Math.abs(velocityX) > Math.abs(velocityY)) { + return mPositionController.flingFilmX(vx); + } + // If we scrolled in Y direction fast enough, treat it as a delete + // gesture. + if (!mFilmMode || mTouchBoxIndex == Integer.MAX_VALUE + || !mTouchBoxDeletable) { + return false; + } + int maxVelocity = GalleryUtils.dpToPixel(MAX_DISMISS_VELOCITY); + int escapeVelocity = GalleryUtils.dpToPixel(SWIPE_ESCAPE_VELOCITY); + int escapeDistance = GalleryUtils.dpToPixel(SWIPE_ESCAPE_DISTANCE); + int centerY = mPositionController.getPosition(mTouchBoxIndex) + .centerY(); + boolean fastEnough = (Math.abs(vy) > escapeVelocity) + && (Math.abs(vy) > Math.abs(vx)) + && ((vy > 0) == (centerY > getHeight() / 2)) + && dY >= escapeDistance; + if (fastEnough) { + vy = Math.min(vy, maxVelocity); + int duration = mPositionController.flingFilmY(mTouchBoxIndex, vy); + if (duration >= 0) { + mPositionController.setPopFromTop(vy < 0); + deleteAfterAnimation(duration); + // We reset mTouchBoxIndex, so up() won't check if Y + // scrolled far enough to be a delete gesture. + mTouchBoxIndex = Integer.MAX_VALUE; + return true; + } + } + return false; + } + + private void deleteAfterAnimation(int duration) { + MediaItem item = mModel.getMediaItem(mTouchBoxIndex); + if (item == null) return; + mListener.onCommitDeleteImage(); + mUndoIndexHint = mModel.getCurrentIndex() + mTouchBoxIndex; + mHolding |= HOLD_DELETE; + Message m = mHandler.obtainMessage(MSG_DELETE_ANIMATION_DONE); + m.obj = item.getPath(); + m.arg1 = mTouchBoxIndex; + mHandler.sendMessageDelayed(m, duration); + } + + @Override + public boolean onScaleBegin(float focusX, float focusY) { + if (mIgnoreSwipingGesture) return true; + // We ignore the scaling gesture if it is a camera preview. + mIgnoreScalingGesture = mPictures.get(0).isCamera(); + if (mIgnoreScalingGesture) { + return true; + } + mPositionController.beginScale(focusX, focusY); + // We can change mode if we are in film mode, or we are in page + // mode and at minimal scale. + mCanChangeMode = mFilmMode + || mPositionController.isAtMinimalScale(); + mAccScale = 1f; + return true; + } + + @Override + public boolean onScale(float focusX, float focusY, float scale) { + if (mIgnoreSwipingGesture) return true; + if (mIgnoreScalingGesture) return true; + if (mModeChanged) return true; + if (Float.isNaN(scale) || Float.isInfinite(scale)) return false; + + int outOfRange = mPositionController.scaleBy(scale, focusX, focusY); + + // We wait for a large enough scale change before changing mode. + // Otherwise we may mistakenly treat a zoom-in gesture as zoom-out + // or vice versa. + mAccScale *= scale; + boolean largeEnough = (mAccScale < 0.97f || mAccScale > 1.03f); + + // If mode changes, we treat this scaling gesture has ended. + if (mCanChangeMode && largeEnough) { + if ((outOfRange < 0 && !mFilmMode) || + (outOfRange > 0 && mFilmMode)) { + stopExtraScalingIfNeeded(); + + // Removing the touch down flag allows snapback to happen + // for film mode change. + mHolding &= ~HOLD_TOUCH_DOWN; + if (mFilmMode) { + UsageStatistics.setPendingTransitionCause( + UsageStatistics.TRANSITION_PINCH_OUT); + } else { + UsageStatistics.setPendingTransitionCause( + UsageStatistics.TRANSITION_PINCH_IN); + } + setFilmMode(!mFilmMode); + + + // We need to call onScaleEnd() before setting mModeChanged + // to true. + onScaleEnd(); + mModeChanged = true; + return true; + } + } + + if (outOfRange != 0) { + startExtraScalingIfNeeded(); + } else { + stopExtraScalingIfNeeded(); + } + return true; + } + + @Override + public void onScaleEnd() { + if (mIgnoreSwipingGesture) return; + if (mIgnoreScalingGesture) return; + if (mModeChanged) return; + mPositionController.endScale(); + } + + private void startExtraScalingIfNeeded() { + if (!mCancelExtraScalingPending) { + mHandler.sendEmptyMessageDelayed( + MSG_CANCEL_EXTRA_SCALING, 700); + mPositionController.setExtraScalingRange(true); + mCancelExtraScalingPending = true; + } + } + + private void stopExtraScalingIfNeeded() { + if (mCancelExtraScalingPending) { + mHandler.removeMessages(MSG_CANCEL_EXTRA_SCALING); + mPositionController.setExtraScalingRange(false); + mCancelExtraScalingPending = false; + } + } + + @Override + public void onDown(float x, float y) { + checkHideUndoBar(UNDO_BAR_TOUCHED); + + mDeltaY = 0; + mModeChanged = false; + + if (mIgnoreSwipingGesture) return; + + mHolding |= HOLD_TOUCH_DOWN; + + if (mFilmMode && mPositionController.isScrolling()) { + mDownInScrolling = true; + mPositionController.stopScrolling(); + } else { + mDownInScrolling = false; + } + mHadFling = false; + mScrolledAfterDown = false; + if (mFilmMode) { + int xi = (int) (x + 0.5f); + int yi = (int) (y + 0.5f); + // We only care about being within the x bounds, necessary for + // handling very wide images which are otherwise very hard to fling + mTouchBoxIndex = mPositionController.hitTest(xi, getHeight() / 2); + + if (mTouchBoxIndex < mPrevBound || mTouchBoxIndex > mNextBound) { + mTouchBoxIndex = Integer.MAX_VALUE; + } else { + mTouchBoxDeletable = + mPictures.get(mTouchBoxIndex).isDeletable(); + } + } else { + mTouchBoxIndex = Integer.MAX_VALUE; + } + } + + @Override + public void onUp() { + if (mIgnoreSwipingGesture) return; + + mHolding &= ~HOLD_TOUCH_DOWN; + mEdgeView.onRelease(); + + // If we scrolled in Y direction far enough, treat it as a delete + // gesture. + if (mFilmMode && mScrolledAfterDown && !mFirstScrollX + && mTouchBoxIndex != Integer.MAX_VALUE) { + Rect r = mPositionController.getPosition(mTouchBoxIndex); + int h = getHeight(); + if (Math.abs(r.centerY() - h * 0.5f) > 0.4f * h) { + int duration = mPositionController + .flingFilmY(mTouchBoxIndex, 0); + if (duration >= 0) { + mPositionController.setPopFromTop(r.centerY() < h * 0.5f); + deleteAfterAnimation(duration); + } + } + } + + if (mIgnoreUpEvent) { + mIgnoreUpEvent = false; + return; + } + + if (!(mFilmMode && !mHadFling && mFirstScrollX + && snapToNeighborImage())) { + snapback(); + } + } + + public void setSwipingEnabled(boolean enabled) { + mIgnoreSwipingGesture = !enabled; + } + } + + public void setSwipingEnabled(boolean enabled) { + mGestureListener.setSwipingEnabled(enabled); + } + + private void updateActionBar() { + boolean isCamera = mPictures.get(0).isCamera(); + if (isCamera && !mFilmMode) { + // Move into camera in page mode, lock + mListener.onActionBarAllowed(false); + } else { + mListener.onActionBarAllowed(true); + if (mFilmMode) mListener.onActionBarWanted(); + } + } + + public void setFilmMode(boolean enabled) { + if (mFilmMode == enabled) return; + mFilmMode = enabled; + mPositionController.setFilmMode(mFilmMode); + mModel.setNeedFullImage(!enabled); + mModel.setFocusHintDirection( + mFilmMode ? Model.FOCUS_HINT_PREVIOUS : Model.FOCUS_HINT_NEXT); + updateActionBar(); + mListener.onFilmModeChanged(enabled); + } + + public boolean getFilmMode() { + return mFilmMode; + } + + //////////////////////////////////////////////////////////////////////////// + // Framework events + //////////////////////////////////////////////////////////////////////////// + + public void pause() { + mPositionController.skipAnimation(); + mTileView.freeTextures(); + for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) { + mPictures.get(i).setScreenNail(null); + } + hideUndoBar(); + } + + public void resume() { + mTileView.prepareTextures(); + mPositionController.skipToFinalPosition(); + } + + // move to the camera preview and show controls after resume + public void resetToFirstPicture() { + mModel.moveTo(0); + setFilmMode(false); + } + + //////////////////////////////////////////////////////////////////////////// + // Undo Bar + //////////////////////////////////////////////////////////////////////////// + + private int mUndoBarState; + private static final int UNDO_BAR_SHOW = 1; + private static final int UNDO_BAR_TIMEOUT = 2; + private static final int UNDO_BAR_TOUCHED = 4; + private static final int UNDO_BAR_FULL_CAMERA = 8; + private static final int UNDO_BAR_DELETE_LAST = 16; + + // "deleteLast" means if the deletion is on the last remaining picture in + // the album. + private void showUndoBar(boolean deleteLast) { + mHandler.removeMessages(MSG_UNDO_BAR_TIMEOUT); + mUndoBarState = UNDO_BAR_SHOW; + if(deleteLast) mUndoBarState |= UNDO_BAR_DELETE_LAST; + mUndoBar.animateVisibility(GLView.VISIBLE); + mHandler.sendEmptyMessageDelayed(MSG_UNDO_BAR_TIMEOUT, 3000); + if (mListener != null) mListener.onUndoBarVisibilityChanged(true); + } + + private void hideUndoBar() { + mHandler.removeMessages(MSG_UNDO_BAR_TIMEOUT); + mListener.onCommitDeleteImage(); + mUndoBar.animateVisibility(GLView.INVISIBLE); + mUndoBarState = 0; + mUndoIndexHint = Integer.MAX_VALUE; + mListener.onUndoBarVisibilityChanged(false); + } + + // Check if the one of the conditions for hiding the undo bar has been + // met. The conditions are: + // + // 1. It has been three seconds since last showing, and (a) the user has + // touched, or (b) the deleted picture is the last remaining picture in the + // album. + // + // 2. The camera is shown in full screen. + private void checkHideUndoBar(int addition) { + mUndoBarState |= addition; + if ((mUndoBarState & UNDO_BAR_SHOW) == 0) return; + boolean timeout = (mUndoBarState & UNDO_BAR_TIMEOUT) != 0; + boolean touched = (mUndoBarState & UNDO_BAR_TOUCHED) != 0; + boolean fullCamera = (mUndoBarState & UNDO_BAR_FULL_CAMERA) != 0; + boolean deleteLast = (mUndoBarState & UNDO_BAR_DELETE_LAST) != 0; + if ((timeout && deleteLast) || fullCamera || touched) { + hideUndoBar(); + } + } + + public boolean canUndo() { + return (mUndoBarState & UNDO_BAR_SHOW) != 0; + } + + //////////////////////////////////////////////////////////////////////////// + // Rendering + //////////////////////////////////////////////////////////////////////////// + + @Override + protected void render(GLCanvas canvas) { + if (mFirst) { + // Make sure the fields are properly initialized before checking + // whether isCamera() + mPictures.get(0).reload(); + } + // Check if the camera preview occupies the full screen. + boolean full = !mFilmMode && mPictures.get(0).isCamera() + && mPositionController.isCenter() + && mPositionController.isAtMinimalScale(); + if (mFirst || full != mFullScreenCamera) { + mFullScreenCamera = full; + mFirst = false; + mListener.onFullScreenChanged(full); + if (full) mHandler.sendEmptyMessage(MSG_UNDO_BAR_FULL_CAMERA); + } + + // Determine how many photos we need to draw in addition to the center + // one. + int neighbors; + if (mFullScreenCamera) { + neighbors = 0; + } else { + // In page mode, we draw only one previous/next photo. But if we are + // doing capture animation, we want to draw all photos. + boolean inPageMode = (mPositionController.getFilmRatio() == 0f); + boolean inCaptureAnimation = + ((mHolding & HOLD_CAPTURE_ANIMATION) != 0); + if (inPageMode && !inCaptureAnimation) { + neighbors = 1; + } else { + neighbors = SCREEN_NAIL_MAX; + } + } + + // Draw photos from back to front + for (int i = neighbors; i >= -neighbors; i--) { + Rect r = mPositionController.getPosition(i); + mPictures.get(i).draw(canvas, r); + } + + renderChild(canvas, mEdgeView); + renderChild(canvas, mUndoBar); + + mPositionController.advanceAnimation(); + checkFocusSwitching(); + } + + //////////////////////////////////////////////////////////////////////////// + // Film mode focus switching + //////////////////////////////////////////////////////////////////////////// + + // Runs in GL thread. + private void checkFocusSwitching() { + if (!mFilmMode) return; + if (mHandler.hasMessages(MSG_SWITCH_FOCUS)) return; + if (switchPosition() != 0) { + mHandler.sendEmptyMessage(MSG_SWITCH_FOCUS); + } + } + + // Runs in main thread. + private void switchFocus() { + if (mHolding != 0) return; + switch (switchPosition()) { + case -1: + switchToPrevImage(); + break; + case 1: + switchToNextImage(); + break; + } + } + + // Returns -1 if we should switch focus to the previous picture, +1 if we + // should switch to the next, 0 otherwise. + private int switchPosition() { + Rect curr = mPositionController.getPosition(0); + int center = getWidth() / 2; + + if (curr.left > center && mPrevBound < 0) { + Rect prev = mPositionController.getPosition(-1); + int currDist = curr.left - center; + int prevDist = center - prev.right; + if (prevDist < currDist) { + return -1; + } + } else if (curr.right < center && mNextBound > 0) { + Rect next = mPositionController.getPosition(1); + int currDist = center - curr.right; + int nextDist = next.left - center; + if (nextDist < currDist) { + return 1; + } + } + + return 0; + } + + // Switch to the previous or next picture if the hit position is inside + // one of their boxes. This runs in main thread. + private void switchToHitPicture(int x, int y) { + if (mPrevBound < 0) { + Rect r = mPositionController.getPosition(-1); + if (r.right >= x) { + slideToPrevPicture(); + return; + } + } + + if (mNextBound > 0) { + Rect r = mPositionController.getPosition(1); + if (r.left <= x) { + slideToNextPicture(); + return; + } + } + } + + //////////////////////////////////////////////////////////////////////////// + // Page mode focus switching + // + // We slide image to the next one or the previous one in two cases: 1: If + // the user did a fling gesture with enough velocity. 2 If the user has + // moved the picture a lot. + //////////////////////////////////////////////////////////////////////////// + + private boolean swipeImages(float velocityX, float velocityY) { + if (mFilmMode) return false; + + // Avoid swiping images if we're possibly flinging to view the + // zoomed in picture vertically. + PositionController controller = mPositionController; + boolean isMinimal = controller.isAtMinimalScale(); + int edges = controller.getImageAtEdges(); + if (!isMinimal && Math.abs(velocityY) > Math.abs(velocityX)) + if ((edges & PositionController.IMAGE_AT_TOP_EDGE) == 0 + || (edges & PositionController.IMAGE_AT_BOTTOM_EDGE) == 0) + return false; + + // If we are at the edge of the current photo and the sweeping velocity + // exceeds the threshold, slide to the next / previous image. + if (velocityX < -SWIPE_THRESHOLD && (isMinimal + || (edges & PositionController.IMAGE_AT_RIGHT_EDGE) != 0)) { + return slideToNextPicture(); + } else if (velocityX > SWIPE_THRESHOLD && (isMinimal + || (edges & PositionController.IMAGE_AT_LEFT_EDGE) != 0)) { + return slideToPrevPicture(); + } + + return false; + } + + private void snapback() { + if ((mHolding & ~HOLD_DELETE) != 0) return; + if (mFilmMode || !snapToNeighborImage()) { + mPositionController.snapback(); + } + } + + private boolean snapToNeighborImage() { + Rect r = mPositionController.getPosition(0); + int viewW = getWidth(); + // Setting the move threshold proportional to the width of the view + int moveThreshold = viewW / 5 ; + int threshold = moveThreshold + gapToSide(r.width(), viewW); + + // If we have moved the picture a lot, switching. + if (viewW - r.right > threshold) { + return slideToNextPicture(); + } else if (r.left > threshold) { + return slideToPrevPicture(); + } + + return false; + } + + private boolean slideToNextPicture() { + if (mNextBound <= 0) return false; + switchToNextImage(); + mPositionController.startHorizontalSlide(); + return true; + } + + private boolean slideToPrevPicture() { + if (mPrevBound >= 0) return false; + switchToPrevImage(); + mPositionController.startHorizontalSlide(); + return true; + } + + private static int gapToSide(int imageWidth, int viewWidth) { + return Math.max(0, (viewWidth - imageWidth) / 2); + } + + //////////////////////////////////////////////////////////////////////////// + // Focus switching + //////////////////////////////////////////////////////////////////////////// + + public void switchToImage(int index) { + mModel.moveTo(index); + } + + private void switchToNextImage() { + mModel.moveTo(mModel.getCurrentIndex() + 1); + } + + private void switchToPrevImage() { + mModel.moveTo(mModel.getCurrentIndex() - 1); + } + + private void switchToFirstImage() { + mModel.moveTo(0); + } + + //////////////////////////////////////////////////////////////////////////// + // Opening Animation + //////////////////////////////////////////////////////////////////////////// + + public void setOpenAnimationRect(Rect rect) { + mPositionController.setOpenAnimationRect(rect); + } + + //////////////////////////////////////////////////////////////////////////// + // Capture Animation + //////////////////////////////////////////////////////////////////////////// + + public boolean switchWithCaptureAnimation(int offset) { + GLRoot root = getGLRoot(); + if(root == null) return false; + root.lockRenderThread(); + try { + return switchWithCaptureAnimationLocked(offset); + } finally { + root.unlockRenderThread(); + } + } + + private boolean switchWithCaptureAnimationLocked(int offset) { + if (mHolding != 0) return true; + if (offset == 1) { + if (mNextBound <= 0) return false; + // Temporary disable action bar until the capture animation is done. + if (!mFilmMode) mListener.onActionBarAllowed(false); + switchToNextImage(); + mPositionController.startCaptureAnimationSlide(-1); + } else if (offset == -1) { + if (mPrevBound >= 0) return false; + if (mFilmMode) setFilmMode(false); + + // If we are too far away from the first image (so that we don't + // have all the ScreenNails in-between), we go directly without + // animation. + if (mModel.getCurrentIndex() > SCREEN_NAIL_MAX) { + switchToFirstImage(); + mPositionController.skipToFinalPosition(); + return true; + } + + switchToFirstImage(); + mPositionController.startCaptureAnimationSlide(1); + } else { + return false; + } + mHolding |= HOLD_CAPTURE_ANIMATION; + Message m = mHandler.obtainMessage(MSG_CAPTURE_ANIMATION_DONE, offset, 0); + mHandler.sendMessageDelayed(m, PositionController.CAPTURE_ANIMATION_TIME); + return true; + } + + private void captureAnimationDone(int offset) { + mHolding &= ~HOLD_CAPTURE_ANIMATION; + if (offset == 1 && !mFilmMode) { + // Now the capture animation is done, enable the action bar. + mListener.onActionBarAllowed(true); + mListener.onActionBarWanted(); + } + snapback(); + } + + //////////////////////////////////////////////////////////////////////////// + // Card deck effect calculation + //////////////////////////////////////////////////////////////////////////// + + // Returns the scrolling progress value for an object moving out of a + // view. The progress value measures how much the object has moving out of + // the view. The object currently displays in [left, right), and the view is + // at [0, viewWidth]. + // + // The returned value is negative when the object is moving right, and + // positive when the object is moving left. The value goes to -1 or 1 when + // the object just moves out of the view completely. The value is 0 if the + // object currently fills the view. + private static float calculateMoveOutProgress(int left, int right, + int viewWidth) { + // w = object width + // viewWidth = view width + int w = right - left; + + // If the object width is smaller than the view width, + // |....view....| + // |<-->| progress = -1 when left = viewWidth + // |<-->| progress = 0 when left = viewWidth / 2 - w / 2 + // |<-->| progress = 1 when left = -w + if (w < viewWidth) { + int zx = viewWidth / 2 - w / 2; + if (left > zx) { + return -(left - zx) / (float) (viewWidth - zx); // progress = (0, -1] + } else { + return (left - zx) / (float) (-w - zx); // progress = [0, 1] + } + } + + // If the object width is larger than the view width, + // |..view..| + // |<--------->| progress = -1 when left = viewWidth + // |<--------->| progress = 0 between left = 0 + // |<--------->| and right = viewWidth + // |<--------->| progress = 1 when right = 0 + if (left > 0) { + return -left / (float) viewWidth; + } + + if (right < viewWidth) { + return (viewWidth - right) / (float) viewWidth; + } + + return 0; + } + + // Maps a scrolling progress value to the alpha factor in the fading + // animation. + private float getScrollAlpha(float scrollProgress) { + return scrollProgress < 0 ? mAlphaInterpolator.getInterpolation( + 1 - Math.abs(scrollProgress)) : 1.0f; + } + + // Maps a scrolling progress value to the scaling factor in the fading + // animation. + private float getScrollScale(float scrollProgress) { + float interpolatedProgress = mScaleInterpolator.getInterpolation( + Math.abs(scrollProgress)); + float scale = (1 - interpolatedProgress) + + interpolatedProgress * TRANSITION_SCALE_FACTOR; + return scale; + } + + + // This interpolator emulates the rate at which the perceived scale of an + // object changes as its distance from a camera increases. When this + // interpolator is applied to a scale animation on a view, it evokes the + // sense that the object is shrinking due to moving away from the camera. + private static class ZInterpolator { + private float focalLength; + + public ZInterpolator(float foc) { + focalLength = foc; + } + + public float getInterpolation(float input) { + return (1.0f - focalLength / (focalLength + input)) / + (1.0f - focalLength / (focalLength + 1.0f)); + } + } + + // Returns an interpolated value for the page/film transition. + // When ratio = 0, the result is from. + // When ratio = 1, the result is to. + private static float interpolate(float ratio, float from, float to) { + return from + (to - from) * ratio * ratio; + } + + // Returns the alpha factor in film mode if a picture is not in the center. + // The 0.03 lower bound is to make the item always visible a bit. + private float getOffsetAlpha(float offset) { + offset /= 0.5f; + float alpha = (offset > 0) ? (1 - offset) : (1 + offset); + return Utils.clamp(alpha, 0.03f, 1f); + } + + //////////////////////////////////////////////////////////////////////////// + // Simple public utilities + //////////////////////////////////////////////////////////////////////////// + + public void setListener(Listener listener) { + mListener = listener; + } + + public Rect getPhotoRect(int index) { + return mPositionController.getPosition(index); + } + + public PhotoFallbackEffect buildFallbackEffect(GLView root, GLCanvas canvas) { + Rect location = new Rect(); + Utils.assertTrue(root.getBoundsOf(this, location)); + + Rect fullRect = bounds(); + PhotoFallbackEffect effect = new PhotoFallbackEffect(); + for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) { + MediaItem item = mModel.getMediaItem(i); + if (item == null) continue; + ScreenNail sc = mModel.getScreenNail(i); + if (!(sc instanceof TiledScreenNail) + || ((TiledScreenNail) sc).isShowingPlaceholder()) continue; + + // Now, sc is BitmapScreenNail and is not showing placeholder + Rect rect = new Rect(getPhotoRect(i)); + if (!Rect.intersects(fullRect, rect)) continue; + rect.offset(location.left, location.top); + + int width = sc.getWidth(); + int height = sc.getHeight(); + + int rotation = mModel.getImageRotation(i); + RawTexture texture; + if ((rotation % 180) == 0) { + texture = new RawTexture(width, height, true); + canvas.beginRenderTarget(texture); + canvas.translate(width / 2f, height / 2f); + } else { + texture = new RawTexture(height, width, true); + canvas.beginRenderTarget(texture); + canvas.translate(height / 2f, width / 2f); + } + + canvas.rotate(rotation, 0, 0, 1); + canvas.translate(-width / 2f, -height / 2f); + sc.draw(canvas, 0, 0, width, height); + canvas.endRenderTarget(); + effect.addEntry(item.getPath(), rect, texture); + } + return effect; + } +} diff --git a/src/com/android/gallery3d/ui/PopupList.java b/src/com/android/gallery3d/ui/PopupList.java new file mode 100644 index 000000000..248f50b25 --- /dev/null +++ b/src/com/android/gallery3d/ui/PopupList.java @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2012 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.ui; + +import android.content.Context; +import android.graphics.Rect; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.MeasureSpec; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.ViewTreeObserver.OnGlobalLayoutListener; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.BaseAdapter; +import android.widget.ListView; +import android.widget.PopupWindow; +import android.widget.TextView; + +import com.android.gallery3d.R; + +import java.util.ArrayList; + +public class PopupList { + + public static interface OnPopupItemClickListener { + public boolean onPopupItemClick(int itemId); + } + + public static class Item { + public final int id; + public String title; + + public Item(int id, String title) { + this.id = id; + this.title = title; + } + + public void setTitle(String title) { + this.title = title; + } + } + + private final Context mContext; + private final View mAnchorView; + private final ArrayList<Item> mItems = new ArrayList<Item>(); + private PopupWindow mPopupWindow; + private ListView mContentList; + private OnPopupItemClickListener mOnPopupItemClickListener; + private int mPopupOffsetX; + private int mPopupOffsetY; + private int mPopupWidth; + private int mPopupHeight; + + public PopupList(Context context, View anchorView) { + mContext = context; + mAnchorView = anchorView; + } + + public void setOnPopupItemClickListener(OnPopupItemClickListener listener) { + mOnPopupItemClickListener = listener; + } + + public void addItem(int id, String title) { + mItems.add(new Item(id, title)); + } + + public void clearItems() { + mItems.clear(); + } + + private final PopupWindow.OnDismissListener mOnDismissListener = + new PopupWindow.OnDismissListener() { + @SuppressWarnings("deprecation") + @Override + public void onDismiss() { + if (mPopupWindow == null) return; + mPopupWindow = null; + ViewTreeObserver observer = mAnchorView.getViewTreeObserver(); + if (observer.isAlive()) { + // We used the deprecated function for backward compatibility + // The new "removeOnGlobalLayoutListener" is introduced in API level 16 + observer.removeGlobalOnLayoutListener(mOnGLobalLayoutListener); + } + } + }; + + private final OnItemClickListener mOnItemClickListener = + new OnItemClickListener() { + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + if (mPopupWindow == null) return; + mPopupWindow.dismiss(); + if (mOnPopupItemClickListener != null) { + mOnPopupItemClickListener.onPopupItemClick((int) id); + } + } + }; + + private final OnGlobalLayoutListener mOnGLobalLayoutListener = + new OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + if (mPopupWindow == null) return; + updatePopupLayoutParams(); + // Need to update the position of the popup window + mPopupWindow.update(mAnchorView, + mPopupOffsetX, mPopupOffsetY, mPopupWidth, mPopupHeight); + } + }; + + public void show() { + if (mPopupWindow != null) return; + mAnchorView.getViewTreeObserver() + .addOnGlobalLayoutListener(mOnGLobalLayoutListener); + mPopupWindow = createPopupWindow(); + updatePopupLayoutParams(); + mPopupWindow.setWidth(mPopupWidth); + mPopupWindow.setHeight(mPopupHeight); + mPopupWindow.showAsDropDown(mAnchorView, mPopupOffsetX, mPopupOffsetY); + } + + private void updatePopupLayoutParams() { + ListView content = mContentList; + PopupWindow popup = mPopupWindow; + + Rect p = new Rect(); + popup.getBackground().getPadding(p); + + int maxHeight = mPopupWindow.getMaxAvailableHeight(mAnchorView) - p.top - p.bottom; + mContentList.measure( + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), + MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.AT_MOST)); + mPopupWidth = content.getMeasuredWidth() + p.top + p.bottom; + mPopupHeight = Math.min(maxHeight, content.getMeasuredHeight() + p.left + p.right); + mPopupOffsetX = -p.left; + mPopupOffsetY = -p.top; + } + + private PopupWindow createPopupWindow() { + PopupWindow popup = new PopupWindow(mContext); + popup.setOnDismissListener(mOnDismissListener); + + popup.setBackgroundDrawable(mContext.getResources().getDrawable( + R.drawable.menu_dropdown_panel_holo_dark)); + + mContentList = new ListView(mContext, null, + android.R.attr.dropDownListViewStyle); + mContentList.setAdapter(new ItemDataAdapter()); + mContentList.setOnItemClickListener(mOnItemClickListener); + popup.setContentView(mContentList); + popup.setFocusable(true); + popup.setOutsideTouchable(true); + + return popup; + } + + public Item findItem(int id) { + for (Item item : mItems) { + if (item.id == id) return item; + } + return null; + } + + private class ItemDataAdapter extends BaseAdapter { + @Override + public int getCount() { + return mItems.size(); + } + + @Override + public Object getItem(int position) { + return mItems.get(position); + } + + @Override + public long getItemId(int position) { + return mItems.get(position).id; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = LayoutInflater.from(mContext) + .inflate(R.layout.popup_list_item, null); + } + TextView text = (TextView) convertView.findViewById(android.R.id.text1); + text.setText(mItems.get(position).title); + return convertView; + } + } +} diff --git a/src/com/android/gallery3d/ui/PositionController.java b/src/com/android/gallery3d/ui/PositionController.java new file mode 100644 index 000000000..6a4bcea87 --- /dev/null +++ b/src/com/android/gallery3d/ui/PositionController.java @@ -0,0 +1,1821 @@ +/* + * Copyright (C) 2011 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.ui; + +import android.content.Context; +import android.graphics.Rect; +import android.util.Log; +import android.widget.Scroller; + +import com.android.gallery3d.app.PhotoPage; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.ui.PhotoView.Size; +import com.android.gallery3d.util.GalleryUtils; +import com.android.gallery3d.util.RangeArray; +import com.android.gallery3d.util.RangeIntArray; + +class PositionController { + private static final String TAG = "PositionController"; + + public static final int IMAGE_AT_LEFT_EDGE = 1; + public static final int IMAGE_AT_RIGHT_EDGE = 2; + public static final int IMAGE_AT_TOP_EDGE = 4; + public static final int IMAGE_AT_BOTTOM_EDGE = 8; + + public static final int CAPTURE_ANIMATION_TIME = 700; + public static final int SNAPBACK_ANIMATION_TIME = 600; + + // Special values for animation time. + private static final long NO_ANIMATION = -1; + private static final long LAST_ANIMATION = -2; + + private static final int ANIM_KIND_NONE = -1; + private static final int ANIM_KIND_SCROLL = 0; + private static final int ANIM_KIND_SCALE = 1; + private static final int ANIM_KIND_SNAPBACK = 2; + private static final int ANIM_KIND_SLIDE = 3; + private static final int ANIM_KIND_ZOOM = 4; + private static final int ANIM_KIND_OPENING = 5; + private static final int ANIM_KIND_FLING = 6; + private static final int ANIM_KIND_FLING_X = 7; + private static final int ANIM_KIND_DELETE = 8; + private static final int ANIM_KIND_CAPTURE = 9; + + // Animation time in milliseconds. The order must match ANIM_KIND_* above. + // + // The values for ANIM_KIND_FLING_X does't matter because we use + // mFilmScroller.isFinished() to decide when to stop. We set it to 0 so it's + // faster for Animatable.advanceAnimation() to calculate the progress + // (always 1). + private static final int ANIM_TIME[] = { + 0, // ANIM_KIND_SCROLL + 0, // ANIM_KIND_SCALE + SNAPBACK_ANIMATION_TIME, // ANIM_KIND_SNAPBACK + 400, // ANIM_KIND_SLIDE + 300, // ANIM_KIND_ZOOM + 300, // ANIM_KIND_OPENING + 0, // ANIM_KIND_FLING (the duration is calculated dynamically) + 0, // ANIM_KIND_FLING_X (see the comment above) + 0, // ANIM_KIND_DELETE (the duration is calculated dynamically) + CAPTURE_ANIMATION_TIME, // ANIM_KIND_CAPTURE + }; + + // We try to scale up the image to fill the screen. But in order not to + // scale too much for small icons, we limit the max up-scaling factor here. + private static final float SCALE_LIMIT = 4; + + // For user's gestures, we give a temporary extra scaling range which goes + // above or below the usual scaling limits. + private static final float SCALE_MIN_EXTRA = 0.7f; + private static final float SCALE_MAX_EXTRA = 1.4f; + + // Setting this true makes the extra scaling range permanent (until this is + // set to false again). + private boolean mExtraScalingRange = false; + + // Film Mode v.s. Page Mode: in film mode we show smaller pictures. + private boolean mFilmMode = false; + + // These are the limits for width / height of the picture in film mode. + private static final float FILM_MODE_PORTRAIT_HEIGHT = 0.48f; + private static final float FILM_MODE_PORTRAIT_WIDTH = 0.7f; + private static final float FILM_MODE_LANDSCAPE_HEIGHT = 0.7f; + private static final float FILM_MODE_LANDSCAPE_WIDTH = 0.7f; + + // In addition to the focused box (index == 0). We also keep information + // about this many boxes on each side. + private static final int BOX_MAX = PhotoView.SCREEN_NAIL_MAX; + private static final int[] CENTER_OUT_INDEX = new int[2 * BOX_MAX + 1]; + + private static final int IMAGE_GAP = GalleryUtils.dpToPixel(16); + private static final int HORIZONTAL_SLACK = GalleryUtils.dpToPixel(12); + + // These are constants for the delete gesture. + private static final int DEFAULT_DELETE_ANIMATION_DURATION = 200; // ms + private static final int MAX_DELETE_ANIMATION_DURATION = 400; // ms + + private Listener mListener; + private volatile Rect mOpenAnimationRect; + + // Use a large enough value, so we won't see the gray shadow in the beginning. + private int mViewW = 1200; + private int mViewH = 1200; + + // A scaling gesture is in progress. + private boolean mInScale; + // The focus point of the scaling gesture, relative to the center of the + // picture in bitmap pixels. + private float mFocusX, mFocusY; + + // whether there is a previous/next picture. + private boolean mHasPrev, mHasNext; + + // This is used by the fling animation (page mode). + private FlingScroller mPageScroller; + + // This is used by the fling animation (film mode). + private Scroller mFilmScroller; + + // The bound of the stable region that the focused box can stay, see the + // comments above calculateStableBound() for details. + private int mBoundLeft, mBoundRight, mBoundTop, mBoundBottom; + + // Constrained frame is a rectangle that the focused box should fit into if + // it is constrained. It has two effects: + // + // (1) In page mode, if the focused box is constrained, scaling for the + // focused box is adjusted to fit into the constrained frame, instead of the + // whole view. + // + // (2) In page mode, if the focused box is constrained, the mPlatform's + // default center (mDefaultX/Y) is moved to the center of the constrained + // frame, instead of the view center. + // + private Rect mConstrainedFrame = new Rect(); + + // Whether the focused box is constrained. + // + // Our current program's first call to moveBox() sets constrained = true, so + // we set the initial value of this variable to true, and we will not see + // see unwanted transition animation. + private boolean mConstrained = true; + + // + // ___________________________________________________________ + // | _____ _____ _____ _____ _____ | + // | | | | | | | | | | | | + // | | Box | | Box | | Box*| | Box | | Box | | + // | |_____|.....|_____|.....|_____|.....|_____|.....|_____| | + // | Gap Gap Gap Gap | + // |___________________________________________________________| + // + // <-- Platform --> + // + // The focused box (Box*) centers at mPlatform's (mCurrentX, mCurrentY) + + private Platform mPlatform = new Platform(); + private RangeArray<Box> mBoxes = new RangeArray<Box>(-BOX_MAX, BOX_MAX); + // The gap at the right of a Box i is at index i. The gap at the left of a + // Box i is at index i - 1. + private RangeArray<Gap> mGaps = new RangeArray<Gap>(-BOX_MAX, BOX_MAX - 1); + private FilmRatio mFilmRatio = new FilmRatio(); + + // These are only used during moveBox(). + private RangeArray<Box> mTempBoxes = new RangeArray<Box>(-BOX_MAX, BOX_MAX); + private RangeArray<Gap> mTempGaps = + new RangeArray<Gap>(-BOX_MAX, BOX_MAX - 1); + + // The output of the PositionController. Available through getPosition(). + private RangeArray<Rect> mRects = new RangeArray<Rect>(-BOX_MAX, BOX_MAX); + + // The direction of a new picture should appear. New pictures pop from top + // if this value is true, or from bottom if this value is false. + boolean mPopFromTop; + + public interface Listener { + void invalidate(); + boolean isHoldingDown(); + boolean isHoldingDelete(); + + // EdgeView + void onPull(int offset, int direction); + void onRelease(); + void onAbsorb(int velocity, int direction); + } + + static { + // Initialize the CENTER_OUT_INDEX array. + // The array maps 0, 1, 2, 3, 4, ..., 2 * BOX_MAX + // to 0, 1, -1, 2, -2, ..., BOX_MAX, -BOX_MAX + for (int i = 0; i < CENTER_OUT_INDEX.length; i++) { + int j = (i + 1) / 2; + if ((i & 1) == 0) j = -j; + CENTER_OUT_INDEX[i] = j; + } + } + + public PositionController(Context context, Listener listener) { + mListener = listener; + mPageScroller = new FlingScroller(); + mFilmScroller = new Scroller(context, null, false); + + // Initialize the areas. + initPlatform(); + for (int i = -BOX_MAX; i <= BOX_MAX; i++) { + mBoxes.put(i, new Box()); + initBox(i); + mRects.put(i, new Rect()); + } + for (int i = -BOX_MAX; i < BOX_MAX; i++) { + mGaps.put(i, new Gap()); + initGap(i); + } + } + + public void setOpenAnimationRect(Rect r) { + mOpenAnimationRect = r; + } + + public void setViewSize(int viewW, int viewH) { + if (viewW == mViewW && viewH == mViewH) return; + + boolean wasMinimal = isAtMinimalScale(); + + mViewW = viewW; + mViewH = viewH; + initPlatform(); + + for (int i = -BOX_MAX; i <= BOX_MAX; i++) { + setBoxSize(i, viewW, viewH, true); + } + + updateScaleAndGapLimit(); + + // If the focused box was at minimal scale, we try to make it the + // minimal scale under the new view size. + if (wasMinimal) { + Box b = mBoxes.get(0); + b.mCurrentScale = b.mScaleMin; + } + + // If we have the opening animation, do it. Otherwise go directly to the + // right position. + if (!startOpeningAnimationIfNeeded()) { + skipToFinalPosition(); + } + } + + public void setConstrainedFrame(Rect cFrame) { + if (mConstrainedFrame.equals(cFrame)) return; + mConstrainedFrame.set(cFrame); + mPlatform.updateDefaultXY(); + updateScaleAndGapLimit(); + snapAndRedraw(); + } + + public void forceImageSize(int index, Size s) { + if (s.width == 0 || s.height == 0) return; + Box b = mBoxes.get(index); + b.mImageW = s.width; + b.mImageH = s.height; + return; + } + + public void setImageSize(int index, Size s, Rect cFrame) { + if (s.width == 0 || s.height == 0) return; + + boolean needUpdate = false; + if (cFrame != null && !mConstrainedFrame.equals(cFrame)) { + mConstrainedFrame.set(cFrame); + mPlatform.updateDefaultXY(); + needUpdate = true; + } + needUpdate |= setBoxSize(index, s.width, s.height, false); + + if (!needUpdate) return; + updateScaleAndGapLimit(); + snapAndRedraw(); + } + + // Returns false if the box size doesn't change. + private boolean setBoxSize(int i, int width, int height, boolean isViewSize) { + Box b = mBoxes.get(i); + boolean wasViewSize = b.mUseViewSize; + + // If we already have an image size, we don't want to use the view size. + if (!wasViewSize && isViewSize) return false; + + b.mUseViewSize = isViewSize; + + if (width == b.mImageW && height == b.mImageH) { + return false; + } + + // The ratio of the old size and the new size. + // + // If the aspect ratio changes, we don't know if it is because one side + // grows or the other side shrinks. Currently we just assume the view + // angle of the longer side doesn't change (so the aspect ratio change + // is because the view angle of the shorter side changes). This matches + // what camera preview does. + float ratio = (width > height) + ? (float) b.mImageW / width + : (float) b.mImageH / height; + + b.mImageW = width; + b.mImageH = height; + + // If this is the first time we receive an image size or we are in fullscreen, + // we change the scale directly. Otherwise adjust the scales by a ratio, + // and snapback will animate the scale into the min/max bounds if necessary. + if ((wasViewSize && !isViewSize) || !mFilmMode) { + b.mCurrentScale = getMinimalScale(b); + b.mAnimationStartTime = NO_ANIMATION; + } else { + b.mCurrentScale *= ratio; + b.mFromScale *= ratio; + b.mToScale *= ratio; + } + + if (i == 0) { + mFocusX /= ratio; + mFocusY /= ratio; + } + + return true; + } + + private boolean startOpeningAnimationIfNeeded() { + if (mOpenAnimationRect == null) return false; + Box b = mBoxes.get(0); + if (b.mUseViewSize) return false; + + // Start animation from the saved rectangle if we have one. + Rect r = mOpenAnimationRect; + mOpenAnimationRect = null; + + mPlatform.mCurrentX = r.centerX() - mViewW / 2; + b.mCurrentY = r.centerY() - mViewH / 2; + b.mCurrentScale = Math.max(r.width() / (float) b.mImageW, + r.height() / (float) b.mImageH); + startAnimation(mPlatform.mDefaultX, 0, b.mScaleMin, + ANIM_KIND_OPENING); + + // Animate from large gaps for neighbor boxes to avoid them + // shown on the screen during opening animation. + for (int i = -1; i < 1; i++) { + Gap g = mGaps.get(i); + g.mCurrentGap = mViewW; + g.doAnimation(g.mDefaultSize, ANIM_KIND_OPENING); + } + + return true; + } + + public void setFilmMode(boolean enabled) { + if (enabled == mFilmMode) return; + mFilmMode = enabled; + + mPlatform.updateDefaultXY(); + updateScaleAndGapLimit(); + stopAnimation(); + snapAndRedraw(); + } + + public void setExtraScalingRange(boolean enabled) { + if (mExtraScalingRange == enabled) return; + mExtraScalingRange = enabled; + if (!enabled) { + snapAndRedraw(); + } + } + + // This should be called whenever the scale range of boxes or the default + // gap size may change. Currently this can happen due to change of view + // size, image size, mFilmMode, mConstrained, and mConstrainedFrame. + private void updateScaleAndGapLimit() { + for (int i = -BOX_MAX; i <= BOX_MAX; i++) { + Box b = mBoxes.get(i); + b.mScaleMin = getMinimalScale(b); + b.mScaleMax = getMaximalScale(b); + } + + for (int i = -BOX_MAX; i < BOX_MAX; i++) { + Gap g = mGaps.get(i); + g.mDefaultSize = getDefaultGapSize(i); + } + } + + // Returns the default gap size according the the size of the boxes around + // the gap and the current mode. + private int getDefaultGapSize(int i) { + if (mFilmMode) return IMAGE_GAP; + Box a = mBoxes.get(i); + Box b = mBoxes.get(i + 1); + return IMAGE_GAP + Math.max(gapToSide(a), gapToSide(b)); + } + + // Here is how we layout the boxes in the page mode. + // + // previous current next + // ___________ ________________ __________ + // | _______ | | __________ | | ______ | + // | | | | | | right->| | | | | | + // | | |<-------->|<--left | | | | | | + // | |_______| | | | |__________| | | |______| | + // |___________| | |________________| |__________| + // | <--> gapToSide() + // | + // IMAGE_GAP + MAX(gapToSide(previous), gapToSide(current)) + private int gapToSide(Box b) { + return (int) ((mViewW - getMinimalScale(b) * b.mImageW) / 2 + 0.5f); + } + + // Stop all animations at where they are now. + public void stopAnimation() { + mPlatform.mAnimationStartTime = NO_ANIMATION; + for (int i = -BOX_MAX; i <= BOX_MAX; i++) { + mBoxes.get(i).mAnimationStartTime = NO_ANIMATION; + } + for (int i = -BOX_MAX; i < BOX_MAX; i++) { + mGaps.get(i).mAnimationStartTime = NO_ANIMATION; + } + } + + public void skipAnimation() { + if (mPlatform.mAnimationStartTime != NO_ANIMATION) { + mPlatform.mCurrentX = mPlatform.mToX; + mPlatform.mCurrentY = mPlatform.mToY; + mPlatform.mAnimationStartTime = NO_ANIMATION; + } + for (int i = -BOX_MAX; i <= BOX_MAX; i++) { + Box b = mBoxes.get(i); + if (b.mAnimationStartTime == NO_ANIMATION) continue; + b.mCurrentY = b.mToY; + b.mCurrentScale = b.mToScale; + b.mAnimationStartTime = NO_ANIMATION; + } + for (int i = -BOX_MAX; i < BOX_MAX; i++) { + Gap g = mGaps.get(i); + if (g.mAnimationStartTime == NO_ANIMATION) continue; + g.mCurrentGap = g.mToGap; + g.mAnimationStartTime = NO_ANIMATION; + } + redraw(); + } + + public void snapback() { + snapAndRedraw(); + } + + public void skipToFinalPosition() { + stopAnimation(); + snapAndRedraw(); + skipAnimation(); + } + + //////////////////////////////////////////////////////////////////////////// + // Start an animations for the focused box + //////////////////////////////////////////////////////////////////////////// + + public void zoomIn(float tapX, float tapY, float targetScale) { + tapX -= mViewW / 2; + tapY -= mViewH / 2; + Box b = mBoxes.get(0); + + // Convert the tap position to distance to center in bitmap coordinates + float tempX = (tapX - mPlatform.mCurrentX) / b.mCurrentScale; + float tempY = (tapY - b.mCurrentY) / b.mCurrentScale; + + int x = (int) (-tempX * targetScale + 0.5f); + int y = (int) (-tempY * targetScale + 0.5f); + + calculateStableBound(targetScale); + int targetX = Utils.clamp(x, mBoundLeft, mBoundRight); + int targetY = Utils.clamp(y, mBoundTop, mBoundBottom); + targetScale = Utils.clamp(targetScale, b.mScaleMin, b.mScaleMax); + + startAnimation(targetX, targetY, targetScale, ANIM_KIND_ZOOM); + } + + public void resetToFullView() { + Box b = mBoxes.get(0); + startAnimation(mPlatform.mDefaultX, 0, b.mScaleMin, ANIM_KIND_ZOOM); + } + + public void beginScale(float focusX, float focusY) { + focusX -= mViewW / 2; + focusY -= mViewH / 2; + Box b = mBoxes.get(0); + Platform p = mPlatform; + mInScale = true; + mFocusX = (int) ((focusX - p.mCurrentX) / b.mCurrentScale + 0.5f); + mFocusY = (int) ((focusY - b.mCurrentY) / b.mCurrentScale + 0.5f); + } + + // Scales the image by the given factor. + // Returns an out-of-range indicator: + // 1 if the intended scale is too large for the stable range. + // 0 if the intended scale is in the stable range. + // -1 if the intended scale is too small for the stable range. + public int scaleBy(float s, float focusX, float focusY) { + focusX -= mViewW / 2; + focusY -= mViewH / 2; + Box b = mBoxes.get(0); + Platform p = mPlatform; + + // We want to keep the focus point (on the bitmap) the same as when we + // begin the scale gesture, that is, + // + // (focusX' - currentX') / scale' = (focusX - currentX) / scale + // + s = b.clampScale(s * getTargetScale(b)); + int x = mFilmMode ? p.mCurrentX : (int) (focusX - s * mFocusX + 0.5f); + int y = mFilmMode ? b.mCurrentY : (int) (focusY - s * mFocusY + 0.5f); + startAnimation(x, y, s, ANIM_KIND_SCALE); + if (s < b.mScaleMin) return -1; + if (s > b.mScaleMax) return 1; + return 0; + } + + public void endScale() { + mInScale = false; + snapAndRedraw(); + } + + // Slide the focused box to the center of the view. + public void startHorizontalSlide() { + Box b = mBoxes.get(0); + startAnimation(mPlatform.mDefaultX, 0, b.mScaleMin, ANIM_KIND_SLIDE); + } + + // Slide the focused box to the center of the view with the capture + // animation. In addition to the sliding, the animation will also scale the + // the focused box, the specified neighbor box, and the gap between the + // two. The specified offset should be 1 or -1. + public void startCaptureAnimationSlide(int offset) { + Box b = mBoxes.get(0); + Box n = mBoxes.get(offset); // the neighbor box + Gap g = mGaps.get(offset); // the gap between the two boxes + + mPlatform.doAnimation(mPlatform.mDefaultX, mPlatform.mDefaultY, + ANIM_KIND_CAPTURE); + b.doAnimation(0, b.mScaleMin, ANIM_KIND_CAPTURE); + n.doAnimation(0, n.mScaleMin, ANIM_KIND_CAPTURE); + g.doAnimation(g.mDefaultSize, ANIM_KIND_CAPTURE); + redraw(); + } + + // Only allow scrolling when we are not currently in an animation or we + // are in some animation with can be interrupted. + private boolean canScroll() { + Box b = mBoxes.get(0); + if (b.mAnimationStartTime == NO_ANIMATION) return true; + switch (b.mAnimationKind) { + case ANIM_KIND_SCROLL: + case ANIM_KIND_FLING: + case ANIM_KIND_FLING_X: + return true; + } + return false; + } + + public void scrollPage(int dx, int dy) { + if (!canScroll()) return; + + Box b = mBoxes.get(0); + Platform p = mPlatform; + + calculateStableBound(b.mCurrentScale); + + int x = p.mCurrentX + dx; + int y = b.mCurrentY + dy; + + // Vertical direction: If we have space to move in the vertical + // direction, we show the edge effect when scrolling reaches the edge. + if (mBoundTop != mBoundBottom) { + if (y < mBoundTop) { + mListener.onPull(mBoundTop - y, EdgeView.BOTTOM); + } else if (y > mBoundBottom) { + mListener.onPull(y - mBoundBottom, EdgeView.TOP); + } + } + + y = Utils.clamp(y, mBoundTop, mBoundBottom); + + // Horizontal direction: we show the edge effect when the scrolling + // tries to go left of the first image or go right of the last image. + if (!mHasPrev && x > mBoundRight) { + int pixels = x - mBoundRight; + mListener.onPull(pixels, EdgeView.LEFT); + x = mBoundRight; + } else if (!mHasNext && x < mBoundLeft) { + int pixels = mBoundLeft - x; + mListener.onPull(pixels, EdgeView.RIGHT); + x = mBoundLeft; + } + + startAnimation(x, y, b.mCurrentScale, ANIM_KIND_SCROLL); + } + + public void scrollFilmX(int dx) { + if (!canScroll()) return; + + Box b = mBoxes.get(0); + Platform p = mPlatform; + + // Only allow scrolling when we are not currently in an animation or we + // are in some animation with can be interrupted. + if (b.mAnimationStartTime != NO_ANIMATION) { + switch (b.mAnimationKind) { + case ANIM_KIND_SCROLL: + case ANIM_KIND_FLING: + case ANIM_KIND_FLING_X: + break; + default: + return; + } + } + + int x = p.mCurrentX + dx; + + // Horizontal direction: we show the edge effect when the scrolling + // tries to go left of the first image or go right of the last image. + x -= mPlatform.mDefaultX; + if (!mHasPrev && x > 0) { + mListener.onPull(x, EdgeView.LEFT); + x = 0; + } else if (!mHasNext && x < 0) { + mListener.onPull(-x, EdgeView.RIGHT); + x = 0; + } + x += mPlatform.mDefaultX; + startAnimation(x, b.mCurrentY, b.mCurrentScale, ANIM_KIND_SCROLL); + } + + public void scrollFilmY(int boxIndex, int dy) { + if (!canScroll()) return; + + Box b = mBoxes.get(boxIndex); + int y = b.mCurrentY + dy; + b.doAnimation(y, b.mCurrentScale, ANIM_KIND_SCROLL); + redraw(); + } + + public boolean flingPage(int velocityX, int velocityY) { + Box b = mBoxes.get(0); + Platform p = mPlatform; + + // We only want to do fling when the picture is zoomed-in. + if (viewWiderThanScaledImage(b.mCurrentScale) && + viewTallerThanScaledImage(b.mCurrentScale)) { + return false; + } + + // We only allow flinging in the directions where it won't go over the + // picture. + int edges = getImageAtEdges(); + if ((velocityX > 0 && (edges & IMAGE_AT_LEFT_EDGE) != 0) || + (velocityX < 0 && (edges & IMAGE_AT_RIGHT_EDGE) != 0)) { + velocityX = 0; + } + if ((velocityY > 0 && (edges & IMAGE_AT_TOP_EDGE) != 0) || + (velocityY < 0 && (edges & IMAGE_AT_BOTTOM_EDGE) != 0)) { + velocityY = 0; + } + + if (velocityX == 0 && velocityY == 0) return false; + + mPageScroller.fling(p.mCurrentX, b.mCurrentY, velocityX, velocityY, + mBoundLeft, mBoundRight, mBoundTop, mBoundBottom); + int targetX = mPageScroller.getFinalX(); + int targetY = mPageScroller.getFinalY(); + ANIM_TIME[ANIM_KIND_FLING] = mPageScroller.getDuration(); + return startAnimation(targetX, targetY, b.mCurrentScale, ANIM_KIND_FLING); + } + + public boolean flingFilmX(int velocityX) { + if (velocityX == 0) return false; + + Box b = mBoxes.get(0); + Platform p = mPlatform; + + // If we are already at the edge, don't start the fling. + int defaultX = p.mDefaultX; + if ((!mHasPrev && p.mCurrentX >= defaultX) + || (!mHasNext && p.mCurrentX <= defaultX)) { + return false; + } + + mFilmScroller.fling(p.mCurrentX, 0, velocityX, 0, + Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0); + int targetX = mFilmScroller.getFinalX(); + return startAnimation( + targetX, b.mCurrentY, b.mCurrentScale, ANIM_KIND_FLING_X); + } + + // Moves the specified box out of screen. If velocityY is 0, a default + // velocity is used. Returns the time for the duration, or -1 if we cannot + // not do the animation. + public int flingFilmY(int boxIndex, int velocityY) { + Box b = mBoxes.get(boxIndex); + + // Calculate targetY + int h = heightOf(b); + int targetY; + int FUZZY = 3; // TODO: figure out why this is needed. + if (velocityY < 0 || (velocityY == 0 && b.mCurrentY <= 0)) { + targetY = -mViewH / 2 - (h + 1) / 2 - FUZZY; + } else { + targetY = (mViewH + 1) / 2 + h / 2 + FUZZY; + } + + // Calculate duration + int duration; + if (velocityY != 0) { + duration = (int) (Math.abs(targetY - b.mCurrentY) * 1000f + / Math.abs(velocityY)); + duration = Math.min(MAX_DELETE_ANIMATION_DURATION, duration); + } else { + duration = DEFAULT_DELETE_ANIMATION_DURATION; + } + + // Start animation + ANIM_TIME[ANIM_KIND_DELETE] = duration; + if (b.doAnimation(targetY, b.mCurrentScale, ANIM_KIND_DELETE)) { + redraw(); + return duration; + } + return -1; + } + + // Returns the index of the box which contains the given point (x, y) + // Returns Integer.MAX_VALUE if there is no hit. There may be more than + // one box contains the given point, and we want to give priority to the + // one closer to the focused index (0). + public int hitTest(int x, int y) { + for (int i = 0; i < 2 * BOX_MAX + 1; i++) { + int j = CENTER_OUT_INDEX[i]; + Rect r = mRects.get(j); + if (r.contains(x, y)) { + return j; + } + } + + return Integer.MAX_VALUE; + } + + //////////////////////////////////////////////////////////////////////////// + // Redraw + // + // If a method changes box positions directly, redraw() + // should be called. + // + // If a method may also cause a snapback to happen, snapAndRedraw() should + // be called. + // + // If a method starts an animation to change the position of focused box, + // startAnimation() should be called. + // + // If time advances to change the box position, advanceAnimation() should + // be called. + //////////////////////////////////////////////////////////////////////////// + private void redraw() { + layoutAndSetPosition(); + mListener.invalidate(); + } + + private void snapAndRedraw() { + mPlatform.startSnapback(); + for (int i = -BOX_MAX; i <= BOX_MAX; i++) { + mBoxes.get(i).startSnapback(); + } + for (int i = -BOX_MAX; i < BOX_MAX; i++) { + mGaps.get(i).startSnapback(); + } + mFilmRatio.startSnapback(); + redraw(); + } + + private boolean startAnimation(int targetX, int targetY, float targetScale, + int kind) { + boolean changed = false; + changed |= mPlatform.doAnimation(targetX, mPlatform.mDefaultY, kind); + changed |= mBoxes.get(0).doAnimation(targetY, targetScale, kind); + if (changed) redraw(); + return changed; + } + + public void advanceAnimation() { + boolean changed = false; + changed |= mPlatform.advanceAnimation(); + for (int i = -BOX_MAX; i <= BOX_MAX; i++) { + changed |= mBoxes.get(i).advanceAnimation(); + } + for (int i = -BOX_MAX; i < BOX_MAX; i++) { + changed |= mGaps.get(i).advanceAnimation(); + } + changed |= mFilmRatio.advanceAnimation(); + if (changed) redraw(); + } + + public boolean inOpeningAnimation() { + return (mPlatform.mAnimationKind == ANIM_KIND_OPENING && + mPlatform.mAnimationStartTime != NO_ANIMATION) || + (mBoxes.get(0).mAnimationKind == ANIM_KIND_OPENING && + mBoxes.get(0).mAnimationStartTime != NO_ANIMATION); + } + + //////////////////////////////////////////////////////////////////////////// + // Layout + //////////////////////////////////////////////////////////////////////////// + + // Returns the display width of this box. + private int widthOf(Box b) { + return (int) (b.mImageW * b.mCurrentScale + 0.5f); + } + + // Returns the display height of this box. + private int heightOf(Box b) { + return (int) (b.mImageH * b.mCurrentScale + 0.5f); + } + + // Returns the display width of this box, using the given scale. + private int widthOf(Box b, float scale) { + return (int) (b.mImageW * scale + 0.5f); + } + + // Returns the display height of this box, using the given scale. + private int heightOf(Box b, float scale) { + return (int) (b.mImageH * scale + 0.5f); + } + + // Convert the information in mPlatform and mBoxes to mRects, so the user + // can get the position of each box by getPosition(). + // + // Note we go from center-out because each box's X coordinate + // is relative to its anchor box (except the focused box). + private void layoutAndSetPosition() { + for (int i = 0; i < 2 * BOX_MAX + 1; i++) { + convertBoxToRect(CENTER_OUT_INDEX[i]); + } + //dumpState(); + } + + @SuppressWarnings("unused") + private void dumpState() { + for (int i = -BOX_MAX; i < BOX_MAX; i++) { + Log.d(TAG, "Gap " + i + ": " + mGaps.get(i).mCurrentGap); + } + + for (int i = 0; i < 2 * BOX_MAX + 1; i++) { + dumpRect(CENTER_OUT_INDEX[i]); + } + + for (int i = -BOX_MAX; i <= BOX_MAX; i++) { + for (int j = i + 1; j <= BOX_MAX; j++) { + if (Rect.intersects(mRects.get(i), mRects.get(j))) { + Log.d(TAG, "rect " + i + " and rect " + j + "intersects!"); + } + } + } + } + + private void dumpRect(int i) { + StringBuilder sb = new StringBuilder(); + Rect r = mRects.get(i); + sb.append("Rect " + i + ":"); + sb.append("("); + sb.append(r.centerX()); + sb.append(","); + sb.append(r.centerY()); + sb.append(") ["); + sb.append(r.width()); + sb.append("x"); + sb.append(r.height()); + sb.append("]"); + Log.d(TAG, sb.toString()); + } + + private void convertBoxToRect(int i) { + Box b = mBoxes.get(i); + Rect r = mRects.get(i); + int y = b.mCurrentY + mPlatform.mCurrentY + mViewH / 2; + int w = widthOf(b); + int h = heightOf(b); + if (i == 0) { + int x = mPlatform.mCurrentX + mViewW / 2; + r.left = x - w / 2; + r.right = r.left + w; + } else if (i > 0) { + Rect a = mRects.get(i - 1); + Gap g = mGaps.get(i - 1); + r.left = a.right + g.mCurrentGap; + r.right = r.left + w; + } else { // i < 0 + Rect a = mRects.get(i + 1); + Gap g = mGaps.get(i); + r.right = a.left - g.mCurrentGap; + r.left = r.right - w; + } + r.top = y - h / 2; + r.bottom = r.top + h; + } + + // Returns the position of a box. + public Rect getPosition(int index) { + return mRects.get(index); + } + + //////////////////////////////////////////////////////////////////////////// + // Box management + //////////////////////////////////////////////////////////////////////////// + + // Initialize the platform to be at the view center. + private void initPlatform() { + mPlatform.updateDefaultXY(); + mPlatform.mCurrentX = mPlatform.mDefaultX; + mPlatform.mCurrentY = mPlatform.mDefaultY; + mPlatform.mAnimationStartTime = NO_ANIMATION; + } + + // Initialize a box to have the size of the view. + private void initBox(int index) { + Box b = mBoxes.get(index); + b.mImageW = mViewW; + b.mImageH = mViewH; + b.mUseViewSize = true; + b.mScaleMin = getMinimalScale(b); + b.mScaleMax = getMaximalScale(b); + b.mCurrentY = 0; + b.mCurrentScale = b.mScaleMin; + b.mAnimationStartTime = NO_ANIMATION; + b.mAnimationKind = ANIM_KIND_NONE; + } + + // Initialize a box to a given size. + private void initBox(int index, Size size) { + if (size.width == 0 || size.height == 0) { + initBox(index); + return; + } + Box b = mBoxes.get(index); + b.mImageW = size.width; + b.mImageH = size.height; + b.mUseViewSize = false; + b.mScaleMin = getMinimalScale(b); + b.mScaleMax = getMaximalScale(b); + b.mCurrentY = 0; + b.mCurrentScale = b.mScaleMin; + b.mAnimationStartTime = NO_ANIMATION; + b.mAnimationKind = ANIM_KIND_NONE; + } + + // Initialize a gap. This can only be called after the boxes around the gap + // has been initialized. + private void initGap(int index) { + Gap g = mGaps.get(index); + g.mDefaultSize = getDefaultGapSize(index); + g.mCurrentGap = g.mDefaultSize; + g.mAnimationStartTime = NO_ANIMATION; + } + + private void initGap(int index, int size) { + Gap g = mGaps.get(index); + g.mDefaultSize = getDefaultGapSize(index); + g.mCurrentGap = size; + g.mAnimationStartTime = NO_ANIMATION; + } + + @SuppressWarnings("unused") + private void debugMoveBox(int fromIndex[]) { + StringBuilder s = new StringBuilder("moveBox:"); + for (int i = 0; i < fromIndex.length; i++) { + int j = fromIndex[i]; + if (j == Integer.MAX_VALUE) { + s.append(" N"); + } else { + s.append(" "); + s.append(fromIndex[i]); + } + } + Log.d(TAG, s.toString()); + } + + // Move the boxes: it may indicate focus change, box deleted, box appearing, + // box reordered, etc. + // + // Each element in the fromIndex array indicates where each box was in the + // old array. If the value is Integer.MAX_VALUE (pictured as N below), it + // means the box is new. + // + // For example: + // N N N N N N N -- all new boxes + // -3 -2 -1 0 1 2 3 -- nothing changed + // -2 -1 0 1 2 3 N -- focus goes to the next box + // N -3 -2 -1 0 1 2 -- focus goes to the previous box + // -3 -2 -1 1 2 3 N -- the focused box was deleted. + // + // hasPrev/hasNext indicates if there are previous/next boxes for the + // focused box. constrained indicates whether the focused box should be put + // into the constrained frame. + public void moveBox(int fromIndex[], boolean hasPrev, boolean hasNext, + boolean constrained, Size[] sizes) { + //debugMoveBox(fromIndex); + mHasPrev = hasPrev; + mHasNext = hasNext; + + RangeIntArray from = new RangeIntArray(fromIndex, -BOX_MAX, BOX_MAX); + + // 1. Get the absolute X coordinates for the boxes. + layoutAndSetPosition(); + for (int i = -BOX_MAX; i <= BOX_MAX; i++) { + Box b = mBoxes.get(i); + Rect r = mRects.get(i); + b.mAbsoluteX = r.centerX() - mViewW / 2; + } + + // 2. copy boxes and gaps to temporary storage. + for (int i = -BOX_MAX; i <= BOX_MAX; i++) { + mTempBoxes.put(i, mBoxes.get(i)); + mBoxes.put(i, null); + } + for (int i = -BOX_MAX; i < BOX_MAX; i++) { + mTempGaps.put(i, mGaps.get(i)); + mGaps.put(i, null); + } + + // 3. move back boxes that are used in the new array. + for (int i = -BOX_MAX; i <= BOX_MAX; i++) { + int j = from.get(i); + if (j == Integer.MAX_VALUE) continue; + mBoxes.put(i, mTempBoxes.get(j)); + mTempBoxes.put(j, null); + } + + // 4. move back gaps if both boxes around it are kept together. + for (int i = -BOX_MAX; i < BOX_MAX; i++) { + int j = from.get(i); + if (j == Integer.MAX_VALUE) continue; + int k = from.get(i + 1); + if (k == Integer.MAX_VALUE) continue; + if (j + 1 == k) { + mGaps.put(i, mTempGaps.get(j)); + mTempGaps.put(j, null); + } + } + + // 5. recycle the boxes that are not used in the new array. + int k = -BOX_MAX; + for (int i = -BOX_MAX; i <= BOX_MAX; i++) { + if (mBoxes.get(i) != null) continue; + while (mTempBoxes.get(k) == null) { + k++; + } + mBoxes.put(i, mTempBoxes.get(k++)); + initBox(i, sizes[i + BOX_MAX]); + } + + // 6. Now give the recycled box a reasonable absolute X position. + // + // First try to find the first and the last box which the absolute X + // position is known. + int first, last; + for (first = -BOX_MAX; first <= BOX_MAX; first++) { + if (from.get(first) != Integer.MAX_VALUE) break; + } + for (last = BOX_MAX; last >= -BOX_MAX; last--) { + if (from.get(last) != Integer.MAX_VALUE) break; + } + // If there is no box has known X position at all, make the focused one + // as known. + if (first > BOX_MAX) { + mBoxes.get(0).mAbsoluteX = mPlatform.mCurrentX; + first = last = 0; + } + // Now for those boxes between first and last, assign their position to + // align to the previous box or the next box with known position. For + // the boxes before first or after last, we will use a new default gap + // size below. + + // Align to the previous box + for (int i = Math.max(0, first + 1); i < last; i++) { + if (from.get(i) != Integer.MAX_VALUE) continue; + Box a = mBoxes.get(i - 1); + Box b = mBoxes.get(i); + int wa = widthOf(a); + int wb = widthOf(b); + b.mAbsoluteX = a.mAbsoluteX + (wa - wa / 2) + wb / 2 + + getDefaultGapSize(i); + if (mPopFromTop) { + b.mCurrentY = -(mViewH / 2 + heightOf(b) / 2); + } else { + b.mCurrentY = (mViewH / 2 + heightOf(b) / 2); + } + } + + // Align to the next box + for (int i = Math.min(-1, last - 1); i > first; i--) { + if (from.get(i) != Integer.MAX_VALUE) continue; + Box a = mBoxes.get(i + 1); + Box b = mBoxes.get(i); + int wa = widthOf(a); + int wb = widthOf(b); + b.mAbsoluteX = a.mAbsoluteX - wa / 2 - (wb - wb / 2) + - getDefaultGapSize(i); + if (mPopFromTop) { + b.mCurrentY = -(mViewH / 2 + heightOf(b) / 2); + } else { + b.mCurrentY = (mViewH / 2 + heightOf(b) / 2); + } + } + + // 7. recycle the gaps that are not used in the new array. + k = -BOX_MAX; + for (int i = -BOX_MAX; i < BOX_MAX; i++) { + if (mGaps.get(i) != null) continue; + while (mTempGaps.get(k) == null) { + k++; + } + mGaps.put(i, mTempGaps.get(k++)); + Box a = mBoxes.get(i); + Box b = mBoxes.get(i + 1); + int wa = widthOf(a); + int wb = widthOf(b); + if (i >= first && i < last) { + int g = b.mAbsoluteX - a.mAbsoluteX - wb / 2 - (wa - wa / 2); + initGap(i, g); + } else { + initGap(i); + } + } + + // 8. calculate the new absolute X coordinates for those box before + // first or after last. + for (int i = first - 1; i >= -BOX_MAX; i--) { + Box a = mBoxes.get(i + 1); + Box b = mBoxes.get(i); + int wa = widthOf(a); + int wb = widthOf(b); + Gap g = mGaps.get(i); + b.mAbsoluteX = a.mAbsoluteX - wa / 2 - (wb - wb / 2) - g.mCurrentGap; + } + + for (int i = last + 1; i <= BOX_MAX; i++) { + Box a = mBoxes.get(i - 1); + Box b = mBoxes.get(i); + int wa = widthOf(a); + int wb = widthOf(b); + Gap g = mGaps.get(i - 1); + b.mAbsoluteX = a.mAbsoluteX + (wa - wa / 2) + wb / 2 + g.mCurrentGap; + } + + // 9. offset the Platform position + int dx = mBoxes.get(0).mAbsoluteX - mPlatform.mCurrentX; + mPlatform.mCurrentX += dx; + mPlatform.mFromX += dx; + mPlatform.mToX += dx; + mPlatform.mFlingOffset += dx; + + if (mConstrained != constrained) { + mConstrained = constrained; + mPlatform.updateDefaultXY(); + updateScaleAndGapLimit(); + } + + snapAndRedraw(); + } + + //////////////////////////////////////////////////////////////////////////// + // Public utilities + //////////////////////////////////////////////////////////////////////////// + + public boolean isAtMinimalScale() { + Box b = mBoxes.get(0); + return isAlmostEqual(b.mCurrentScale, b.mScaleMin); + } + + public boolean isCenter() { + Box b = mBoxes.get(0); + return mPlatform.mCurrentX == mPlatform.mDefaultX + && b.mCurrentY == 0; + } + + public int getImageWidth() { + Box b = mBoxes.get(0); + return b.mImageW; + } + + public int getImageHeight() { + Box b = mBoxes.get(0); + return b.mImageH; + } + + public float getImageScale() { + Box b = mBoxes.get(0); + return b.mCurrentScale; + } + + public int getImageAtEdges() { + Box b = mBoxes.get(0); + Platform p = mPlatform; + calculateStableBound(b.mCurrentScale); + int edges = 0; + if (p.mCurrentX <= mBoundLeft) { + edges |= IMAGE_AT_RIGHT_EDGE; + } + if (p.mCurrentX >= mBoundRight) { + edges |= IMAGE_AT_LEFT_EDGE; + } + if (b.mCurrentY <= mBoundTop) { + edges |= IMAGE_AT_BOTTOM_EDGE; + } + if (b.mCurrentY >= mBoundBottom) { + edges |= IMAGE_AT_TOP_EDGE; + } + return edges; + } + + public boolean isScrolling() { + return mPlatform.mAnimationStartTime != NO_ANIMATION + && mPlatform.mCurrentX != mPlatform.mToX; + } + + public void stopScrolling() { + if (mPlatform.mAnimationStartTime == NO_ANIMATION) return; + if (mFilmMode) mFilmScroller.forceFinished(true); + mPlatform.mFromX = mPlatform.mToX = mPlatform.mCurrentX; + } + + public float getFilmRatio() { + return mFilmRatio.mCurrentRatio; + } + + public void setPopFromTop(boolean top) { + mPopFromTop = top; + } + + public boolean hasDeletingBox() { + for(int i = -BOX_MAX; i <= BOX_MAX; i++) { + if (mBoxes.get(i).mAnimationKind == ANIM_KIND_DELETE) { + return true; + } + } + return false; + } + + //////////////////////////////////////////////////////////////////////////// + // Private utilities + //////////////////////////////////////////////////////////////////////////// + + private float getMinimalScale(Box b) { + float wFactor = 1.0f; + float hFactor = 1.0f; + int viewW, viewH; + + if (!mFilmMode && mConstrained && !mConstrainedFrame.isEmpty() + && b == mBoxes.get(0)) { + viewW = mConstrainedFrame.width(); + viewH = mConstrainedFrame.height(); + } else { + viewW = mViewW; + viewH = mViewH; + } + + if (mFilmMode) { + if (mViewH > mViewW) { // portrait + wFactor = FILM_MODE_PORTRAIT_WIDTH; + hFactor = FILM_MODE_PORTRAIT_HEIGHT; + } else { // landscape + wFactor = FILM_MODE_LANDSCAPE_WIDTH; + hFactor = FILM_MODE_LANDSCAPE_HEIGHT; + } + } + + float s = Math.min(wFactor * viewW / b.mImageW, + hFactor * viewH / b.mImageH); + return Math.min(SCALE_LIMIT, s); + } + + private float getMaximalScale(Box b) { + if (mFilmMode) return getMinimalScale(b); + if (mConstrained && !mConstrainedFrame.isEmpty()) return getMinimalScale(b); + return SCALE_LIMIT; + } + + private static boolean isAlmostEqual(float a, float b) { + float diff = a - b; + return (diff < 0 ? -diff : diff) < 0.02f; + } + + // Calculates the stable region of mPlatform.mCurrentX and + // mBoxes.get(0).mCurrentY, where "stable" means + // + // (1) If the dimension of scaled image >= view dimension, we will not + // see black region outside the image (at that dimension). + // (2) If the dimension of scaled image < view dimension, we will center + // the scaled image. + // + // We might temporarily go out of this stable during user interaction, + // but will "snap back" after user stops interaction. + // + // The results are stored in mBound{Left/Right/Top/Bottom}. + // + // An extra parameter "horizontalSlack" (which has the value of 0 usually) + // is used to extend the stable region by some pixels on each side + // horizontally. + private void calculateStableBound(float scale, int horizontalSlack) { + Box b = mBoxes.get(0); + + // The width and height of the box in number of view pixels + int w = widthOf(b, scale); + int h = heightOf(b, scale); + + // When the edge of the view is aligned with the edge of the box + mBoundLeft = (mViewW + 1) / 2 - (w + 1) / 2 - horizontalSlack; + mBoundRight = w / 2 - mViewW / 2 + horizontalSlack; + mBoundTop = (mViewH + 1) / 2 - (h + 1) / 2; + mBoundBottom = h / 2 - mViewH / 2; + + // If the scaled height is smaller than the view height, + // force it to be in the center. + if (viewTallerThanScaledImage(scale)) { + mBoundTop = mBoundBottom = 0; + } + + // Same for width + if (viewWiderThanScaledImage(scale)) { + mBoundLeft = mBoundRight = mPlatform.mDefaultX; + } + } + + private void calculateStableBound(float scale) { + calculateStableBound(scale, 0); + } + + private boolean viewTallerThanScaledImage(float scale) { + return mViewH >= heightOf(mBoxes.get(0), scale); + } + + private boolean viewWiderThanScaledImage(float scale) { + return mViewW >= widthOf(mBoxes.get(0), scale); + } + + private float getTargetScale(Box b) { + return b.mAnimationStartTime == NO_ANIMATION + ? b.mCurrentScale : b.mToScale; + } + + //////////////////////////////////////////////////////////////////////////// + // Animatable: an thing which can do animation. + //////////////////////////////////////////////////////////////////////////// + private abstract static class Animatable { + public long mAnimationStartTime; + public int mAnimationKind; + public int mAnimationDuration; + + // This should be overridden in subclass to change the animation values + // give the progress value in [0, 1]. + protected abstract boolean interpolate(float progress); + public abstract boolean startSnapback(); + + // Returns true if the animation values changes, so things need to be + // redrawn. + public boolean advanceAnimation() { + if (mAnimationStartTime == NO_ANIMATION) { + return false; + } + if (mAnimationStartTime == LAST_ANIMATION) { + mAnimationStartTime = NO_ANIMATION; + return startSnapback(); + } + + float progress; + if (mAnimationDuration == 0) { + progress = 1; + } else { + long now = AnimationTime.get(); + progress = + (float) (now - mAnimationStartTime) / mAnimationDuration; + } + + if (progress >= 1) { + progress = 1; + } else { + progress = applyInterpolationCurve(mAnimationKind, progress); + } + + boolean done = interpolate(progress); + + if (done) { + mAnimationStartTime = LAST_ANIMATION; + } + + return true; + } + + private static float applyInterpolationCurve(int kind, float progress) { + float f = 1 - progress; + switch (kind) { + case ANIM_KIND_SCROLL: + case ANIM_KIND_FLING: + case ANIM_KIND_FLING_X: + case ANIM_KIND_DELETE: + case ANIM_KIND_CAPTURE: + progress = 1 - f; // linear + break; + case ANIM_KIND_OPENING: + case ANIM_KIND_SCALE: + progress = 1 - f * f; // quadratic + break; + case ANIM_KIND_SNAPBACK: + case ANIM_KIND_ZOOM: + case ANIM_KIND_SLIDE: + progress = 1 - f * f * f * f * f; // x^5 + break; + } + return progress; + } + } + + //////////////////////////////////////////////////////////////////////////// + // Platform: captures the global X/Y movement. + //////////////////////////////////////////////////////////////////////////// + private class Platform extends Animatable { + public int mCurrentX, mFromX, mToX, mDefaultX; + public int mCurrentY, mFromY, mToY, mDefaultY; + public int mFlingOffset; + + @Override + public boolean startSnapback() { + if (mAnimationStartTime != NO_ANIMATION) return false; + if (mAnimationKind == ANIM_KIND_SCROLL + && mListener.isHoldingDown()) return false; + if (mInScale) return false; + + Box b = mBoxes.get(0); + float scaleMin = mExtraScalingRange ? + b.mScaleMin * SCALE_MIN_EXTRA : b.mScaleMin; + float scaleMax = mExtraScalingRange ? + b.mScaleMax * SCALE_MAX_EXTRA : b.mScaleMax; + float scale = Utils.clamp(b.mCurrentScale, scaleMin, scaleMax); + int x = mCurrentX; + int y = mDefaultY; + if (mFilmMode) { + x = mDefaultX; + } else { + calculateStableBound(scale, HORIZONTAL_SLACK); + // If the picture is zoomed-in, we want to keep the focus point + // stay in the same position on screen, so we need to adjust + // target mCurrentX (which is the center of the focused + // box). The position of the focus point on screen (relative the + // the center of the view) is: + // + // mCurrentX + scale * mFocusX = mCurrentX' + scale' * mFocusX + // => mCurrentX' = mCurrentX + (scale - scale') * mFocusX + // + if (!viewWiderThanScaledImage(scale)) { + float scaleDiff = b.mCurrentScale - scale; + x += (int) (mFocusX * scaleDiff + 0.5f); + } + x = Utils.clamp(x, mBoundLeft, mBoundRight); + } + if (mCurrentX != x || mCurrentY != y) { + return doAnimation(x, y, ANIM_KIND_SNAPBACK); + } + return false; + } + + // The updateDefaultXY() should be called whenever these variables + // changes: (1) mConstrained (2) mConstrainedFrame (3) mViewW/H (4) + // mFilmMode + public void updateDefaultXY() { + // We don't check mFilmMode and return 0 for mDefaultX. Because + // otherwise if we decide to leave film mode because we are + // centered, we will immediately back into film mode because we find + // we are not centered. + if (mConstrained && !mConstrainedFrame.isEmpty()) { + mDefaultX = mConstrainedFrame.centerX() - mViewW / 2; + mDefaultY = mFilmMode ? 0 : + mConstrainedFrame.centerY() - mViewH / 2; + } else { + mDefaultX = 0; + mDefaultY = 0; + } + } + + // Starts an animation for the platform. + private boolean doAnimation(int targetX, int targetY, int kind) { + if (mCurrentX == targetX && mCurrentY == targetY) return false; + mAnimationKind = kind; + mFromX = mCurrentX; + mFromY = mCurrentY; + mToX = targetX; + mToY = targetY; + mAnimationStartTime = AnimationTime.startTime(); + mAnimationDuration = ANIM_TIME[kind]; + mFlingOffset = 0; + advanceAnimation(); + return true; + } + + @Override + protected boolean interpolate(float progress) { + if (mAnimationKind == ANIM_KIND_FLING) { + return interpolateFlingPage(progress); + } else if (mAnimationKind == ANIM_KIND_FLING_X) { + return interpolateFlingFilm(progress); + } else { + return interpolateLinear(progress); + } + } + + private boolean interpolateFlingFilm(float progress) { + mFilmScroller.computeScrollOffset(); + mCurrentX = mFilmScroller.getCurrX() + mFlingOffset; + + int dir = EdgeView.INVALID_DIRECTION; + if (mCurrentX < mDefaultX) { + if (!mHasNext) { + dir = EdgeView.RIGHT; + } + } else if (mCurrentX > mDefaultX) { + if (!mHasPrev) { + dir = EdgeView.LEFT; + } + } + if (dir != EdgeView.INVALID_DIRECTION) { + // TODO: restore this onAbsorb call + //int v = (int) (mFilmScroller.getCurrVelocity() + 0.5f); + //mListener.onAbsorb(v, dir); + mFilmScroller.forceFinished(true); + mCurrentX = mDefaultX; + } + return mFilmScroller.isFinished(); + } + + private boolean interpolateFlingPage(float progress) { + mPageScroller.computeScrollOffset(progress); + Box b = mBoxes.get(0); + calculateStableBound(b.mCurrentScale); + + int oldX = mCurrentX; + mCurrentX = mPageScroller.getCurrX(); + + // Check if we hit the edges; show edge effects if we do. + if (oldX > mBoundLeft && mCurrentX == mBoundLeft) { + int v = (int) (-mPageScroller.getCurrVelocityX() + 0.5f); + mListener.onAbsorb(v, EdgeView.RIGHT); + } else if (oldX < mBoundRight && mCurrentX == mBoundRight) { + int v = (int) (mPageScroller.getCurrVelocityX() + 0.5f); + mListener.onAbsorb(v, EdgeView.LEFT); + } + + return progress >= 1; + } + + private boolean interpolateLinear(float progress) { + // Other animations + if (progress >= 1) { + mCurrentX = mToX; + mCurrentY = mToY; + return true; + } else { + if (mAnimationKind == ANIM_KIND_CAPTURE) { + progress = CaptureAnimation.calculateSlide(progress); + } + mCurrentX = (int) (mFromX + progress * (mToX - mFromX)); + mCurrentY = (int) (mFromY + progress * (mToY - mFromY)); + if (mAnimationKind == ANIM_KIND_CAPTURE) { + return false; + } else { + return (mCurrentX == mToX && mCurrentY == mToY); + } + } + } + } + + //////////////////////////////////////////////////////////////////////////// + // Box: represents a rectangular area which shows a picture. + //////////////////////////////////////////////////////////////////////////// + private class Box extends Animatable { + // Size of the bitmap + public int mImageW, mImageH; + + // This is true if we assume the image size is the same as view size + // until we know the actual size of image. This is also used to + // determine if there is an image ready to show. + public boolean mUseViewSize; + + // The minimum and maximum scale we allow for this box. + public float mScaleMin, mScaleMax; + + // The X/Y value indicates where the center of the box is on the view + // coordinate. We always keep the mCurrent{X,Y,Scale} sync with the + // actual values used currently. Note that the X values are implicitly + // defined by Platform and Gaps. + public int mCurrentY, mFromY, mToY; + public float mCurrentScale, mFromScale, mToScale; + + // The absolute X coordinate of the center of the box. This is only used + // during moveBox(). + public int mAbsoluteX; + + @Override + public boolean startSnapback() { + if (mAnimationStartTime != NO_ANIMATION) return false; + if (mAnimationKind == ANIM_KIND_SCROLL + && mListener.isHoldingDown()) return false; + if (mAnimationKind == ANIM_KIND_DELETE + && mListener.isHoldingDelete()) return false; + if (mInScale && this == mBoxes.get(0)) return false; + + int y = mCurrentY; + float scale; + + if (this == mBoxes.get(0)) { + float scaleMin = mExtraScalingRange ? + mScaleMin * SCALE_MIN_EXTRA : mScaleMin; + float scaleMax = mExtraScalingRange ? + mScaleMax * SCALE_MAX_EXTRA : mScaleMax; + scale = Utils.clamp(mCurrentScale, scaleMin, scaleMax); + if (mFilmMode) { + y = 0; + } else { + calculateStableBound(scale, HORIZONTAL_SLACK); + // If the picture is zoomed-in, we want to keep the focus + // point stay in the same position on screen. See the + // comment in Platform.startSnapback for details. + if (!viewTallerThanScaledImage(scale)) { + float scaleDiff = mCurrentScale - scale; + y += (int) (mFocusY * scaleDiff + 0.5f); + } + y = Utils.clamp(y, mBoundTop, mBoundBottom); + } + } else { + y = 0; + scale = mScaleMin; + } + + if (mCurrentY != y || mCurrentScale != scale) { + return doAnimation(y, scale, ANIM_KIND_SNAPBACK); + } + return false; + } + + private boolean doAnimation(int targetY, float targetScale, int kind) { + targetScale = clampScale(targetScale); + + if (mCurrentY == targetY && mCurrentScale == targetScale + && kind != ANIM_KIND_CAPTURE) { + return false; + } + + // Now starts an animation for the box. + mAnimationKind = kind; + mFromY = mCurrentY; + mFromScale = mCurrentScale; + mToY = targetY; + mToScale = targetScale; + mAnimationStartTime = AnimationTime.startTime(); + mAnimationDuration = ANIM_TIME[kind]; + advanceAnimation(); + return true; + } + + // Clamps the input scale to the range that doAnimation() can reach. + public float clampScale(float s) { + return Utils.clamp(s, + SCALE_MIN_EXTRA * mScaleMin, + SCALE_MAX_EXTRA * mScaleMax); + } + + @Override + protected boolean interpolate(float progress) { + if (mAnimationKind == ANIM_KIND_FLING) { + return interpolateFlingPage(progress); + } else { + return interpolateLinear(progress); + } + } + + private boolean interpolateFlingPage(float progress) { + mPageScroller.computeScrollOffset(progress); + calculateStableBound(mCurrentScale); + + int oldY = mCurrentY; + mCurrentY = mPageScroller.getCurrY(); + + // Check if we hit the edges; show edge effects if we do. + if (oldY > mBoundTop && mCurrentY == mBoundTop) { + int v = (int) (-mPageScroller.getCurrVelocityY() + 0.5f); + mListener.onAbsorb(v, EdgeView.BOTTOM); + } else if (oldY < mBoundBottom && mCurrentY == mBoundBottom) { + int v = (int) (mPageScroller.getCurrVelocityY() + 0.5f); + mListener.onAbsorb(v, EdgeView.TOP); + } + + return progress >= 1; + } + + private boolean interpolateLinear(float progress) { + if (progress >= 1) { + mCurrentY = mToY; + mCurrentScale = mToScale; + return true; + } else { + mCurrentY = (int) (mFromY + progress * (mToY - mFromY)); + mCurrentScale = mFromScale + progress * (mToScale - mFromScale); + if (mAnimationKind == ANIM_KIND_CAPTURE) { + float f = CaptureAnimation.calculateScale(progress); + mCurrentScale *= f; + return false; + } else { + return (mCurrentY == mToY && mCurrentScale == mToScale); + } + } + } + } + + //////////////////////////////////////////////////////////////////////////// + // Gap: represents a rectangular area which is between two boxes. + //////////////////////////////////////////////////////////////////////////// + private class Gap extends Animatable { + // The default gap size between two boxes. The value may vary for + // different image size of the boxes and for different modes (page or + // film). + public int mDefaultSize; + + // The gap size between the two boxes. + public int mCurrentGap, mFromGap, mToGap; + + @Override + public boolean startSnapback() { + if (mAnimationStartTime != NO_ANIMATION) return false; + return doAnimation(mDefaultSize, ANIM_KIND_SNAPBACK); + } + + // Starts an animation for a gap. + public boolean doAnimation(int targetSize, int kind) { + if (mCurrentGap == targetSize && kind != ANIM_KIND_CAPTURE) { + return false; + } + mAnimationKind = kind; + mFromGap = mCurrentGap; + mToGap = targetSize; + mAnimationStartTime = AnimationTime.startTime(); + mAnimationDuration = ANIM_TIME[mAnimationKind]; + advanceAnimation(); + return true; + } + + @Override + protected boolean interpolate(float progress) { + if (progress >= 1) { + mCurrentGap = mToGap; + return true; + } else { + mCurrentGap = (int) (mFromGap + progress * (mToGap - mFromGap)); + if (mAnimationKind == ANIM_KIND_CAPTURE) { + float f = CaptureAnimation.calculateScale(progress); + mCurrentGap = (int) (mCurrentGap * f); + return false; + } else { + return (mCurrentGap == mToGap); + } + } + } + } + + //////////////////////////////////////////////////////////////////////////// + // FilmRatio: represents the progress of film mode change. + //////////////////////////////////////////////////////////////////////////// + private class FilmRatio extends Animatable { + // The film ratio: 1 means switching to film mode is complete, 0 means + // switching to page mode is complete. + public float mCurrentRatio, mFromRatio, mToRatio; + + @Override + public boolean startSnapback() { + float target = mFilmMode ? 1f : 0f; + if (target == mToRatio) return false; + return doAnimation(target, ANIM_KIND_SNAPBACK); + } + + // Starts an animation for the film ratio. + private boolean doAnimation(float targetRatio, int kind) { + mAnimationKind = kind; + mFromRatio = mCurrentRatio; + mToRatio = targetRatio; + mAnimationStartTime = AnimationTime.startTime(); + mAnimationDuration = ANIM_TIME[mAnimationKind]; + advanceAnimation(); + return true; + } + + @Override + protected boolean interpolate(float progress) { + if (progress >= 1) { + mCurrentRatio = mToRatio; + return true; + } else { + mCurrentRatio = mFromRatio + progress * (mToRatio - mFromRatio); + return (mCurrentRatio == mToRatio); + } + } + } +} diff --git a/src/com/android/gallery3d/ui/PreparePageFadeoutTexture.java b/src/com/android/gallery3d/ui/PreparePageFadeoutTexture.java new file mode 100644 index 000000000..ce672f211 --- /dev/null +++ b/src/com/android/gallery3d/ui/PreparePageFadeoutTexture.java @@ -0,0 +1,85 @@ +package com.android.gallery3d.ui; + +import android.os.ConditionVariable; + +import com.android.gallery3d.app.AbstractGalleryActivity; +import com.android.gallery3d.glrenderer.GLCanvas; +import com.android.gallery3d.glrenderer.RawTexture; +import com.android.gallery3d.ui.GLRoot.OnGLIdleListener; + +public class PreparePageFadeoutTexture implements OnGLIdleListener { + private static final long TIMEOUT = 200; + public static final String KEY_FADE_TEXTURE = "fade_texture"; + + private RawTexture mTexture; + private ConditionVariable mResultReady = new ConditionVariable(false); + private boolean mCancelled = false; + private GLView mRootPane; + + public PreparePageFadeoutTexture(GLView rootPane) { + if (rootPane == null) { + mCancelled = true; + return; + } + int w = rootPane.getWidth(); + int h = rootPane.getHeight(); + if (w == 0 || h == 0) { + mCancelled = true; + return; + } + mTexture = new RawTexture(w, h, true); + mRootPane = rootPane; + } + + public boolean isCancelled() { + return mCancelled; + } + + public synchronized RawTexture get() { + if (mCancelled) { + return null; + } else if (mResultReady.block(TIMEOUT)) { + return mTexture; + } else { + mCancelled = true; + return null; + } + } + + @Override + public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) { + if (!mCancelled) { + try { + canvas.beginRenderTarget(mTexture); + mRootPane.render(canvas); + canvas.endRenderTarget(); + } catch (RuntimeException e) { + mTexture = null; + } + } else { + mTexture = null; + } + mResultReady.open(); + return false; + } + + public static void prepareFadeOutTexture(AbstractGalleryActivity activity, + GLView rootPane) { + PreparePageFadeoutTexture task = new PreparePageFadeoutTexture(rootPane); + if (task.isCancelled()) return; + GLRoot root = activity.getGLRoot(); + RawTexture texture = null; + root.unlockRenderThread(); + try { + root.addOnGLIdleListener(task); + texture = task.get(); + } finally { + root.lockRenderThread(); + } + + if (texture == null) { + return; + } + activity.getTransitionStore().put(KEY_FADE_TEXTURE, texture); + } +} diff --git a/src/com/android/gallery3d/ui/ProgressSpinner.java b/src/com/android/gallery3d/ui/ProgressSpinner.java new file mode 100644 index 000000000..1b31af278 --- /dev/null +++ b/src/com/android/gallery3d/ui/ProgressSpinner.java @@ -0,0 +1,80 @@ +/* + * 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.ui; + +import android.content.Context; + +import com.android.gallery3d.R; +import com.android.gallery3d.glrenderer.GLCanvas; +import com.android.gallery3d.glrenderer.ResourceTexture; + +public class ProgressSpinner { + private static float ROTATE_SPEED_OUTER = 1080f / 3500f; + private static float ROTATE_SPEED_INNER = -720f / 3500f; + private final ResourceTexture mOuter; + private final ResourceTexture mInner; + private final int mWidth; + private final int mHeight; + + private float mInnerDegree = 0f; + private float mOuterDegree = 0f; + private long mAnimationTimestamp = -1; + + public ProgressSpinner(Context context) { + mOuter = new ResourceTexture(context, R.drawable.spinner_76_outer_holo); + mInner = new ResourceTexture(context, R.drawable.spinner_76_inner_holo); + + mWidth = Math.max(mOuter.getWidth(), mInner.getWidth()); + mHeight = Math.max(mOuter.getHeight(), mInner.getHeight()); + } + + public int getWidth() { + return mWidth; + } + + public int getHeight() { + return mHeight; + } + + public void startAnimation() { + mAnimationTimestamp = -1; + mOuterDegree = 0; + mInnerDegree = 0; + } + + public void draw(GLCanvas canvas, int x, int y) { + long now = AnimationTime.get(); + if (mAnimationTimestamp == -1) mAnimationTimestamp = now; + mOuterDegree += (now - mAnimationTimestamp) * ROTATE_SPEED_OUTER; + mInnerDegree += (now - mAnimationTimestamp) * ROTATE_SPEED_INNER; + + mAnimationTimestamp = now; + + // just preventing overflow + if (mOuterDegree > 360) mOuterDegree -= 360f; + if (mInnerDegree < 0) mInnerDegree += 360f; + + canvas.save(GLCanvas.SAVE_FLAG_MATRIX); + + canvas.translate(x + mWidth / 2, y + mHeight / 2); + canvas.rotate(mInnerDegree, 0, 0, 1); + mOuter.draw(canvas, -mOuter.getWidth() / 2, -mOuter.getHeight() / 2); + canvas.rotate(mOuterDegree - mInnerDegree, 0, 0, 1); + mInner.draw(canvas, -mInner.getWidth() / 2, -mInner.getHeight() / 2); + canvas.restore(); + } +} diff --git a/src/com/android/gallery3d/ui/RelativePosition.java b/src/com/android/gallery3d/ui/RelativePosition.java new file mode 100644 index 000000000..0f2bfd812 --- /dev/null +++ b/src/com/android/gallery3d/ui/RelativePosition.java @@ -0,0 +1,42 @@ +/* + * 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.ui; + +public class RelativePosition { + private float mAbsoluteX; + private float mAbsoluteY; + private float mReferenceX; + private float mReferenceY; + + public void setAbsolutePosition(int absoluteX, int absoluteY) { + mAbsoluteX = absoluteX; + mAbsoluteY = absoluteY; + } + + public void setReferencePosition(int x, int y) { + mReferenceX = x; + mReferenceY = y; + } + + public float getX() { + return mAbsoluteX - mReferenceX; + } + + public float getY() { + return mAbsoluteY - mReferenceY; + } +} diff --git a/src/com/android/gallery3d/ui/ScreenNail.java b/src/com/android/gallery3d/ui/ScreenNail.java new file mode 100644 index 000000000..965bf0b54 --- /dev/null +++ b/src/com/android/gallery3d/ui/ScreenNail.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2012 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.ui; + +import android.graphics.RectF; + +import com.android.gallery3d.glrenderer.GLCanvas; + +public interface ScreenNail { + public int getWidth(); + public int getHeight(); + public void draw(GLCanvas canvas, int x, int y, int width, int height); + + // We do not need to draw this ScreenNail in this frame. + public void noDraw(); + + // This ScreenNail will not be used anymore. Release related resources. + public void recycle(); + + // This is only used by TileImageView to back up the tiles not yet loaded. + public void draw(GLCanvas canvas, RectF source, RectF dest); +} diff --git a/src/com/android/gallery3d/ui/ScrollBarView.java b/src/com/android/gallery3d/ui/ScrollBarView.java new file mode 100644 index 000000000..34fbcef7a --- /dev/null +++ b/src/com/android/gallery3d/ui/ScrollBarView.java @@ -0,0 +1,97 @@ +/* + * 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.ui; + +import android.content.Context; +import android.graphics.Rect; +import android.util.TypedValue; + +import com.android.gallery3d.glrenderer.GLCanvas; +import com.android.gallery3d.glrenderer.NinePatchTexture; + +public class ScrollBarView extends GLView { + @SuppressWarnings("unused") + private static final String TAG = "ScrollBarView"; + + private int mBarHeight; + + private int mGripHeight; + private int mGripPosition; // left side of the grip + private int mGripWidth; // zero if the grip is disabled + private int mGivenGripWidth; + + private int mContentPosition; + private int mContentTotal; + + private NinePatchTexture mScrollBarTexture; + + public ScrollBarView(Context context, int gripHeight, int gripWidth) { + TypedValue outValue = new TypedValue(); + context.getTheme().resolveAttribute( + android.R.attr.scrollbarThumbHorizontal, outValue, true); + mScrollBarTexture = new NinePatchTexture( + context, outValue.resourceId); + mGripPosition = 0; + mGripWidth = 0; + mGivenGripWidth = gripWidth; + mGripHeight = gripHeight; + } + + @Override + protected void onLayout( + boolean changed, int left, int top, int right, int bottom) { + if (!changed) return; + mBarHeight = bottom - top; + } + + // The content position is between 0 to "total". The current position is + // in "position". + public void setContentPosition(int position, int total) { + if (position == mContentPosition && total == mContentTotal) { + return; + } + + invalidate(); + + mContentPosition = position; + mContentTotal = total; + + // If the grip cannot move, don't draw it. + if (mContentTotal <= 0) { + mGripPosition = 0; + mGripWidth = 0; + return; + } + + // Map from the content range to scroll bar range. + // + // mContentTotal --> getWidth() - mGripWidth + // mContentPosition --> mGripPosition + mGripWidth = mGivenGripWidth; + float r = (getWidth() - mGripWidth) / (float) mContentTotal; + mGripPosition = Math.round(r * mContentPosition); + } + + @Override + protected void render(GLCanvas canvas) { + super.render(canvas); + if (mGripWidth == 0) return; + Rect b = bounds(); + int y = (mBarHeight - mGripHeight) / 2; + mScrollBarTexture.draw(canvas, mGripPosition, y, mGripWidth, mGripHeight); + } +} diff --git a/src/com/android/gallery3d/ui/ScrollerHelper.java b/src/com/android/gallery3d/ui/ScrollerHelper.java new file mode 100644 index 000000000..aa68d19d9 --- /dev/null +++ b/src/com/android/gallery3d/ui/ScrollerHelper.java @@ -0,0 +1,97 @@ +/* + * 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.ui; + +import android.content.Context; +import android.view.ViewConfiguration; + +import com.android.gallery3d.common.OverScroller; +import com.android.gallery3d.common.Utils; + +public class ScrollerHelper { + private OverScroller mScroller; + private int mOverflingDistance; + private boolean mOverflingEnabled; + + public ScrollerHelper(Context context) { + mScroller = new OverScroller(context); + ViewConfiguration configuration = ViewConfiguration.get(context); + mOverflingDistance = configuration.getScaledOverflingDistance(); + } + + public void setOverfling(boolean enabled) { + mOverflingEnabled = enabled; + } + + /** + * Call this when you want to know the new location. The position will be + * updated and can be obtained by getPosition(). Returns true if the + * animation is not yet finished. + */ + public boolean advanceAnimation(long currentTimeMillis) { + return mScroller.computeScrollOffset(); + } + + public boolean isFinished() { + return mScroller.isFinished(); + } + + public void forceFinished() { + mScroller.forceFinished(true); + } + + public int getPosition() { + return mScroller.getCurrX(); + } + + public float getCurrVelocity() { + return mScroller.getCurrVelocity(); + } + + public void setPosition(int position) { + mScroller.startScroll( + position, 0, // startX, startY + 0, 0, 0); // dx, dy, duration + + // This forces the scroller to reach the final position. + mScroller.abortAnimation(); + } + + public void fling(int velocity, int min, int max) { + int currX = getPosition(); + mScroller.fling( + currX, 0, // startX, startY + velocity, 0, // velocityX, velocityY + min, max, // minX, maxX + 0, 0, // minY, maxY + mOverflingEnabled ? mOverflingDistance : 0, 0); + } + + // Returns the distance that over the scroll limit. + public int startScroll(int distance, int min, int max) { + int currPosition = mScroller.getCurrX(); + int finalPosition = mScroller.isFinished() ? currPosition : + mScroller.getFinalX(); + int newPosition = Utils.clamp(finalPosition + distance, min, max); + if (newPosition != currPosition) { + mScroller.startScroll( + currPosition, 0, // startX, startY + newPosition - currPosition, 0, 0); // dx, dy, duration + } + return finalPosition + distance - newPosition; + } +} diff --git a/src/com/android/gallery3d/ui/SelectionManager.java b/src/com/android/gallery3d/ui/SelectionManager.java new file mode 100644 index 000000000..be6811bc1 --- /dev/null +++ b/src/com/android/gallery3d/ui/SelectionManager.java @@ -0,0 +1,251 @@ +/* + * 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.ui; + +import com.android.gallery3d.app.AbstractGalleryActivity; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.Path; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +public class SelectionManager { + @SuppressWarnings("unused") + private static final String TAG = "SelectionManager"; + + public static final int ENTER_SELECTION_MODE = 1; + public static final int LEAVE_SELECTION_MODE = 2; + public static final int SELECT_ALL_MODE = 3; + + private Set<Path> mClickedSet; + private MediaSet mSourceMediaSet; + private SelectionListener mListener; + private DataManager mDataManager; + private boolean mInverseSelection; + private boolean mIsAlbumSet; + private boolean mInSelectionMode; + private boolean mAutoLeave = true; + private int mTotal; + + public interface SelectionListener { + public void onSelectionModeChange(int mode); + public void onSelectionChange(Path path, boolean selected); + } + + public SelectionManager(AbstractGalleryActivity activity, boolean isAlbumSet) { + mDataManager = activity.getDataManager(); + mClickedSet = new HashSet<Path>(); + mIsAlbumSet = isAlbumSet; + mTotal = -1; + } + + // Whether we will leave selection mode automatically once the number of + // selected items is down to zero. + public void setAutoLeaveSelectionMode(boolean enable) { + mAutoLeave = enable; + } + + public void setSelectionListener(SelectionListener listener) { + mListener = listener; + } + + public void selectAll() { + mInverseSelection = true; + mClickedSet.clear(); + enterSelectionMode(); + if (mListener != null) mListener.onSelectionModeChange(SELECT_ALL_MODE); + } + + public void deSelectAll() { + leaveSelectionMode(); + mInverseSelection = false; + mClickedSet.clear(); + } + + public boolean inSelectAllMode() { + return mInverseSelection; + } + + public boolean inSelectionMode() { + return mInSelectionMode; + } + + public void enterSelectionMode() { + if (mInSelectionMode) return; + + mInSelectionMode = true; + if (mListener != null) mListener.onSelectionModeChange(ENTER_SELECTION_MODE); + } + + public void leaveSelectionMode() { + if (!mInSelectionMode) return; + + mInSelectionMode = false; + mInverseSelection = false; + mClickedSet.clear(); + if (mListener != null) mListener.onSelectionModeChange(LEAVE_SELECTION_MODE); + } + + public boolean isItemSelected(Path itemId) { + return mInverseSelection ^ mClickedSet.contains(itemId); + } + + private int getTotalCount() { + if (mSourceMediaSet == null) return -1; + + if (mTotal < 0) { + mTotal = mIsAlbumSet + ? mSourceMediaSet.getSubMediaSetCount() + : mSourceMediaSet.getMediaItemCount(); + } + return mTotal; + } + + public int getSelectedCount() { + int count = mClickedSet.size(); + if (mInverseSelection) { + count = getTotalCount() - count; + } + return count; + } + + public void toggle(Path path) { + if (mClickedSet.contains(path)) { + mClickedSet.remove(path); + } else { + enterSelectionMode(); + mClickedSet.add(path); + } + + // Convert to inverse selection mode if everything is selected. + int count = getSelectedCount(); + if (count == getTotalCount()) { + selectAll(); + } + + if (mListener != null) mListener.onSelectionChange(path, isItemSelected(path)); + if (count == 0 && mAutoLeave) { + leaveSelectionMode(); + } + } + + private static boolean expandMediaSet(ArrayList<Path> items, MediaSet set, int maxSelection) { + int subCount = set.getSubMediaSetCount(); + for (int i = 0; i < subCount; i++) { + if (!expandMediaSet(items, set.getSubMediaSet(i), maxSelection)) { + return false; + } + } + int total = set.getMediaItemCount(); + int batch = 50; + int index = 0; + + while (index < total) { + int count = index + batch < total + ? batch + : total - index; + ArrayList<MediaItem> list = set.getMediaItem(index, count); + if (list != null + && list.size() > (maxSelection - items.size())) { + return false; + } + for (MediaItem item : list) { + items.add(item.getPath()); + } + index += batch; + } + return true; + } + + public ArrayList<Path> getSelected(boolean expandSet) { + return getSelected(expandSet, Integer.MAX_VALUE); + } + + public ArrayList<Path> getSelected(boolean expandSet, int maxSelection) { + ArrayList<Path> selected = new ArrayList<Path>(); + if (mIsAlbumSet) { + if (mInverseSelection) { + int total = getTotalCount(); + for (int i = 0; i < total; i++) { + MediaSet set = mSourceMediaSet.getSubMediaSet(i); + Path id = set.getPath(); + if (!mClickedSet.contains(id)) { + if (expandSet) { + if (!expandMediaSet(selected, set, maxSelection)) { + return null; + } + } else { + selected.add(id); + if (selected.size() > maxSelection) { + return null; + } + } + } + } + } else { + for (Path id : mClickedSet) { + if (expandSet) { + if (!expandMediaSet(selected, mDataManager.getMediaSet(id), + maxSelection)) { + return null; + } + } else { + selected.add(id); + if (selected.size() > maxSelection) { + return null; + } + } + } + } + } else { + if (mInverseSelection) { + int total = getTotalCount(); + int index = 0; + while (index < total) { + int count = Math.min(total - index, MediaSet.MEDIAITEM_BATCH_FETCH_COUNT); + ArrayList<MediaItem> list = mSourceMediaSet.getMediaItem(index, count); + for (MediaItem item : list) { + Path id = item.getPath(); + if (!mClickedSet.contains(id)) { + selected.add(id); + if (selected.size() > maxSelection) { + return null; + } + } + } + index += count; + } + } else { + for (Path id : mClickedSet) { + selected.add(id); + if (selected.size() > maxSelection) { + return null; + } + } + } + } + return selected; + } + + public void setSourceMediaSet(MediaSet set) { + mSourceMediaSet = set; + mTotal = -1; + } +} diff --git a/src/com/android/gallery3d/ui/SelectionMenu.java b/src/com/android/gallery3d/ui/SelectionMenu.java new file mode 100644 index 000000000..5b0828328 --- /dev/null +++ b/src/com/android/gallery3d/ui/SelectionMenu.java @@ -0,0 +1,61 @@ +/* + * 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.ui; + +import android.content.Context; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; + +import com.android.gallery3d.R; +import com.android.gallery3d.ui.PopupList.OnPopupItemClickListener; + +public class SelectionMenu implements OnClickListener { + @SuppressWarnings("unused") + private static final String TAG = "SelectionMenu"; + + private final Context mContext; + private final Button mButton; + private final PopupList mPopupList; + + public SelectionMenu(Context context, Button button, OnPopupItemClickListener listener) { + mContext = context; + mButton = button; + mPopupList = new PopupList(context, mButton); + mPopupList.addItem(R.id.action_select_all, + context.getString(R.string.select_all)); + mPopupList.setOnPopupItemClickListener(listener); + mButton.setOnClickListener(this); + } + + @Override + public void onClick(View v) { + mPopupList.show(); + } + + public void updateSelectAllMode(boolean inSelectAllMode) { + PopupList.Item item = mPopupList.findItem(R.id.action_select_all); + if (item != null) { + item.setTitle(mContext.getString( + inSelectAllMode ? R.string.deselect_all : R.string.select_all)); + } + } + + public void setTitle(CharSequence title) { + mButton.setText(title); + } +} diff --git a/src/com/android/gallery3d/ui/SlideshowView.java b/src/com/android/gallery3d/ui/SlideshowView.java new file mode 100644 index 000000000..43784232d --- /dev/null +++ b/src/com/android/gallery3d/ui/SlideshowView.java @@ -0,0 +1,163 @@ +/* + * 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.ui; + +import android.graphics.Bitmap; +import android.graphics.PointF; + +import com.android.gallery3d.anim.CanvasAnimation; +import com.android.gallery3d.anim.FloatAnimation; +import com.android.gallery3d.glrenderer.BitmapTexture; +import com.android.gallery3d.glrenderer.GLCanvas; + +import java.util.Random; + +public class SlideshowView extends GLView { + @SuppressWarnings("unused") + private static final String TAG = "SlideshowView"; + + private static final int SLIDESHOW_DURATION = 3500; + private static final int TRANSITION_DURATION = 1000; + + private static final float SCALE_SPEED = 0.20f ; + private static final float MOVE_SPEED = SCALE_SPEED; + + private int mCurrentRotation; + private BitmapTexture mCurrentTexture; + private SlideshowAnimation mCurrentAnimation; + + private int mPrevRotation; + private BitmapTexture mPrevTexture; + private SlideshowAnimation mPrevAnimation; + + private final FloatAnimation mTransitionAnimation = + new FloatAnimation(0, 1, TRANSITION_DURATION); + + private Random mRandom = new Random(); + + public void next(Bitmap bitmap, int rotation) { + + mTransitionAnimation.start(); + + if (mPrevTexture != null) { + mPrevTexture.getBitmap().recycle(); + mPrevTexture.recycle(); + } + + mPrevTexture = mCurrentTexture; + mPrevAnimation = mCurrentAnimation; + mPrevRotation = mCurrentRotation; + + mCurrentRotation = rotation; + mCurrentTexture = new BitmapTexture(bitmap); + if (((rotation / 90) & 0x01) == 0) { + mCurrentAnimation = new SlideshowAnimation( + mCurrentTexture.getWidth(), mCurrentTexture.getHeight(), + mRandom); + } else { + mCurrentAnimation = new SlideshowAnimation( + mCurrentTexture.getHeight(), mCurrentTexture.getWidth(), + mRandom); + } + mCurrentAnimation.start(); + + invalidate(); + } + + public void release() { + if (mPrevTexture != null) { + mPrevTexture.recycle(); + mPrevTexture = null; + } + if (mCurrentTexture != null) { + mCurrentTexture.recycle(); + mCurrentTexture = null; + } + } + + @Override + protected void render(GLCanvas canvas) { + long animTime = AnimationTime.get(); + boolean requestRender = mTransitionAnimation.calculate(animTime); + float alpha = mPrevTexture == null ? 1f : mTransitionAnimation.get(); + + if (mPrevTexture != null && alpha != 1f) { + requestRender |= mPrevAnimation.calculate(animTime); + canvas.save(GLCanvas.SAVE_FLAG_ALPHA | GLCanvas.SAVE_FLAG_MATRIX); + canvas.setAlpha(1f - alpha); + mPrevAnimation.apply(canvas); + canvas.rotate(mPrevRotation, 0, 0, 1); + mPrevTexture.draw(canvas, -mPrevTexture.getWidth() / 2, + -mPrevTexture.getHeight() / 2); + canvas.restore(); + } + if (mCurrentTexture != null) { + requestRender |= mCurrentAnimation.calculate(animTime); + canvas.save(GLCanvas.SAVE_FLAG_ALPHA | GLCanvas.SAVE_FLAG_MATRIX); + canvas.setAlpha(alpha); + mCurrentAnimation.apply(canvas); + canvas.rotate(mCurrentRotation, 0, 0, 1); + mCurrentTexture.draw(canvas, -mCurrentTexture.getWidth() / 2, + -mCurrentTexture.getHeight() / 2); + canvas.restore(); + } + if (requestRender) invalidate(); + } + + private class SlideshowAnimation extends CanvasAnimation { + private final int mWidth; + private final int mHeight; + + private final PointF mMovingVector; + private float mProgress; + + public SlideshowAnimation(int width, int height, Random random) { + mWidth = width; + mHeight = height; + mMovingVector = new PointF( + MOVE_SPEED * mWidth * (random.nextFloat() - 0.5f), + MOVE_SPEED * mHeight * (random.nextFloat() - 0.5f)); + setDuration(SLIDESHOW_DURATION); + } + + @Override + public void apply(GLCanvas canvas) { + int viewWidth = getWidth(); + int viewHeight = getHeight(); + + float initScale = Math.min((float) + viewWidth / mWidth, (float) viewHeight / mHeight); + float scale = initScale * (1 + SCALE_SPEED * mProgress); + + float centerX = viewWidth / 2 + mMovingVector.x * mProgress; + float centerY = viewHeight / 2 + mMovingVector.y * mProgress; + + canvas.translate(centerX, centerY); + canvas.scale(scale, scale, 0); + } + + @Override + public int getCanvasSaveFlags() { + return GLCanvas.SAVE_FLAG_MATRIX; + } + + @Override + protected void onCalculate(float progress) { + mProgress = progress; + } + } +} diff --git a/src/com/android/gallery3d/ui/SlotView.java b/src/com/android/gallery3d/ui/SlotView.java new file mode 100644 index 000000000..bd0ffdc15 --- /dev/null +++ b/src/com/android/gallery3d/ui/SlotView.java @@ -0,0 +1,788 @@ +/* + * 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.ui; + +import android.graphics.Rect; +import android.os.Handler; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.animation.DecelerateInterpolator; + +import com.android.gallery3d.anim.Animation; +import com.android.gallery3d.app.AbstractGalleryActivity; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.glrenderer.GLCanvas; + +public class SlotView extends GLView { + @SuppressWarnings("unused") + private static final String TAG = "SlotView"; + + private static final boolean WIDE = true; + private static final int INDEX_NONE = -1; + + public static final int RENDER_MORE_PASS = 1; + public static final int RENDER_MORE_FRAME = 2; + + public interface Listener { + public void onDown(int index); + public void onUp(boolean followedByLongPress); + public void onSingleTapUp(int index); + public void onLongTap(int index); + public void onScrollPositionChanged(int position, int total); + } + + public static class SimpleListener implements Listener { + @Override public void onDown(int index) {} + @Override public void onUp(boolean followedByLongPress) {} + @Override public void onSingleTapUp(int index) {} + @Override public void onLongTap(int index) {} + @Override public void onScrollPositionChanged(int position, int total) {} + } + + public static interface SlotRenderer { + public void prepareDrawing(); + public void onVisibleRangeChanged(int visibleStart, int visibleEnd); + public void onSlotSizeChanged(int width, int height); + public int renderSlot(GLCanvas canvas, int index, int pass, int width, int height); + } + + private final GestureDetector mGestureDetector; + private final ScrollerHelper mScroller; + private final Paper mPaper = new Paper(); + + private Listener mListener; + private UserInteractionListener mUIListener; + + private boolean mMoreAnimation = false; + private SlotAnimation mAnimation = null; + private final Layout mLayout = new Layout(); + private int mStartIndex = INDEX_NONE; + + // whether the down action happened while the view is scrolling. + private boolean mDownInScrolling; + private int mOverscrollEffect = OVERSCROLL_3D; + private final Handler mHandler; + + private SlotRenderer mRenderer; + + private int[] mRequestRenderSlots = new int[16]; + + public static final int OVERSCROLL_3D = 0; + public static final int OVERSCROLL_SYSTEM = 1; + public static final int OVERSCROLL_NONE = 2; + + // to prevent allocating memory + private final Rect mTempRect = new Rect(); + + public SlotView(AbstractGalleryActivity activity, Spec spec) { + mGestureDetector = new GestureDetector(activity, new MyGestureListener()); + mScroller = new ScrollerHelper(activity); + mHandler = new SynchronizedHandler(activity.getGLRoot()); + setSlotSpec(spec); + } + + public void setSlotRenderer(SlotRenderer slotDrawer) { + mRenderer = slotDrawer; + if (mRenderer != null) { + mRenderer.onSlotSizeChanged(mLayout.mSlotWidth, mLayout.mSlotHeight); + mRenderer.onVisibleRangeChanged(getVisibleStart(), getVisibleEnd()); + } + } + + public void setCenterIndex(int index) { + int slotCount = mLayout.mSlotCount; + if (index < 0 || index >= slotCount) { + return; + } + Rect rect = mLayout.getSlotRect(index, mTempRect); + int position = WIDE + ? (rect.left + rect.right - getWidth()) / 2 + : (rect.top + rect.bottom - getHeight()) / 2; + setScrollPosition(position); + } + + public void makeSlotVisible(int index) { + Rect rect = mLayout.getSlotRect(index, mTempRect); + int visibleBegin = WIDE ? mScrollX : mScrollY; + int visibleLength = WIDE ? getWidth() : getHeight(); + int visibleEnd = visibleBegin + visibleLength; + int slotBegin = WIDE ? rect.left : rect.top; + int slotEnd = WIDE ? rect.right : rect.bottom; + + int position = visibleBegin; + if (visibleLength < slotEnd - slotBegin) { + position = visibleBegin; + } else if (slotBegin < visibleBegin) { + position = slotBegin; + } else if (slotEnd > visibleEnd) { + position = slotEnd - visibleLength; + } + + setScrollPosition(position); + } + + public void setScrollPosition(int position) { + position = Utils.clamp(position, 0, mLayout.getScrollLimit()); + mScroller.setPosition(position); + updateScrollPosition(position, false); + } + + public void setSlotSpec(Spec spec) { + mLayout.setSlotSpec(spec); + } + + @Override + public void addComponent(GLView view) { + throw new UnsupportedOperationException(); + } + + @Override + protected void onLayout(boolean changeSize, int l, int t, int r, int b) { + if (!changeSize) return; + + // Make sure we are still at a resonable scroll position after the size + // is changed (like orientation change). We choose to keep the center + // visible slot still visible. This is arbitrary but reasonable. + int visibleIndex = + (mLayout.getVisibleStart() + mLayout.getVisibleEnd()) / 2; + mLayout.setSize(r - l, b - t); + makeSlotVisible(visibleIndex); + if (mOverscrollEffect == OVERSCROLL_3D) { + mPaper.setSize(r - l, b - t); + } + } + + public void startScatteringAnimation(RelativePosition position) { + mAnimation = new ScatteringAnimation(position); + mAnimation.start(); + if (mLayout.mSlotCount != 0) invalidate(); + } + + public void startRisingAnimation() { + mAnimation = new RisingAnimation(); + mAnimation.start(); + if (mLayout.mSlotCount != 0) invalidate(); + } + + private void updateScrollPosition(int position, boolean force) { + if (!force && (WIDE ? position == mScrollX : position == mScrollY)) return; + if (WIDE) { + mScrollX = position; + } else { + mScrollY = position; + } + mLayout.setScrollPosition(position); + onScrollPositionChanged(position); + } + + protected void onScrollPositionChanged(int newPosition) { + int limit = mLayout.getScrollLimit(); + mListener.onScrollPositionChanged(newPosition, limit); + } + + public Rect getSlotRect(int slotIndex) { + return mLayout.getSlotRect(slotIndex, new Rect()); + } + + @Override + protected boolean onTouch(MotionEvent event) { + if (mUIListener != null) mUIListener.onUserInteraction(); + mGestureDetector.onTouchEvent(event); + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + mDownInScrolling = !mScroller.isFinished(); + mScroller.forceFinished(); + break; + case MotionEvent.ACTION_UP: + mPaper.onRelease(); + invalidate(); + break; + } + return true; + } + + public void setListener(Listener listener) { + mListener = listener; + } + + public void setUserInteractionListener(UserInteractionListener listener) { + mUIListener = listener; + } + + public void setOverscrollEffect(int kind) { + mOverscrollEffect = kind; + mScroller.setOverfling(kind == OVERSCROLL_SYSTEM); + } + + private static int[] expandIntArray(int array[], int capacity) { + while (array.length < capacity) { + array = new int[array.length * 2]; + } + return array; + } + + @Override + protected void render(GLCanvas canvas) { + super.render(canvas); + + if (mRenderer == null) return; + mRenderer.prepareDrawing(); + + long animTime = AnimationTime.get(); + boolean more = mScroller.advanceAnimation(animTime); + more |= mLayout.advanceAnimation(animTime); + int oldX = mScrollX; + updateScrollPosition(mScroller.getPosition(), false); + + boolean paperActive = false; + if (mOverscrollEffect == OVERSCROLL_3D) { + // Check if an edge is reached and notify mPaper if so. + int newX = mScrollX; + int limit = mLayout.getScrollLimit(); + if (oldX > 0 && newX == 0 || oldX < limit && newX == limit) { + float v = mScroller.getCurrVelocity(); + if (newX == limit) v = -v; + + // I don't know why, but getCurrVelocity() can return NaN. + if (!Float.isNaN(v)) { + mPaper.edgeReached(v); + } + } + paperActive = mPaper.advanceAnimation(); + } + + more |= paperActive; + + if (mAnimation != null) { + more |= mAnimation.calculate(animTime); + } + + canvas.translate(-mScrollX, -mScrollY); + + int requestCount = 0; + int requestedSlot[] = expandIntArray(mRequestRenderSlots, + mLayout.mVisibleEnd - mLayout.mVisibleStart); + + for (int i = mLayout.mVisibleEnd - 1; i >= mLayout.mVisibleStart; --i) { + int r = renderItem(canvas, i, 0, paperActive); + if ((r & RENDER_MORE_FRAME) != 0) more = true; + if ((r & RENDER_MORE_PASS) != 0) requestedSlot[requestCount++] = i; + } + + for (int pass = 1; requestCount != 0; ++pass) { + int newCount = 0; + for (int i = 0; i < requestCount; ++i) { + int r = renderItem(canvas, + requestedSlot[i], pass, paperActive); + if ((r & RENDER_MORE_FRAME) != 0) more = true; + if ((r & RENDER_MORE_PASS) != 0) requestedSlot[newCount++] = i; + } + requestCount = newCount; + } + + canvas.translate(mScrollX, mScrollY); + + if (more) invalidate(); + + final UserInteractionListener listener = mUIListener; + if (mMoreAnimation && !more && listener != null) { + mHandler.post(new Runnable() { + @Override + public void run() { + listener.onUserInteractionEnd(); + } + }); + } + mMoreAnimation = more; + } + + private int renderItem( + GLCanvas canvas, int index, int pass, boolean paperActive) { + canvas.save(GLCanvas.SAVE_FLAG_ALPHA | GLCanvas.SAVE_FLAG_MATRIX); + Rect rect = mLayout.getSlotRect(index, mTempRect); + if (paperActive) { + canvas.multiplyMatrix(mPaper.getTransform(rect, mScrollX), 0); + } else { + canvas.translate(rect.left, rect.top, 0); + } + if (mAnimation != null && mAnimation.isActive()) { + mAnimation.apply(canvas, index, rect); + } + int result = mRenderer.renderSlot( + canvas, index, pass, rect.right - rect.left, rect.bottom - rect.top); + canvas.restore(); + return result; + } + + public static abstract class SlotAnimation extends Animation { + protected float mProgress = 0; + + public SlotAnimation() { + setInterpolator(new DecelerateInterpolator(4)); + setDuration(1500); + } + + @Override + protected void onCalculate(float progress) { + mProgress = progress; + } + + abstract public void apply(GLCanvas canvas, int slotIndex, Rect target); + } + + public static class RisingAnimation extends SlotAnimation { + private static final int RISING_DISTANCE = 128; + + @Override + public void apply(GLCanvas canvas, int slotIndex, Rect target) { + canvas.translate(0, 0, RISING_DISTANCE * (1 - mProgress)); + } + } + + public static class ScatteringAnimation extends SlotAnimation { + private int PHOTO_DISTANCE = 1000; + private RelativePosition mCenter; + + public ScatteringAnimation(RelativePosition center) { + mCenter = center; + } + + @Override + public void apply(GLCanvas canvas, int slotIndex, Rect target) { + canvas.translate( + (mCenter.getX() - target.centerX()) * (1 - mProgress), + (mCenter.getY() - target.centerY()) * (1 - mProgress), + slotIndex * PHOTO_DISTANCE * (1 - mProgress)); + canvas.setAlpha(mProgress); + } + } + + // This Spec class is used to specify the size of each slot in the SlotView. + // There are two ways to do it: + // + // (1) Specify slotWidth and slotHeight: they specify the width and height + // of each slot. The number of rows and the gap between slots will be + // determined automatically. + // (2) Specify rowsLand, rowsPort, and slotGap: they specify the number + // of rows in landscape/portrait mode and the gap between slots. The + // width and height of each slot is determined automatically. + // + // The initial value of -1 means they are not specified. + public static class Spec { + public int slotWidth = -1; + public int slotHeight = -1; + public int slotHeightAdditional = 0; + + public int rowsLand = -1; + public int rowsPort = -1; + public int slotGap = -1; + } + + public class Layout { + + private int mVisibleStart; + private int mVisibleEnd; + + private int mSlotCount; + private int mSlotWidth; + private int mSlotHeight; + private int mSlotGap; + + private Spec mSpec; + + private int mWidth; + private int mHeight; + + private int mUnitCount; + private int mContentLength; + private int mScrollPosition; + + private IntegerAnimation mVerticalPadding = new IntegerAnimation(); + private IntegerAnimation mHorizontalPadding = new IntegerAnimation(); + + public void setSlotSpec(Spec spec) { + mSpec = spec; + } + + public boolean setSlotCount(int slotCount) { + if (slotCount == mSlotCount) return false; + if (mSlotCount != 0) { + mHorizontalPadding.setEnabled(true); + mVerticalPadding.setEnabled(true); + } + mSlotCount = slotCount; + int hPadding = mHorizontalPadding.getTarget(); + int vPadding = mVerticalPadding.getTarget(); + initLayoutParameters(); + return vPadding != mVerticalPadding.getTarget() + || hPadding != mHorizontalPadding.getTarget(); + } + + public Rect getSlotRect(int index, Rect rect) { + int col, row; + if (WIDE) { + col = index / mUnitCount; + row = index - col * mUnitCount; + } else { + row = index / mUnitCount; + col = index - row * mUnitCount; + } + + int x = mHorizontalPadding.get() + col * (mSlotWidth + mSlotGap); + int y = mVerticalPadding.get() + row * (mSlotHeight + mSlotGap); + rect.set(x, y, x + mSlotWidth, y + mSlotHeight); + return rect; + } + + public int getSlotWidth() { + return mSlotWidth; + } + + public int getSlotHeight() { + return mSlotHeight; + } + + // Calculate + // (1) mUnitCount: the number of slots we can fit into one column (or row). + // (2) mContentLength: the width (or height) we need to display all the + // columns (rows). + // (3) padding[]: the vertical and horizontal padding we need in order + // to put the slots towards to the center of the display. + // + // The "major" direction is the direction the user can scroll. The other + // direction is the "minor" direction. + // + // The comments inside this method are the description when the major + // directon is horizontal (X), and the minor directon is vertical (Y). + private void initLayoutParameters( + int majorLength, int minorLength, /* The view width and height */ + int majorUnitSize, int minorUnitSize, /* The slot width and height */ + int[] padding) { + int unitCount = (minorLength + mSlotGap) / (minorUnitSize + mSlotGap); + if (unitCount == 0) unitCount = 1; + mUnitCount = unitCount; + + // We put extra padding above and below the column. + int availableUnits = Math.min(mUnitCount, mSlotCount); + int usedMinorLength = availableUnits * minorUnitSize + + (availableUnits - 1) * mSlotGap; + padding[0] = (minorLength - usedMinorLength) / 2; + + // Then calculate how many columns we need for all slots. + int count = ((mSlotCount + mUnitCount - 1) / mUnitCount); + mContentLength = count * majorUnitSize + (count - 1) * mSlotGap; + + // If the content length is less then the screen width, put + // extra padding in left and right. + padding[1] = Math.max(0, (majorLength - mContentLength) / 2); + } + + private void initLayoutParameters() { + // Initialize mSlotWidth and mSlotHeight from mSpec + if (mSpec.slotWidth != -1) { + mSlotGap = 0; + mSlotWidth = mSpec.slotWidth; + mSlotHeight = mSpec.slotHeight; + } else { + int rows = (mWidth > mHeight) ? mSpec.rowsLand : mSpec.rowsPort; + mSlotGap = mSpec.slotGap; + mSlotHeight = Math.max(1, (mHeight - (rows - 1) * mSlotGap) / rows); + mSlotWidth = mSlotHeight - mSpec.slotHeightAdditional; + } + + if (mRenderer != null) { + mRenderer.onSlotSizeChanged(mSlotWidth, mSlotHeight); + } + + int[] padding = new int[2]; + if (WIDE) { + initLayoutParameters(mWidth, mHeight, mSlotWidth, mSlotHeight, padding); + mVerticalPadding.startAnimateTo(padding[0]); + mHorizontalPadding.startAnimateTo(padding[1]); + } else { + initLayoutParameters(mHeight, mWidth, mSlotHeight, mSlotWidth, padding); + mVerticalPadding.startAnimateTo(padding[1]); + mHorizontalPadding.startAnimateTo(padding[0]); + } + updateVisibleSlotRange(); + } + + public void setSize(int width, int height) { + mWidth = width; + mHeight = height; + initLayoutParameters(); + } + + private void updateVisibleSlotRange() { + int position = mScrollPosition; + + if (WIDE) { + int startCol = position / (mSlotWidth + mSlotGap); + int start = Math.max(0, mUnitCount * startCol); + int endCol = (position + mWidth + mSlotWidth + mSlotGap - 1) / + (mSlotWidth + mSlotGap); + int end = Math.min(mSlotCount, mUnitCount * endCol); + setVisibleRange(start, end); + } else { + int startRow = position / (mSlotHeight + mSlotGap); + int start = Math.max(0, mUnitCount * startRow); + int endRow = (position + mHeight + mSlotHeight + mSlotGap - 1) / + (mSlotHeight + mSlotGap); + int end = Math.min(mSlotCount, mUnitCount * endRow); + setVisibleRange(start, end); + } + } + + public void setScrollPosition(int position) { + if (mScrollPosition == position) return; + mScrollPosition = position; + updateVisibleSlotRange(); + } + + private void setVisibleRange(int start, int end) { + if (start == mVisibleStart && end == mVisibleEnd) return; + if (start < end) { + mVisibleStart = start; + mVisibleEnd = end; + } else { + mVisibleStart = mVisibleEnd = 0; + } + if (mRenderer != null) { + mRenderer.onVisibleRangeChanged(mVisibleStart, mVisibleEnd); + } + } + + public int getVisibleStart() { + return mVisibleStart; + } + + public int getVisibleEnd() { + return mVisibleEnd; + } + + public int getSlotIndexByPosition(float x, float y) { + int absoluteX = Math.round(x) + (WIDE ? mScrollPosition : 0); + int absoluteY = Math.round(y) + (WIDE ? 0 : mScrollPosition); + + absoluteX -= mHorizontalPadding.get(); + absoluteY -= mVerticalPadding.get(); + + if (absoluteX < 0 || absoluteY < 0) { + return INDEX_NONE; + } + + int columnIdx = absoluteX / (mSlotWidth + mSlotGap); + int rowIdx = absoluteY / (mSlotHeight + mSlotGap); + + if (!WIDE && columnIdx >= mUnitCount) { + return INDEX_NONE; + } + + if (WIDE && rowIdx >= mUnitCount) { + return INDEX_NONE; + } + + if (absoluteX % (mSlotWidth + mSlotGap) >= mSlotWidth) { + return INDEX_NONE; + } + + if (absoluteY % (mSlotHeight + mSlotGap) >= mSlotHeight) { + return INDEX_NONE; + } + + int index = WIDE + ? (columnIdx * mUnitCount + rowIdx) + : (rowIdx * mUnitCount + columnIdx); + + return index >= mSlotCount ? INDEX_NONE : index; + } + + public int getScrollLimit() { + int limit = WIDE ? mContentLength - mWidth : mContentLength - mHeight; + return limit <= 0 ? 0 : limit; + } + + public boolean advanceAnimation(long animTime) { + // use '|' to make sure both sides will be executed + return mVerticalPadding.calculate(animTime) | mHorizontalPadding.calculate(animTime); + } + } + + private class MyGestureListener implements GestureDetector.OnGestureListener { + private boolean isDown; + + // We call the listener's onDown() when our onShowPress() is called and + // call the listener's onUp() when we receive any further event. + @Override + public void onShowPress(MotionEvent e) { + GLRoot root = getGLRoot(); + root.lockRenderThread(); + try { + if (isDown) return; + int index = mLayout.getSlotIndexByPosition(e.getX(), e.getY()); + if (index != INDEX_NONE) { + isDown = true; + mListener.onDown(index); + } + } finally { + root.unlockRenderThread(); + } + } + + private void cancelDown(boolean byLongPress) { + if (!isDown) return; + isDown = false; + mListener.onUp(byLongPress); + } + + @Override + public boolean onDown(MotionEvent e) { + return false; + } + + @Override + public boolean onFling(MotionEvent e1, + MotionEvent e2, float velocityX, float velocityY) { + cancelDown(false); + int scrollLimit = mLayout.getScrollLimit(); + if (scrollLimit == 0) return false; + float velocity = WIDE ? velocityX : velocityY; + mScroller.fling((int) -velocity, 0, scrollLimit); + if (mUIListener != null) mUIListener.onUserInteractionBegin(); + invalidate(); + return true; + } + + @Override + public boolean onScroll(MotionEvent e1, + MotionEvent e2, float distanceX, float distanceY) { + cancelDown(false); + float distance = WIDE ? distanceX : distanceY; + int overDistance = mScroller.startScroll( + Math.round(distance), 0, mLayout.getScrollLimit()); + if (mOverscrollEffect == OVERSCROLL_3D && overDistance != 0) { + mPaper.overScroll(overDistance); + } + invalidate(); + return true; + } + + @Override + public boolean onSingleTapUp(MotionEvent e) { + cancelDown(false); + if (mDownInScrolling) return true; + int index = mLayout.getSlotIndexByPosition(e.getX(), e.getY()); + if (index != INDEX_NONE) mListener.onSingleTapUp(index); + return true; + } + + @Override + public void onLongPress(MotionEvent e) { + cancelDown(true); + if (mDownInScrolling) return; + lockRendering(); + try { + int index = mLayout.getSlotIndexByPosition(e.getX(), e.getY()); + if (index != INDEX_NONE) mListener.onLongTap(index); + } finally { + unlockRendering(); + } + } + } + + public void setStartIndex(int index) { + mStartIndex = index; + } + + // Return true if the layout parameters have been changed + public boolean setSlotCount(int slotCount) { + boolean changed = mLayout.setSlotCount(slotCount); + + // mStartIndex is applied the first time setSlotCount is called. + if (mStartIndex != INDEX_NONE) { + setCenterIndex(mStartIndex); + mStartIndex = INDEX_NONE; + } + // Reset the scroll position to avoid scrolling over the updated limit. + setScrollPosition(WIDE ? mScrollX : mScrollY); + return changed; + } + + public int getVisibleStart() { + return mLayout.getVisibleStart(); + } + + public int getVisibleEnd() { + return mLayout.getVisibleEnd(); + } + + public int getScrollX() { + return mScrollX; + } + + public int getScrollY() { + return mScrollY; + } + + public Rect getSlotRect(int slotIndex, GLView rootPane) { + // Get slot rectangle relative to this root pane. + Rect offset = new Rect(); + rootPane.getBoundsOf(this, offset); + Rect r = getSlotRect(slotIndex); + r.offset(offset.left - getScrollX(), + offset.top - getScrollY()); + return r; + } + + private static class IntegerAnimation extends Animation { + private int mTarget; + private int mCurrent = 0; + private int mFrom = 0; + private boolean mEnabled = false; + + public void setEnabled(boolean enabled) { + mEnabled = enabled; + } + + public void startAnimateTo(int target) { + if (!mEnabled) { + mTarget = mCurrent = target; + return; + } + if (target == mTarget) return; + + mFrom = mCurrent; + mTarget = target; + setDuration(180); + start(); + } + + public int get() { + return mCurrent; + } + + public int getTarget() { + return mTarget; + } + + @Override + protected void onCalculate(float progress) { + mCurrent = Math.round(mFrom + progress * (mTarget - mFrom)); + if (progress == 1f) mEnabled = false; + } + } +} diff --git a/src/com/android/gallery3d/ui/SurfaceTextureScreenNail.java b/src/com/android/gallery3d/ui/SurfaceTextureScreenNail.java new file mode 100644 index 000000000..18121e63b --- /dev/null +++ b/src/com/android/gallery3d/ui/SurfaceTextureScreenNail.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2012 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.ui; + +import android.annotation.TargetApi; +import android.graphics.RectF; +import android.graphics.SurfaceTexture; + +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.glrenderer.ExtTexture; +import com.android.gallery3d.glrenderer.GLCanvas; + +@TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) +public abstract class SurfaceTextureScreenNail implements ScreenNail, + SurfaceTexture.OnFrameAvailableListener { + @SuppressWarnings("unused") + private static final String TAG = "SurfaceTextureScreenNail"; + // This constant is not available in API level before 15, but it was just an + // oversight. + private static final int GL_TEXTURE_EXTERNAL_OES = 0x8D65; + + protected ExtTexture mExtTexture; + private SurfaceTexture mSurfaceTexture; + private int mWidth, mHeight; + private float[] mTransform = new float[16]; + private boolean mHasTexture = false; + + public SurfaceTextureScreenNail() { + } + + public void acquireSurfaceTexture(GLCanvas canvas) { + mExtTexture = new ExtTexture(canvas, GL_TEXTURE_EXTERNAL_OES); + mExtTexture.setSize(mWidth, mHeight); + mSurfaceTexture = new SurfaceTexture(mExtTexture.getId()); + setDefaultBufferSize(mSurfaceTexture, mWidth, mHeight); + mSurfaceTexture.setOnFrameAvailableListener(this); + synchronized (this) { + mHasTexture = true; + } + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) + private static void setDefaultBufferSize(SurfaceTexture st, int width, int height) { + if (ApiHelper.HAS_SET_DEFALT_BUFFER_SIZE) { + st.setDefaultBufferSize(width, height); + } + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + private static void releaseSurfaceTexture(SurfaceTexture st) { + st.setOnFrameAvailableListener(null); + if (ApiHelper.HAS_RELEASE_SURFACE_TEXTURE) { + st.release(); + } + } + + public SurfaceTexture getSurfaceTexture() { + return mSurfaceTexture; + } + + public void releaseSurfaceTexture() { + synchronized (this) { + mHasTexture = false; + } + mExtTexture.recycle(); + mExtTexture = null; + releaseSurfaceTexture(mSurfaceTexture); + mSurfaceTexture = null; + } + + public void setSize(int width, int height) { + mWidth = width; + mHeight = height; + } + + public void resizeTexture() { + if (mExtTexture != null) { + mExtTexture.setSize(mWidth, mHeight); + setDefaultBufferSize(mSurfaceTexture, mWidth, mHeight); + } + } + + @Override + public int getWidth() { + return mWidth; + } + + @Override + public int getHeight() { + return mHeight; + } + + @Override + public void draw(GLCanvas canvas, int x, int y, int width, int height) { + synchronized (this) { + if (!mHasTexture) return; + mSurfaceTexture.updateTexImage(); + mSurfaceTexture.getTransformMatrix(mTransform); + + // Flip vertically. + canvas.save(GLCanvas.SAVE_FLAG_MATRIX); + int cx = x + width / 2; + int cy = y + height / 2; + canvas.translate(cx, cy); + canvas.scale(1, -1, 1); + canvas.translate(-cx, -cy); + updateTransformMatrix(mTransform); + canvas.drawTexture(mExtTexture, mTransform, x, y, width, height); + canvas.restore(); + } + } + + @Override + public void draw(GLCanvas canvas, RectF source, RectF dest) { + throw new UnsupportedOperationException(); + } + + protected void updateTransformMatrix(float[] matrix) {} + + @Override + abstract public void noDraw(); + + @Override + abstract public void recycle(); + + @Override + abstract public void onFrameAvailable(SurfaceTexture surfaceTexture); +} diff --git a/src/com/android/gallery3d/ui/SynchronizedHandler.java b/src/com/android/gallery3d/ui/SynchronizedHandler.java new file mode 100644 index 000000000..ba1035747 --- /dev/null +++ b/src/com/android/gallery3d/ui/SynchronizedHandler.java @@ -0,0 +1,41 @@ +/* + * 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.ui; + +import android.os.Handler; +import android.os.Message; + +import com.android.gallery3d.common.Utils; + +public class SynchronizedHandler extends Handler { + + private final GLRoot mRoot; + + public SynchronizedHandler(GLRoot root) { + mRoot = Utils.checkNotNull(root); + } + + @Override + public void dispatchMessage(Message message) { + mRoot.lockRenderThread(); + try { + super.dispatchMessage(message); + } finally { + mRoot.unlockRenderThread(); + } + } +} diff --git a/src/com/android/gallery3d/ui/TileImageView.java b/src/com/android/gallery3d/ui/TileImageView.java new file mode 100644 index 000000000..3185c7598 --- /dev/null +++ b/src/com/android/gallery3d/ui/TileImageView.java @@ -0,0 +1,786 @@ +/* + * 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.ui; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.RectF; +import android.support.v4.util.LongSparseArray; +import android.util.DisplayMetrics; +import android.util.FloatMath; +import android.view.WindowManager; + +import com.android.gallery3d.app.GalleryContext; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.DecodeUtils; +import com.android.photos.data.GalleryBitmapPool; +import com.android.gallery3d.glrenderer.GLCanvas; +import com.android.gallery3d.glrenderer.UploadedTexture; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.ThreadPool; +import com.android.gallery3d.util.ThreadPool.CancelListener; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import java.util.concurrent.atomic.AtomicBoolean; + +public class TileImageView extends GLView { + public static final int SIZE_UNKNOWN = -1; + + @SuppressWarnings("unused") + private static final String TAG = "TileImageView"; + private static final int UPLOAD_LIMIT = 1; + + // TILE_SIZE must be 2^N + private static int sTileSize; + + /* + * This is the tile state in the CPU side. + * Life of a Tile: + * ACTIVATED (initial state) + * --> IN_QUEUE - by queueForDecode() + * --> RECYCLED - by recycleTile() + * IN_QUEUE --> DECODING - by decodeTile() + * --> RECYCLED - by recycleTile) + * DECODING --> RECYCLING - by recycleTile() + * --> DECODED - by decodeTile() + * --> DECODE_FAIL - by decodeTile() + * RECYCLING --> RECYCLED - by decodeTile() + * DECODED --> ACTIVATED - (after the decoded bitmap is uploaded) + * DECODED --> RECYCLED - by recycleTile() + * DECODE_FAIL -> RECYCLED - by recycleTile() + * RECYCLED --> ACTIVATED - by obtainTile() + */ + private static final int STATE_ACTIVATED = 0x01; + private static final int STATE_IN_QUEUE = 0x02; + private static final int STATE_DECODING = 0x04; + private static final int STATE_DECODED = 0x08; + private static final int STATE_DECODE_FAIL = 0x10; + private static final int STATE_RECYCLING = 0x20; + private static final int STATE_RECYCLED = 0x40; + + private TileSource mModel; + private ScreenNail mScreenNail; + protected int mLevelCount; // cache the value of mScaledBitmaps.length + + // The mLevel variable indicates which level of bitmap we should use. + // Level 0 means the original full-sized bitmap, and a larger value means + // a smaller scaled bitmap (The width and height of each scaled bitmap is + // half size of the previous one). If the value is in [0, mLevelCount), we + // use the bitmap in mScaledBitmaps[mLevel] for display, otherwise the value + // is mLevelCount, and that means we use mScreenNail for display. + private int mLevel = 0; + + // The offsets of the (left, top) of the upper-left tile to the (left, top) + // of the view. + private int mOffsetX; + private int mOffsetY; + + private int mUploadQuota; + private boolean mRenderComplete; + + private final RectF mSourceRect = new RectF(); + private final RectF mTargetRect = new RectF(); + + private final LongSparseArray<Tile> mActiveTiles = new LongSparseArray<Tile>(); + + // The following three queue is guarded by TileImageView.this + private final TileQueue mRecycledQueue = new TileQueue(); + private final TileQueue mUploadQueue = new TileQueue(); + private final TileQueue mDecodeQueue = new TileQueue(); + + // The width and height of the full-sized bitmap + protected int mImageWidth = SIZE_UNKNOWN; + protected int mImageHeight = SIZE_UNKNOWN; + + protected int mCenterX; + protected int mCenterY; + protected float mScale; + protected int mRotation; + + // Temp variables to avoid memory allocation + private final Rect mTileRange = new Rect(); + private final Rect mActiveRange[] = {new Rect(), new Rect()}; + + private final TileUploader mTileUploader = new TileUploader(); + private boolean mIsTextureFreed; + private Future<Void> mTileDecoder; + private final ThreadPool mThreadPool; + private boolean mBackgroundTileUploaded; + + public static interface TileSource { + public int getLevelCount(); + public ScreenNail getScreenNail(); + public int getImageWidth(); + public int getImageHeight(); + + // The tile returned by this method can be specified this way: Assuming + // the image size is (width, height), first take the intersection of (0, + // 0) - (width, height) and (x, y) - (x + tileSize, y + tileSize). If + // in extending the region, we found some part of the region are outside + // the image, those pixels are filled with black. + // + // If level > 0, it does the same operation on a down-scaled version of + // the original image (down-scaled by a factor of 2^level), but (x, y) + // still refers to the coordinate on the original image. + // + // The method would be called in another thread. + public Bitmap getTile(int level, int x, int y, int tileSize); + } + + public static boolean isHighResolution(Context context) { + DisplayMetrics metrics = new DisplayMetrics(); + WindowManager wm = (WindowManager) + context.getSystemService(Context.WINDOW_SERVICE); + wm.getDefaultDisplay().getMetrics(metrics); + return metrics.heightPixels > 2048 || metrics.widthPixels > 2048; + } + + public TileImageView(GalleryContext context) { + mThreadPool = context.getThreadPool(); + mTileDecoder = mThreadPool.submit(new TileDecoder()); + if (sTileSize == 0) { + if (isHighResolution(context.getAndroidContext())) { + sTileSize = 512 ; + } else { + sTileSize = 256; + } + } + } + + public void setModel(TileSource model) { + mModel = model; + if (model != null) notifyModelInvalidated(); + } + + public void setScreenNail(ScreenNail s) { + mScreenNail = s; + } + + public void notifyModelInvalidated() { + invalidateTiles(); + if (mModel == null) { + mScreenNail = null; + mImageWidth = 0; + mImageHeight = 0; + mLevelCount = 0; + } else { + setScreenNail(mModel.getScreenNail()); + mImageWidth = mModel.getImageWidth(); + mImageHeight = mModel.getImageHeight(); + mLevelCount = mModel.getLevelCount(); + } + layoutTiles(mCenterX, mCenterY, mScale, mRotation); + invalidate(); + } + + @Override + protected void onLayout( + boolean changeSize, int left, int top, int right, int bottom) { + super.onLayout(changeSize, left, top, right, bottom); + if (changeSize) layoutTiles(mCenterX, mCenterY, mScale, mRotation); + } + + // Prepare the tiles we want to use for display. + // + // 1. Decide the tile level we want to use for display. + // 2. Decide the tile levels we want to keep as texture (in addition to + // the one we use for display). + // 3. Recycle unused tiles. + // 4. Activate the tiles we want. + private void layoutTiles(int centerX, int centerY, float scale, int rotation) { + // The width and height of this view. + int width = getWidth(); + int height = getHeight(); + + // The tile levels we want to keep as texture is in the range + // [fromLevel, endLevel). + int fromLevel; + int endLevel; + + // We want to use a texture larger than or equal to the display size. + mLevel = Utils.clamp(Utils.floorLog2(1f / scale), 0, mLevelCount); + + // We want to keep one more tile level as texture in addition to what + // we use for display. So it can be faster when the scale moves to the + // next level. We choose a level closer to the current scale. + if (mLevel != mLevelCount) { + Rect range = mTileRange; + getRange(range, centerX, centerY, mLevel, scale, rotation); + mOffsetX = Math.round(width / 2f + (range.left - centerX) * scale); + mOffsetY = Math.round(height / 2f + (range.top - centerY) * scale); + fromLevel = scale * (1 << mLevel) > 0.75f ? mLevel - 1 : mLevel; + } else { + // Activate the tiles of the smallest two levels. + fromLevel = mLevel - 2; + mOffsetX = Math.round(width / 2f - centerX * scale); + mOffsetY = Math.round(height / 2f - centerY * scale); + } + + fromLevel = Math.max(0, Math.min(fromLevel, mLevelCount - 2)); + endLevel = Math.min(fromLevel + 2, mLevelCount); + + Rect range[] = mActiveRange; + for (int i = fromLevel; i < endLevel; ++i) { + getRange(range[i - fromLevel], centerX, centerY, i, rotation); + } + + // If rotation is transient, don't update the tile. + if (rotation % 90 != 0) return; + + synchronized (this) { + mDecodeQueue.clean(); + mUploadQueue.clean(); + mBackgroundTileUploaded = false; + + // Recycle unused tiles: if the level of the active tile is outside the + // range [fromLevel, endLevel) or not in the visible range. + int n = mActiveTiles.size(); + for (int i = 0; i < n; i++) { + Tile tile = mActiveTiles.valueAt(i); + int level = tile.mTileLevel; + if (level < fromLevel || level >= endLevel + || !range[level - fromLevel].contains(tile.mX, tile.mY)) { + mActiveTiles.removeAt(i); + i--; + n--; + recycleTile(tile); + } + } + } + + for (int i = fromLevel; i < endLevel; ++i) { + int size = sTileSize << i; + Rect r = range[i - fromLevel]; + for (int y = r.top, bottom = r.bottom; y < bottom; y += size) { + for (int x = r.left, right = r.right; x < right; x += size) { + activateTile(x, y, i); + } + } + } + invalidate(); + } + + protected synchronized void invalidateTiles() { + mDecodeQueue.clean(); + mUploadQueue.clean(); + + // TODO disable decoder + int n = mActiveTiles.size(); + for (int i = 0; i < n; i++) { + Tile tile = mActiveTiles.valueAt(i); + recycleTile(tile); + } + mActiveTiles.clear(); + } + + private void getRange(Rect out, int cX, int cY, int level, int rotation) { + getRange(out, cX, cY, level, 1f / (1 << (level + 1)), rotation); + } + + // If the bitmap is scaled by the given factor "scale", return the + // rectangle containing visible range. The left-top coordinate returned is + // aligned to the tile boundary. + // + // (cX, cY) is the point on the original bitmap which will be put in the + // center of the ImageViewer. + private void getRange(Rect out, + int cX, int cY, int level, float scale, int rotation) { + + double radians = Math.toRadians(-rotation); + double w = getWidth(); + double h = getHeight(); + + double cos = Math.cos(radians); + double sin = Math.sin(radians); + int width = (int) Math.ceil(Math.max( + Math.abs(cos * w - sin * h), Math.abs(cos * w + sin * h))); + int height = (int) Math.ceil(Math.max( + Math.abs(sin * w + cos * h), Math.abs(sin * w - cos * h))); + + int left = (int) FloatMath.floor(cX - width / (2f * scale)); + int top = (int) FloatMath.floor(cY - height / (2f * scale)); + int right = (int) FloatMath.ceil(left + width / scale); + int bottom = (int) FloatMath.ceil(top + height / scale); + + // align the rectangle to tile boundary + int size = sTileSize << level; + left = Math.max(0, size * (left / size)); + top = Math.max(0, size * (top / size)); + right = Math.min(mImageWidth, right); + bottom = Math.min(mImageHeight, bottom); + + out.set(left, top, right, bottom); + } + + // Calculate where the center of the image is, in the view coordinates. + public void getImageCenter(Point center) { + // The width and height of this view. + int viewW = getWidth(); + int viewH = getHeight(); + + // The distance between the center of the view to the center of the + // bitmap, in bitmap units. (mCenterX and mCenterY are the bitmap + // coordinates correspond to the center of view) + int distW, distH; + if (mRotation % 180 == 0) { + distW = mImageWidth / 2 - mCenterX; + distH = mImageHeight / 2 - mCenterY; + } else { + distW = mImageHeight / 2 - mCenterY; + distH = mImageWidth / 2 - mCenterX; + } + + // Convert to view coordinates. mScale translates from bitmap units to + // view units. + center.x = Math.round(viewW / 2f + distW * mScale); + center.y = Math.round(viewH / 2f + distH * mScale); + } + + public boolean setPosition(int centerX, int centerY, float scale, int rotation) { + if (mCenterX == centerX && mCenterY == centerY + && mScale == scale && mRotation == rotation) return false; + mCenterX = centerX; + mCenterY = centerY; + mScale = scale; + mRotation = rotation; + layoutTiles(centerX, centerY, scale, rotation); + invalidate(); + return true; + } + + public void freeTextures() { + mIsTextureFreed = true; + + if (mTileDecoder != null) { + mTileDecoder.cancel(); + mTileDecoder.get(); + mTileDecoder = null; + } + + int n = mActiveTiles.size(); + for (int i = 0; i < n; i++) { + Tile texture = mActiveTiles.valueAt(i); + texture.recycle(); + } + mActiveTiles.clear(); + mTileRange.set(0, 0, 0, 0); + + synchronized (this) { + mUploadQueue.clean(); + mDecodeQueue.clean(); + Tile tile = mRecycledQueue.pop(); + while (tile != null) { + tile.recycle(); + tile = mRecycledQueue.pop(); + } + } + setScreenNail(null); + } + + public void prepareTextures() { + if (mTileDecoder == null) { + mTileDecoder = mThreadPool.submit(new TileDecoder()); + } + if (mIsTextureFreed) { + layoutTiles(mCenterX, mCenterY, mScale, mRotation); + mIsTextureFreed = false; + setScreenNail(mModel == null ? null : mModel.getScreenNail()); + } + } + + @Override + protected void render(GLCanvas canvas) { + mUploadQuota = UPLOAD_LIMIT; + mRenderComplete = true; + + int level = mLevel; + int rotation = mRotation; + int flags = 0; + if (rotation != 0) flags |= GLCanvas.SAVE_FLAG_MATRIX; + + if (flags != 0) { + canvas.save(flags); + if (rotation != 0) { + int centerX = getWidth() / 2, centerY = getHeight() / 2; + canvas.translate(centerX, centerY); + canvas.rotate(rotation, 0, 0, 1); + canvas.translate(-centerX, -centerY); + } + } + try { + if (level != mLevelCount && !isScreenNailAnimating()) { + if (mScreenNail != null) { + mScreenNail.noDraw(); + } + + int size = (sTileSize << level); + float length = size * mScale; + Rect r = mTileRange; + + for (int ty = r.top, i = 0; ty < r.bottom; ty += size, i++) { + float y = mOffsetY + i * length; + for (int tx = r.left, j = 0; tx < r.right; tx += size, j++) { + float x = mOffsetX + j * length; + drawTile(canvas, tx, ty, level, x, y, length); + } + } + } else if (mScreenNail != null) { + mScreenNail.draw(canvas, mOffsetX, mOffsetY, + Math.round(mImageWidth * mScale), + Math.round(mImageHeight * mScale)); + if (isScreenNailAnimating()) { + invalidate(); + } + } + } finally { + if (flags != 0) canvas.restore(); + } + + if (mRenderComplete) { + if (!mBackgroundTileUploaded) uploadBackgroundTiles(canvas); + } else { + invalidate(); + } + } + + private boolean isScreenNailAnimating() { + return (mScreenNail instanceof TiledScreenNail) + && ((TiledScreenNail) mScreenNail).isAnimating(); + } + + private void uploadBackgroundTiles(GLCanvas canvas) { + mBackgroundTileUploaded = true; + int n = mActiveTiles.size(); + for (int i = 0; i < n; i++) { + Tile tile = mActiveTiles.valueAt(i); + if (!tile.isContentValid()) queueForDecode(tile); + } + } + + void queueForUpload(Tile tile) { + synchronized (this) { + mUploadQueue.push(tile); + } + if (mTileUploader.mActive.compareAndSet(false, true)) { + getGLRoot().addOnGLIdleListener(mTileUploader); + } + } + + synchronized void queueForDecode(Tile tile) { + if (tile.mTileState == STATE_ACTIVATED) { + tile.mTileState = STATE_IN_QUEUE; + if (mDecodeQueue.push(tile)) notifyAll(); + } + } + + boolean decodeTile(Tile tile) { + synchronized (this) { + if (tile.mTileState != STATE_IN_QUEUE) return false; + tile.mTileState = STATE_DECODING; + } + boolean decodeComplete = tile.decode(); + synchronized (this) { + if (tile.mTileState == STATE_RECYCLING) { + tile.mTileState = STATE_RECYCLED; + if (tile.mDecodedTile != null) { + GalleryBitmapPool.getInstance().put(tile.mDecodedTile); + tile.mDecodedTile = null; + } + mRecycledQueue.push(tile); + return false; + } + tile.mTileState = decodeComplete ? STATE_DECODED : STATE_DECODE_FAIL; + return decodeComplete; + } + } + + private synchronized Tile obtainTile(int x, int y, int level) { + Tile tile = mRecycledQueue.pop(); + if (tile != null) { + tile.mTileState = STATE_ACTIVATED; + tile.update(x, y, level); + return tile; + } + return new Tile(x, y, level); + } + + synchronized void recycleTile(Tile tile) { + if (tile.mTileState == STATE_DECODING) { + tile.mTileState = STATE_RECYCLING; + return; + } + tile.mTileState = STATE_RECYCLED; + if (tile.mDecodedTile != null) { + GalleryBitmapPool.getInstance().put(tile.mDecodedTile); + tile.mDecodedTile = null; + } + mRecycledQueue.push(tile); + } + + private void activateTile(int x, int y, int level) { + long key = makeTileKey(x, y, level); + Tile tile = mActiveTiles.get(key); + if (tile != null) { + if (tile.mTileState == STATE_IN_QUEUE) { + tile.mTileState = STATE_ACTIVATED; + } + return; + } + tile = obtainTile(x, y, level); + mActiveTiles.put(key, tile); + } + + private Tile getTile(int x, int y, int level) { + return mActiveTiles.get(makeTileKey(x, y, level)); + } + + private static long makeTileKey(int x, int y, int level) { + long result = x; + result = (result << 16) | y; + result = (result << 16) | level; + return result; + } + + private class TileUploader implements GLRoot.OnGLIdleListener { + AtomicBoolean mActive = new AtomicBoolean(false); + + @Override + public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) { + // Skips uploading if there is a pending rendering request. + // Returns true to keep uploading in next rendering loop. + if (renderRequested) return true; + int quota = UPLOAD_LIMIT; + Tile tile = null; + while (quota > 0) { + synchronized (TileImageView.this) { + tile = mUploadQueue.pop(); + } + if (tile == null) break; + if (!tile.isContentValid()) { + boolean hasBeenLoaded = tile.isLoaded(); + Utils.assertTrue(tile.mTileState == STATE_DECODED); + tile.updateContent(canvas); + if (!hasBeenLoaded) tile.draw(canvas, 0, 0); + --quota; + } + } + if (tile == null) mActive.set(false); + return tile != null; + } + } + + // Draw the tile to a square at canvas that locates at (x, y) and + // has a side length of length. + public void drawTile(GLCanvas canvas, + int tx, int ty, int level, float x, float y, float length) { + RectF source = mSourceRect; + RectF target = mTargetRect; + target.set(x, y, x + length, y + length); + source.set(0, 0, sTileSize, sTileSize); + + Tile tile = getTile(tx, ty, level); + if (tile != null) { + if (!tile.isContentValid()) { + if (tile.mTileState == STATE_DECODED) { + if (mUploadQuota > 0) { + --mUploadQuota; + tile.updateContent(canvas); + } else { + mRenderComplete = false; + } + } else if (tile.mTileState != STATE_DECODE_FAIL){ + mRenderComplete = false; + queueForDecode(tile); + } + } + if (drawTile(tile, canvas, source, target)) return; + } + if (mScreenNail != null) { + int size = sTileSize << level; + float scaleX = (float) mScreenNail.getWidth() / mImageWidth; + float scaleY = (float) mScreenNail.getHeight() / mImageHeight; + source.set(tx * scaleX, ty * scaleY, (tx + size) * scaleX, + (ty + size) * scaleY); + mScreenNail.draw(canvas, source, target); + } + } + + static boolean drawTile( + Tile tile, GLCanvas canvas, RectF source, RectF target) { + while (true) { + if (tile.isContentValid()) { + canvas.drawTexture(tile, source, target); + return true; + } + + // Parent can be divided to four quads and tile is one of the four. + Tile parent = tile.getParentTile(); + if (parent == null) return false; + if (tile.mX == parent.mX) { + source.left /= 2f; + source.right /= 2f; + } else { + source.left = (sTileSize + source.left) / 2f; + source.right = (sTileSize + source.right) / 2f; + } + if (tile.mY == parent.mY) { + source.top /= 2f; + source.bottom /= 2f; + } else { + source.top = (sTileSize + source.top) / 2f; + source.bottom = (sTileSize + source.bottom) / 2f; + } + tile = parent; + } + } + + private class Tile extends UploadedTexture { + public int mX; + public int mY; + public int mTileLevel; + public Tile mNext; + public Bitmap mDecodedTile; + public volatile int mTileState = STATE_ACTIVATED; + + public Tile(int x, int y, int level) { + mX = x; + mY = y; + mTileLevel = level; + } + + @Override + protected void onFreeBitmap(Bitmap bitmap) { + GalleryBitmapPool.getInstance().put(bitmap); + } + + boolean decode() { + // Get a tile from the original image. The tile is down-scaled + // by (1 << mTilelevel) from a region in the original image. + try { + mDecodedTile = DecodeUtils.ensureGLCompatibleBitmap(mModel.getTile( + mTileLevel, mX, mY, sTileSize)); + } catch (Throwable t) { + Log.w(TAG, "fail to decode tile", t); + } + return mDecodedTile != null; + } + + @Override + protected Bitmap onGetBitmap() { + Utils.assertTrue(mTileState == STATE_DECODED); + + // We need to override the width and height, so that we won't + // draw beyond the boundaries. + int rightEdge = ((mImageWidth - mX) >> mTileLevel); + int bottomEdge = ((mImageHeight - mY) >> mTileLevel); + setSize(Math.min(sTileSize, rightEdge), Math.min(sTileSize, bottomEdge)); + + Bitmap bitmap = mDecodedTile; + mDecodedTile = null; + mTileState = STATE_ACTIVATED; + return bitmap; + } + + // We override getTextureWidth() and getTextureHeight() here, so the + // texture can be re-used for different tiles regardless of the actual + // size of the tile (which may be small because it is a tile at the + // boundary). + @Override + public int getTextureWidth() { + return sTileSize; + } + + @Override + public int getTextureHeight() { + return sTileSize; + } + + public void update(int x, int y, int level) { + mX = x; + mY = y; + mTileLevel = level; + invalidateContent(); + } + + public Tile getParentTile() { + if (mTileLevel + 1 == mLevelCount) return null; + int size = sTileSize << (mTileLevel + 1); + int x = size * (mX / size); + int y = size * (mY / size); + return getTile(x, y, mTileLevel + 1); + } + + @Override + public String toString() { + return String.format("tile(%s, %s, %s / %s)", + mX / sTileSize, mY / sTileSize, mLevel, mLevelCount); + } + } + + private static class TileQueue { + private Tile mHead; + + public Tile pop() { + Tile tile = mHead; + if (tile != null) mHead = tile.mNext; + return tile; + } + + public boolean push(Tile tile) { + boolean wasEmpty = mHead == null; + tile.mNext = mHead; + mHead = tile; + return wasEmpty; + } + + public void clean() { + mHead = null; + } + } + + private class TileDecoder implements ThreadPool.Job<Void> { + + private CancelListener mNotifier = new CancelListener() { + @Override + public void onCancel() { + synchronized (TileImageView.this) { + TileImageView.this.notifyAll(); + } + } + }; + + @Override + public Void run(JobContext jc) { + jc.setMode(ThreadPool.MODE_NONE); + jc.setCancelListener(mNotifier); + while (!jc.isCancelled()) { + Tile tile = null; + synchronized(TileImageView.this) { + tile = mDecodeQueue.pop(); + if (tile == null && !jc.isCancelled()) { + Utils.waitWithoutInterrupt(TileImageView.this); + } + } + if (tile == null) continue; + if (decodeTile(tile)) queueForUpload(tile); + } + return null; + } + } +} diff --git a/src/com/android/gallery3d/ui/TileImageViewAdapter.java b/src/com/android/gallery3d/ui/TileImageViewAdapter.java new file mode 100644 index 000000000..0c1f66d0c --- /dev/null +++ b/src/com/android/gallery3d/ui/TileImageViewAdapter.java @@ -0,0 +1,200 @@ +/* + * 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.ui; + +import android.annotation.TargetApi; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.BitmapFactory; +import android.graphics.BitmapRegionDecoder; +import android.graphics.Canvas; +import android.graphics.Rect; + +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.common.Utils; +import com.android.photos.data.GalleryBitmapPool; + +public class TileImageViewAdapter implements TileImageView.TileSource { + private static final String TAG = "TileImageViewAdapter"; + protected ScreenNail mScreenNail; + protected boolean mOwnScreenNail; + protected BitmapRegionDecoder mRegionDecoder; + protected int mImageWidth; + protected int mImageHeight; + protected int mLevelCount; + + public TileImageViewAdapter() { + } + + public synchronized void clear() { + mScreenNail = null; + mImageWidth = 0; + mImageHeight = 0; + mLevelCount = 0; + mRegionDecoder = null; + } + + // Caller is responsible to recycle the ScreenNail + public synchronized void setScreenNail( + ScreenNail screenNail, int width, int height) { + Utils.checkNotNull(screenNail); + mScreenNail = screenNail; + mImageWidth = width; + mImageHeight = height; + mRegionDecoder = null; + mLevelCount = 0; + } + + public synchronized void setRegionDecoder(BitmapRegionDecoder decoder) { + mRegionDecoder = Utils.checkNotNull(decoder); + mImageWidth = decoder.getWidth(); + mImageHeight = decoder.getHeight(); + mLevelCount = calculateLevelCount(); + } + + private int calculateLevelCount() { + return Math.max(0, Utils.ceilLog2( + (float) mImageWidth / mScreenNail.getWidth())); + } + + // Gets a sub image on a rectangle of the current photo. For example, + // getTile(1, 50, 50, 100, 3, pool) means to get the region located + // at (50, 50) with sample level 1 (ie, down sampled by 2^1) and the + // target tile size (after sampling) 100 with border 3. + // + // From this spec, we can infer the actual tile size to be + // 100 + 3x2 = 106, and the size of the region to be extracted from the + // photo to be 200 with border 6. + // + // As a result, we should decode region (50-6, 50-6, 250+6, 250+6) or + // (44, 44, 256, 256) from the original photo and down sample it to 106. + @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) + @Override + public Bitmap getTile(int level, int x, int y, int tileSize) { + if (!ApiHelper.HAS_REUSING_BITMAP_IN_BITMAP_REGION_DECODER) { + return getTileWithoutReusingBitmap(level, x, y, tileSize); + } + + int t = tileSize << level; + + Rect wantRegion = new Rect(x, y, x + t, y + t); + + boolean needClear; + BitmapRegionDecoder regionDecoder = null; + + synchronized (this) { + regionDecoder = mRegionDecoder; + if (regionDecoder == null) return null; + + // We need to clear a reused bitmap, if wantRegion is not fully + // within the image. + needClear = !new Rect(0, 0, mImageWidth, mImageHeight) + .contains(wantRegion); + } + + Bitmap bitmap = GalleryBitmapPool.getInstance().get(tileSize, tileSize); + if (bitmap != null) { + if (needClear) bitmap.eraseColor(0); + } else { + bitmap = Bitmap.createBitmap(tileSize, tileSize, Config.ARGB_8888); + } + + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Config.ARGB_8888; + options.inPreferQualityOverSpeed = true; + options.inSampleSize = (1 << level); + options.inBitmap = bitmap; + + try { + // In CropImage, we may call the decodeRegion() concurrently. + synchronized (regionDecoder) { + bitmap = regionDecoder.decodeRegion(wantRegion, options); + } + } finally { + if (options.inBitmap != bitmap && options.inBitmap != null) { + GalleryBitmapPool.getInstance().put(options.inBitmap); + options.inBitmap = null; + } + } + + if (bitmap == null) { + Log.w(TAG, "fail in decoding region"); + } + return bitmap; + } + + private Bitmap getTileWithoutReusingBitmap( + int level, int x, int y, int tileSize) { + int t = tileSize << level; + Rect wantRegion = new Rect(x, y, x + t, y + t); + + BitmapRegionDecoder regionDecoder; + Rect overlapRegion; + + synchronized (this) { + regionDecoder = mRegionDecoder; + if (regionDecoder == null) return null; + overlapRegion = new Rect(0, 0, mImageWidth, mImageHeight); + Utils.assertTrue(overlapRegion.intersect(wantRegion)); + } + + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Config.ARGB_8888; + options.inPreferQualityOverSpeed = true; + options.inSampleSize = (1 << level); + Bitmap bitmap = null; + + // In CropImage, we may call the decodeRegion() concurrently. + synchronized (regionDecoder) { + bitmap = regionDecoder.decodeRegion(overlapRegion, options); + } + + if (bitmap == null) { + Log.w(TAG, "fail in decoding region"); + } + + if (wantRegion.equals(overlapRegion)) return bitmap; + + Bitmap result = Bitmap.createBitmap(tileSize, tileSize, Config.ARGB_8888); + Canvas canvas = new Canvas(result); + canvas.drawBitmap(bitmap, + (overlapRegion.left - wantRegion.left) >> level, + (overlapRegion.top - wantRegion.top) >> level, null); + return result; + } + + + @Override + public ScreenNail getScreenNail() { + return mScreenNail; + } + + @Override + public int getImageHeight() { + return mImageHeight; + } + + @Override + public int getImageWidth() { + return mImageWidth; + } + + @Override + public int getLevelCount() { + return mLevelCount; + } +} diff --git a/src/com/android/gallery3d/ui/TiledScreenNail.java b/src/com/android/gallery3d/ui/TiledScreenNail.java new file mode 100644 index 000000000..860e230bb --- /dev/null +++ b/src/com/android/gallery3d/ui/TiledScreenNail.java @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2012 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.ui; + +import android.graphics.Bitmap; +import android.graphics.RectF; + +import com.android.gallery3d.common.Utils; +import com.android.photos.data.GalleryBitmapPool; +import com.android.gallery3d.glrenderer.GLCanvas; +import com.android.gallery3d.glrenderer.TiledTexture; + +// This is a ScreenNail wraps a Bitmap. There are some extra functions: +// +// - If we need to draw before the bitmap is available, we draw a rectange of +// placeholder color (gray). +// +// - When the the bitmap is available, and we have drawn the placeholder color +// before, we will do a fade-in animation. +public class TiledScreenNail implements ScreenNail { + @SuppressWarnings("unused") + private static final String TAG = "TiledScreenNail"; + + // The duration of the fading animation in milliseconds + private static final int DURATION = 180; + + private static int sMaxSide = 640; + + // These are special values for mAnimationStartTime + private static final long ANIMATION_NOT_NEEDED = -1; + private static final long ANIMATION_NEEDED = -2; + private static final long ANIMATION_DONE = -3; + + private int mWidth; + private int mHeight; + private long mAnimationStartTime = ANIMATION_NOT_NEEDED; + + private Bitmap mBitmap; + private TiledTexture mTexture; + + public TiledScreenNail(Bitmap bitmap) { + mWidth = bitmap.getWidth(); + mHeight = bitmap.getHeight(); + mBitmap = bitmap; + mTexture = new TiledTexture(bitmap); + } + + public TiledScreenNail(int width, int height) { + setSize(width, height); + } + + // This gets overridden by bitmap_screennail_placeholder + // in GalleryUtils.initialize + private static int mPlaceholderColor = 0xFF222222; + private static boolean mDrawPlaceholder = true; + + public static void setPlaceholderColor(int color) { + mPlaceholderColor = color; + } + + private void setSize(int width, int height) { + if (width == 0 || height == 0) { + width = sMaxSide; + height = sMaxSide * 3 / 4; + } + float scale = Math.min(1, (float) sMaxSide / Math.max(width, height)); + mWidth = Math.round(scale * width); + mHeight = Math.round(scale * height); + } + + // Combines the two ScreenNails. + // Returns the used one and recycle the unused one. + public ScreenNail combine(ScreenNail other) { + if (other == null) { + return this; + } + + if (!(other instanceof TiledScreenNail)) { + recycle(); + return other; + } + + // Now both are TiledScreenNail. Move over the information about width, + // height, and Bitmap, then recycle the other. + TiledScreenNail newer = (TiledScreenNail) other; + mWidth = newer.mWidth; + mHeight = newer.mHeight; + if (newer.mTexture != null) { + if (mBitmap != null) GalleryBitmapPool.getInstance().put(mBitmap); + if (mTexture != null) mTexture.recycle(); + mBitmap = newer.mBitmap; + mTexture = newer.mTexture; + newer.mBitmap = null; + newer.mTexture = null; + } + newer.recycle(); + return this; + } + + public void updatePlaceholderSize(int width, int height) { + if (mBitmap != null) return; + if (width == 0 || height == 0) return; + setSize(width, height); + } + + @Override + public int getWidth() { + return mWidth; + } + + @Override + public int getHeight() { + return mHeight; + } + + @Override + public void noDraw() { + } + + @Override + public void recycle() { + if (mTexture != null) { + mTexture.recycle(); + mTexture = null; + } + if (mBitmap != null) { + GalleryBitmapPool.getInstance().put(mBitmap); + mBitmap = null; + } + } + + public static void disableDrawPlaceholder() { + mDrawPlaceholder = false; + } + + public static void enableDrawPlaceholder() { + mDrawPlaceholder = true; + } + + @Override + public void draw(GLCanvas canvas, int x, int y, int width, int height) { + if (mTexture == null || !mTexture.isReady()) { + if (mAnimationStartTime == ANIMATION_NOT_NEEDED) { + mAnimationStartTime = ANIMATION_NEEDED; + } + if(mDrawPlaceholder) { + canvas.fillRect(x, y, width, height, mPlaceholderColor); + } + return; + } + + if (mAnimationStartTime == ANIMATION_NEEDED) { + mAnimationStartTime = AnimationTime.get(); + } + + if (isAnimating()) { + mTexture.drawMixed(canvas, mPlaceholderColor, getRatio(), x, y, + width, height); + } else { + mTexture.draw(canvas, x, y, width, height); + } + } + + @Override + public void draw(GLCanvas canvas, RectF source, RectF dest) { + if (mTexture == null || !mTexture.isReady()) { + canvas.fillRect(dest.left, dest.top, dest.width(), dest.height(), + mPlaceholderColor); + return; + } + + mTexture.draw(canvas, source, dest); + } + + public boolean isAnimating() { + // The TiledTexture may not be uploaded completely yet. + // In that case, we count it as animating state and we will draw + // the placeholder in TileImageView. + if (mTexture == null || !mTexture.isReady()) return true; + if (mAnimationStartTime < 0) return false; + if (AnimationTime.get() - mAnimationStartTime >= DURATION) { + mAnimationStartTime = ANIMATION_DONE; + return false; + } + return true; + } + + private float getRatio() { + float r = (float) (AnimationTime.get() - mAnimationStartTime) / DURATION; + return Utils.clamp(1.0f - r, 0.0f, 1.0f); + } + + public boolean isShowingPlaceholder() { + return (mBitmap == null) || isAnimating(); + } + + public TiledTexture getTexture() { + return mTexture; + } + + public static void setMaxSide(int size) { + sMaxSide = size; + } +} diff --git a/src/com/android/gallery3d/ui/UndoBarView.java b/src/com/android/gallery3d/ui/UndoBarView.java new file mode 100644 index 000000000..42f12ae72 --- /dev/null +++ b/src/com/android/gallery3d/ui/UndoBarView.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2012 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.ui; + +import android.content.Context; +import android.view.MotionEvent; + +import com.android.gallery3d.R; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.glrenderer.GLCanvas; +import com.android.gallery3d.glrenderer.NinePatchTexture; +import com.android.gallery3d.glrenderer.ResourceTexture; +import com.android.gallery3d.glrenderer.StringTexture; +import com.android.gallery3d.util.GalleryUtils; + +public class UndoBarView extends GLView { + @SuppressWarnings("unused") + private static final String TAG = "UndoBarView"; + + private static final int WHITE = 0xFFFFFFFF; + private static final int GRAY = 0xFFAAAAAA; + + private final NinePatchTexture mPanel; + private final StringTexture mUndoText; + private final StringTexture mDeletedText; + private final ResourceTexture mUndoIcon; + private final int mBarHeight; + private final int mBarMargin; + private final int mUndoTextMargin; + private final int mIconSize; + private final int mIconMargin; + private final int mSeparatorTopMargin; + private final int mSeparatorBottomMargin; + private final int mSeparatorRightMargin; + private final int mSeparatorWidth; + private final int mDeletedTextMargin; + private final int mClickRegion; + + private OnClickListener mOnClickListener; + private boolean mDownOnButton; + + // This is the layout of UndoBarView. The unit is dp. + // + // +-+----+----------------+-+--+----+-+------+--+-+ + // 48 | | | Deleted | | | <- | | UNDO | | | + // +-+----+----------------+-+--+----+-+------+--+-+ + // 4 16 1 12 32 8 16 4 + public UndoBarView(Context context) { + mBarHeight = GalleryUtils.dpToPixel(48); + mBarMargin = GalleryUtils.dpToPixel(4); + mUndoTextMargin = GalleryUtils.dpToPixel(16); + mIconMargin = GalleryUtils.dpToPixel(8); + mIconSize = GalleryUtils.dpToPixel(32); + mSeparatorRightMargin = GalleryUtils.dpToPixel(12); + mSeparatorTopMargin = GalleryUtils.dpToPixel(10); + mSeparatorBottomMargin = GalleryUtils.dpToPixel(10); + mSeparatorWidth = GalleryUtils.dpToPixel(1); + mDeletedTextMargin = GalleryUtils.dpToPixel(16); + + mPanel = new NinePatchTexture(context, R.drawable.panel_undo_holo); + mUndoText = StringTexture.newInstance(context.getString(R.string.undo), + GalleryUtils.dpToPixel(12), GRAY, 0, true); + mDeletedText = StringTexture.newInstance( + context.getString(R.string.deleted), + GalleryUtils.dpToPixel(16), WHITE); + mUndoIcon = new ResourceTexture( + context, R.drawable.ic_menu_revert_holo_dark); + mClickRegion = mBarMargin + mUndoTextMargin + mUndoText.getWidth() + + mIconMargin + mIconSize + mSeparatorRightMargin; + } + + public void setOnClickListener(OnClickListener listener) { + mOnClickListener = listener; + } + + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + setMeasuredSize(0 /* unused */, mBarHeight); + } + + @Override + protected void render(GLCanvas canvas) { + super.render(canvas); + advanceAnimation(); + + canvas.save(GLCanvas.SAVE_FLAG_ALPHA); + canvas.multiplyAlpha(mAlpha); + + int w = getWidth(); + int h = getHeight(); + mPanel.draw(canvas, mBarMargin, 0, w - mBarMargin * 2, mBarHeight); + + int x = w - mBarMargin; + int y; + + x -= mUndoTextMargin + mUndoText.getWidth(); + y = (mBarHeight - mUndoText.getHeight()) / 2; + mUndoText.draw(canvas, x, y); + + x -= mIconMargin + mIconSize; + y = (mBarHeight - mIconSize) / 2; + mUndoIcon.draw(canvas, x, y, mIconSize, mIconSize); + + x -= mSeparatorRightMargin + mSeparatorWidth; + y = mSeparatorTopMargin; + canvas.fillRect(x, y, mSeparatorWidth, + mBarHeight - mSeparatorTopMargin - mSeparatorBottomMargin, GRAY); + + x = mBarMargin + mDeletedTextMargin; + y = (mBarHeight - mDeletedText.getHeight()) / 2; + mDeletedText.draw(canvas, x, y); + + canvas.restore(); + } + + @Override + protected boolean onTouch(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + mDownOnButton = inUndoButton(event); + break; + case MotionEvent.ACTION_UP: + if (mDownOnButton) { + if (mOnClickListener != null && inUndoButton(event)) { + mOnClickListener.onClick(this); + } + mDownOnButton = false; + } + break; + case MotionEvent.ACTION_CANCEL: + mDownOnButton = false; + break; + } + return true; + } + + // Check if the event is on the right of the separator + private boolean inUndoButton(MotionEvent event) { + float x = event.getX(); + float y = event.getY(); + int w = getWidth(); + int h = getHeight(); + return (x >= w - mClickRegion && x < w && y >= 0 && y < h); + } + + //////////////////////////////////////////////////////////////////////////// + // Alpha Animation + //////////////////////////////////////////////////////////////////////////// + + private static final long NO_ANIMATION = -1; + private static long ANIM_TIME = 200; + private long mAnimationStartTime = NO_ANIMATION; + private float mFromAlpha, mToAlpha; + private float mAlpha; + + private static float getTargetAlpha(int visibility) { + return (visibility == VISIBLE) ? 1f : 0f; + } + + @Override + public void setVisibility(int visibility) { + mAlpha = getTargetAlpha(visibility); + mAnimationStartTime = NO_ANIMATION; + super.setVisibility(visibility); + invalidate(); + } + + public void animateVisibility(int visibility) { + float target = getTargetAlpha(visibility); + if (mAnimationStartTime == NO_ANIMATION && mAlpha == target) return; + if (mAnimationStartTime != NO_ANIMATION && mToAlpha == target) return; + + mFromAlpha = mAlpha; + mToAlpha = target; + mAnimationStartTime = AnimationTime.startTime(); + + super.setVisibility(VISIBLE); + invalidate(); + } + + private void advanceAnimation() { + if (mAnimationStartTime == NO_ANIMATION) return; + + float delta = (float) (AnimationTime.get() - mAnimationStartTime) / + ANIM_TIME; + mAlpha = mFromAlpha + ((mToAlpha > mFromAlpha) ? delta : -delta); + mAlpha = Utils.clamp(mAlpha, 0f, 1f); + + if (mAlpha == mToAlpha) { + mAnimationStartTime = NO_ANIMATION; + if (mAlpha == 0) { + super.setVisibility(INVISIBLE); + } + } + invalidate(); + } +} diff --git a/src/com/android/gallery3d/ui/UserInteractionListener.java b/src/com/android/gallery3d/ui/UserInteractionListener.java new file mode 100644 index 000000000..bc4a71800 --- /dev/null +++ b/src/com/android/gallery3d/ui/UserInteractionListener.java @@ -0,0 +1,26 @@ +/* + * 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.ui; + +public interface UserInteractionListener { + // Called when a user interaction begins (for example, fling). + public void onUserInteractionBegin(); + // Called when the user interaction ends. + public void onUserInteractionEnd(); + // Other one-shot user interactions. + public void onUserInteraction(); +} diff --git a/src/com/android/gallery3d/ui/WakeLockHoldingProgressListener.java b/src/com/android/gallery3d/ui/WakeLockHoldingProgressListener.java new file mode 100644 index 000000000..ee61d8edb --- /dev/null +++ b/src/com/android/gallery3d/ui/WakeLockHoldingProgressListener.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2012 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.ui; + +import android.app.Activity; +import android.content.Context; +import android.os.PowerManager; + +import com.android.gallery3d.app.AbstractGalleryActivity; + +public class WakeLockHoldingProgressListener implements MenuExecutor.ProgressListener { + static private final String DEFAULT_WAKE_LOCK_LABEL = "Gallery Progress Listener"; + private AbstractGalleryActivity mActivity; + private PowerManager.WakeLock mWakeLock; + + public WakeLockHoldingProgressListener(AbstractGalleryActivity galleryActivity) { + this(galleryActivity, DEFAULT_WAKE_LOCK_LABEL); + } + + public WakeLockHoldingProgressListener(AbstractGalleryActivity galleryActivity, String label) { + mActivity = galleryActivity; + PowerManager pm = + (PowerManager) ((Activity) mActivity).getSystemService(Context.POWER_SERVICE); + mWakeLock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, label); + } + + @Override + public void onProgressComplete(int result) { + mWakeLock.release(); + } + + @Override + public void onProgressStart() { + mWakeLock.acquire(); + } + + protected AbstractGalleryActivity getActivity() { + return mActivity; + } + + @Override + public void onProgressUpdate(int index) { + } + + @Override + public void onConfirmDialogDismissed(boolean confirmed) { + } + + @Override + public void onConfirmDialogShown() { + } +} diff --git a/src/com/android/gallery3d/util/AccessibilityUtils.java b/src/com/android/gallery3d/util/AccessibilityUtils.java new file mode 100644 index 000000000..9df8e4ece --- /dev/null +++ b/src/com/android/gallery3d/util/AccessibilityUtils.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2012 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.util; + +import android.content.Context; +import android.support.v4.view.accessibility.AccessibilityRecordCompat; +import android.view.View; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; + +import com.android.gallery3d.common.ApiHelper; + +/** + * AccessibilityUtils provides functions needed in accessibility mode. All the functions + * in this class are made compatible with gingerbread and later API's +*/ +public class AccessibilityUtils { + public static void makeAnnouncement(View view, CharSequence announcement) { + if (view == null) + return; + if (ApiHelper.HAS_ANNOUNCE_FOR_ACCESSIBILITY) { + view.announceForAccessibility(announcement); + } else { + // For API 15 and earlier, we need to construct an accessibility event + Context ctx = view.getContext(); + AccessibilityManager am = (AccessibilityManager) ctx.getSystemService( + Context.ACCESSIBILITY_SERVICE); + if (!am.isEnabled()) return; + AccessibilityEvent event = AccessibilityEvent.obtain( + AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED); + AccessibilityRecordCompat arc = new AccessibilityRecordCompat(event); + arc.setSource(view); + event.setClassName(view.getClass().getName()); + event.setPackageName(view.getContext().getPackageName()); + event.setEnabled(view.isEnabled()); + event.getText().add(announcement); + am.sendAccessibilityEvent(event); + } + } +}
\ No newline at end of file diff --git a/src/com/android/gallery3d/util/BucketNames.java b/src/com/android/gallery3d/util/BucketNames.java new file mode 100644 index 000000000..990dc8224 --- /dev/null +++ b/src/com/android/gallery3d/util/BucketNames.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2012 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.util; + +/** + * Bucket names for buckets that are created and used in the Gallery. + */ +public class BucketNames { + + public static final String CAMERA = "DCIM/Camera"; + public static final String IMPORTED = "Imported"; + public static final String DOWNLOAD = "download"; + public static final String EDITED_ONLINE_PHOTOS = "EditedOnlinePhotos"; + public static final String SCREENSHOTS = "Pictures/Screenshots"; +} diff --git a/src/com/android/gallery3d/util/CacheManager.java b/src/com/android/gallery3d/util/CacheManager.java new file mode 100644 index 000000000..ba466f79b --- /dev/null +++ b/src/com/android/gallery3d/util/CacheManager.java @@ -0,0 +1,82 @@ +/* + * 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.util; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +import com.android.gallery3d.common.BlobCache; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; + +public class CacheManager { + private static final String TAG = "CacheManager"; + private static final String KEY_CACHE_UP_TO_DATE = "cache-up-to-date"; + private static HashMap<String, BlobCache> sCacheMap = + new HashMap<String, BlobCache>(); + private static boolean sOldCheckDone = false; + + // Return null when we cannot instantiate a BlobCache, e.g.: + // there is no SD card found. + // This can only be called from data thread. + public static BlobCache getCache(Context context, String filename, + int maxEntries, int maxBytes, int version) { + synchronized (sCacheMap) { + if (!sOldCheckDone) { + removeOldFilesIfNecessary(context); + sOldCheckDone = true; + } + BlobCache cache = sCacheMap.get(filename); + if (cache == null) { + File cacheDir = context.getExternalCacheDir(); + String path = cacheDir.getAbsolutePath() + "/" + filename; + try { + cache = new BlobCache(path, maxEntries, maxBytes, false, + version); + sCacheMap.put(filename, cache); + } catch (IOException e) { + Log.e(TAG, "Cannot instantiate cache!", e); + } + } + return cache; + } + } + + // Removes the old files if the data is wiped. + private static void removeOldFilesIfNecessary(Context context) { + SharedPreferences pref = PreferenceManager + .getDefaultSharedPreferences(context); + int n = 0; + try { + n = pref.getInt(KEY_CACHE_UP_TO_DATE, 0); + } catch (Throwable t) { + // ignore. + } + if (n != 0) return; + pref.edit().putInt(KEY_CACHE_UP_TO_DATE, 1).commit(); + + File cacheDir = context.getExternalCacheDir(); + String prefix = cacheDir.getAbsolutePath() + "/"; + + BlobCache.deleteFiles(prefix + "imgcache"); + BlobCache.deleteFiles(prefix + "rev_geocoding"); + BlobCache.deleteFiles(prefix + "bookmark"); + } +} diff --git a/src/com/android/gallery3d/util/GalleryUtils.java b/src/com/android/gallery3d/util/GalleryUtils.java new file mode 100644 index 000000000..9245e2c5f --- /dev/null +++ b/src/com/android/gallery3d/util/GalleryUtils.java @@ -0,0 +1,404 @@ +/* + * 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.util; + +import android.annotation.TargetApi; +import android.content.ActivityNotFoundException; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.graphics.Color; +import android.net.Uri; +import android.os.ConditionVariable; +import android.os.Environment; +import android.os.StatFs; +import android.preference.PreferenceManager; +import android.provider.MediaStore; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.WindowManager; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.Gallery; +import com.android.gallery3d.app.PackagesMonitor; +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.ui.TiledScreenNail; +import com.android.gallery3d.util.ThreadPool.CancelListener; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import java.io.File; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +public class GalleryUtils { + private static final String TAG = "GalleryUtils"; + private static final String MAPS_PACKAGE_NAME = "com.google.android.apps.maps"; + private static final String MAPS_CLASS_NAME = "com.google.android.maps.MapsActivity"; + private static final String CAMERA_LAUNCHER_NAME = "com.android.camera.CameraLauncher"; + + public static final String MIME_TYPE_IMAGE = "image/*"; + public static final String MIME_TYPE_VIDEO = "video/*"; + public static final String MIME_TYPE_PANORAMA360 = "application/vnd.google.panorama360+jpg"; + public static final String MIME_TYPE_ALL = "*/*"; + + private static final String DIR_TYPE_IMAGE = "vnd.android.cursor.dir/image"; + private static final String DIR_TYPE_VIDEO = "vnd.android.cursor.dir/video"; + + private static final String PREFIX_PHOTO_EDITOR_UPDATE = "editor-update-"; + private static final String PREFIX_HAS_PHOTO_EDITOR = "has-editor-"; + + private static final String KEY_CAMERA_UPDATE = "camera-update"; + private static final String KEY_HAS_CAMERA = "has-camera"; + + private static float sPixelDensity = -1f; + private static boolean sCameraAvailableInitialized = false; + private static boolean sCameraAvailable; + + public static void initialize(Context context) { + DisplayMetrics metrics = new DisplayMetrics(); + WindowManager wm = (WindowManager) + context.getSystemService(Context.WINDOW_SERVICE); + wm.getDefaultDisplay().getMetrics(metrics); + sPixelDensity = metrics.density; + Resources r = context.getResources(); + TiledScreenNail.setPlaceholderColor(r.getColor( + R.color.bitmap_screennail_placeholder)); + initializeThumbnailSizes(metrics, r); + } + + private static void initializeThumbnailSizes(DisplayMetrics metrics, Resources r) { + int maxPixels = Math.max(metrics.heightPixels, metrics.widthPixels); + + // For screen-nails, we never need to completely fill the screen + MediaItem.setThumbnailSizes(maxPixels / 2, maxPixels / 5); + TiledScreenNail.setMaxSide(maxPixels / 2); + } + + public static float[] intColorToFloatARGBArray(int from) { + return new float[] { + Color.alpha(from) / 255f, + Color.red(from) / 255f, + Color.green(from) / 255f, + Color.blue(from) / 255f + }; + } + + public static float dpToPixel(float dp) { + return sPixelDensity * dp; + } + + public static int dpToPixel(int dp) { + return Math.round(dpToPixel((float) dp)); + } + + public static int meterToPixel(float meter) { + // 1 meter = 39.37 inches, 1 inch = 160 dp. + return Math.round(dpToPixel(meter * 39.37f * 160)); + } + + public static byte[] getBytes(String in) { + byte[] result = new byte[in.length() * 2]; + int output = 0; + for (char ch : in.toCharArray()) { + result[output++] = (byte) (ch & 0xFF); + result[output++] = (byte) (ch >> 8); + } + return result; + } + + // Below are used the detect using database in the render thread. It only + // works most of the time, but that's ok because it's for debugging only. + + private static volatile Thread sCurrentThread; + private static volatile boolean sWarned; + + public static void setRenderThread() { + sCurrentThread = Thread.currentThread(); + } + + public static void assertNotInRenderThread() { + if (!sWarned) { + if (Thread.currentThread() == sCurrentThread) { + sWarned = true; + Log.w(TAG, new Throwable("Should not do this in render thread")); + } + } + } + + private static final double RAD_PER_DEG = Math.PI / 180.0; + private static final double EARTH_RADIUS_METERS = 6367000.0; + + public static double fastDistanceMeters(double latRad1, double lngRad1, + double latRad2, double lngRad2) { + if ((Math.abs(latRad1 - latRad2) > RAD_PER_DEG) + || (Math.abs(lngRad1 - lngRad2) > RAD_PER_DEG)) { + return accurateDistanceMeters(latRad1, lngRad1, latRad2, lngRad2); + } + // Approximate sin(x) = x. + double sineLat = (latRad1 - latRad2); + + // Approximate sin(x) = x. + double sineLng = (lngRad1 - lngRad2); + + // Approximate cos(lat1) * cos(lat2) using + // cos((lat1 + lat2)/2) ^ 2 + double cosTerms = Math.cos((latRad1 + latRad2) / 2.0); + cosTerms = cosTerms * cosTerms; + double trigTerm = sineLat * sineLat + cosTerms * sineLng * sineLng; + trigTerm = Math.sqrt(trigTerm); + + // Approximate arcsin(x) = x + return EARTH_RADIUS_METERS * trigTerm; + } + + public static double accurateDistanceMeters(double lat1, double lng1, + double lat2, double lng2) { + double dlat = Math.sin(0.5 * (lat2 - lat1)); + double dlng = Math.sin(0.5 * (lng2 - lng1)); + double x = dlat * dlat + dlng * dlng * Math.cos(lat1) * Math.cos(lat2); + return (2 * Math.atan2(Math.sqrt(x), Math.sqrt(Math.max(0.0, + 1.0 - x)))) * EARTH_RADIUS_METERS; + } + + + public static final double toMile(double meter) { + return meter / 1609; + } + + // For debugging, it will block the caller for timeout millis. + public static void fakeBusy(JobContext jc, int timeout) { + final ConditionVariable cv = new ConditionVariable(); + jc.setCancelListener(new CancelListener() { + @Override + public void onCancel() { + cv.open(); + } + }); + cv.block(timeout); + jc.setCancelListener(null); + } + + public static boolean isEditorAvailable(Context context, String mimeType) { + int version = PackagesMonitor.getPackagesVersion(context); + + String updateKey = PREFIX_PHOTO_EDITOR_UPDATE + mimeType; + String hasKey = PREFIX_HAS_PHOTO_EDITOR + mimeType; + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + if (prefs.getInt(updateKey, 0) != version) { + PackageManager packageManager = context.getPackageManager(); + List<ResolveInfo> infos = packageManager.queryIntentActivities( + new Intent(Intent.ACTION_EDIT).setType(mimeType), 0); + prefs.edit().putInt(updateKey, version) + .putBoolean(hasKey, !infos.isEmpty()) + .commit(); + } + + return prefs.getBoolean(hasKey, true); + } + + public static boolean isAnyCameraAvailable(Context context) { + int version = PackagesMonitor.getPackagesVersion(context); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + if (prefs.getInt(KEY_CAMERA_UPDATE, 0) != version) { + PackageManager packageManager = context.getPackageManager(); + List<ResolveInfo> infos = packageManager.queryIntentActivities( + new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA), 0); + prefs.edit().putInt(KEY_CAMERA_UPDATE, version) + .putBoolean(KEY_HAS_CAMERA, !infos.isEmpty()) + .commit(); + } + return prefs.getBoolean(KEY_HAS_CAMERA, true); + } + + public static boolean isCameraAvailable(Context context) { + if (sCameraAvailableInitialized) return sCameraAvailable; + PackageManager pm = context.getPackageManager(); + ComponentName name = new ComponentName(context, CAMERA_LAUNCHER_NAME); + int state = pm.getComponentEnabledSetting(name); + sCameraAvailableInitialized = true; + sCameraAvailable = + (state == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) + || (state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED); + return sCameraAvailable; + } + + public static void startCameraActivity(Context context) { + Intent intent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA) + .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP + | Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } + + public static void startGalleryActivity(Context context) { + Intent intent = new Intent(context, Gallery.class) + .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP + | Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } + + public static boolean isValidLocation(double latitude, double longitude) { + // TODO: change || to && after we fix the default location issue + return (latitude != MediaItem.INVALID_LATLNG || longitude != MediaItem.INVALID_LATLNG); + } + + public static String formatLatitudeLongitude(String format, double latitude, + double longitude) { + // We need to specify the locale otherwise it may go wrong in some language + // (e.g. Locale.FRENCH) + return String.format(Locale.ENGLISH, format, latitude, longitude); + } + + public static void showOnMap(Context context, double latitude, double longitude) { + try { + // We don't use "geo:latitude,longitude" because it only centers + // the MapView to the specified location, but we need a marker + // for further operations (routing to/from). + // The q=(lat, lng) syntax is suggested by geo-team. + String uri = formatLatitudeLongitude("http://maps.google.com/maps?f=q&q=(%f,%f)", + latitude, longitude); + ComponentName compName = new ComponentName(MAPS_PACKAGE_NAME, + MAPS_CLASS_NAME); + Intent mapsIntent = new Intent(Intent.ACTION_VIEW, + Uri.parse(uri)).setComponent(compName); + context.startActivity(mapsIntent); + } catch (ActivityNotFoundException e) { + // Use the "geo intent" if no GMM is installed + Log.e(TAG, "GMM activity not found!", e); + String url = formatLatitudeLongitude("geo:%f,%f", latitude, longitude); + Intent mapsIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + context.startActivity(mapsIntent); + } + } + + public static void setViewPointMatrix( + float matrix[], float x, float y, float z) { + // The matrix is + // -z, 0, x, 0 + // 0, -z, y, 0 + // 0, 0, 1, 0 + // 0, 0, 1, -z + Arrays.fill(matrix, 0, 16, 0); + matrix[0] = matrix[5] = matrix[15] = -z; + matrix[8] = x; + matrix[9] = y; + matrix[10] = matrix[11] = 1; + } + + public static int getBucketId(String path) { + return path.toLowerCase().hashCode(); + } + + // Return the local path that matches the given bucketId. If no match is + // found, return null + public static String searchDirForPath(File dir, int bucketId) { + File[] files = dir.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + String path = file.getAbsolutePath(); + if (GalleryUtils.getBucketId(path) == bucketId) { + return path; + } else { + path = searchDirForPath(file, bucketId); + if (path != null) return path; + } + } + } + } + return null; + } + + // Returns a (localized) string for the given duration (in seconds). + public static String formatDuration(final Context context, int duration) { + int h = duration / 3600; + int m = (duration - h * 3600) / 60; + int s = duration - (h * 3600 + m * 60); + String durationValue; + if (h == 0) { + durationValue = String.format(context.getString(R.string.details_ms), m, s); + } else { + durationValue = String.format(context.getString(R.string.details_hms), h, m, s); + } + return durationValue; + } + + @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) + public static int determineTypeBits(Context context, Intent intent) { + int typeBits = 0; + String type = intent.resolveType(context); + + if (MIME_TYPE_ALL.equals(type)) { + typeBits = DataManager.INCLUDE_ALL; + } else if (MIME_TYPE_IMAGE.equals(type) || + DIR_TYPE_IMAGE.equals(type)) { + typeBits = DataManager.INCLUDE_IMAGE; + } else if (MIME_TYPE_VIDEO.equals(type) || + DIR_TYPE_VIDEO.equals(type)) { + typeBits = DataManager.INCLUDE_VIDEO; + } else { + typeBits = DataManager.INCLUDE_ALL; + } + + if (ApiHelper.HAS_INTENT_EXTRA_LOCAL_ONLY) { + if (intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false)) { + typeBits |= DataManager.INCLUDE_LOCAL_ONLY; + } + } + + return typeBits; + } + + public static int getSelectionModePrompt(int typeBits) { + if ((typeBits & DataManager.INCLUDE_VIDEO) != 0) { + return (typeBits & DataManager.INCLUDE_IMAGE) == 0 + ? R.string.select_video + : R.string.select_item; + } + return R.string.select_image; + } + + public static boolean hasSpaceForSize(long size) { + String state = Environment.getExternalStorageState(); + if (!Environment.MEDIA_MOUNTED.equals(state)) { + return false; + } + + String path = Environment.getExternalStorageDirectory().getPath(); + try { + StatFs stat = new StatFs(path); + return stat.getAvailableBlocks() * (long) stat.getBlockSize() > size; + } catch (Exception e) { + Log.i(TAG, "Fail to access external storage", e); + } + return false; + } + + public static boolean isPanorama(MediaItem item) { + if (item == null) return false; + int w = item.getWidth(); + int h = item.getHeight(); + return (h > 0 && w / h >= 2); + } +} diff --git a/src/com/android/gallery3d/util/Holder.java b/src/com/android/gallery3d/util/Holder.java new file mode 100644 index 000000000..0ce914c1d --- /dev/null +++ b/src/com/android/gallery3d/util/Holder.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2012 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.util; + +public class Holder<T> { + private T mObject; + + public void set(T object) { + mObject = object; + } + + public T get() { + return mObject; + } +} diff --git a/src/com/android/gallery3d/util/IdentityCache.java b/src/com/android/gallery3d/util/IdentityCache.java new file mode 100644 index 000000000..3edc424a3 --- /dev/null +++ b/src/com/android/gallery3d/util/IdentityCache.java @@ -0,0 +1,78 @@ +/* + * 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.util; + +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Set; + +public class IdentityCache<K, V> { + + private final HashMap<K, Entry<K, V>> mWeakMap = + new HashMap<K, Entry<K, V>>(); + private ReferenceQueue<V> mQueue = new ReferenceQueue<V>(); + + public IdentityCache() { + } + + private static class Entry<K, V> extends WeakReference<V> { + K mKey; + + public Entry(K key, V value, ReferenceQueue<V> queue) { + super(value, queue); + mKey = key; + } + } + + private void cleanUpWeakMap() { + Entry<K, V> entry = (Entry<K, V>) mQueue.poll(); + while (entry != null) { + mWeakMap.remove(entry.mKey); + entry = (Entry<K, V>) mQueue.poll(); + } + } + + public synchronized V put(K key, V value) { + cleanUpWeakMap(); + Entry<K, V> entry = mWeakMap.put( + key, new Entry<K, V>(key, value, mQueue)); + return entry == null ? null : entry.get(); + } + + public synchronized V get(K key) { + cleanUpWeakMap(); + Entry<K, V> entry = mWeakMap.get(key); + return entry == null ? null : entry.get(); + } + + // This is currently unused. + /* + public synchronized void clear() { + mWeakMap.clear(); + mQueue = new ReferenceQueue<V>(); + } + */ + + // This is for debugging only + public synchronized ArrayList<K> keys() { + Set<K> set = mWeakMap.keySet(); + ArrayList<K> result = new ArrayList<K>(set); + return result; + } +} diff --git a/src/com/android/gallery3d/util/IntArray.java b/src/com/android/gallery3d/util/IntArray.java new file mode 100644 index 000000000..2c4dc2c83 --- /dev/null +++ b/src/com/android/gallery3d/util/IntArray.java @@ -0,0 +1,60 @@ +/* + * 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.util; + +public class IntArray { + private static final int INIT_CAPACITY = 8; + + private int mData[] = new int[INIT_CAPACITY]; + private int mSize = 0; + + public void add(int value) { + if (mData.length == mSize) { + int temp[] = new int[mSize + mSize]; + System.arraycopy(mData, 0, temp, 0, mSize); + mData = temp; + } + mData[mSize++] = value; + } + + public int removeLast() { + mSize--; + return mData[mSize]; + } + + public int size() { + return mSize; + } + + // For testing only + public int[] toArray(int[] result) { + if (result == null || result.length < mSize) { + result = new int[mSize]; + } + System.arraycopy(mData, 0, result, 0, mSize); + return result; + } + + public int[] getInternalArray() { + return mData; + } + + public void clear() { + mSize = 0; + if (mData.length != INIT_CAPACITY) mData = new int[INIT_CAPACITY]; + } +} diff --git a/src/com/android/gallery3d/util/InterruptableOutputStream.java b/src/com/android/gallery3d/util/InterruptableOutputStream.java new file mode 100644 index 000000000..1ab62ab98 --- /dev/null +++ b/src/com/android/gallery3d/util/InterruptableOutputStream.java @@ -0,0 +1,67 @@ +/* + * 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.util; + +import com.android.gallery3d.common.Utils; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.io.OutputStream; + +public class InterruptableOutputStream extends OutputStream { + + private static final int MAX_WRITE_BYTES = 4096; + + private OutputStream mOutputStream; + private volatile boolean mIsInterrupted = false; + + public InterruptableOutputStream(OutputStream outputStream) { + mOutputStream = Utils.checkNotNull(outputStream); + } + + @Override + public void write(int oneByte) throws IOException { + if (mIsInterrupted) throw new InterruptedIOException(); + mOutputStream.write(oneByte); + } + + @Override + public void write(byte[] buffer, int offset, int count) throws IOException { + int end = offset + count; + while (offset < end) { + if (mIsInterrupted) throw new InterruptedIOException(); + int bytesCount = Math.min(MAX_WRITE_BYTES, end - offset); + mOutputStream.write(buffer, offset, bytesCount); + offset += bytesCount; + } + } + + @Override + public void close() throws IOException { + mOutputStream.close(); + } + + @Override + public void flush() throws IOException { + if (mIsInterrupted) throw new InterruptedIOException(); + mOutputStream.flush(); + } + + public void interrupt() { + mIsInterrupted = true; + } +} diff --git a/src/com/android/gallery3d/util/JobLimiter.java b/src/com/android/gallery3d/util/JobLimiter.java new file mode 100644 index 000000000..42b754153 --- /dev/null +++ b/src/com/android/gallery3d/util/JobLimiter.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2011 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.util; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import java.util.LinkedList; + +// Limit the number of concurrent jobs that has been submitted into a ThreadPool +@SuppressWarnings("rawtypes") +public class JobLimiter implements FutureListener { + private static final String TAG = "JobLimiter"; + + // State Transition: + // INIT -> DONE, CANCELLED + // DONE -> CANCELLED + private static final int STATE_INIT = 0; + private static final int STATE_DONE = 1; + private static final int STATE_CANCELLED = 2; + + private final LinkedList<JobWrapper<?>> mJobs = new LinkedList<JobWrapper<?>>(); + private final ThreadPool mPool; + private int mLimit; + + private static class JobWrapper<T> implements Future<T>, Job<T> { + private int mState = STATE_INIT; + private Job<T> mJob; + private Future<T> mDelegate; + private FutureListener<T> mListener; + private T mResult; + + public JobWrapper(Job<T> job, FutureListener<T> listener) { + mJob = job; + mListener = listener; + } + + public synchronized void setFuture(Future<T> future) { + if (mState != STATE_INIT) return; + mDelegate = future; + } + + @Override + public void cancel() { + FutureListener<T> listener = null; + synchronized (this) { + if (mState != STATE_DONE) { + listener = mListener; + mJob = null; + mListener = null; + if (mDelegate != null) { + mDelegate.cancel(); + mDelegate = null; + } + } + mState = STATE_CANCELLED; + mResult = null; + notifyAll(); + } + if (listener != null) listener.onFutureDone(this); + } + + @Override + public synchronized boolean isCancelled() { + return mState == STATE_CANCELLED; + } + + @Override + public boolean isDone() { + // Both CANCELLED AND DONE is considered as done + return mState != STATE_INIT; + } + + @Override + public synchronized T get() { + while (mState == STATE_INIT) { + // handle the interrupted exception of wait() + Utils.waitWithoutInterrupt(this); + } + return mResult; + } + + @Override + public void waitDone() { + get(); + } + + @Override + public T run(JobContext jc) { + Job<T> job = null; + synchronized (this) { + if (mState == STATE_CANCELLED) return null; + job = mJob; + } + T result = null; + try { + result = job.run(jc); + } catch (Throwable t) { + Log.w(TAG, "error executing job: " + job, t); + } + FutureListener<T> listener = null; + synchronized (this) { + if (mState == STATE_CANCELLED) return null; + mState = STATE_DONE; + listener = mListener; + mListener = null; + mJob = null; + mResult = result; + notifyAll(); + } + if (listener != null) listener.onFutureDone(this); + return result; + } + } + + public JobLimiter(ThreadPool pool, int limit) { + mPool = Utils.checkNotNull(pool); + mLimit = limit; + } + + public synchronized <T> Future<T> submit(Job<T> job, FutureListener<T> listener) { + JobWrapper<T> future = new JobWrapper<T>(Utils.checkNotNull(job), listener); + mJobs.addLast(future); + submitTasksIfAllowed(); + return future; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private void submitTasksIfAllowed() { + while (mLimit > 0 && !mJobs.isEmpty()) { + JobWrapper wrapper = mJobs.removeFirst(); + if (!wrapper.isCancelled()) { + --mLimit; + wrapper.setFuture(mPool.submit(wrapper, this)); + } + } + } + + @Override + public synchronized void onFutureDone(Future future) { + ++mLimit; + submitTasksIfAllowed(); + } +} diff --git a/src/com/android/gallery3d/util/LinkedNode.java b/src/com/android/gallery3d/util/LinkedNode.java new file mode 100644 index 000000000..4cfc3cded --- /dev/null +++ b/src/com/android/gallery3d/util/LinkedNode.java @@ -0,0 +1,71 @@ +/* + * 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.util; + + +public class LinkedNode { + private LinkedNode mPrev; + private LinkedNode mNext; + + public LinkedNode() { + mPrev = mNext = this; + } + + public void insert(LinkedNode node) { + node.mNext = mNext; + mNext.mPrev = node; + node.mPrev = this; + mNext = node; + } + + public void remove() { + if (mNext == this) throw new IllegalStateException(); + mPrev.mNext = mNext; + mNext.mPrev = mPrev; + mPrev = mNext = null; + } + + @SuppressWarnings("unchecked") + public static class List<T extends LinkedNode> { + private LinkedNode mHead = new LinkedNode(); + + public void insertLast(T node) { + mHead.mPrev.insert(node); + } + + public T getFirst() { + return (T) (mHead.mNext == mHead ? null : mHead.mNext); + } + + public T getLast() { + return (T) (mHead.mPrev == mHead ? null : mHead.mPrev); + } + + public T nextOf(T node) { + return (T) (node.mNext == mHead ? null : node.mNext); + } + + public T previousOf(T node) { + return (T) (node.mPrev == mHead ? null : node.mPrev); + } + + } + + public static <T extends LinkedNode> List<T> newList() { + return new List<T>(); + } +} diff --git a/src/com/android/gallery3d/util/Log.java b/src/com/android/gallery3d/util/Log.java new file mode 100644 index 000000000..d7f8e85d0 --- /dev/null +++ b/src/com/android/gallery3d/util/Log.java @@ -0,0 +1,53 @@ +/* + * 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.util; + +public class Log { + public static int v(String tag, String msg) { + return android.util.Log.v(tag, msg); + } + public static int v(String tag, String msg, Throwable tr) { + return android.util.Log.v(tag, msg, tr); + } + public static int d(String tag, String msg) { + return android.util.Log.d(tag, msg); + } + public static int d(String tag, String msg, Throwable tr) { + return android.util.Log.d(tag, msg, tr); + } + public static int i(String tag, String msg) { + return android.util.Log.i(tag, msg); + } + public static int i(String tag, String msg, Throwable tr) { + return android.util.Log.i(tag, msg, tr); + } + public static int w(String tag, String msg) { + return android.util.Log.w(tag, msg); + } + public static int w(String tag, String msg, Throwable tr) { + return android.util.Log.w(tag, msg, tr); + } + public static int w(String tag, Throwable tr) { + return android.util.Log.w(tag, tr); + } + public static int e(String tag, String msg) { + return android.util.Log.e(tag, msg); + } + public static int e(String tag, String msg, Throwable tr) { + return android.util.Log.e(tag, msg, tr); + } +} diff --git a/src/com/android/gallery3d/util/MediaSetUtils.java b/src/com/android/gallery3d/util/MediaSetUtils.java new file mode 100644 index 000000000..043800561 --- /dev/null +++ b/src/com/android/gallery3d/util/MediaSetUtils.java @@ -0,0 +1,66 @@ +/* + * 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.util; + +import android.os.Environment; + +import com.android.gallery3d.data.LocalAlbum; +import com.android.gallery3d.data.LocalMergeAlbum; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.Path; + +import java.util.Comparator; + +public class MediaSetUtils { + public static final Comparator<MediaSet> NAME_COMPARATOR = new NameComparator(); + + public static final int CAMERA_BUCKET_ID = GalleryUtils.getBucketId( + Environment.getExternalStorageDirectory().toString() + "/" + + BucketNames.CAMERA); + public static final int DOWNLOAD_BUCKET_ID = GalleryUtils.getBucketId( + Environment.getExternalStorageDirectory().toString() + "/" + + BucketNames.DOWNLOAD); + public static final int EDITED_ONLINE_PHOTOS_BUCKET_ID = GalleryUtils.getBucketId( + Environment.getExternalStorageDirectory().toString() + "/" + + BucketNames.EDITED_ONLINE_PHOTOS); + public static final int IMPORTED_BUCKET_ID = GalleryUtils.getBucketId( + Environment.getExternalStorageDirectory().toString() + "/" + + BucketNames.IMPORTED); + public static final int SNAPSHOT_BUCKET_ID = GalleryUtils.getBucketId( + Environment.getExternalStorageDirectory().toString() + + "/" + BucketNames.SCREENSHOTS); + + private static final Path[] CAMERA_PATHS = { + Path.fromString("/local/all/" + CAMERA_BUCKET_ID), + Path.fromString("/local/image/" + CAMERA_BUCKET_ID), + Path.fromString("/local/video/" + CAMERA_BUCKET_ID)}; + + public static boolean isCameraSource(Path path) { + return CAMERA_PATHS[0] == path || CAMERA_PATHS[1] == path + || CAMERA_PATHS[2] == path; + } + + // Sort MediaSets by name + public static class NameComparator implements Comparator<MediaSet> { + @Override + public int compare(MediaSet set1, MediaSet set2) { + int result = set1.getName().compareToIgnoreCase(set2.getName()); + if (result != 0) return result; + return set1.getPath().toString().compareTo(set2.getPath().toString()); + } + } +} diff --git a/src/com/android/gallery3d/util/MotionEventHelper.java b/src/com/android/gallery3d/util/MotionEventHelper.java new file mode 100644 index 000000000..715f7fa69 --- /dev/null +++ b/src/com/android/gallery3d/util/MotionEventHelper.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2012 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.util; + +import android.annotation.TargetApi; +import android.graphics.Matrix; +import android.util.FloatMath; +import android.view.MotionEvent; +import android.view.MotionEvent.PointerCoords; + +import com.android.gallery3d.common.ApiHelper; + +public final class MotionEventHelper { + private MotionEventHelper() {} + + public static MotionEvent transformEvent(MotionEvent e, Matrix m) { + // We try to use the new transform method if possible because it uses + // less memory. + if (ApiHelper.HAS_MOTION_EVENT_TRANSFORM) { + return transformEventNew(e, m); + } else { + return transformEventOld(e, m); + } + } + + @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) + private static MotionEvent transformEventNew(MotionEvent e, Matrix m) { + MotionEvent newEvent = MotionEvent.obtain(e); + newEvent.transform(m); + return newEvent; + } + + // This is copied from Input.cpp in the android framework. + private static MotionEvent transformEventOld(MotionEvent e, Matrix m) { + long downTime = e.getDownTime(); + long eventTime = e.getEventTime(); + int action = e.getAction(); + int pointerCount = e.getPointerCount(); + int[] pointerIds = getPointerIds(e); + PointerCoords[] pointerCoords = getPointerCoords(e); + int metaState = e.getMetaState(); + float xPrecision = e.getXPrecision(); + float yPrecision = e.getYPrecision(); + int deviceId = e.getDeviceId(); + int edgeFlags = e.getEdgeFlags(); + int source = e.getSource(); + int flags = e.getFlags(); + + // Copy the x and y coordinates into an array, map them, and copy back. + float[] xy = new float[pointerCoords.length * 2]; + for (int i = 0; i < pointerCount;i++) { + xy[2 * i] = pointerCoords[i].x; + xy[2 * i + 1] = pointerCoords[i].y; + } + m.mapPoints(xy); + for (int i = 0; i < pointerCount;i++) { + pointerCoords[i].x = xy[2 * i]; + pointerCoords[i].y = xy[2 * i + 1]; + pointerCoords[i].orientation = transformAngle( + m, pointerCoords[i].orientation); + } + + MotionEvent n = MotionEvent.obtain(downTime, eventTime, action, + pointerCount, pointerIds, pointerCoords, metaState, xPrecision, + yPrecision, deviceId, edgeFlags, source, flags); + + return n; + } + + private static int[] getPointerIds(MotionEvent e) { + int n = e.getPointerCount(); + int[] r = new int[n]; + for (int i = 0; i < n; i++) { + r[i] = e.getPointerId(i); + } + return r; + } + + private static PointerCoords[] getPointerCoords(MotionEvent e) { + int n = e.getPointerCount(); + PointerCoords[] r = new PointerCoords[n]; + for (int i = 0; i < n; i++) { + r[i] = new PointerCoords(); + e.getPointerCoords(i, r[i]); + } + return r; + } + + private static float transformAngle(Matrix m, float angleRadians) { + // Construct and transform a vector oriented at the specified clockwise + // angle from vertical. Coordinate system: down is increasing Y, right is + // increasing X. + float[] v = new float[2]; + v[0] = FloatMath.sin(angleRadians); + v[1] = -FloatMath.cos(angleRadians); + m.mapVectors(v); + + // Derive the transformed vector's clockwise angle from vertical. + float result = (float) Math.atan2(v[0], -v[1]); + if (result < -Math.PI / 2) { + result += Math.PI; + } else if (result > Math.PI / 2) { + result -= Math.PI; + } + return result; + } +} diff --git a/src/com/android/gallery3d/util/Profile.java b/src/com/android/gallery3d/util/Profile.java new file mode 100644 index 000000000..7ed72c90e --- /dev/null +++ b/src/com/android/gallery3d/util/Profile.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2012 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.util; + +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Process; + +import java.util.ArrayList; +import java.util.Random; + +// The Profile class is used to collect profiling information for a thread. It +// samples stack traces for a thread periodically. enable() and disable() is +// used to enable and disable profiling for the calling thread. The profiling +// information can then be dumped to a file using the dumpToFile() method. +// +// The disableAll() method can be used to disable profiling for all threads and +// can be called in onPause() to ensure all profiling is disabled when an +// activity is paused. +public class Profile { + @SuppressWarnings("unused") + private static final String TAG = "Profile"; + private static final int NS_PER_MS = 1000000; + + // This is a watchdog entry for one thread. + // For every cycleTime period, we dump the stack of the thread. + private static class WatchEntry { + Thread thread; + + // Both are in milliseconds + int cycleTime; + int wakeTime; + + boolean isHolding; + ArrayList<String[]> holdingStacks = new ArrayList<String[]>(); + } + + // This is a watchdog thread which dumps stacks of other threads periodically. + private static Watchdog sWatchdog = new Watchdog(); + + private static class Watchdog { + private ArrayList<WatchEntry> mList = new ArrayList<WatchEntry>(); + private HandlerThread mHandlerThread; + private Handler mHandler; + private Runnable mProcessRunnable = new Runnable() { + @Override + public void run() { + synchronized (Watchdog.this) { + processList(); + } + } + }; + private Random mRandom = new Random(); + private ProfileData mProfileData = new ProfileData(); + + public Watchdog() { + mHandlerThread = new HandlerThread("Watchdog Handler", + Process.THREAD_PRIORITY_FOREGROUND); + mHandlerThread.start(); + mHandler = new Handler(mHandlerThread.getLooper()); + } + + public synchronized void addWatchEntry(Thread thread, int cycleTime) { + WatchEntry e = new WatchEntry(); + e.thread = thread; + e.cycleTime = cycleTime; + int firstDelay = 1 + mRandom.nextInt(cycleTime); + e.wakeTime = (int) (System.nanoTime() / NS_PER_MS) + firstDelay; + mList.add(e); + processList(); + } + + public synchronized void removeWatchEntry(Thread thread) { + for (int i = 0; i < mList.size(); i++) { + if (mList.get(i).thread == thread) { + mList.remove(i); + break; + } + } + processList(); + } + + public synchronized void removeAllWatchEntries() { + mList.clear(); + processList(); + } + + private void processList() { + mHandler.removeCallbacks(mProcessRunnable); + if (mList.size() == 0) return; + + int currentTime = (int) (System.nanoTime() / NS_PER_MS); + int nextWakeTime = 0; + + for (WatchEntry entry : mList) { + if (currentTime > entry.wakeTime) { + entry.wakeTime += entry.cycleTime; + Thread thread = entry.thread; + sampleStack(entry); + } + + if (entry.wakeTime > nextWakeTime) { + nextWakeTime = entry.wakeTime; + } + } + + long delay = nextWakeTime - currentTime; + mHandler.postDelayed(mProcessRunnable, delay); + } + + private void sampleStack(WatchEntry entry) { + Thread thread = entry.thread; + StackTraceElement[] stack = thread.getStackTrace(); + String[] lines = new String[stack.length]; + for (int i = 0; i < stack.length; i++) { + lines[i] = stack[i].toString(); + } + if (entry.isHolding) { + entry.holdingStacks.add(lines); + } else { + mProfileData.addSample(lines); + } + } + + private WatchEntry findEntry(Thread thread) { + for (int i = 0; i < mList.size(); i++) { + WatchEntry entry = mList.get(i); + if (entry.thread == thread) return entry; + } + return null; + } + + public synchronized void dumpToFile(String filename) { + mProfileData.dumpToFile(filename); + } + + public synchronized void reset() { + mProfileData.reset(); + } + + public synchronized void hold(Thread t) { + WatchEntry entry = findEntry(t); + + // This can happen if the profiling is disabled (probably from + // another thread). Same check is applied in commit() and drop() + // below. + if (entry == null) return; + + entry.isHolding = true; + } + + public synchronized void commit(Thread t) { + WatchEntry entry = findEntry(t); + if (entry == null) return; + ArrayList<String[]> stacks = entry.holdingStacks; + for (int i = 0; i < stacks.size(); i++) { + mProfileData.addSample(stacks.get(i)); + } + entry.isHolding = false; + entry.holdingStacks.clear(); + } + + public synchronized void drop(Thread t) { + WatchEntry entry = findEntry(t); + if (entry == null) return; + entry.isHolding = false; + entry.holdingStacks.clear(); + } + } + + // Enable profiling for the calling thread. Periodically (every cycleTimeInMs + // milliseconds) sample the stack trace of the calling thread. + public static void enable(int cycleTimeInMs) { + Thread t = Thread.currentThread(); + sWatchdog.addWatchEntry(t, cycleTimeInMs); + } + + // Disable profiling for the calling thread. + public static void disable() { + sWatchdog.removeWatchEntry(Thread.currentThread()); + } + + // Disable profiling for all threads. + public static void disableAll() { + sWatchdog.removeAllWatchEntries(); + } + + // Dump the profiling data to a file. + public static void dumpToFile(String filename) { + sWatchdog.dumpToFile(filename); + } + + // Reset the collected profiling data. + public static void reset() { + sWatchdog.reset(); + } + + // Hold the future samples coming from current thread until commit() or + // drop() is called, and those samples are recorded or ignored as a result. + // This must called after enable() to be effective. + public static void hold() { + sWatchdog.hold(Thread.currentThread()); + } + + public static void commit() { + sWatchdog.commit(Thread.currentThread()); + } + + public static void drop() { + sWatchdog.drop(Thread.currentThread()); + } +} diff --git a/src/com/android/gallery3d/util/ProfileData.java b/src/com/android/gallery3d/util/ProfileData.java new file mode 100644 index 000000000..a1bb8e1e4 --- /dev/null +++ b/src/com/android/gallery3d/util/ProfileData.java @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2012 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.util; + +import android.util.Log; + +import com.android.gallery3d.common.Utils; + +import java.io.DataOutputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map.Entry; + +// ProfileData keeps profiling samples in a tree structure. +// The addSample() method adds a sample. The dumpToFile() method saves the data +// to a file. The reset() method clears all samples. +public class ProfileData { + @SuppressWarnings("unused") + private static final String TAG = "ProfileData"; + + private static class Node { + public int id; // this is the name of this node, mapped from mNameToId + public Node parent; + public int sampleCount; + public ArrayList<Node> children; + public Node(Node parent, int id) { + this.parent = parent; + this.id = id; + } + } + + private Node mRoot; + private int mNextId; + private HashMap<String, Integer> mNameToId; + private DataOutputStream mOut; + private byte mScratch[] = new byte[4]; // scratch space for writeInt() + + public ProfileData() { + mRoot = new Node(null, -1); // The id of the root node is unused. + mNameToId = new HashMap<String, Integer>(); + } + + public void reset() { + mRoot = new Node(null, -1); + mNameToId.clear(); + mNextId = 0; + } + + private int nameToId(String name) { + Integer id = mNameToId.get(name); + if (id == null) { + id = ++mNextId; // The tool doesn't want id=0, so we start from 1. + mNameToId.put(name, id); + } + return id; + } + + public void addSample(String[] stack) { + int[] ids = new int[stack.length]; + for (int i = 0; i < stack.length; i++) { + ids[i] = nameToId(stack[i]); + } + + Node node = mRoot; + for (int i = stack.length - 1; i >= 0; i--) { + if (node.children == null) { + node.children = new ArrayList<Node>(); + } + + int id = ids[i]; + ArrayList<Node> children = node.children; + int j; + for (j = 0; j < children.size(); j++) { + if (children.get(j).id == id) break; + } + if (j == children.size()) { + children.add(new Node(node, id)); + } + + node = children.get(j); + } + + node.sampleCount++; + } + + public void dumpToFile(String filename) { + try { + mOut = new DataOutputStream(new FileOutputStream(filename)); + // Start record + writeInt(0); + writeInt(3); + writeInt(1); + writeInt(20000); // Sampling period: 20ms + writeInt(0); + + // Samples + writeAllStacks(mRoot, 0); + + // End record + writeInt(0); + writeInt(1); + writeInt(0); + writeAllSymbols(); + } catch (IOException ex) { + Log.w("Failed to dump to file", ex); + } finally { + Utils.closeSilently(mOut); + } + } + + // Writes out one stack, consisting of N+2 words: + // first word: sample count + // second word: depth of the stack (N) + // N words: each word is the id of one address in the stack + private void writeOneStack(Node node, int depth) throws IOException { + writeInt(node.sampleCount); + writeInt(depth); + while (depth-- > 0) { + writeInt(node.id); + node = node.parent; + } + } + + private void writeAllStacks(Node node, int depth) throws IOException { + if (node.sampleCount > 0) { + writeOneStack(node, depth); + } + + ArrayList<Node> children = node.children; + if (children != null) { + for (int i = 0; i < children.size(); i++) { + writeAllStacks(children.get(i), depth + 1); + } + } + } + + // Writes out the symbol table. Each line is like: + // 0x17e java.util.ArrayList.isEmpty(ArrayList.java:319) + private void writeAllSymbols() throws IOException { + for (Entry<String, Integer> entry : mNameToId.entrySet()) { + mOut.writeBytes(String.format("0x%x %s\n", entry.getValue(), entry.getKey())); + } + } + + private void writeInt(int v) throws IOException { + mScratch[0] = (byte) v; + mScratch[1] = (byte) (v >> 8); + mScratch[2] = (byte) (v >> 16); + mScratch[3] = (byte) (v >> 24); + mOut.write(mScratch); + } +} diff --git a/src/com/android/gallery3d/util/RangeArray.java b/src/com/android/gallery3d/util/RangeArray.java new file mode 100644 index 000000000..8e61348a3 --- /dev/null +++ b/src/com/android/gallery3d/util/RangeArray.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2012 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.util; + +// This is an array whose index ranges from min to max (inclusive). +public class RangeArray<T> { + private T[] mData; + private int mOffset; + + public RangeArray(int min, int max) { + mData = (T[]) new Object[max - min + 1]; + mOffset = min; + } + + // Wraps around an existing array + public RangeArray(T[] src, int min, int max) { + if (max - min + 1 != src.length) { + throw new AssertionError(); + } + mData = src; + mOffset = min; + } + + public void put(int i, T object) { + mData[i - mOffset] = object; + } + + public T get(int i) { + return mData[i - mOffset]; + } + + public int indexOf(T object) { + for (int i = 0; i < mData.length; i++) { + if (mData[i] == object) return i + mOffset; + } + return Integer.MAX_VALUE; + } +} diff --git a/src/com/android/gallery3d/util/RangeBoolArray.java b/src/com/android/gallery3d/util/RangeBoolArray.java new file mode 100644 index 000000000..035fc40a4 --- /dev/null +++ b/src/com/android/gallery3d/util/RangeBoolArray.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2012 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.util; + +// This is an array whose index ranges from min to max (inclusive). +public class RangeBoolArray { + private boolean[] mData; + private int mOffset; + + public RangeBoolArray(int min, int max) { + mData = new boolean[max - min + 1]; + mOffset = min; + } + + // Wraps around an existing array + public RangeBoolArray(boolean[] src, int min, int max) { + mData = src; + mOffset = min; + } + + public void put(int i, boolean object) { + mData[i - mOffset] = object; + } + + public boolean get(int i) { + return mData[i - mOffset]; + } + + public int indexOf(boolean object) { + for (int i = 0; i < mData.length; i++) { + if (mData[i] == object) return i + mOffset; + } + return Integer.MAX_VALUE; + } +} diff --git a/src/com/android/gallery3d/util/RangeIntArray.java b/src/com/android/gallery3d/util/RangeIntArray.java new file mode 100644 index 000000000..9dbb99fac --- /dev/null +++ b/src/com/android/gallery3d/util/RangeIntArray.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2012 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.util; + +// This is an array whose index ranges from min to max (inclusive). +public class RangeIntArray { + private int[] mData; + private int mOffset; + + public RangeIntArray(int min, int max) { + mData = new int[max - min + 1]; + mOffset = min; + } + + // Wraps around an existing array + public RangeIntArray(int[] src, int min, int max) { + mData = src; + mOffset = min; + } + + public void put(int i, int object) { + mData[i - mOffset] = object; + } + + public int get(int i) { + return mData[i - mOffset]; + } + + public int indexOf(int object) { + for (int i = 0; i < mData.length; i++) { + if (mData[i] == object) return i + mOffset; + } + return Integer.MAX_VALUE; + } +} diff --git a/src/com/android/gallery3d/util/ReverseGeocoder.java b/src/com/android/gallery3d/util/ReverseGeocoder.java new file mode 100644 index 000000000..a8b26d9b5 --- /dev/null +++ b/src/com/android/gallery3d/util/ReverseGeocoder.java @@ -0,0 +1,418 @@ +/* + * 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.util; + +import android.content.Context; +import android.location.Address; +import android.location.Geocoder; +import android.location.Location; +import android.location.LocationManager; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; + +import com.android.gallery3d.common.BlobCache; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.List; +import java.util.Locale; + +public class ReverseGeocoder { + @SuppressWarnings("unused") + private static final String TAG = "ReverseGeocoder"; + public static final int EARTH_RADIUS_METERS = 6378137; + public static final int LAT_MIN = -90; + public static final int LAT_MAX = 90; + public static final int LON_MIN = -180; + public static final int LON_MAX = 180; + private static final int MAX_COUNTRY_NAME_LENGTH = 8; + // If two points are within 20 miles of each other, use + // "Around Palo Alto, CA" or "Around Mountain View, CA". + // instead of directly jumping to the next level and saying + // "California, US". + private static final int MAX_LOCALITY_MILE_RANGE = 20; + + private static final String GEO_CACHE_FILE = "rev_geocoding"; + private static final int GEO_CACHE_MAX_ENTRIES = 1000; + private static final int GEO_CACHE_MAX_BYTES = 500 * 1024; + private static final int GEO_CACHE_VERSION = 0; + + public static class SetLatLong { + // The latitude and longitude of the min latitude point. + public double mMinLatLatitude = LAT_MAX; + public double mMinLatLongitude; + // The latitude and longitude of the max latitude point. + public double mMaxLatLatitude = LAT_MIN; + public double mMaxLatLongitude; + // The latitude and longitude of the min longitude point. + public double mMinLonLatitude; + public double mMinLonLongitude = LON_MAX; + // The latitude and longitude of the max longitude point. + public double mMaxLonLatitude; + public double mMaxLonLongitude = LON_MIN; + } + + private Context mContext; + private Geocoder mGeocoder; + private BlobCache mGeoCache; + private ConnectivityManager mConnectivityManager; + private static Address sCurrentAddress; // last known address + + public ReverseGeocoder(Context context) { + mContext = context; + mGeocoder = new Geocoder(mContext); + mGeoCache = CacheManager.getCache(context, GEO_CACHE_FILE, + GEO_CACHE_MAX_ENTRIES, GEO_CACHE_MAX_BYTES, + GEO_CACHE_VERSION); + mConnectivityManager = (ConnectivityManager) + context.getSystemService(Context.CONNECTIVITY_SERVICE); + } + + public String computeAddress(SetLatLong set) { + // The overall min and max latitudes and longitudes of the set. + double setMinLatitude = set.mMinLatLatitude; + double setMinLongitude = set.mMinLatLongitude; + double setMaxLatitude = set.mMaxLatLatitude; + double setMaxLongitude = set.mMaxLatLongitude; + if (Math.abs(set.mMaxLatLatitude - set.mMinLatLatitude) + < Math.abs(set.mMaxLonLongitude - set.mMinLonLongitude)) { + setMinLatitude = set.mMinLonLatitude; + setMinLongitude = set.mMinLonLongitude; + setMaxLatitude = set.mMaxLonLatitude; + setMaxLongitude = set.mMaxLonLongitude; + } + Address addr1 = lookupAddress(setMinLatitude, setMinLongitude, true); + Address addr2 = lookupAddress(setMaxLatitude, setMaxLongitude, true); + if (addr1 == null) + addr1 = addr2; + if (addr2 == null) + addr2 = addr1; + if (addr1 == null || addr2 == null) { + return null; + } + + // Get current location, we decide the granularity of the string based + // on this. + LocationManager locationManager = + (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE); + Location location = null; + List<String> providers = locationManager.getAllProviders(); + for (int i = 0; i < providers.size(); ++i) { + String provider = providers.get(i); + location = (provider != null) ? locationManager.getLastKnownLocation(provider) : null; + if (location != null) + break; + } + String currentCity = ""; + String currentAdminArea = ""; + String currentCountry = Locale.getDefault().getCountry(); + if (location != null) { + Address currentAddress = lookupAddress( + location.getLatitude(), location.getLongitude(), true); + if (currentAddress == null) { + currentAddress = sCurrentAddress; + } else { + sCurrentAddress = currentAddress; + } + if (currentAddress != null && currentAddress.getCountryCode() != null) { + currentCity = checkNull(currentAddress.getLocality()); + currentCountry = checkNull(currentAddress.getCountryCode()); + currentAdminArea = checkNull(currentAddress.getAdminArea()); + } + } + + String closestCommonLocation = null; + String addr1Locality = checkNull(addr1.getLocality()); + String addr2Locality = checkNull(addr2.getLocality()); + String addr1AdminArea = checkNull(addr1.getAdminArea()); + String addr2AdminArea = checkNull(addr2.getAdminArea()); + String addr1CountryCode = checkNull(addr1.getCountryCode()); + String addr2CountryCode = checkNull(addr2.getCountryCode()); + + if (currentCity.equals(addr1Locality) || currentCity.equals(addr2Locality)) { + String otherCity = currentCity; + if (currentCity.equals(addr1Locality)) { + otherCity = addr2Locality; + if (otherCity.length() == 0) { + otherCity = addr2AdminArea; + if (!currentCountry.equals(addr2CountryCode)) { + otherCity += " " + addr2CountryCode; + } + } + addr2Locality = addr1Locality; + addr2AdminArea = addr1AdminArea; + addr2CountryCode = addr1CountryCode; + } else { + otherCity = addr1Locality; + if (otherCity.length() == 0) { + otherCity = addr1AdminArea; + if (!currentCountry.equals(addr1CountryCode)) { + otherCity += " " + addr1CountryCode; + } + } + addr1Locality = addr2Locality; + addr1AdminArea = addr2AdminArea; + addr1CountryCode = addr2CountryCode; + } + closestCommonLocation = valueIfEqual(addr1.getAddressLine(0), addr2.getAddressLine(0)); + if (closestCommonLocation != null && !("null".equals(closestCommonLocation))) { + if (!currentCity.equals(otherCity)) { + closestCommonLocation += " - " + otherCity; + } + return closestCommonLocation; + } + + // Compare thoroughfare (street address) next. + closestCommonLocation = valueIfEqual(addr1.getThoroughfare(), addr2.getThoroughfare()); + if (closestCommonLocation != null && !("null".equals(closestCommonLocation))) { + return closestCommonLocation; + } + } + + // Compare the locality. + closestCommonLocation = valueIfEqual(addr1Locality, addr2Locality); + if (closestCommonLocation != null && !("".equals(closestCommonLocation))) { + String adminArea = addr1AdminArea; + String countryCode = addr1CountryCode; + if (adminArea != null && adminArea.length() > 0) { + if (!countryCode.equals(currentCountry)) { + closestCommonLocation += ", " + adminArea + " " + countryCode; + } else { + closestCommonLocation += ", " + adminArea; + } + } + return closestCommonLocation; + } + + // If the admin area is the same as the current location, we hide it and + // instead show the city name. + if (currentAdminArea.equals(addr1AdminArea) && currentAdminArea.equals(addr2AdminArea)) { + if ("".equals(addr1Locality)) { + addr1Locality = addr2Locality; + } + if ("".equals(addr2Locality)) { + addr2Locality = addr1Locality; + } + if (!"".equals(addr1Locality)) { + if (addr1Locality.equals(addr2Locality)) { + closestCommonLocation = addr1Locality + ", " + currentAdminArea; + } else { + closestCommonLocation = addr1Locality + " - " + addr2Locality; + } + return closestCommonLocation; + } + } + + // Just choose one of the localities if within a MAX_LOCALITY_MILE_RANGE + // mile radius. + float[] distanceFloat = new float[1]; + Location.distanceBetween(setMinLatitude, setMinLongitude, + setMaxLatitude, setMaxLongitude, distanceFloat); + int distance = (int) GalleryUtils.toMile(distanceFloat[0]); + if (distance < MAX_LOCALITY_MILE_RANGE) { + // Try each of the points and just return the first one to have a + // valid address. + closestCommonLocation = getLocalityAdminForAddress(addr1, true); + if (closestCommonLocation != null) { + return closestCommonLocation; + } + closestCommonLocation = getLocalityAdminForAddress(addr2, true); + if (closestCommonLocation != null) { + return closestCommonLocation; + } + } + + // Check the administrative area. + closestCommonLocation = valueIfEqual(addr1AdminArea, addr2AdminArea); + if (closestCommonLocation != null && !("".equals(closestCommonLocation))) { + String countryCode = addr1CountryCode; + if (!countryCode.equals(currentCountry)) { + if (countryCode != null && countryCode.length() > 0) { + closestCommonLocation += " " + countryCode; + } + } + return closestCommonLocation; + } + + // Check the country codes. + closestCommonLocation = valueIfEqual(addr1CountryCode, addr2CountryCode); + if (closestCommonLocation != null && !("".equals(closestCommonLocation))) { + return closestCommonLocation; + } + // There is no intersection, let's choose a nicer name. + String addr1Country = addr1.getCountryName(); + String addr2Country = addr2.getCountryName(); + if (addr1Country == null) + addr1Country = addr1CountryCode; + if (addr2Country == null) + addr2Country = addr2CountryCode; + if (addr1Country == null || addr2Country == null) + return null; + if (addr1Country.length() > MAX_COUNTRY_NAME_LENGTH || addr2Country.length() > MAX_COUNTRY_NAME_LENGTH) { + closestCommonLocation = addr1CountryCode + " - " + addr2CountryCode; + } else { + closestCommonLocation = addr1Country + " - " + addr2Country; + } + return closestCommonLocation; + } + + private String checkNull(String locality) { + if (locality == null) + return ""; + if (locality.equals("null")) + return ""; + return locality; + } + + private String getLocalityAdminForAddress(final Address addr, final boolean approxLocation) { + if (addr == null) + return ""; + String localityAdminStr = addr.getLocality(); + if (localityAdminStr != null && !("null".equals(localityAdminStr))) { + if (approxLocation) { + // TODO: Uncomment these lines as soon as we may translations + // for Res.string.around. + // localityAdminStr = + // mContext.getResources().getString(Res.string.around) + " " + + // localityAdminStr; + } + String adminArea = addr.getAdminArea(); + if (adminArea != null && adminArea.length() > 0) { + localityAdminStr += ", " + adminArea; + } + return localityAdminStr; + } + return null; + } + + public Address lookupAddress(final double latitude, final double longitude, + boolean useCache) { + try { + long locationKey = (long) (((latitude + LAT_MAX) * 2 * LAT_MAX + + (longitude + LON_MAX)) * EARTH_RADIUS_METERS); + byte[] cachedLocation = null; + if (useCache && mGeoCache != null) { + cachedLocation = mGeoCache.lookup(locationKey); + } + Address address = null; + NetworkInfo networkInfo = mConnectivityManager.getActiveNetworkInfo(); + if (cachedLocation == null || cachedLocation.length == 0) { + if (networkInfo == null || !networkInfo.isConnected()) { + return null; + } + List<Address> addresses = mGeocoder.getFromLocation(latitude, longitude, 1); + if (!addresses.isEmpty()) { + address = addresses.get(0); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + Locale locale = address.getLocale(); + writeUTF(dos, locale.getLanguage()); + writeUTF(dos, locale.getCountry()); + writeUTF(dos, locale.getVariant()); + + writeUTF(dos, address.getThoroughfare()); + int numAddressLines = address.getMaxAddressLineIndex(); + dos.writeInt(numAddressLines); + for (int i = 0; i < numAddressLines; ++i) { + writeUTF(dos, address.getAddressLine(i)); + } + writeUTF(dos, address.getFeatureName()); + writeUTF(dos, address.getLocality()); + writeUTF(dos, address.getAdminArea()); + writeUTF(dos, address.getSubAdminArea()); + + writeUTF(dos, address.getCountryName()); + writeUTF(dos, address.getCountryCode()); + writeUTF(dos, address.getPostalCode()); + writeUTF(dos, address.getPhone()); + writeUTF(dos, address.getUrl()); + + dos.flush(); + if (mGeoCache != null) { + mGeoCache.insert(locationKey, bos.toByteArray()); + } + dos.close(); + } + } else { + // Parsing the address from the byte stream. + DataInputStream dis = new DataInputStream( + new ByteArrayInputStream(cachedLocation)); + String language = readUTF(dis); + String country = readUTF(dis); + String variant = readUTF(dis); + Locale locale = null; + if (language != null) { + if (country == null) { + locale = new Locale(language); + } else if (variant == null) { + locale = new Locale(language, country); + } else { + locale = new Locale(language, country, variant); + } + } + if (!locale.getLanguage().equals(Locale.getDefault().getLanguage())) { + dis.close(); + return lookupAddress(latitude, longitude, false); + } + address = new Address(locale); + + address.setThoroughfare(readUTF(dis)); + int numAddressLines = dis.readInt(); + for (int i = 0; i < numAddressLines; ++i) { + address.setAddressLine(i, readUTF(dis)); + } + address.setFeatureName(readUTF(dis)); + address.setLocality(readUTF(dis)); + address.setAdminArea(readUTF(dis)); + address.setSubAdminArea(readUTF(dis)); + + address.setCountryName(readUTF(dis)); + address.setCountryCode(readUTF(dis)); + address.setPostalCode(readUTF(dis)); + address.setPhone(readUTF(dis)); + address.setUrl(readUTF(dis)); + dis.close(); + } + return address; + } catch (Exception e) { + // Ignore. + } + return null; + } + + private String valueIfEqual(String a, String b) { + return (a != null && b != null && a.equalsIgnoreCase(b)) ? a : null; + } + + public static final void writeUTF(DataOutputStream dos, String string) throws IOException { + if (string == null) { + dos.writeUTF(""); + } else { + dos.writeUTF(string); + } + } + + public static final String readUTF(DataInputStream dis) throws IOException { + String retVal = dis.readUTF(); + if (retVal.length() == 0) + return null; + return retVal; + } +} diff --git a/src/com/android/gallery3d/util/SaveVideoFileInfo.java b/src/com/android/gallery3d/util/SaveVideoFileInfo.java new file mode 100644 index 000000000..c7e5e8568 --- /dev/null +++ b/src/com/android/gallery3d/util/SaveVideoFileInfo.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2012 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.util; + +import java.io.File; + +public class SaveVideoFileInfo { + public File mFile = null; + public String mFileName = null; + // This the full directory path. + public File mDirectory = null; + // This is just the folder's name. + public String mFolderName = null; + +} diff --git a/src/com/android/gallery3d/util/SaveVideoFileUtils.java b/src/com/android/gallery3d/util/SaveVideoFileUtils.java new file mode 100644 index 000000000..10c41de90 --- /dev/null +++ b/src/com/android/gallery3d/util/SaveVideoFileUtils.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2012 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.util; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.database.Cursor; +import android.media.MediaMetadataRetriever; +import android.net.Uri; +import android.os.Environment; +import android.provider.MediaStore.Video; +import android.provider.MediaStore.Video.VideoColumns; + +import com.android.gallery3d.filtershow.tools.SaveImage.ContentResolverQueryCallback; + +import java.io.File; +import java.sql.Date; +import java.text.SimpleDateFormat; + +public class SaveVideoFileUtils { + // This function can decide which folder to save the video file, and generate + // the needed information for the video file including filename. + public static SaveVideoFileInfo getDstMp4FileInfo(String fileNameFormat, + ContentResolver contentResolver, Uri uri, String defaultFolderName) { + SaveVideoFileInfo dstFileInfo = new SaveVideoFileInfo(); + // Use the default save directory if the source directory cannot be + // saved. + dstFileInfo.mDirectory = getSaveDirectory(contentResolver, uri); + if ((dstFileInfo.mDirectory == null) || !dstFileInfo.mDirectory.canWrite()) { + dstFileInfo.mDirectory = new File(Environment.getExternalStorageDirectory(), + BucketNames.DOWNLOAD); + dstFileInfo.mFolderName = defaultFolderName; + } else { + dstFileInfo.mFolderName = dstFileInfo.mDirectory.getName(); + } + dstFileInfo.mFileName = new SimpleDateFormat(fileNameFormat).format( + new Date(System.currentTimeMillis())); + + dstFileInfo.mFile = new File(dstFileInfo.mDirectory, dstFileInfo.mFileName + ".mp4"); + return dstFileInfo; + } + + private static void querySource(ContentResolver contentResolver, Uri uri, + String[] projection, ContentResolverQueryCallback callback) { + Cursor cursor = null; + try { + cursor = contentResolver.query(uri, projection, null, null, null); + if ((cursor != null) && cursor.moveToNext()) { + callback.onCursorResult(cursor); + } + } catch (Exception e) { + // Ignore error for lacking the data column from the source. + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + + private static File getSaveDirectory(ContentResolver contentResolver, Uri uri) { + final File[] dir = new File[1]; + querySource(contentResolver, uri, + new String[] { VideoColumns.DATA }, + new ContentResolverQueryCallback() { + @Override + public void onCursorResult(Cursor cursor) { + dir[0] = new File(cursor.getString(0)).getParentFile(); + } + }); + return dir[0]; + } + + + /** + * Insert the content (saved file) with proper video properties. + */ + public static Uri insertContent(SaveVideoFileInfo mDstFileInfo, + ContentResolver contentResolver, Uri uri ) { + long nowInMs = System.currentTimeMillis(); + long nowInSec = nowInMs / 1000; + final ContentValues values = new ContentValues(13); + values.put(Video.Media.TITLE, mDstFileInfo.mFileName); + values.put(Video.Media.DISPLAY_NAME, mDstFileInfo.mFile.getName()); + values.put(Video.Media.MIME_TYPE, "video/mp4"); + values.put(Video.Media.DATE_TAKEN, nowInMs); + values.put(Video.Media.DATE_MODIFIED, nowInSec); + values.put(Video.Media.DATE_ADDED, nowInSec); + values.put(Video.Media.DATA, mDstFileInfo.mFile.getAbsolutePath()); + values.put(Video.Media.SIZE, mDstFileInfo.mFile.length()); + int durationMs = retriveVideoDurationMs(mDstFileInfo.mFile.getPath()); + values.put(Video.Media.DURATION, durationMs); + // Copy the data taken and location info from src. + String[] projection = new String[] { + VideoColumns.DATE_TAKEN, + VideoColumns.LATITUDE, + VideoColumns.LONGITUDE, + VideoColumns.RESOLUTION, + }; + + // Copy some info from the source file. + querySource(contentResolver, uri, projection, + new ContentResolverQueryCallback() { + @Override + public void onCursorResult(Cursor cursor) { + long timeTaken = cursor.getLong(0); + if (timeTaken > 0) { + values.put(Video.Media.DATE_TAKEN, timeTaken); + } + double latitude = cursor.getDouble(1); + double longitude = cursor.getDouble(2); + // TODO: Change || to && after the default location + // issue is + // fixed. + if ((latitude != 0f) || (longitude != 0f)) { + values.put(Video.Media.LATITUDE, latitude); + values.put(Video.Media.LONGITUDE, longitude); + } + values.put(Video.Media.RESOLUTION, cursor.getString(3)); + + } + }); + + return contentResolver.insert(Video.Media.EXTERNAL_CONTENT_URI, values); + } + + public static int retriveVideoDurationMs(String path) { + int durationMs = 0; + // Calculate the duration of the destination file. + MediaMetadataRetriever retriever = new MediaMetadataRetriever(); + retriever.setDataSource(path); + String duration = retriever.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_DURATION); + if (duration != null) { + durationMs = Integer.parseInt(duration); + } + retriever.release(); + return durationMs; + } + +} diff --git a/src/com/android/gallery3d/util/UpdateHelper.java b/src/com/android/gallery3d/util/UpdateHelper.java new file mode 100644 index 000000000..f76705d06 --- /dev/null +++ b/src/com/android/gallery3d/util/UpdateHelper.java @@ -0,0 +1,59 @@ +/* + * 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.util; + +import com.android.gallery3d.common.Utils; + +public class UpdateHelper { + + private boolean mUpdated = false; + + public int update(int original, int update) { + if (original != update) { + mUpdated = true; + original = update; + } + return original; + } + + public long update(long original, long update) { + if (original != update) { + mUpdated = true; + original = update; + } + return original; + } + + public double update(double original, double update) { + if (original != update) { + mUpdated = true; + original = update; + } + return original; + } + + public <T> T update(T original, T update) { + if (!Utils.equals(original, update)) { + mUpdated = true; + original = update; + } + return original; + } + + public boolean isUpdated() { + return mUpdated; + } +} |