1 /* 2 * Copyright (C) 2010 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 17 package com.android.gallery3d.ui; 18 19 import android.graphics.Bitmap; 20 import android.os.Message; 21 22 import com.android.gallery3d.app.AlbumDataLoader; 23 import com.android.gallery3d.app.GalleryActivity; 24 import com.android.gallery3d.common.Utils; 25 import com.android.gallery3d.data.MediaItem; 26 import com.android.gallery3d.data.Path; 27 import com.android.gallery3d.util.Future; 28 import com.android.gallery3d.util.FutureListener; 29 import com.android.gallery3d.util.GalleryUtils; 30 import com.android.gallery3d.util.JobLimiter; 31 32 public class AlbumSlidingWindow implements AlbumDataLoader.DataListener { 33 @SuppressWarnings("unused") 34 private static final String TAG = "AlbumSlidingWindow"; 35 36 private static final int MSG_UPDATE_ENTRY = 0; 37 private static final int JOB_LIMIT = 2; 38 39 public static interface Listener { 40 public void onSizeChanged(int size); 41 public void onContentChanged(); 42 } 43 44 public static class AlbumEntry { 45 public MediaItem item; 46 public Path path; 47 public boolean isPanorama; 48 public int rotation; 49 public int mediaType; 50 public boolean isWaitDisplayed; 51 public BitmapTexture bitmapTexture; 52 public Texture content; 53 private BitmapLoader contentLoader; 54 } 55 56 private final AlbumDataLoader mSource; 57 private final AlbumEntry mData[]; 58 private final SynchronizedHandler mHandler; 59 private final JobLimiter mThreadPool; 60 private final TextureUploader mTextureUploader; 61 62 private int mSize; 63 64 private int mContentStart = 0; 65 private int mContentEnd = 0; 66 67 private int mActiveStart = 0; 68 private int mActiveEnd = 0; 69 70 private Listener mListener; 71 72 private int mActiveRequestCount = 0; 73 private boolean mIsActive = false; 74 75 public AlbumSlidingWindow(GalleryActivity activity, 76 AlbumDataLoader source, int cacheSize) { 77 source.setDataListener(this); 78 mSource = source; 79 mData = new AlbumEntry[cacheSize]; 80 mSize = source.size(); 81 82 mHandler = new SynchronizedHandler(activity.getGLRoot()) { 83 @Override 84 public void handleMessage(Message message) { 85 Utils.assertTrue(message.what == MSG_UPDATE_ENTRY); 86 ((ThumbnailLoader) message.obj).updateEntry(); 87 } 88 }; 89 90 mThreadPool = new JobLimiter(activity.getThreadPool(), JOB_LIMIT); 91 mTextureUploader = new TextureUploader(activity.getGLRoot()); 92 } 93 94 public void setListener(Listener listener) { 95 mListener = listener; 96 } 97 98 public AlbumEntry get(int slotIndex) { 99 if (!isActiveSlot(slotIndex)) { 100 Utils.fail("invalid slot: %s outsides (%s, %s)", 101 slotIndex, mActiveStart, mActiveEnd); 102 } 103 return mData[slotIndex % mData.length]; 104 } 105 106 public boolean isActiveSlot(int slotIndex) { 107 return slotIndex >= mActiveStart && slotIndex < mActiveEnd; 108 } 109 110 private void setContentWindow(int contentStart, int contentEnd) { 111 if (contentStart == mContentStart && contentEnd == mContentEnd) return; 112 113 if (!mIsActive) { 114 mContentStart = contentStart; 115 mContentEnd = contentEnd; 116 mSource.setActiveWindow(contentStart, contentEnd); 117 return; 118 } 119 120 if (contentStart >= mContentEnd || mContentStart >= contentEnd) { 121 for (int i = mContentStart, n = mContentEnd; i < n; ++i) { 122 freeSlotContent(i); 123 } 124 mSource.setActiveWindow(contentStart, contentEnd); 125 for (int i = contentStart; i < contentEnd; ++i) { 126 prepareSlotContent(i); 127 } 128 } else { 129 for (int i = mContentStart; i < contentStart; ++i) { 130 freeSlotContent(i); 131 } 132 for (int i = contentEnd, n = mContentEnd; i < n; ++i) { 133 freeSlotContent(i); 134 } 135 mSource.setActiveWindow(contentStart, contentEnd); 136 for (int i = contentStart, n = mContentStart; i < n; ++i) { 137 prepareSlotContent(i); 138 } 139 for (int i = mContentEnd; i < contentEnd; ++i) { 140 prepareSlotContent(i); 141 } 142 } 143 144 mContentStart = contentStart; 145 mContentEnd = contentEnd; 146 } 147 148 public void setActiveWindow(int start, int end) { 149 if (!(start <= end && end - start <= mData.length && end <= mSize)) { 150 Utils.fail("%s, %s, %s, %s", start, end, mData.length, mSize); 151 } 152 AlbumEntry data[] = mData; 153 154 mActiveStart = start; 155 mActiveEnd = end; 156 157 int contentStart = Utils.clamp((start + end) / 2 - data.length / 2, 158 0, Math.max(0, mSize - data.length)); 159 int contentEnd = Math.min(contentStart + data.length, mSize); 160 setContentWindow(contentStart, contentEnd); 161 updateTextureUploadQueue(); 162 if (mIsActive) updateAllImageRequests(); 163 } 164 165 private void uploadBgTextureInSlot(int index) { 166 if (index < mContentEnd && index >= mContentStart) { 167 AlbumEntry entry = mData[index % mData.length]; 168 if (entry.bitmapTexture != null) { 169 mTextureUploader.addBgTexture(entry.bitmapTexture); 170 } 171 } 172 } 173 174 private void updateTextureUploadQueue() { 175 if (!mIsActive) return; 176 mTextureUploader.clear(); 177 178 // add foreground textures 179 for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) { 180 AlbumEntry entry = mData[i % mData.length]; 181 if (entry.bitmapTexture != null) { 182 mTextureUploader.addFgTexture(entry.bitmapTexture); 183 } 184 } 185 186 // add background textures 187 int range = Math.max( 188 (mContentEnd - mActiveEnd), (mActiveStart - mContentStart)); 189 for (int i = 0; i < range; ++i) { 190 uploadBgTextureInSlot(mActiveEnd + i); 191 uploadBgTextureInSlot(mActiveStart - i - 1); 192 } 193 } 194 195 // We would like to request non active slots in the following order: 196 // Order: 8 6 4 2 1 3 5 7 197 // |---------|---------------|---------| 198 // |<- active ->| 199 // |<-------- cached range ----------->| 200 private void requestNonactiveImages() { 201 int range = Math.max( 202 (mContentEnd - mActiveEnd), (mActiveStart - mContentStart)); 203 for (int i = 0 ;i < range; ++i) { 204 requestSlotImage(mActiveEnd + i); 205 requestSlotImage(mActiveStart - 1 - i); 206 } 207 } 208 209 // return whether the request is in progress or not 210 private boolean requestSlotImage(int slotIndex) { 211 if (slotIndex < mContentStart || slotIndex >= mContentEnd) return false; 212 AlbumEntry entry = mData[slotIndex % mData.length]; 213 if (entry.content != null || entry.item == null) return false; 214 215 entry.contentLoader.startLoad(); 216 return entry.contentLoader.isRequestInProgress(); 217 } 218 219 private void cancelNonactiveImages() { 220 int range = Math.max( 221 (mContentEnd - mActiveEnd), (mActiveStart - mContentStart)); 222 for (int i = 0 ;i < range; ++i) { 223 cancelSlotImage(mActiveEnd + i); 224 cancelSlotImage(mActiveStart - 1 - i); 225 } 226 } 227 228 private void cancelSlotImage(int slotIndex) { 229 if (slotIndex < mContentStart || slotIndex >= mContentEnd) return; 230 AlbumEntry item = mData[slotIndex % mData.length]; 231 if (item.contentLoader != null) item.contentLoader.cancelLoad(); 232 } 233 234 private void freeSlotContent(int slotIndex) { 235 AlbumEntry data[] = mData; 236 int index = slotIndex % data.length; 237 AlbumEntry entry = data[index]; 238 if (entry.contentLoader != null) entry.contentLoader.recycle(); 239 if (entry.bitmapTexture != null) entry.bitmapTexture.recycle(); 240 data[index] = null; 241 } 242 243 private void prepareSlotContent(int slotIndex) { 244 AlbumEntry entry = new AlbumEntry(); 245 MediaItem item = mSource.get(slotIndex); // item could be null; 246 entry.item = item; 247 entry.isPanorama = GalleryUtils.isPanorama(entry.item); 248 entry.mediaType = (item == null) 249 ? MediaItem.MEDIA_TYPE_UNKNOWN 250 : entry.item.getMediaType(); 251 entry.path = (item == null) ? null : item.getPath(); 252 entry.rotation = (item == null) ? 0 : item.getRotation(); 253 entry.contentLoader = new ThumbnailLoader(slotIndex, entry.item); 254 mData[slotIndex % mData.length] = entry; 255 } 256 257 private void updateAllImageRequests() { 258 mActiveRequestCount = 0; 259 for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) { 260 if (requestSlotImage(i)) ++mActiveRequestCount; 261 } 262 if (mActiveRequestCount == 0) { 263 requestNonactiveImages(); 264 } else { 265 cancelNonactiveImages(); 266 } 267 } 268 269 private class ThumbnailLoader extends BitmapLoader { 270 private final int mSlotIndex; 271 private final MediaItem mItem; 272 273 public ThumbnailLoader(int slotIndex, MediaItem item) { 274 mSlotIndex = slotIndex; 275 mItem = item; 276 } 277 278 @Override 279 protected void recycleBitmap(Bitmap bitmap) { 280 MediaItem.getMicroThumbPool().recycle(bitmap); 281 } 282 283 @Override 284 protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l) { 285 return mThreadPool.submit( 286 mItem.requestImage(MediaItem.TYPE_MICROTHUMBNAIL), this); 287 } 288 289 @Override 290 protected void onLoadComplete(Bitmap bitmap) { 291 mHandler.obtainMessage(MSG_UPDATE_ENTRY, this).sendToTarget(); 292 } 293 294 public void updateEntry() { 295 Bitmap bitmap = getBitmap(); 296 if (bitmap == null) return; // error or recycled 297 298 AlbumEntry entry = mData[mSlotIndex % mData.length]; 299 entry.bitmapTexture = new BitmapTexture(bitmap); 300 entry.content = entry.bitmapTexture; 301 302 if (isActiveSlot(mSlotIndex)) { 303 mTextureUploader.addFgTexture(entry.bitmapTexture); 304 --mActiveRequestCount; 305 if (mActiveRequestCount == 0) requestNonactiveImages(); 306 if (mListener != null) mListener.onContentChanged(); 307 } else { 308 mTextureUploader.addBgTexture(entry.bitmapTexture); 309 } 310 } 311 } 312 313 @Override 314 public void onSizeChanged(int size) { 315 if (mSize != size) { 316 mSize = size; 317 if (mListener != null) mListener.onSizeChanged(mSize); 318 if (mContentEnd > mSize) mContentEnd = mSize; 319 if (mActiveEnd > mSize) mActiveEnd = mSize; 320 } 321 } 322 323 @Override 324 public void onContentChanged(int index) { 325 if (index >= mContentStart && index < mContentEnd && mIsActive) { 326 freeSlotContent(index); 327 prepareSlotContent(index); 328 updateAllImageRequests(); 329 if (mListener != null && isActiveSlot(index)) { 330 mListener.onContentChanged(); 331 } 332 } 333 } 334 335 public void resume() { 336 mIsActive = true; 337 for (int i = mContentStart, n = mContentEnd; i < n; ++i) { 338 prepareSlotContent(i); 339 } 340 updateAllImageRequests(); 341 } 342 343 public void pause() { 344 mIsActive = false; 345 mTextureUploader.clear(); 346 for (int i = mContentStart, n = mContentEnd; i < n; ++i) { 347 freeSlotContent(i); 348 } 349 } 350 } 351