1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.gallery3d.ingest.ui;
18
19import com.android.gallery3d.R;
20import com.android.gallery3d.ingest.data.BitmapWithMetadata;
21import com.android.gallery3d.ingest.data.IngestObjectInfo;
22import com.android.gallery3d.ingest.data.MtpBitmapFetch;
23import com.android.gallery3d.ingest.data.MtpDeviceIndex;
24
25import android.content.Context;
26import android.graphics.Canvas;
27import android.graphics.Matrix;
28import android.graphics.drawable.Drawable;
29import android.mtp.MtpDevice;
30import android.os.Handler;
31import android.os.HandlerThread;
32import android.os.Looper;
33import android.os.Message;
34import android.util.AttributeSet;
35import android.widget.ImageView;
36
37import java.lang.ref.WeakReference;
38
39/**
40 * View for images from an MTP devices
41 */
42public class MtpImageView extends ImageView {
43  // We will use the thumbnail for images larger than this threshold
44  private static final int MAX_FULLSIZE_PREVIEW_SIZE = 8388608; // 8 megabytes
45
46  private int mObjectHandle;
47  private int mGeneration;
48
49  private WeakReference<MtpImageView> mWeakReference = new WeakReference<MtpImageView>(this);
50  private Object mFetchLock = new Object();
51  private boolean mFetchPending = false;
52  private IngestObjectInfo mFetchObjectInfo;
53  private MtpDevice mFetchDevice;
54  private Object mFetchResult;
55  private Drawable mOverlayIcon;
56  private boolean mShowOverlayIcon;
57
58  private static final FetchImageHandler sFetchHandler = FetchImageHandler.createOnNewThread();
59  private static final ShowImageHandler sFetchCompleteHandler = new ShowImageHandler();
60
61  private void init() {
62    showPlaceholder();
63  }
64
65  public MtpImageView(Context context) {
66    super(context);
67    init();
68  }
69
70  public MtpImageView(Context context, AttributeSet attrs) {
71    super(context, attrs);
72    init();
73  }
74
75  public MtpImageView(Context context, AttributeSet attrs, int defStyle) {
76    super(context, attrs, defStyle);
77    init();
78  }
79
80  private void showPlaceholder() {
81    setImageResource(android.R.color.transparent);
82  }
83
84  public void setMtpDeviceAndObjectInfo(MtpDevice device, IngestObjectInfo object, int gen) {
85    int handle = object.getObjectHandle();
86    if (handle == mObjectHandle && gen == mGeneration) {
87      return;
88    }
89    cancelLoadingAndClear();
90    showPlaceholder();
91    mGeneration = gen;
92    mObjectHandle = handle;
93    mShowOverlayIcon = MtpDeviceIndex.SUPPORTED_VIDEO_FORMATS.contains(object.getFormat());
94    if (mShowOverlayIcon && mOverlayIcon == null) {
95      mOverlayIcon = getResources().getDrawable(R.drawable.ic_control_play);
96      updateOverlayIconBounds();
97    }
98    synchronized (mFetchLock) {
99      mFetchObjectInfo = object;
100      mFetchDevice = device;
101      if (mFetchPending) {
102        return;
103      }
104      mFetchPending = true;
105      sFetchHandler.sendMessage(
106          sFetchHandler.obtainMessage(0, mWeakReference));
107    }
108  }
109
110  protected Object fetchMtpImageDataFromDevice(MtpDevice device, IngestObjectInfo info) {
111    if (info.getCompressedSize() <= MAX_FULLSIZE_PREVIEW_SIZE
112        && MtpDeviceIndex.SUPPORTED_IMAGE_FORMATS.contains(info.getFormat())) {
113      return MtpBitmapFetch.getFullsize(device, info);
114    } else {
115      return new BitmapWithMetadata(MtpBitmapFetch.getThumbnail(device, info), 0);
116    }
117  }
118
119  private float mLastBitmapWidth;
120  private float mLastBitmapHeight;
121  private int mLastRotationDegrees;
122  private Matrix mDrawMatrix = new Matrix();
123
124  private void updateDrawMatrix() {
125    mDrawMatrix.reset();
126    float dwidth;
127    float dheight;
128    float vheight = getHeight();
129    float vwidth = getWidth();
130    float scale;
131    boolean rotated90 = (mLastRotationDegrees % 180 != 0);
132    if (rotated90) {
133      dwidth = mLastBitmapHeight;
134      dheight = mLastBitmapWidth;
135    } else {
136      dwidth = mLastBitmapWidth;
137      dheight = mLastBitmapHeight;
138    }
139    if (dwidth <= vwidth && dheight <= vheight) {
140      scale = 1.0f;
141    } else {
142      scale = Math.min(vwidth / dwidth, vheight / dheight);
143    }
144    mDrawMatrix.setScale(scale, scale);
145    if (rotated90) {
146      mDrawMatrix.postTranslate(-dheight * scale * 0.5f,
147          -dwidth * scale * 0.5f);
148      mDrawMatrix.postRotate(mLastRotationDegrees);
149      mDrawMatrix.postTranslate(dwidth * scale * 0.5f,
150          dheight * scale * 0.5f);
151    }
152    mDrawMatrix.postTranslate((vwidth - dwidth * scale) * 0.5f,
153        (vheight - dheight * scale) * 0.5f);
154    if (!rotated90 && mLastRotationDegrees > 0) {
155      // rotated by a multiple of 180
156      mDrawMatrix.postRotate(mLastRotationDegrees, vwidth / 2, vheight / 2);
157    }
158    setImageMatrix(mDrawMatrix);
159  }
160
161  private static final int OVERLAY_ICON_SIZE_DENOMINATOR = 4;
162
163  private void updateOverlayIconBounds() {
164    int iheight = mOverlayIcon.getIntrinsicHeight();
165    int iwidth = mOverlayIcon.getIntrinsicWidth();
166    int vheight = getHeight();
167    int vwidth = getWidth();
168    float scaleHeight = ((float) vheight) / (iheight * OVERLAY_ICON_SIZE_DENOMINATOR);
169    float scaleWidth = ((float) vwidth) / (iwidth * OVERLAY_ICON_SIZE_DENOMINATOR);
170    if (scaleHeight >= 1f && scaleWidth >= 1f) {
171      mOverlayIcon.setBounds((vwidth - iwidth) / 2,
172          (vheight - iheight) / 2,
173          (vwidth + iwidth) / 2,
174          (vheight + iheight) / 2);
175    } else {
176      float scale = Math.min(scaleHeight, scaleWidth);
177      mOverlayIcon.setBounds((int) (vwidth - scale * iwidth) / 2,
178          (int) (vheight - scale * iheight) / 2,
179          (int) (vwidth + scale * iwidth) / 2,
180          (int) (vheight + scale * iheight) / 2);
181    }
182  }
183
184  @Override
185  protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
186    super.onLayout(changed, left, top, right, bottom);
187    if (changed && getScaleType() == ScaleType.MATRIX) {
188      updateDrawMatrix();
189    }
190    if (mShowOverlayIcon && changed && mOverlayIcon != null) {
191      updateOverlayIconBounds();
192    }
193  }
194
195  @Override
196  protected void onDraw(Canvas canvas) {
197    super.onDraw(canvas);
198    if (mShowOverlayIcon && mOverlayIcon != null) {
199      mOverlayIcon.draw(canvas);
200    }
201  }
202
203  protected void onMtpImageDataFetchedFromDevice(Object result) {
204    BitmapWithMetadata bitmapWithMetadata = (BitmapWithMetadata) result;
205    if (getScaleType() == ScaleType.MATRIX) {
206      mLastBitmapHeight = bitmapWithMetadata.bitmap.getHeight();
207      mLastBitmapWidth = bitmapWithMetadata.bitmap.getWidth();
208      mLastRotationDegrees = bitmapWithMetadata.rotationDegrees;
209      updateDrawMatrix();
210    } else {
211      setRotation(bitmapWithMetadata.rotationDegrees);
212    }
213    setAlpha(0f);
214    setImageBitmap(bitmapWithMetadata.bitmap);
215    animate().alpha(1f);
216  }
217
218  protected void cancelLoadingAndClear() {
219    synchronized (mFetchLock) {
220      mFetchDevice = null;
221      mFetchObjectInfo = null;
222      mFetchResult = null;
223    }
224    animate().cancel();
225    setImageResource(android.R.color.transparent);
226  }
227
228  @Override
229  public void onDetachedFromWindow() {
230    cancelLoadingAndClear();
231    super.onDetachedFromWindow();
232  }
233
234  private static class FetchImageHandler extends Handler {
235    public FetchImageHandler(Looper l) {
236      super(l);
237    }
238
239    public static FetchImageHandler createOnNewThread() {
240      HandlerThread t = new HandlerThread("MtpImageView Fetch");
241      t.start();
242      return new FetchImageHandler(t.getLooper());
243    }
244
245    @Override
246    public void handleMessage(Message msg) {
247      @SuppressWarnings("unchecked")
248      MtpImageView parent = ((WeakReference<MtpImageView>) msg.obj).get();
249      if (parent == null) {
250        return;
251      }
252      IngestObjectInfo objectInfo;
253      MtpDevice device;
254      synchronized (parent.mFetchLock) {
255        parent.mFetchPending = false;
256        device = parent.mFetchDevice;
257        objectInfo = parent.mFetchObjectInfo;
258      }
259      if (device == null) {
260        return;
261      }
262      Object result = parent.fetchMtpImageDataFromDevice(device, objectInfo);
263      if (result == null) {
264        return;
265      }
266      synchronized (parent.mFetchLock) {
267        if (parent.mFetchObjectInfo != objectInfo) {
268          return;
269        }
270        parent.mFetchResult = result;
271        parent.mFetchDevice = null;
272        parent.mFetchObjectInfo = null;
273        sFetchCompleteHandler.sendMessage(
274            sFetchCompleteHandler.obtainMessage(0, parent.mWeakReference));
275      }
276    }
277  }
278
279  private static class ShowImageHandler extends Handler {
280    @Override
281    public void handleMessage(Message msg) {
282      @SuppressWarnings("unchecked")
283      MtpImageView parent = ((WeakReference<MtpImageView>) msg.obj).get();
284      if (parent == null) {
285        return;
286      }
287      Object result;
288      synchronized (parent.mFetchLock) {
289        result = parent.mFetchResult;
290      }
291      if (result == null) {
292        return;
293      }
294      parent.onMtpImageDataFetchedFromDevice(result);
295    }
296  }
297}
298