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