Merge "Added "discard unsaved changes" behavior for exiting." into gb-ub-photos-bryce
diff --git a/src/com/android/gallery3d/app/GalleryAppImpl.java b/src/com/android/gallery3d/app/GalleryAppImpl.java
index 5b4a872..2abdaa0 100644
--- a/src/com/android/gallery3d/app/GalleryAppImpl.java
+++ b/src/com/android/gallery3d/app/GalleryAppImpl.java
@@ -17,12 +17,9 @@
package com.android.gallery3d.app;
import android.app.Application;
-import android.content.ComponentName;
import android.content.Context;
-import android.content.pm.PackageManager;
import android.os.AsyncTask;
-import com.android.gallery3d.common.ApiHelper;
import com.android.gallery3d.data.DataManager;
import com.android.gallery3d.data.DownloadCache;
import com.android.gallery3d.data.ImageCacheService;
@@ -32,6 +29,7 @@
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;
@@ -56,6 +54,7 @@
WidgetUtils.initialize(this);
PicasaSource.initialize(this);
UsageStatistics.initialize(this);
+ MediaCache.initialize(this);
mStitchingProgressManager = LightCycleHelper.createStitchingManagerInstance(this);
if (mStitchingProgressManager != null) {
diff --git a/src/com/android/gallery3d/filtershow/PanelController.java b/src/com/android/gallery3d/filtershow/PanelController.java
index 8352032..6b20fe1 100644
--- a/src/com/android/gallery3d/filtershow/PanelController.java
+++ b/src/com/android/gallery3d/filtershow/PanelController.java
@@ -151,7 +151,7 @@
private final View mView;
private final LinearLayout mAccessoryViewList;
private Vector<View> mAccessoryViews = new Vector<View>();
- private final TextView mTextView;
+ private final Button mTextView;
private boolean mSelected = false;
private String mEffectName = null;
private int mParameterValue = 0;
@@ -160,10 +160,9 @@
public UtilityPanel(Context context, View utilityPanel) {
mView = utilityPanel;
View accessoryViewList = mView.findViewById(R.id.panelAccessoryViewList);
- Button textView = (Button) mView.findViewById(R.id.applyEffect);
+ mTextView = (Button) mView.findViewById(R.id.applyEffect);
mContext = context;
mAccessoryViewList = (LinearLayout) accessoryViewList;
- mTextView = (TextView) textView;
}
public boolean selected() {
@@ -212,6 +211,16 @@
public View getEditControl() {
return mView.findViewById(R.id.controlArea);
}
+
+ public void removeControlChildren() {
+ LinearLayout controlArea = (LinearLayout) mView.findViewById(R.id.controlArea);
+ controlArea.removeAllViews();
+ }
+
+ public Button getEditTitle() {
+ return mTextView;
+ }
+
public void updateText() {
String s;
if (mCurrentEditor == null) {
@@ -589,8 +598,10 @@
if (mEditorPlaceHolder.contains(representation.getEditorId())) {
mCurrentEditor = mEditorPlaceHolder.showEditor(
representation.getEditorId());
- mCurrentEditor.setUtilityPanelUI(
- mUtilityPanel.getActionControl(), mUtilityPanel.getEditControl());
+ mUtilityPanel.removeControlChildren();
+ mCurrentEditor.setUpEditorUI(
+ mUtilityPanel.getActionControl(), mUtilityPanel.getEditControl(),
+ mUtilityPanel.getEditTitle());
mCurrentImage = mCurrentEditor.getImageShow();
mCurrentEditor.setPanelController(this);
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 0000000..6ed2467
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/controller/ActionSlider.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.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 mActionButton;
+ public ActionSlider() {
+ mLayoutID = R.layout.filtershow_control_action_slider;
+ }
+
+ @Override
+ public void setUp(ViewGroup container, Parameter parameter, Editor editor) {
+ super.setUp(container, parameter, editor);
+ mActionButton = (ImageButton) mTopView.findViewById(R.id.actionButton);
+ mActionButton.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ ((ParameterActionAndInt) mParameter).fireAction();
+ }
+ });
+ }
+
+ @Override
+ public void updateUI() {
+ super.updateUI();
+ if (mActionButton != null) {
+ int iconId = ((ParameterActionAndInt) mParameter).getActionIcon();
+ mActionButton.setImageResource(iconId);
+ }
+ }
+}
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 0000000..c54fe77
--- /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) {
+ 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 0000000..4342290
--- /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/Parameter.java b/src/com/android/gallery3d/filtershow/controller/Parameter.java
new file mode 100644
index 0000000..1e8694a
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/controller/Parameter.java
@@ -0,0 +1,27 @@
+/*
+ * 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 Parameter {
+ String getParameterName();
+
+ String getParameterType();
+
+ String getValueString();
+
+ public void setController(Control c);
+}
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 0000000..04567e2
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/controller/ParameterActionAndInt.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;
+
+public interface ParameterActionAndInt extends ParameterInteger {
+ static String sParameterType = "ParameterActionAndInt";
+
+ public void fireAction();
+
+ public int getActionIcon();
+}
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 0000000..0bfd201
--- /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/TitledSlider.java b/src/com/android/gallery3d/filtershow/controller/TitledSlider.java
new file mode 100644
index 0000000..30b6fdb
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/controller/TitledSlider.java
@@ -0,0 +1,105 @@
+/*
+ * 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) {
+ 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) {
+ mControlName.setText(mParameter.getParameterName());
+ }
+ 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/editors/Editor.java b/src/com/android/gallery3d/filtershow/editors/Editor.java
index 69266ff..013590c 100644
--- a/src/com/android/gallery3d/filtershow/editors/Editor.java
+++ b/src/com/android/gallery3d/filtershow/editors/Editor.java
@@ -17,18 +17,22 @@
package com.android.gallery3d.filtershow.editors;
import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.text.Html;
+import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.*;
+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.PanelController;
import com.android.gallery3d.filtershow.cache.ImageLoader;
+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;
@@ -43,6 +47,7 @@
protected ImageShow mImageShow;
protected FrameLayout mFrameLayout;
protected SeekBar mSeekBar;
+ Button mEditTitle;
protected PanelController mPanelController;
protected int mID;
private final String LOGTAG = "Editor";
@@ -57,7 +62,7 @@
}
public String calculateUserMessage(Context context, String effectName, Object parameterValue) {
- String apply = context.getString(R.string.apply_effect);
+ String apply = "";
if (mShowParameter == SHOW_VALUE_INT) {
apply += " " + effectName + " " + parameterValue;
} else {
@@ -83,6 +88,12 @@
return true;
}
+ public void setUpEditorUI(View actionButton, View editControl, Button editTitle) {
+ this.mEditTitle = editTitle;
+ setMenuIcon(true);
+ setUtilityPanelUI(actionButton, editControl);
+ }
+
public boolean showsPopupIndicator() {
return true;
}
@@ -92,17 +103,28 @@
* @param editControl this is the black area for sliders etc
*/
public void setUtilityPanelUI(View actionButton, View editControl) {
- mSeekBar = (SeekBar) editControl.findViewById(R.id.primarySeekBar);
+
+ 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);
}
+
Button button = (Button) actionButton.findViewById(R.id.applyEffect);
if (button != null) {
if (showsPopupIndicator()) {
- button.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.filtershow_menu_marker, 0);
+ button.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0,
+ R.drawable.filtershow_menu_marker, 0);
} else {
button.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0);
}
@@ -205,19 +227,29 @@
}
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) {
diff --git a/src/com/android/gallery3d/filtershow/editors/EditorVignette.java b/src/com/android/gallery3d/filtershow/editors/EditorVignette.java
index a7d99e4..7127b21 100644
--- a/src/com/android/gallery3d/filtershow/editors/EditorVignette.java
+++ b/src/com/android/gallery3d/filtershow/editors/EditorVignette.java
@@ -24,7 +24,7 @@
import com.android.gallery3d.filtershow.filters.FilterVignetteRepresentation;
import com.android.gallery3d.filtershow.imageshow.ImageVignette;
-public class EditorVignette extends BasicEditor {
+public class EditorVignette extends ParametricEditor {
public static final int ID = R.id.vignetteEditor;
private static final String LOGTAG = "EditorVignettePlanet";
ImageVignette mImageVignette;
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 0000000..cf00f4a
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/ParametricEditor.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.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.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;
+
+ 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);
+ }
+
+ 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 + " " + interval.getStateRepresentation();
+ } else {
+ apply += " " + effectName + " " + parameterValue;
+ }
+ } else {
+ apply += " " + effectName;
+ }
+ 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 when 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;
+ }
+
+ @Override
+ public void setUtilityPanelUI(View actionButton, View editControl) {
+ FilterRepresentation rep = getLocalRepresentation();
+ if (this instanceof Parameter) {
+ control((Parameter) this, editControl);
+ } else if (rep instanceof Parameter) {
+ control((Parameter) rep, 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();
+ 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 commitLocalRepresentation() {
+ super.commitLocalRepresentation();
+ FilterRepresentation rep = getLocalRepresentation();
+ }
+
+ @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/filters/FilterBasicRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterBasicRepresentation.java
index 2410ebe..34323c4 100644
--- a/src/com/android/gallery3d/filtershow/filters/FilterBasicRepresentation.java
+++ b/src/com/android/gallery3d/filtershow/filters/FilterBasicRepresentation.java
@@ -17,8 +17,10 @@
package com.android.gallery3d.filtershow.filters;
import com.android.gallery3d.app.Log;
+import com.android.gallery3d.filtershow.controller.Control;
+import com.android.gallery3d.filtershow.controller.ParameterInteger;
-public class FilterBasicRepresentation extends FilterRepresentation {
+public class FilterBasicRepresentation extends FilterRepresentation implements ParameterInteger {
private static final String LOGTAG = "FilterBasicRepresentation";
private int mMinimum;
private int mValue;
@@ -33,6 +35,7 @@
setValue(value);
}
+ @Override
public String toString() {
return getName() + " : " + mMinimum + " < " + mValue + " < " + mMaximum;
}
@@ -47,6 +50,7 @@
return representation;
}
+ @Override
public void useParametersFrom(FilterRepresentation a) {
if (a instanceof FilterBasicRepresentation) {
FilterBasicRepresentation representation = (FilterBasicRepresentation) a;
@@ -76,6 +80,7 @@
return false;
}
+ @Override
public int getMinimum() {
return mMinimum;
}
@@ -84,10 +89,12 @@
mMinimum = minimum;
}
+ @Override
public int getValue() {
return mValue;
}
+ @Override
public void setValue(int value) {
mValue = value;
if (mValue < mMinimum) {
@@ -98,6 +105,7 @@
}
}
+ @Override
public int getMaximum() {
return mMaximum;
}
@@ -110,6 +118,7 @@
mDefaultValue = defaultValue;
}
+ @Override
public int getDefaultValue() {
return mDefaultValue;
}
@@ -122,7 +131,27 @@
mPreviewValue = previewValue;
}
+ @Override
public String getStateRepresentation() {
return "" + getValue();
}
+
+ @Override
+ public String getParameterType(){
+ return sParameterType;
+ }
+
+ @Override
+ public void setController(Control control) {
+ }
+
+ @Override
+ public String getValueString() {
+ return getStateRepresentation();
+ }
+
+ @Override
+ public String getParameterName() {
+ return getName();
+ }
}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageVignette.java b/src/com/android/gallery3d/filtershow/imageshow/ImageVignette.java
index 1149263..7ce9e51 100644
--- a/src/com/android/gallery3d/filtershow/imageshow/ImageVignette.java
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageVignette.java
@@ -65,10 +65,6 @@
mActiveHandle = -1;
break;
case MotionEvent.ACTION_DOWN:
- if (event.getPointerCount() == 1) {
- Log.v(LOGTAG, "################### ACTION_DOWN odd " + mActiveHandle
- + " touches=1");
- }
break;
}
}
diff --git a/src/com/android/photos/data/FileRetriever.java b/src/com/android/photos/data/FileRetriever.java
new file mode 100644
index 0000000..eb7686e
--- /dev/null
+++ b/src/com/android/photos/data/FileRetriever.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.photos.data;
+
+import android.graphics.Bitmap;
+import android.media.ExifInterface;
+import android.net.Uri;
+import android.util.Log;
+import android.webkit.MimeTypeMap;
+
+import com.android.gallery3d.common.BitmapUtils;
+
+import java.io.File;
+import java.io.IOException;
+
+public class FileRetriever implements MediaRetriever {
+ private static final String TAG = FileRetriever.class.getSimpleName();
+
+ @Override
+ public File getLocalFile(Uri contentUri) {
+ return new File(contentUri.getPath());
+ }
+
+ @Override
+ public MediaSize getFastImageSize(Uri contentUri, MediaSize size) {
+ if (isVideo(contentUri)) {
+ return null;
+ }
+ return MediaSize.TemporaryThumbnail;
+ }
+
+ @Override
+ public byte[] getTemporaryImage(Uri contentUri, MediaSize fastImageSize) {
+
+ try {
+ ExifInterface exif = new ExifInterface(contentUri.getPath());
+ if (exif.hasThumbnail()) {
+ return exif.getThumbnail();
+ }
+ } catch (IOException e) {
+ Log.w(TAG, "Unable to load exif for " + contentUri);
+ }
+ return null;
+ }
+
+ @Override
+ public boolean getMedia(Uri contentUri, MediaSize imageSize, File tempFile) {
+ if (imageSize == MediaSize.Original) {
+ return false; // getLocalFile should always return the original.
+ }
+ if (imageSize == MediaSize.Thumbnail) {
+ File preview = MediaCache.getInstance().getCachedFile(contentUri, MediaSize.Preview);
+ if (preview != null) {
+ // Just downsample the preview, it is faster.
+ return MediaCacheUtils.downsample(preview, imageSize, tempFile);
+ }
+ }
+ File highRes = new File(contentUri.getPath());
+ boolean success;
+ if (!isVideo(contentUri)) {
+ success = MediaCacheUtils.downsample(highRes, imageSize, tempFile);
+ } else {
+ // Video needs to extract the bitmap.
+ Bitmap bitmap = BitmapUtils.createVideoThumbnail(highRes.getPath());
+ if (bitmap == null) {
+ return false;
+ } else if (imageSize == MediaSize.Thumbnail
+ && !MediaCacheUtils.needsDownsample(bitmap, MediaSize.Preview)
+ && MediaCacheUtils.writeToFile(bitmap, tempFile)) {
+ // Opportunistically save preview
+ MediaCache mediaCache = MediaCache.getInstance();
+ mediaCache.insertIntoCache(contentUri, MediaSize.Preview, tempFile);
+ }
+ // Now scale the image
+ success = MediaCacheUtils.downsample(bitmap, imageSize, tempFile);
+ }
+ return success;
+ }
+
+ @Override
+ public Uri normalizeUri(Uri contentUri, MediaSize size) {
+ return contentUri;
+ }
+
+ @Override
+ public MediaSize normalizeMediaSize(Uri contentUri, MediaSize size) {
+ return size;
+ }
+
+ private static boolean isVideo(Uri uri) {
+ MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
+ String extension = MimeTypeMap.getFileExtensionFromUrl(uri.toString());
+ String mimeType = mimeTypeMap.getMimeTypeFromExtension(extension);
+ return (mimeType != null && mimeType.startsWith("video/"));
+ }
+}
diff --git a/src/com/android/photos/data/MediaCache.java b/src/com/android/photos/data/MediaCache.java
new file mode 100644
index 0000000..7b5eca5
--- /dev/null
+++ b/src/com/android/photos/data/MediaCache.java
@@ -0,0 +1,649 @@
+/*
+ * 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.photos.data;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.os.Environment;
+import android.util.Log;
+
+import com.android.photos.data.MediaCacheDatabase.Action;
+import com.android.photos.data.MediaRetriever.MediaSize;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+
+/**
+ * MediaCache keeps a cache of images, videos, thumbnails and previews. Calls to
+ * retrieve a specific media item are executed asynchronously. The caller has an
+ * option to receive a notification for lower resolution images that happen to
+ * be available prior to the one requested.
+ * <p>
+ * When an media item has been retrieved, the notification for it is called on a
+ * separate notifier thread. This thread should not be held for a long time so
+ * that other notifications may happen.
+ * </p>
+ * <p>
+ * Media items are uniquely identified by their content URIs. Each
+ * scheme/authority can offer its own MediaRetriever, running in its own thread.
+ * </p>
+ * <p>
+ * The MediaCache is an LRU cache, but does not allow the thumbnail cache to
+ * drop below a minimum size. This prevents browsing through original images to
+ * wipe out the thumbnails.
+ * </p>
+ */
+public class MediaCache {
+ static final String TAG = MediaCache.class.getSimpleName();
+ /** Subdirectory containing the image cache. */
+ static final String IMAGE_CACHE_SUBDIR = "image_cache";
+ /** File name extension to use for cached images. */
+ static final String IMAGE_EXTENSION = ".cache";
+ /** File name extension to use for temporary cached images while retrieving. */
+ static final String TEMP_IMAGE_EXTENSION = ".temp";
+
+ public static interface ImageReady {
+ void imageReady(InputStream bitmapInputStream);
+ }
+
+ public static interface OriginalReady {
+ void originalReady(File originalFile);
+ }
+
+ /** A Thread for each MediaRetriever */
+ private class ProcessQueue extends Thread {
+ private Queue<ProcessingJob> mQueue;
+
+ public ProcessQueue(Queue<ProcessingJob> queue) {
+ mQueue = queue;
+ }
+
+ @Override
+ public void run() {
+ while (mRunning) {
+ ProcessingJob status;
+ synchronized (mQueue) {
+ while (mQueue.isEmpty()) {
+ try {
+ mQueue.wait();
+ } catch (InterruptedException e) {
+ if (!mRunning) {
+ return;
+ }
+ Log.w(TAG, "Unexpected interruption", e);
+ }
+ }
+ status = mQueue.remove();
+ }
+ processTask(status);
+ }
+ }
+ };
+
+ private interface NotifyReady {
+ void notifyReady();
+
+ void setFile(File file) throws FileNotFoundException;
+ }
+
+ private static class NotifyOriginalReady implements NotifyReady {
+ private final OriginalReady mCallback;
+ private File mFile;
+
+ public NotifyOriginalReady(OriginalReady callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ public void notifyReady() {
+ mCallback.originalReady(mFile);
+ }
+
+ @Override
+ public void setFile(File file) {
+ mFile = file;
+ }
+ }
+
+ private static class NotifyImageReady implements NotifyReady {
+ private final ImageReady mCallback;
+ private InputStream mInputStream;
+
+ public NotifyImageReady(ImageReady callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ public void notifyReady() {
+ mCallback.imageReady(mInputStream);
+ }
+
+ @Override
+ public void setFile(File file) throws FileNotFoundException {
+ mInputStream = new FileInputStream(file);
+ }
+
+ public void setBytes(byte[] bytes) {
+ mInputStream = new ByteArrayInputStream(bytes);
+ }
+ }
+
+ /** A media item to be retrieved and its notifications. */
+ private static class ProcessingJob {
+ public ProcessingJob(Uri uri, MediaSize size, NotifyReady complete,
+ NotifyImageReady lowResolution) {
+ this.contentUri = uri;
+ this.size = size;
+ this.complete = complete;
+ this.lowResolution = lowResolution;
+ }
+ public Uri contentUri;
+ public MediaSize size;
+ public NotifyImageReady lowResolution;
+ public NotifyReady complete;
+ }
+
+ private boolean mRunning = true;
+ private static MediaCache sInstance;
+ private File mCacheDir;
+ private Context mContext;
+ private Queue<NotifyReady> mCallbacks = new LinkedList<NotifyReady>();
+ private Map<String, MediaRetriever> mRetrievers = new HashMap<String, MediaRetriever>();
+ private Map<String, List<ProcessingJob>> mTasks = new HashMap<String, List<ProcessingJob>>();
+ private List<ProcessQueue> mProcessingThreads = new ArrayList<ProcessQueue>();
+ private MediaCacheDatabase mDatabaseHelper;
+ private long mTempImageNumber = 1;
+ private Object mTempImageNumberLock = new Object();
+
+ private long mMaxCacheSize = 40 * 1024 * 1024; // 40 MB
+ private long mMinThumbCacheSize = 4 * 1024 * 1024; // 4 MB
+ private long mCacheSize = -1;
+ private long mThumbCacheSize = -1;
+ private Object mCacheSizeLock = new Object();
+
+ private Action mNotifyCachedLowResolution = new Action() {
+ @Override
+ public void execute(Uri uri, long id, MediaSize size, Object parameter) {
+ ProcessingJob job = (ProcessingJob) parameter;
+ File file = createCacheImagePath(id);
+ addNotification(job.lowResolution, file);
+ }
+ };
+
+ private Action mMoveTempToCache = new Action() {
+ @Override
+ public void execute(Uri uri, long id, MediaSize size, Object parameter) {
+ File tempFile = (File) parameter;
+ File cacheFile = createCacheImagePath(id);
+ tempFile.renameTo(cacheFile);
+ }
+ };
+
+ private Action mDeleteFile = new Action() {
+ @Override
+ public void execute(Uri uri, long id, MediaSize size, Object parameter) {
+ File file = createCacheImagePath(id);
+ file.delete();
+ synchronized (mCacheSizeLock) {
+ if (mCacheSize != -1) {
+ long length = (Long) parameter;
+ mCacheSize -= length;
+ if (size == MediaSize.Thumbnail) {
+ mThumbCacheSize -= length;
+ }
+ }
+ }
+ }
+ };
+
+ /** The thread used to make ImageReady and OriginalReady callbacks. */
+ private Thread mProcessNotifications = new Thread() {
+ @Override
+ public void run() {
+ while (mRunning) {
+ NotifyReady notifyImage;
+ synchronized (mCallbacks) {
+ while (mCallbacks.isEmpty()) {
+ try {
+ mCallbacks.wait();
+ } catch (InterruptedException e) {
+ if (!mRunning) {
+ return;
+ }
+ Log.w(TAG, "Unexpected Interruption, continuing");
+ }
+ }
+ notifyImage = mCallbacks.remove();
+ }
+
+ notifyImage.notifyReady();
+ }
+ }
+ };
+
+ public static synchronized void initialize(Context context) {
+ if (sInstance == null) {
+ sInstance = new MediaCache(context);
+ MediaCacheUtils.initialize(context);
+ }
+ }
+
+ public static MediaCache getInstance() {
+ return sInstance;
+ }
+
+ public static synchronized void shutdown() {
+ sInstance.mRunning = false;
+ sInstance.mProcessNotifications.interrupt();
+ for (ProcessQueue processingThread : sInstance.mProcessingThreads) {
+ processingThread.interrupt();
+ }
+ sInstance = null;
+ }
+
+ private MediaCache(Context context) {
+ mDatabaseHelper = new MediaCacheDatabase(context);
+ mProcessNotifications.start();
+ mContext = context;
+ }
+
+ // This is used for testing.
+ public void setCacheDir(File cacheDir) {
+ cacheDir.mkdirs();
+ mCacheDir = cacheDir;
+ }
+
+ private File getCacheDir() {
+ synchronized (mContext) {
+ if (mCacheDir == null) {
+ String state = Environment.getExternalStorageState();
+ File baseDir;
+ if (Environment.MEDIA_MOUNTED.equals(state)) {
+ baseDir = mContext.getExternalCacheDir();
+ } else {
+ // Stored in internal cache
+ baseDir = mContext.getCacheDir();
+ }
+ mCacheDir = new File(baseDir, IMAGE_CACHE_SUBDIR);
+ mCacheDir.mkdirs();
+ }
+ return mCacheDir;
+ }
+ }
+
+ /**
+ * Invalidates all cached images related to a given contentUri. This call
+ * doesn't complete until the images have been removed from the cache.
+ */
+ public void invalidate(Uri contentUri) {
+ mDatabaseHelper.delete(contentUri, mDeleteFile);
+ }
+
+ public void clearCacheDir() {
+ File[] cachedFiles = getCacheDir().listFiles();
+ if (cachedFiles != null) {
+ for (File cachedFile : cachedFiles) {
+ cachedFile.delete();
+ }
+ }
+ }
+
+ /**
+ * Add a MediaRetriever for a Uri scheme and authority. This MediaRetriever
+ * will be granted its own thread for retrieving images.
+ */
+ public void addRetriever(String scheme, String authority, MediaRetriever retriever) {
+ String differentiator = getDifferentiator(scheme, authority);
+ synchronized (mRetrievers) {
+ mRetrievers.put(differentiator, retriever);
+ }
+ synchronized (mTasks) {
+ LinkedList<ProcessingJob> queue = new LinkedList<ProcessingJob>();
+ mTasks.put(differentiator, queue);
+ new ProcessQueue(queue).start();
+ }
+ }
+
+ /**
+ * Retrieves a thumbnail. complete will be called when the thumbnail is
+ * available. If lowResolution is not null and a lower resolution thumbnail
+ * is available before the thumbnail, lowResolution will be called prior to
+ * complete. All callbacks will be made on a thread other than the calling
+ * thread.
+ *
+ * @param contentUri The URI for the full resolution image to search for.
+ * @param complete Callback for when the image has been retrieved.
+ * @param lowResolution If not null and a lower resolution image is
+ * available prior to retrieving the thumbnail, this will be
+ * called with the low resolution bitmap.
+ */
+ public void retrieveThumbnail(Uri contentUri, ImageReady complete, ImageReady lowResolution) {
+ addTask(contentUri, complete, lowResolution, MediaSize.Thumbnail);
+ }
+
+ /**
+ * Retrieves a preview. complete will be called when the preview is
+ * available. If lowResolution is not null and a lower resolution preview is
+ * available before the preview, lowResolution will be called prior to
+ * complete. All callbacks will be made on a thread other than the calling
+ * thread.
+ *
+ * @param contentUri The URI for the full resolution image to search for.
+ * @param complete Callback for when the image has been retrieved.
+ * @param lowResolution If not null and a lower resolution image is
+ * available prior to retrieving the preview, this will be called
+ * with the low resolution bitmap.
+ */
+ public void retrievePreview(Uri contentUri, ImageReady complete, ImageReady lowResolution) {
+ addTask(contentUri, complete, lowResolution, MediaSize.Preview);
+ }
+
+ /**
+ * Retrieves the original image or video. complete will be called when the
+ * media is available on the local file system. If lowResolution is not null
+ * and a lower resolution preview is available before the original,
+ * lowResolution will be called prior to complete. All callbacks will be
+ * made on a thread other than the calling thread.
+ *
+ * @param contentUri The URI for the full resolution image to search for.
+ * @param complete Callback for when the image has been retrieved.
+ * @param lowResolution If not null and a lower resolution image is
+ * available prior to retrieving the preview, this will be called
+ * with the low resolution bitmap.
+ */
+ public void retrieveOriginal(Uri contentUri, OriginalReady complete, ImageReady lowResolution) {
+ File localFile = getLocalFile(contentUri);
+ if (localFile != null) {
+ addNotification(new NotifyOriginalReady(complete), localFile);
+ } else {
+ NotifyImageReady notifyLowResolution = (lowResolution == null) ? null
+ : new NotifyImageReady(lowResolution);
+ addTask(contentUri, new NotifyOriginalReady(complete), notifyLowResolution,
+ MediaSize.Original);
+ }
+ }
+
+ /**
+ * Looks for an already cached media at a specific size.
+ *
+ * @param contentUri The original media item content URI
+ * @param size The target size to search for in the cache
+ * @return The cached file location or null if it is not cached.
+ */
+ public File getCachedFile(Uri contentUri, MediaSize size) {
+ Long cachedId = mDatabaseHelper.getCached(contentUri, size);
+ File file = null;
+ if (cachedId != null) {
+ file = createCacheImagePath(cachedId);
+ }
+ return file;
+ }
+
+ /**
+ * Inserts a media item into the cache.
+ *
+ * @param contentUri The original media item URI.
+ * @param size The size of the media item to store in the cache.
+ * @param tempFile The temporary file where the image is stored. This file
+ * will no longer exist after executing this method.
+ * @return The new location, in the cache, of the media item or null if it
+ * wasn't possible to move into the cache.
+ */
+ public File insertIntoCache(Uri contentUri, MediaSize size, File tempFile) {
+ long fileSize = tempFile.length();
+ if (fileSize == 0) {
+ return null;
+ }
+ File cacheFile = null;
+ SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
+ // Ensure that this step is atomic
+ db.beginTransaction();
+ try {
+ Long id = mDatabaseHelper.getCached(contentUri, size);
+ if (id != null) {
+ cacheFile = createCacheImagePath(id);
+ if (tempFile.renameTo(cacheFile)) {
+ mDatabaseHelper.updateLength(id, fileSize);
+ } else {
+ Log.w(TAG, "Could not update cached file with " + tempFile);
+ tempFile.delete();
+ cacheFile = null;
+ }
+ } else {
+ ensureFreeCacheSpace(tempFile.length(), size);
+ id = mDatabaseHelper.insert(contentUri, size, mMoveTempToCache, tempFile);
+ cacheFile = createCacheImagePath(id);
+ }
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ return cacheFile;
+ }
+
+ /**
+ * For testing purposes.
+ */
+ public void setMaxCacheSize(long maxCacheSize) {
+ synchronized (mCacheSizeLock) {
+ mMaxCacheSize = maxCacheSize;
+ mMinThumbCacheSize = mMaxCacheSize / 10;
+ mCacheSize = -1;
+ mThumbCacheSize = -1;
+ }
+ }
+
+ private File createCacheImagePath(long id) {
+ return new File(getCacheDir(), String.valueOf(id) + IMAGE_EXTENSION);
+ }
+
+ private void addTask(Uri contentUri, ImageReady complete, ImageReady lowResolution,
+ MediaSize size) {
+ NotifyReady notifyComplete = new NotifyImageReady(complete);
+ NotifyImageReady notifyLowResolution = null;
+ if (lowResolution != null) {
+ notifyLowResolution = new NotifyImageReady(lowResolution);
+ }
+ addTask(contentUri, notifyComplete, notifyLowResolution, size);
+ }
+
+ private void addTask(Uri contentUri, NotifyReady complete, NotifyImageReady lowResolution,
+ MediaSize size) {
+ MediaRetriever retriever = getMediaRetriever(contentUri);
+ Uri uri = retriever.normalizeUri(contentUri, size);
+ if (uri == null) {
+ throw new IllegalArgumentException("No MediaRetriever for " + contentUri);
+ }
+ size = retriever.normalizeMediaSize(uri, size);
+
+ Long cachedId = mDatabaseHelper.getCached(uri, size);
+ if (cachedId != null) {
+ addNotification(complete, createCacheImagePath(cachedId));
+ return;
+ }
+ String differentiator = getDifferentiator(uri.getScheme(), uri.getAuthority());
+ synchronized (mTasks) {
+ List<ProcessingJob> tasks = mTasks.get(differentiator);
+ if (tasks == null) {
+ throw new IllegalArgumentException("Cannot find retriever for: " + uri);
+ }
+ synchronized (tasks) {
+ ProcessingJob job = new ProcessingJob(uri, size, complete, lowResolution);
+ tasks.add(job);
+ tasks.notifyAll();
+ }
+ }
+ }
+
+ private MediaRetriever getMediaRetriever(Uri uri) {
+ String differentiator = getDifferentiator(uri.getScheme(), uri.getAuthority());
+ MediaRetriever retriever;
+ synchronized (mRetrievers) {
+ retriever = mRetrievers.get(differentiator);
+ }
+ if (retriever == null) {
+ throw new IllegalArgumentException("No MediaRetriever for " + uri);
+ }
+ return retriever;
+ }
+
+ private File getLocalFile(Uri uri) {
+ MediaRetriever retriever = getMediaRetriever(uri);
+ File localFile = null;
+ if (retriever != null) {
+ localFile = retriever.getLocalFile(uri);
+ }
+ return localFile;
+ }
+
+ private MediaSize getFastImageSize(Uri uri, MediaSize size) {
+ MediaRetriever retriever = getMediaRetriever(uri);
+ return retriever.getFastImageSize(uri, size);
+ }
+
+ private boolean isFastImageBetter(MediaSize fastImageType, MediaSize size) {
+ if (fastImageType == null) {
+ return false;
+ }
+ if (size == null) {
+ return true;
+ }
+ return fastImageType.isBetterThan(size);
+ }
+
+ private byte[] getTemporaryImage(Uri uri, MediaSize fastImageType) {
+ MediaRetriever retriever = getMediaRetriever(uri);
+ return retriever.getTemporaryImage(uri, fastImageType);
+ }
+
+ private void processTask(ProcessingJob job) {
+ Long cachedId = mDatabaseHelper.getCached(job.contentUri, job.size);
+ if (cachedId != null) {
+ File file = createCacheImagePath(cachedId);
+ addNotification(job.complete, file);
+ return;
+ }
+
+ boolean hasLowResolution = job.lowResolution != null;
+ if (hasLowResolution) {
+ MediaSize cachedSize = mDatabaseHelper.executeOnBestCached(job.contentUri, job.size,
+ mNotifyCachedLowResolution);
+ MediaSize fastImageSize = getFastImageSize(job.contentUri, job.size);
+ if (isFastImageBetter(fastImageSize, cachedSize)) {
+ if (fastImageSize.isTemporary()) {
+ byte[] bytes = getTemporaryImage(job.contentUri, fastImageSize);
+ if (bytes != null) {
+ addNotification(job.lowResolution, bytes);
+ }
+ } else {
+ File lowFile = getMedia(job.contentUri, fastImageSize);
+ if (lowFile != null) {
+ addNotification(job.lowResolution, lowFile);
+ }
+ }
+ }
+ }
+
+ // Now get the full size desired
+ File fullSizeFile = getMedia(job.contentUri, job.size);
+ if (fullSizeFile != null) {
+ addNotification(job.complete, fullSizeFile);
+ }
+ }
+
+ private void addNotification(NotifyReady callback, File file) {
+ try {
+ callback.setFile(file);
+ synchronized (mCallbacks) {
+ mCallbacks.add(callback);
+ mCallbacks.notifyAll();
+ }
+ } catch (FileNotFoundException e) {
+ Log.e(TAG, "Unable to read file " + file, e);
+ }
+ }
+
+ private void addNotification(NotifyImageReady callback, byte[] bytes) {
+ callback.setBytes(bytes);
+ synchronized (mCallbacks) {
+ mCallbacks.add(callback);
+ mCallbacks.notifyAll();
+ }
+ }
+
+ private File getMedia(Uri uri, MediaSize size) {
+ long imageNumber;
+ synchronized (mTempImageNumberLock) {
+ imageNumber = mTempImageNumber++;
+ }
+ File tempFile = new File(getCacheDir(), String.valueOf(imageNumber) + TEMP_IMAGE_EXTENSION);
+ MediaRetriever retriever = getMediaRetriever(uri);
+ boolean retrieved = retriever.getMedia(uri, size, tempFile);
+ File cachedFile = null;
+ if (retrieved) {
+ ensureFreeCacheSpace(tempFile.length(), size);
+ long id = mDatabaseHelper.insert(uri, size, mMoveTempToCache, tempFile);
+ cachedFile = createCacheImagePath(id);
+ }
+ return cachedFile;
+ }
+
+ private static String getDifferentiator(String scheme, String authority) {
+ if (authority == null) {
+ return scheme;
+ }
+ StringBuilder differentiator = new StringBuilder(scheme);
+ differentiator.append(':');
+ differentiator.append(authority);
+ return differentiator.toString();
+ }
+
+ private void ensureFreeCacheSpace(long size, MediaSize mediaSize) {
+ synchronized (mCacheSizeLock) {
+ if (mCacheSize == -1 || mThumbCacheSize == -1) {
+ mCacheSize = mDatabaseHelper.getCacheSize();
+ mThumbCacheSize = mDatabaseHelper.getThumbnailCacheSize();
+ if (mCacheSize == -1 || mThumbCacheSize == -1) {
+ Log.e(TAG, "Can't determine size of the image cache");
+ return;
+ }
+ }
+ mCacheSize += size;
+ if (mediaSize == MediaSize.Thumbnail) {
+ mThumbCacheSize += size;
+ }
+ if (mCacheSize > mMaxCacheSize) {
+ shrinkCacheLocked();
+ }
+ }
+ }
+
+ private void shrinkCacheLocked() {
+ long deleteSize = mMinThumbCacheSize;
+ boolean includeThumbnails = (mThumbCacheSize - deleteSize) > mMinThumbCacheSize;
+ mDatabaseHelper.deleteOldCached(includeThumbnails, deleteSize, mDeleteFile);
+ }
+}
diff --git a/src/com/android/photos/data/MediaCacheDatabase.java b/src/com/android/photos/data/MediaCacheDatabase.java
new file mode 100644
index 0000000..16265b5
--- /dev/null
+++ b/src/com/android/photos/data/MediaCacheDatabase.java
@@ -0,0 +1,272 @@
+/*
+ * 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.photos.data;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.net.Uri;
+import android.provider.BaseColumns;
+
+import com.android.photos.data.MediaRetriever.MediaSize;
+
+import java.io.File;
+
+class MediaCacheDatabase extends SQLiteOpenHelper {
+ public static final int DB_VERSION = 1;
+ public static final String DB_NAME = "mediacache.db";
+
+ /** Internal database table used for the media cache */
+ public static final String TABLE = "media_cache";
+
+ private static interface Columns extends BaseColumns {
+ /** The Content URI of the original image. */
+ public static final String URI = "uri";
+ /** MediaSize.getValue() values. */
+ public static final String MEDIA_SIZE = "media_size";
+ /** The last time this image was queried. */
+ public static final String LAST_ACCESS = "last_access";
+ /** The image size in bytes. */
+ public static final String SIZE_IN_BYTES = "size";
+ }
+
+ static interface Action {
+ void execute(Uri uri, long id, MediaRetriever.MediaSize size, Object parameter);
+ }
+
+ private static final String[] PROJECTION_ID = {
+ Columns._ID,
+ };
+
+ private static final String[] PROJECTION_CACHED = {
+ Columns._ID, Columns.MEDIA_SIZE, Columns.SIZE_IN_BYTES,
+ };
+
+ private static final String[] PROJECTION_CACHE_SIZE = {
+ "SUM(" + Columns.SIZE_IN_BYTES + ")"
+ };
+
+ private static final String[] PROJECTION_DELETE_OLD = {
+ Columns._ID, Columns.URI, Columns.MEDIA_SIZE, Columns.SIZE_IN_BYTES, Columns.LAST_ACCESS,
+ };
+
+ public static final String CREATE_TABLE = "CREATE TABLE " + TABLE + "("
+ + Columns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ + Columns.URI + " TEXT NOT NULL,"
+ + Columns.MEDIA_SIZE + " INTEGER NOT NULL,"
+ + Columns.LAST_ACCESS + " INTEGER NOT NULL,"
+ + Columns.SIZE_IN_BYTES + " INTEGER NOT NULL,"
+ + "UNIQUE(" + Columns.URI + ", " + Columns.MEDIA_SIZE + "))";
+
+ public static final String DROP_TABLE = "DROP TABLE IF EXISTS " + TABLE;
+
+ public static final String WHERE_THUMBNAIL = Columns.MEDIA_SIZE + " = "
+ + MediaSize.Thumbnail.getValue();
+
+ public static final String WHERE_NOT_THUMBNAIL = Columns.MEDIA_SIZE + " <> "
+ + MediaSize.Thumbnail.getValue();
+
+ public static final String WHERE_CLEAR_CACHE = Columns.LAST_ACCESS + " <= ?";
+
+ public static final String WHERE_CLEAR_CACHE_LARGE = WHERE_CLEAR_CACHE + " AND "
+ + WHERE_NOT_THUMBNAIL;
+
+ static class QueryCacheResults {
+ public QueryCacheResults(long id, int sizeVal) {
+ this.id = id;
+ this.size = MediaRetriever.MediaSize.fromInteger(sizeVal);
+ }
+ public long id;
+ public MediaRetriever.MediaSize size;
+ }
+
+ public MediaCacheDatabase(Context context) {
+ super(context, DB_NAME, null, DB_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL(CREATE_TABLE);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ db.execSQL(DROP_TABLE);
+ onCreate(db);
+ MediaCache.getInstance().clearCacheDir();
+ }
+
+ public Long getCached(Uri uri, MediaRetriever.MediaSize size) {
+ String where = Columns.URI + " = ? AND " + Columns.MEDIA_SIZE + " = ?";
+ SQLiteDatabase db = getWritableDatabase();
+ String[] whereArgs = {
+ uri.toString(), String.valueOf(size.getValue()),
+ };
+ Cursor cursor = db.query(TABLE, PROJECTION_ID, where, whereArgs, null, null, null);
+ Long id = null;
+ if (cursor.moveToNext()) {
+ id = cursor.getLong(0);
+ }
+ cursor.close();
+ if (id != null) {
+ String[] updateArgs = {
+ id.toString()
+ };
+ ContentValues values = new ContentValues();
+ values.put(Columns.LAST_ACCESS, System.currentTimeMillis());
+ db.beginTransaction();
+ try {
+ db.update(TABLE, values, Columns._ID + " = ?", updateArgs);
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+ return id;
+ }
+
+ public MediaRetriever.MediaSize executeOnBestCached(Uri uri, MediaRetriever.MediaSize size, Action action) {
+ String where = Columns.URI + " = ? AND " + Columns.MEDIA_SIZE + " < ?";
+ String orderBy = Columns.MEDIA_SIZE + " DESC";
+ SQLiteDatabase db = getReadableDatabase();
+ String[] whereArgs = {
+ uri.toString(), String.valueOf(size.getValue()),
+ };
+ Cursor cursor = db.query(TABLE, PROJECTION_CACHED, where, whereArgs, null, null, orderBy);
+ MediaRetriever.MediaSize bestSize = null;
+ if (cursor.moveToNext()) {
+ long id = cursor.getLong(0);
+ bestSize = MediaRetriever.MediaSize.fromInteger(cursor.getInt(1));
+ long fileSize = cursor.getLong(2);
+ action.execute(uri, id, bestSize, fileSize);
+ }
+ cursor.close();
+ return bestSize;
+ }
+
+ public long insert(Uri uri, MediaRetriever.MediaSize size, Action action, File tempFile) {
+ SQLiteDatabase db = getWritableDatabase();
+ db.beginTransaction();
+ try {
+ ContentValues values = new ContentValues();
+ values.put(Columns.LAST_ACCESS, System.currentTimeMillis());
+ values.put(Columns.MEDIA_SIZE, size.getValue());
+ values.put(Columns.URI, uri.toString());
+ values.put(Columns.SIZE_IN_BYTES, tempFile.length());
+ long id = db.insert(TABLE, null, values);
+ if (id != -1) {
+ action.execute(uri, id, size, tempFile);
+ db.setTransactionSuccessful();
+ }
+ return id;
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ public void updateLength(long id, long fileSize) {
+ ContentValues values = new ContentValues();
+ values.put(Columns.SIZE_IN_BYTES, fileSize);
+ String[] whereArgs = {
+ String.valueOf(id)
+ };
+ SQLiteDatabase db = getWritableDatabase();
+ db.beginTransaction();
+ try {
+ db.update(TABLE, values, Columns._ID + " = ?", whereArgs);
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ public void delete(Uri uri, Action action) {
+ SQLiteDatabase db = getWritableDatabase();
+ String where = Columns.URI + " = ?";
+ String[] whereArgs = {
+ uri.toString()
+ };
+ Cursor cursor = db.query(TABLE, PROJECTION_CACHED, where, whereArgs, null, null, null);
+ while (cursor.moveToNext()) {
+ long id = cursor.getLong(0);
+ MediaRetriever.MediaSize size = MediaRetriever.MediaSize.fromInteger(cursor.getInt(1));
+ action.execute(uri, id, size, null);
+ }
+ cursor.close();
+ db.beginTransaction();
+ try {
+ db.delete(TABLE, where, whereArgs);
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ public void deleteOldCached(boolean includeThumbnails, long deleteSize, Action action) {
+ String where = includeThumbnails ? null : WHERE_NOT_THUMBNAIL;
+ long lastAccess = 0;
+ SQLiteDatabase db = getWritableDatabase();
+ db.beginTransaction();
+ try {
+ Cursor cursor = db.query(TABLE, PROJECTION_DELETE_OLD, where, null, null, null,
+ Columns.LAST_ACCESS);
+ while (cursor.moveToNext()) {
+ long id = cursor.getLong(0);
+ String uri = cursor.getString(1);
+ MediaSize size = MediaSize.fromInteger(cursor.getInt(2));
+ long length = cursor.getLong(3);
+ long imageLastAccess = cursor.getLong(4);
+
+ if (imageLastAccess != lastAccess && deleteSize < 0) {
+ break; // We've deleted enough.
+ }
+ lastAccess = imageLastAccess;
+ action.execute(Uri.parse(uri), id, size, length);
+ deleteSize -= length;
+ }
+ cursor.close();
+ String[] whereArgs = {
+ String.valueOf(lastAccess),
+ };
+ String whereDelete = includeThumbnails ? WHERE_CLEAR_CACHE : WHERE_CLEAR_CACHE_LARGE;
+ db.delete(TABLE, whereDelete, whereArgs);
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ public long getCacheSize() {
+ return getCacheSize(null);
+ }
+
+ public long getThumbnailCacheSize() {
+ return getCacheSize(WHERE_THUMBNAIL);
+ }
+
+ private long getCacheSize(String where) {
+ SQLiteDatabase db = getReadableDatabase();
+ Cursor cursor = db.query(TABLE, PROJECTION_CACHE_SIZE, where, null, null, null, null);
+ long size = -1;
+ if (cursor.moveToNext()) {
+ size = cursor.getLong(0);
+ }
+ cursor.close();
+ return size;
+ }
+}
diff --git a/src/com/android/photos/data/MediaCacheUtils.java b/src/com/android/photos/data/MediaCacheUtils.java
new file mode 100644
index 0000000..1463d52
--- /dev/null
+++ b/src/com/android/photos/data/MediaCacheUtils.java
@@ -0,0 +1,139 @@
+/*
+ * 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.photos.data;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
+import android.graphics.BitmapFactory;
+import android.util.Log;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.data.DecodeUtils;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.util.ThreadPool.CancelListener;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+import com.android.photos.data.MediaRetriever.MediaSize;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+public class MediaCacheUtils {
+ private static final String TAG = MediaCacheUtils.class.getSimpleName();
+ private static int QUALITY = 80;
+ private static final JobContext sJobStub = new JobContext() {
+
+ @Override
+ public boolean isCancelled() {
+ return false;
+ }
+
+ @Override
+ public void setCancelListener(CancelListener listener) {
+ }
+
+ @Override
+ public boolean setMode(int mode) {
+ return true;
+ }
+ };
+
+ private static int mTargetThumbnailSize;
+ private static int mTargetPreviewSize;
+
+ public static void initialize(Context context) {
+ Resources resources = context.getResources();
+ mTargetThumbnailSize = resources.getDimensionPixelSize(R.dimen.size_thumbnail);
+ mTargetPreviewSize = resources.getDimensionPixelSize(R.dimen.size_preview);
+ }
+
+ public static int getTargetSize(MediaSize size) {
+ return (size == MediaSize.Thumbnail) ? mTargetThumbnailSize : mTargetPreviewSize;
+ }
+
+ public static boolean downsample(File inBitmap, MediaSize targetSize, File outBitmap) {
+ if (MediaSize.Original == targetSize) {
+ return false; // MediaCache should use the local path for this.
+ }
+ int size = getTargetSize(targetSize);
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+ // TODO: remove unnecessary job context from DecodeUtils.
+ Bitmap bitmap = DecodeUtils.decodeThumbnail(sJobStub, inBitmap.getPath(), options, size,
+ MediaItem.TYPE_THUMBNAIL);
+ boolean success = (bitmap != null);
+ if (success) {
+ success = writeAndRecycle(bitmap, outBitmap);
+ }
+ return success;
+ }
+
+ public static boolean downsample(Bitmap inBitmap, MediaSize size, File outBitmap) {
+ if (MediaSize.Original == size) {
+ return false; // MediaCache should use the local path for this.
+ }
+ int targetSize = getTargetSize(size);
+ boolean success;
+ if (!needsDownsample(inBitmap, size)) {
+ success = writeAndRecycle(inBitmap, outBitmap);
+ } else {
+ float maxDimension = Math.max(inBitmap.getWidth(), inBitmap.getHeight());
+ float scale = targetSize / maxDimension;
+ int targetWidth = Math.round(scale * inBitmap.getWidth());
+ int targetHeight = Math.round(scale * inBitmap.getHeight());
+ Bitmap scaled = Bitmap.createScaledBitmap(inBitmap, targetWidth, targetHeight, false);
+ success = writeAndRecycle(scaled, outBitmap);
+ inBitmap.recycle();
+ }
+ return success;
+ }
+
+ public static boolean extractImageFromVideo(File inVideo, File outBitmap) {
+ Bitmap bitmap = BitmapUtils.createVideoThumbnail(inVideo.getPath());
+ return writeAndRecycle(bitmap, outBitmap);
+ }
+
+ public static boolean needsDownsample(Bitmap bitmap, MediaSize size) {
+ if (size == MediaSize.Original) {
+ return false;
+ }
+ int targetSize = getTargetSize(size);
+ int maxDimension = Math.max(bitmap.getWidth(), bitmap.getHeight());
+ return maxDimension > (targetSize * 4 / 3);
+ }
+
+ public static boolean writeAndRecycle(Bitmap bitmap, File outBitmap) {
+ boolean success = writeToFile(bitmap, outBitmap);
+ bitmap.recycle();
+ return success;
+ }
+
+ public static boolean writeToFile(Bitmap bitmap, File outBitmap) {
+ boolean success = false;
+ try {
+ FileOutputStream out = new FileOutputStream(outBitmap);
+ success = bitmap.compress(CompressFormat.JPEG, QUALITY, out);
+ out.close();
+ } catch (IOException e) {
+ Log.w(TAG, "Couldn't write bitmap to cache", e);
+ // success is already false
+ }
+ return success;
+ }
+}
diff --git a/src/com/android/photos/data/MediaRetriever.java b/src/com/android/photos/data/MediaRetriever.java
new file mode 100644
index 0000000..f383e5f
--- /dev/null
+++ b/src/com/android/photos/data/MediaRetriever.java
@@ -0,0 +1,129 @@
+/*
+ * 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.photos.data;
+
+import android.net.Uri;
+
+import java.io.File;
+
+public interface MediaRetriever {
+ public enum MediaSize {
+ TemporaryThumbnail(5), Thumbnail(10), TemporaryPreview(15), Preview(20), Original(30);
+
+ private final int mValue;
+
+ private MediaSize(int value) {
+ mValue = value;
+ }
+
+ public int getValue() {
+ return mValue;
+ }
+
+ static MediaSize fromInteger(int value) {
+ switch (value) {
+ case 10:
+ return MediaSize.Thumbnail;
+ case 20:
+ return MediaSize.Preview;
+ case 30:
+ return MediaSize.Original;
+ default:
+ throw new IllegalArgumentException();
+ }
+ }
+
+ public boolean isBetterThan(MediaSize that) {
+ return mValue > that.mValue;
+ }
+
+ public boolean isTemporary() {
+ return this == TemporaryThumbnail || this == TemporaryPreview;
+ }
+ }
+
+ /**
+ * Returns the local File for the given Uri. If the image is not stored
+ * locally, null should be returned. The image should not be retrieved if it
+ * isn't already available.
+ *
+ * @param contentUri The media URI to search for.
+ * @return The local File of the image if it is available or null if it
+ * isn't.
+ */
+ File getLocalFile(Uri contentUri);
+
+ /**
+ * Returns the fast access image type for a given image size, if supported.
+ * This image should be smaller than size and should be quick to retrieve.
+ * It does not have to obey the expected aspect ratio.
+ *
+ * @param contentUri The original media Uri.
+ * @param size The target size to search for a fast-access image.
+ * @return The fast image type supported for the given image size or null of
+ * no fast image is supported.
+ */
+ MediaSize getFastImageSize(Uri contentUri, MediaSize size);
+
+ /**
+ * Returns a byte array containing the contents of the fast temporary image
+ * for a given image size. For example, a thumbnail may be smaller or of a
+ * different aspect ratio than the generated thumbnail.
+ *
+ * @param contentUri The original media Uri.
+ * @param temporarySize The target media size. Guaranteed to be a MediaSize
+ * for which isTemporary() returns true.
+ * @return A byte array of contents for for the given contentUri and
+ * fastImageType. null can be retrieved if the quick retrieval
+ * fails.
+ */
+ byte[] getTemporaryImage(Uri contentUri, MediaSize temporarySize);
+
+ /**
+ * Retrieves an image and saves it to a file.
+ *
+ * @param contentUri The original media Uri.
+ * @param size The target media size.
+ * @param tempFile The file to write the bitmap to.
+ * @return <code>true</code> on success.
+ */
+ boolean getMedia(Uri contentUri, MediaSize imageSize, File tempFile);
+
+ /**
+ * Normalizes a URI that may have additional parameters. It is fine to
+ * return contentUri. This is executed on the calling thread, so it must be
+ * a fast access operation and cannot depend, for example, on I/O.
+ *
+ * @param contentUri The URI to normalize
+ * @param size The size of the image being requested
+ * @return The normalized URI representation of contentUri.
+ */
+ Uri normalizeUri(Uri contentUri, MediaSize size);
+
+ /**
+ * Normalize the MediaSize for a given URI. Typically the size returned
+ * would be the passed-in size. Some URIs may only have one size used and
+ * should be treaded as Thumbnails, for example. This is executed on the
+ * calling thread, so it must be a fast access operation and cannot depend,
+ * for example, on I/O.
+ *
+ * @param contentUri The URI for the size being normalized.
+ * @param size The size to be normalized.
+ * @return The normalized size of the given URI.
+ */
+ MediaSize normalizeMediaSize(Uri contentUri, MediaSize size);
+}
diff --git a/tests/src/com/android/photos/data/DataTestRunner.java b/tests/src/com/android/photos/data/DataTestRunner.java
index 4322585..10618d6 100644
--- a/tests/src/com/android/photos/data/DataTestRunner.java
+++ b/tests/src/com/android/photos/data/DataTestRunner.java
@@ -18,6 +18,9 @@
import android.test.InstrumentationTestRunner;
import android.test.InstrumentationTestSuite;
+import com.android.photos.data.TestHelper.TestInitialization;
+
+import junit.framework.TestCase;
import junit.framework.TestSuite;
public class DataTestRunner extends InstrumentationTestRunner {
@@ -26,6 +29,13 @@
TestSuite suite = new InstrumentationTestSuite(this);
suite.addTestSuite(PhotoDatabaseTest.class);
suite.addTestSuite(PhotoProviderTest.class);
+ TestHelper.addTests(MediaCacheTest.class, suite, new TestInitialization() {
+ @Override
+ public void initialize(TestCase testCase) {
+ MediaCacheTest test = (MediaCacheTest) testCase;
+ test.setLocalContext(getContext());
+ }
+ });
return suite;
}
diff --git a/tests/src/com/android/photos/data/MediaCacheTest.java b/tests/src/com/android/photos/data/MediaCacheTest.java
new file mode 100644
index 0000000..df990ed
--- /dev/null
+++ b/tests/src/com/android/photos/data/MediaCacheTest.java
@@ -0,0 +1,388 @@
+/*
+ * 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.photos.data;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.SystemClock;
+import android.test.ProviderTestCase2;
+
+import com.android.gallery3d.tests.R;
+import com.android.photos.data.MediaCache.ImageReady;
+import com.android.photos.data.MediaCache.OriginalReady;
+import com.android.photos.data.MediaRetriever.MediaSize;
+import com.android.photos.data.PhotoProvider.Photos;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class MediaCacheTest extends ProviderTestCase2<PhotoProvider> {
+ @SuppressWarnings("unused")
+ private static final String TAG = MediaCacheTest.class.getSimpleName();
+
+ private File mDir;
+ private File mImage;
+ private File mCacheDir;
+ private Resources mResources;
+ private MediaCache mMediaCache;
+ private ReadyCollector mReady;
+
+ public static final long MAX_WAIT = 2000;
+
+ private static class ReadyCollector implements ImageReady, OriginalReady {
+ public File mOriginalFile;
+ public InputStream mInputStream;
+
+ @Override
+ public synchronized void originalReady(File originalFile) {
+ mOriginalFile = originalFile;
+ notifyAll();
+ }
+
+ @Override
+ public synchronized void imageReady(InputStream bitmapInputStream) {
+ mInputStream = bitmapInputStream;
+ notifyAll();
+ }
+
+ public synchronized boolean waitForNotification() {
+ long endWait = SystemClock.uptimeMillis() + MAX_WAIT;
+
+ try {
+ while (mInputStream == null && mOriginalFile == null
+ && SystemClock.uptimeMillis() < endWait) {
+ wait(endWait - SystemClock.uptimeMillis());
+ }
+ } catch (InterruptedException e) {
+ }
+ return mInputStream != null || mOriginalFile != null;
+ }
+ }
+
+ private static class DummyMediaRetriever implements MediaRetriever {
+ private boolean mNullUri = false;
+ @Override
+ public File getLocalFile(Uri contentUri) {
+ return null;
+ }
+
+ @Override
+ public MediaSize getFastImageSize(Uri contentUri, MediaSize size) {
+ return null;
+ }
+
+ @Override
+ public byte[] getTemporaryImage(Uri contentUri, MediaSize temporarySize) {
+ return null;
+ }
+
+ @Override
+ public boolean getMedia(Uri contentUri, MediaSize imageSize, File tempFile) {
+ return false;
+ }
+
+ @Override
+ public Uri normalizeUri(Uri contentUri, MediaSize size) {
+ if (mNullUri) {
+ return null;
+ } else {
+ return contentUri;
+ }
+ }
+
+ @Override
+ public MediaSize normalizeMediaSize(Uri contentUri, MediaSize size) {
+ return size;
+ }
+
+ public void setNullUri() {
+ mNullUri = true;
+ }
+ };
+
+ public MediaCacheTest() {
+ super(PhotoProvider.class, PhotoProvider.AUTHORITY);
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ mReady = new ReadyCollector();
+ File externalDir = Environment.getExternalStorageDirectory();
+ mDir = new File(externalDir, "test");
+ mDir.mkdirs();
+ mCacheDir = new File(externalDir, "test_cache");
+ mImage = new File(mDir, "original.jpg");
+ MediaCache.initialize(getMockContext());
+ MediaCache.getInstance().setCacheDir(mCacheDir);
+ mMediaCache = MediaCache.getInstance();
+ mMediaCache.addRetriever("file", "", new FileRetriever());
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ super.tearDown();
+ mMediaCache.clearCacheDir();
+ MediaCache.shutdown();
+ mMediaCache = null;
+ mImage.delete();
+ mDir.delete();
+ mCacheDir.delete();
+ }
+
+ public void setLocalContext(Context context) {
+ mResources = context.getResources();
+ }
+
+ public void testRetrieveOriginal() throws IOException {
+ copyResourceToFile(R.raw.galaxy_nexus, mImage.getPath());
+ Uri uri = Uri.fromFile(mImage);
+ mMediaCache.retrieveOriginal(uri, mReady, mReady);
+ assertTrue(mReady.waitForNotification());
+ assertNull(mReady.mInputStream);
+ assertEquals(mImage, mReady.mOriginalFile);
+ }
+
+ public void testRetrievePreview() throws IOException {
+ copyResourceToFile(R.raw.galaxy_nexus, mImage.getPath());
+ Uri uri = Uri.fromFile(mImage);
+ mMediaCache.retrievePreview(uri, mReady, null);
+ assertTrue(mReady.waitForNotification());
+ assertNotNull(mReady.mInputStream);
+ assertNull(mReady.mOriginalFile);
+ Bitmap bitmap = BitmapFactory.decodeStream(mReady.mInputStream);
+ mReady.mInputStream.close();
+ assertNotNull(bitmap);
+ Bitmap original = BitmapFactory.decodeFile(mImage.getPath());
+ assertTrue(bitmap.getWidth() < original.getWidth());
+ assertTrue(bitmap.getHeight() < original.getHeight());
+ int maxDimension = Math.max(bitmap.getWidth(), bitmap.getHeight());
+ int targetSize = MediaCacheUtils.getTargetSize(MediaSize.Preview);
+ assertTrue(maxDimension >= targetSize);
+ assertTrue(maxDimension < (targetSize * 2));
+ }
+
+ public void testRetrieveExifThumb() throws IOException {
+ copyResourceToFile(R.raw.galaxy_nexus, mImage.getPath());
+ Uri uri = Uri.fromFile(mImage);
+ ReadyCollector done = new ReadyCollector();
+ mMediaCache.retrieveThumbnail(uri, done, mReady);
+ assertTrue(mReady.waitForNotification());
+ assertNotNull(mReady.mInputStream);
+ assertNull(mReady.mOriginalFile);
+ Bitmap bitmap = BitmapFactory.decodeStream(mReady.mInputStream);
+ mReady.mInputStream.close();
+ assertTrue(done.waitForNotification());
+ assertNotNull(done.mInputStream);
+ done.mInputStream.close();
+ assertNotNull(bitmap);
+ assertEquals(320, bitmap.getWidth());
+ assertEquals(240, bitmap.getHeight());
+ }
+
+ public void testRetrieveThumb() throws IOException {
+ copyResourceToFile(R.raw.galaxy_nexus, mImage.getPath());
+ Uri uri = Uri.fromFile(mImage);
+ long downsampleStart = SystemClock.uptimeMillis();
+ mMediaCache.retrieveThumbnail(uri, mReady, null);
+ assertTrue(mReady.waitForNotification());
+ long downsampleEnd = SystemClock.uptimeMillis();
+ assertNotNull(mReady.mInputStream);
+ assertNull(mReady.mOriginalFile);
+ Bitmap bitmap = BitmapFactory.decodeStream(mReady.mInputStream);
+ mReady.mInputStream.close();
+ assertNotNull(bitmap);
+ Bitmap original = BitmapFactory.decodeFile(mImage.getPath());
+ assertTrue(bitmap.getWidth() < original.getWidth());
+ assertTrue(bitmap.getHeight() < original.getHeight());
+ int maxDimension = Math.max(bitmap.getWidth(), bitmap.getHeight());
+ int targetSize = MediaCacheUtils.getTargetSize(MediaSize.Thumbnail);
+ assertTrue(maxDimension >= targetSize);
+ assertTrue(maxDimension < (targetSize * 2));
+
+ // Retrieve cached thumb.
+ mReady = new ReadyCollector();
+ long start = SystemClock.uptimeMillis();
+ mMediaCache.retrieveThumbnail(uri, mReady, null);
+ assertTrue(mReady.waitForNotification());
+ mReady.mInputStream.close();
+ long end = SystemClock.uptimeMillis();
+ // Already cached. Wait shorter time.
+ assertTrue((end - start) < (downsampleEnd - downsampleStart) / 2);
+ }
+
+ public void testGetVideo() throws IOException {
+ mImage = new File(mDir, "original.mp4");
+ copyResourceToFile(R.raw.android_lawn, mImage.getPath());
+ Uri uri = Uri.fromFile(mImage);
+
+ mMediaCache.retrieveOriginal(uri, mReady, mReady);
+ assertTrue(mReady.waitForNotification());
+ assertNull(mReady.mInputStream);
+ assertNotNull(mReady.mOriginalFile);
+
+ mReady = new ReadyCollector();
+ mMediaCache.retrievePreview(uri, mReady, mReady);
+ assertTrue(mReady.waitForNotification());
+ assertNotNull(mReady.mInputStream);
+ assertNull(mReady.mOriginalFile);
+ Bitmap bitmap = BitmapFactory.decodeStream(mReady.mInputStream);
+ mReady.mInputStream.close();
+ int maxDimension = Math.max(bitmap.getWidth(), bitmap.getHeight());
+ int targetSize = MediaCacheUtils.getTargetSize(MediaSize.Preview);
+ assertTrue(maxDimension >= targetSize);
+ assertTrue(maxDimension < (targetSize * 2));
+
+ mReady = new ReadyCollector();
+ mMediaCache.retrieveThumbnail(uri, mReady, mReady);
+ assertTrue(mReady.waitForNotification());
+ assertNotNull(mReady.mInputStream);
+ assertNull(mReady.mOriginalFile);
+ bitmap = BitmapFactory.decodeStream(mReady.mInputStream);
+ mReady.mInputStream.close();
+ maxDimension = Math.max(bitmap.getWidth(), bitmap.getHeight());
+ targetSize = MediaCacheUtils.getTargetSize(MediaSize.Thumbnail);
+ assertTrue(maxDimension >= targetSize);
+ assertTrue(maxDimension < (targetSize * 2));
+ }
+
+ public void testFastImage() throws IOException {
+ copyResourceToFile(R.raw.galaxy_nexus, mImage.getPath());
+ Uri uri = Uri.fromFile(mImage);
+ mMediaCache.retrieveThumbnail(uri, mReady, mReady);
+ mReady.waitForNotification();
+ mReady.mInputStream.close();
+
+ mMediaCache.retrieveOriginal(uri, mReady, mReady);
+ assertTrue(mReady.waitForNotification());
+ assertNotNull(mReady.mInputStream);
+ mReady.mInputStream.close();
+ }
+
+ public void testBadRetriever() {
+ Uri uri = Photos.CONTENT_URI;
+ try {
+ mMediaCache.retrieveOriginal(uri, mReady, mReady);
+ fail("Expected exception");
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+ }
+
+ public void testInsertIntoCache() throws IOException {
+ // FileRetriever inserts into the cache opportunistically with Videos
+ mImage = new File(mDir, "original.mp4");
+ copyResourceToFile(R.raw.android_lawn, mImage.getPath());
+ Uri uri = Uri.fromFile(mImage);
+
+ mMediaCache.retrieveThumbnail(uri, mReady, mReady);
+ assertTrue(mReady.waitForNotification());
+ mReady.mInputStream.close();
+ assertNotNull(mMediaCache.getCachedFile(uri, MediaSize.Preview));
+ }
+
+ public void testBadNormalizedUri() {
+ DummyMediaRetriever retriever = new DummyMediaRetriever();
+ Uri uri = Uri.fromParts("http", "world", "morestuff");
+ mMediaCache.addRetriever(uri.getScheme(), uri.getAuthority(), retriever);
+ retriever.setNullUri();
+ try {
+ mMediaCache.retrieveOriginal(uri, mReady, mReady);
+ fail("Expected IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+ }
+
+ public void testClearOldCache() throws IOException {
+ copyResourceToFile(R.raw.galaxy_nexus, mImage.getPath());
+ Uri uri = Uri.fromFile(mImage);
+ mMediaCache.retrievePreview(uri, mReady, null);
+ assertTrue(mReady.waitForNotification());
+ mReady.mInputStream.close();
+ mMediaCache.setMaxCacheSize(mMediaCache.getCachedFile(uri, MediaSize.Preview).length());
+ assertNotNull(mMediaCache.getCachedFile(uri, MediaSize.Preview));
+
+ mReady = new ReadyCollector();
+ // This should kick the preview image out of the cache.
+ mMediaCache.retrieveThumbnail(uri, mReady, null);
+ assertTrue(mReady.waitForNotification());
+ mReady.mInputStream.close();
+ assertNull(mMediaCache.getCachedFile(uri, MediaSize.Preview));
+ assertNotNull(mMediaCache.getCachedFile(uri, MediaSize.Thumbnail));
+ }
+
+ public void testClearLargeInCache() throws IOException {
+ copyResourceToFile(R.raw.galaxy_nexus, mImage.getPath());
+ Uri imageUri = Uri.fromFile(mImage);
+ mMediaCache.retrieveThumbnail(imageUri, mReady, null);
+ assertTrue(mReady.waitForNotification());
+ mReady.mInputStream.close();
+ assertNotNull(mMediaCache.getCachedFile(imageUri, MediaSize.Thumbnail));
+ long thumbSize = mMediaCache.getCachedFile(imageUri, MediaSize.Thumbnail).length();
+ mMediaCache.setMaxCacheSize(thumbSize * 10);
+
+ for (int i = 0; i < 9; i++) {
+ File tempImage = new File(mDir, "image" + i + ".jpg");
+ mImage.renameTo(tempImage);
+ Uri tempImageUri = Uri.fromFile(tempImage);
+ mReady = new ReadyCollector();
+ mMediaCache.retrieveThumbnail(tempImageUri, mReady, null);
+ assertTrue(mReady.waitForNotification());
+ mReady.mInputStream.close();
+ tempImage.renameTo(mImage);
+ }
+ assertNotNull(mMediaCache.getCachedFile(imageUri, MediaSize.Thumbnail));
+
+ for (int i = 0; i < 9; i++) {
+ File tempImage = new File(mDir, "image" + i + ".jpg");
+ mImage.renameTo(tempImage);
+ Uri tempImageUri = Uri.fromFile(tempImage);
+ mReady = new ReadyCollector();
+ mMediaCache.retrievePreview(tempImageUri, mReady, null);
+ assertTrue(mReady.waitForNotification());
+ mReady.mInputStream.close();
+ tempImage.renameTo(mImage);
+ }
+ assertNotNull(mMediaCache.getCachedFile(imageUri, MediaSize.Thumbnail));
+ Uri oldestUri = Uri.fromFile(new File(mDir, "image0.jpg"));
+ assertNull(mMediaCache.getCachedFile(oldestUri, MediaSize.Thumbnail));
+ }
+
+ private void copyResourceToFile(int resourceId, String path) throws IOException {
+ File outputDir = new File(path).getParentFile();
+ outputDir.mkdirs();
+
+ InputStream in = mResources.openRawResource(resourceId);
+ FileOutputStream out = new FileOutputStream(path);
+ byte[] buffer = new byte[1000];
+ int bytesRead;
+
+ while ((bytesRead = in.read(buffer)) >= 0) {
+ out.write(buffer, 0, bytesRead);
+ }
+
+ in.close();
+ out.close();
+ }
+}