1 /*T 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.R; 23 import com.android.gallery3d.app.AlbumSetDataLoader; 24 import com.android.gallery3d.app.GalleryActivity; 25 import com.android.gallery3d.common.Utils; 26 import com.android.gallery3d.data.DataSourceType; 27 import com.android.gallery3d.data.MediaItem; 28 import com.android.gallery3d.data.MediaObject; 29 import com.android.gallery3d.data.MediaSet; 30 import com.android.gallery3d.data.Path; 31 import com.android.gallery3d.util.Future; 32 import com.android.gallery3d.util.FutureListener; 33 import com.android.gallery3d.util.GalleryUtils; 34 import com.android.gallery3d.util.ThreadPool; 35 36 public class AlbumSetSlidingWindow implements AlbumSetDataLoader.DataListener { 37 private static final String TAG = "AlbumSetSlidingWindow"; 38 private static final int MSG_UPDATE_ALBUM_ENTRY = 1; 39 40 public static interface Listener { 41 public void onSizeChanged(int size); 42 public void onContentChanged(); 43 } 44 45 private final AlbumSetDataLoader mSource; 46 private int mSize; 47 48 private int mContentStart = 0; 49 private int mContentEnd = 0; 50 51 private int mActiveStart = 0; 52 private int mActiveEnd = 0; 53 54 private Listener mListener; 55 56 private final AlbumSetEntry mData[]; 57 private final SynchronizedHandler mHandler; 58 private final ThreadPool mThreadPool; 59 private final AlbumLabelMaker mLabelMaker; 60 private final String mLoadingText; 61 private final TextureUploader mTextureUploader; 62 63 private int mActiveRequestCount = 0; 64 private boolean mIsActive = false; 65 private BitmapTexture mLoadingLabel; 66 67 private int mSlotWidth; 68 69 public static class AlbumSetEntry { 70 public MediaSet album; 71 public MediaItem coverItem; 72 public Texture content; 73 public BitmapTexture labelTexture; 74 public BitmapTexture bitmapTexture; 75 public Path setPath; 76 public String title; 77 public int totalCount; 78 public int sourceType; 79 public int cacheFlag; 80 public int cacheStatus; 81 public int rotation; 82 public int mediaType; 83 public boolean isPanorama; 84 public boolean isWaitLoadingDisplayed; 85 public long setDataVersion; 86 public long coverDataVersion; 87 private BitmapLoader labelLoader; 88 private BitmapLoader coverLoader; 89 } 90 91 public AlbumSetSlidingWindow(GalleryActivity activity, 92 AlbumSetDataLoader source, AlbumSetSlotRenderer.LabelSpec labelSpec, int cacheSize) { 93 source.setModelListener(this); 94 mSource = source; 95 mData = new AlbumSetEntry[cacheSize]; 96 mSize = source.size(); 97 mThreadPool = activity.getThreadPool(); 98 99 mLabelMaker = new AlbumLabelMaker(activity.getAndroidContext(), labelSpec); 100 mLoadingText = activity.getAndroidContext().getString(R.string.loading); 101 mTextureUploader = new TextureUploader(activity.getGLRoot()); 102 103 mHandler = new SynchronizedHandler(activity.getGLRoot()) { 104 @Override 105 public void handleMessage(Message message) { 106 Utils.assertTrue(message.what == MSG_UPDATE_ALBUM_ENTRY); 107 ((EntryUpdater) message.obj).updateEntry(); 108 } 109 }; 110 } 111 112 public void setListener(Listener listener) { 113 mListener = listener; 114 } 115 116 public AlbumSetEntry get(int slotIndex) { 117 if (!isActiveSlot(slotIndex)) { 118 Utils.fail("invalid slot: %s outsides (%s, %s)", 119 slotIndex, mActiveStart, mActiveEnd); 120 } 121 return mData[slotIndex % mData.length]; 122 } 123 124 public int size() { 125 return mSize; 126 } 127 128 public boolean isActiveSlot(int slotIndex) { 129 return slotIndex >= mActiveStart && slotIndex < mActiveEnd; 130 } 131 132 private void setContentWindow(int contentStart, int contentEnd) { 133 if (contentStart == mContentStart && contentEnd == mContentEnd) return; 134 135 if (contentStart >= mContentEnd || mContentStart >= contentEnd) { 136 for (int i = mContentStart, n = mContentEnd; i < n; ++i) { 137 freeSlotContent(i); 138 } 139 mSource.setActiveWindow(contentStart, contentEnd); 140 for (int i = contentStart; i < contentEnd; ++i) { 141 prepareSlotContent(i); 142 } 143 } else { 144 for (int i = mContentStart; i < contentStart; ++i) { 145 freeSlotContent(i); 146 } 147 for (int i = contentEnd, n = mContentEnd; i < n; ++i) { 148 freeSlotContent(i); 149 } 150 mSource.setActiveWindow(contentStart, contentEnd); 151 for (int i = contentStart, n = mContentStart; i < n; ++i) { 152 prepareSlotContent(i); 153 } 154 for (int i = mContentEnd; i < contentEnd; ++i) { 155 prepareSlotContent(i); 156 } 157 } 158 159 mContentStart = contentStart; 160 mContentEnd = contentEnd; 161 } 162 163 public void setActiveWindow(int start, int end) { 164 if (!(start <= end && end - start <= mData.length && end <= mSize)) { 165 Utils.fail("start = %s, end = %s, length = %s, size = %s", 166 start, end, mData.length, mSize); 167 } 168 169 AlbumSetEntry data[] = mData; 170 mActiveStart = start; 171 mActiveEnd = end; 172 int contentStart = Utils.clamp((start + end) / 2 - data.length / 2, 173 0, Math.max(0, mSize - data.length)); 174 int contentEnd = Math.min(contentStart + data.length, mSize); 175 setContentWindow(contentStart, contentEnd); 176 177 if (mIsActive) { 178 updateTextureUploadQueue(); 179 updateAllImageRequests(); 180 } 181 } 182 183 // We would like to request non active slots in the following order: 184 // Order: 8 6 4 2 1 3 5 7 185 // |---------|---------------|---------| 186 // |<- active ->| 187 // |<-------- cached range ----------->| 188 private void requestNonactiveImages() { 189 int range = Math.max( 190 mContentEnd - mActiveEnd, mActiveStart - mContentStart); 191 for (int i = 0 ;i < range; ++i) { 192 requestImagesInSlot(mActiveEnd + i); 193 requestImagesInSlot(mActiveStart - 1 - i); 194 } 195 } 196 197 private void cancelNonactiveImages() { 198 int range = Math.max( 199 mContentEnd - mActiveEnd, mActiveStart - mContentStart); 200 for (int i = 0 ;i < range; ++i) { 201 cancelImagesInSlot(mActiveEnd + i); 202 cancelImagesInSlot(mActiveStart - 1 - i); 203 } 204 } 205 206 private void requestImagesInSlot(int slotIndex) { 207 if (slotIndex < mContentStart || slotIndex >= mContentEnd) return; 208 AlbumSetEntry entry = mData[slotIndex % mData.length]; 209 if (entry.coverLoader != null) entry.coverLoader.startLoad(); 210 if (entry.labelLoader != null) entry.labelLoader.startLoad(); 211 } 212 213 private void cancelImagesInSlot(int slotIndex) { 214 if (slotIndex < mContentStart || slotIndex >= mContentEnd) return; 215 AlbumSetEntry entry = mData[slotIndex % mData.length]; 216 if (entry.coverLoader != null) entry.coverLoader.cancelLoad(); 217 if (entry.labelLoader != null) entry.labelLoader.cancelLoad(); 218 } 219 220 private static long getDataVersion(MediaObject object) { 221 return object == null 222 ? MediaSet.INVALID_DATA_VERSION 223 : object.getDataVersion(); 224 } 225 226 private void freeSlotContent(int slotIndex) { 227 AlbumSetEntry entry = mData[slotIndex % mData.length]; 228 if (entry.coverLoader != null) entry.coverLoader.recycle(); 229 if (entry.labelLoader != null) entry.labelLoader.recycle(); 230 if (entry.labelTexture != null) entry.labelTexture.recycle(); 231 if (entry.bitmapTexture != null) entry.bitmapTexture.recycle(); 232 mData[slotIndex % mData.length] = null; 233 } 234 235 private boolean isLabelChanged( 236 AlbumSetEntry entry, String title, int totalCount, int sourceType) { 237 return !Utils.equals(entry.title, title) 238 || entry.totalCount != totalCount 239 || entry.sourceType != sourceType; 240 } 241 242 private void updateAlbumSetEntry(AlbumSetEntry entry, int slotIndex) { 243 MediaSet album = mSource.getMediaSet(slotIndex); 244 MediaItem cover = mSource.getCoverItem(slotIndex); 245 int totalCount = mSource.getTotalCount(slotIndex); 246 247 entry.album = album; 248 entry.setDataVersion = getDataVersion(album); 249 entry.cacheFlag = identifyCacheFlag(album); 250 entry.cacheStatus = identifyCacheStatus(album); 251 entry.setPath = (album == null) ? null : album.getPath(); 252 253 String title = (album == null) ? "" : Utils.ensureNotNull(album.getName()); 254 int sourceType = DataSourceType.identifySourceType(album); 255 if (isLabelChanged(entry, title, totalCount, sourceType)) { 256 entry.title = title; 257 entry.totalCount = totalCount; 258 entry.sourceType = sourceType; 259 if (entry.labelLoader != null) { 260 entry.labelLoader.recycle(); 261 entry.labelLoader = null; 262 entry.labelTexture = null; 263 } 264 if (album != null) { 265 entry.labelLoader = new AlbumLabelLoader( 266 slotIndex, title, totalCount, sourceType); 267 } 268 } 269 270 entry.coverItem = cover; 271 if (getDataVersion(cover) != entry.coverDataVersion) { 272 entry.coverDataVersion = getDataVersion(cover); 273 entry.isPanorama = GalleryUtils.isPanorama(cover); 274 entry.rotation = (cover == null) ? 0 : cover.getRotation(); 275 entry.mediaType = (cover == null) ? 0 : cover.getMediaType(); 276 if (entry.coverLoader != null) { 277 entry.coverLoader.recycle(); 278 entry.coverLoader = null; 279 entry.bitmapTexture = null; 280 entry.content = null; 281 } 282 if (cover != null) { 283 entry.coverLoader = new AlbumCoverLoader(slotIndex, cover); 284 } 285 } 286 } 287 288 private void prepareSlotContent(int slotIndex) { 289 AlbumSetEntry entry = new AlbumSetEntry(); 290 updateAlbumSetEntry(entry, slotIndex); 291 mData[slotIndex % mData.length] = entry; 292 } 293 294 private static boolean startLoadBitmap(BitmapLoader loader) { 295 if (loader == null) return false; 296 loader.startLoad(); 297 return loader.isRequestInProgress(); 298 } 299 300 private void uploadBackgroundTextureInSlot(int index) { 301 if (index < mContentStart || index >= mContentEnd) return; 302 AlbumSetEntry entry = mData[index % mData.length]; 303 if (entry.bitmapTexture != null) { 304 mTextureUploader.addBgTexture(entry.bitmapTexture); 305 } 306 if (entry.labelTexture != null) { 307 mTextureUploader.addBgTexture(entry.labelTexture); 308 } 309 } 310 311 private void updateTextureUploadQueue() { 312 if (!mIsActive) return; 313 mTextureUploader.clear(); 314 315 // Upload foreground texture 316 for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) { 317 AlbumSetEntry entry = mData[i % mData.length]; 318 if (entry.bitmapTexture != null) { 319 mTextureUploader.addFgTexture(entry.bitmapTexture); 320 } 321 if (entry.labelTexture != null) { 322 mTextureUploader.addFgTexture(entry.labelTexture); 323 } 324 } 325 326 // add background textures 327 int range = Math.max( 328 (mContentEnd - mActiveEnd), (mActiveStart - mContentStart)); 329 for (int i = 0; i < range; ++i) { 330 uploadBackgroundTextureInSlot(mActiveEnd + i); 331 uploadBackgroundTextureInSlot(mActiveStart - i - 1); 332 } 333 } 334 335 private void updateAllImageRequests() { 336 mActiveRequestCount = 0; 337 for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) { 338 AlbumSetEntry entry = mData[i % mData.length]; 339 if (startLoadBitmap(entry.coverLoader)) ++mActiveRequestCount; 340 if (startLoadBitmap(entry.labelLoader)) ++mActiveRequestCount; 341 } 342 if (mActiveRequestCount == 0) { 343 requestNonactiveImages(); 344 } else { 345 cancelNonactiveImages(); 346 } 347 } 348 349 @Override 350 public void onSizeChanged(int size) { 351 if (mIsActive && mSize != size) { 352 mSize = size; 353 if (mListener != null) mListener.onSizeChanged(mSize); 354 if (mContentEnd > mSize) mContentEnd = mSize; 355 if (mActiveEnd > mSize) mActiveEnd = mSize; 356 } 357 } 358 359 @Override 360 public void onContentChanged(int index) { 361 if (!mIsActive) { 362 // paused, ignore slot changed event 363 return; 364 } 365 366 // If the updated content is not cached, ignore it 367 if (index < mContentStart || index >= mContentEnd) { 368 Log.w(TAG, String.format( 369 "invalid update: %s is outside (%s, %s)", 370 index, mContentStart, mContentEnd) ); 371 return; 372 } 373 374 AlbumSetEntry entry = mData[index % mData.length]; 375 updateAlbumSetEntry(entry, index); 376 updateAllImageRequests(); 377 updateTextureUploadQueue(); 378 if (mListener != null && isActiveSlot(index)) { 379 mListener.onContentChanged(); 380 } 381 } 382 383 public BitmapTexture getLoadingTexture() { 384 if (mLoadingLabel == null) { 385 Bitmap bitmap = mLabelMaker.requestLabel( 386 mLoadingText, "", DataSourceType.TYPE_NOT_CATEGORIZED) 387 .run(ThreadPool.JOB_CONTEXT_STUB); 388 mLoadingLabel = new BitmapTexture(bitmap); 389 mLoadingLabel.setOpaque(false); 390 } 391 return mLoadingLabel; 392 } 393 394 public void pause() { 395 mIsActive = false; 396 mTextureUploader.clear(); 397 for (int i = mContentStart, n = mContentEnd; i < n; ++i) { 398 freeSlotContent(i); 399 } 400 mLabelMaker.clearRecycledLabels(); 401 } 402 403 public void resume() { 404 mIsActive = true; 405 for (int i = mContentStart, n = mContentEnd; i < n; ++i) { 406 prepareSlotContent(i); 407 } 408 updateAllImageRequests(); 409 } 410 411 private static interface EntryUpdater { 412 public void updateEntry(); 413 } 414 415 private class AlbumCoverLoader extends BitmapLoader implements EntryUpdater { 416 private MediaItem mMediaItem; 417 private final int mSlotIndex; 418 419 public AlbumCoverLoader(int slotIndex, MediaItem item) { 420 mSlotIndex = slotIndex; 421 mMediaItem = item; 422 } 423 424 @Override 425 protected void recycleBitmap(Bitmap bitmap) { 426 MediaItem.getMicroThumbPool().recycle(bitmap); 427 } 428 429 @Override 430 protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l) { 431 return mThreadPool.submit(mMediaItem.requestImage( 432 MediaItem.TYPE_MICROTHUMBNAIL), l); 433 } 434 435 @Override 436 protected void onLoadComplete(Bitmap bitmap) { 437 mHandler.obtainMessage(MSG_UPDATE_ALBUM_ENTRY, this).sendToTarget(); 438 } 439 440 @Override 441 public void updateEntry() { 442 Bitmap bitmap = getBitmap(); 443 if (bitmap == null) return; // error or recycled 444 445 AlbumSetEntry entry = mData[mSlotIndex % mData.length]; 446 BitmapTexture texture = new BitmapTexture(bitmap); 447 entry.bitmapTexture = texture; 448 entry.content = texture; 449 450 if (isActiveSlot(mSlotIndex)) { 451 mTextureUploader.addFgTexture(texture); 452 --mActiveRequestCount; 453 if (mActiveRequestCount == 0) requestNonactiveImages(); 454 if (mListener != null) mListener.onContentChanged(); 455 } else { 456 mTextureUploader.addBgTexture(texture); 457 } 458 } 459 } 460 461 private static int identifyCacheFlag(MediaSet set) { 462 if (set == null || (set.getSupportedOperations() 463 & MediaSet.SUPPORT_CACHE) == 0) { 464 return MediaSet.CACHE_FLAG_NO; 465 } 466 467 return set.getCacheFlag(); 468 } 469 470 private static int identifyCacheStatus(MediaSet set) { 471 if (set == null || (set.getSupportedOperations() 472 & MediaSet.SUPPORT_CACHE) == 0) { 473 return MediaSet.CACHE_STATUS_NOT_CACHED; 474 } 475 476 return set.getCacheStatus(); 477 } 478 479 private class AlbumLabelLoader extends BitmapLoader implements EntryUpdater { 480 private final int mSlotIndex; 481 private final String mTitle; 482 private final int mTotalCount; 483 private final int mSourceType; 484 485 public AlbumLabelLoader( 486 int slotIndex, String title, int totalCount, int sourceType) { 487 mSlotIndex = slotIndex; 488 mTitle = title; 489 mTotalCount = totalCount; 490 mSourceType = sourceType; 491 } 492 493 @Override 494 protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l) { 495 return mThreadPool.submit(mLabelMaker.requestLabel( 496 mTitle, String.valueOf(mTotalCount), mSourceType), l); 497 } 498 499 @Override 500 protected void recycleBitmap(Bitmap bitmap) { 501 mLabelMaker.recycleLabel(bitmap); 502 } 503 504 @Override 505 protected void onLoadComplete(Bitmap bitmap) { 506 mHandler.obtainMessage(MSG_UPDATE_ALBUM_ENTRY, this).sendToTarget(); 507 } 508 509 @Override 510 public void updateEntry() { 511 Bitmap bitmap = getBitmap(); 512 if (bitmap == null) return; // Error or recycled 513 514 AlbumSetEntry entry = mData[mSlotIndex % mData.length]; 515 BitmapTexture texture = new BitmapTexture(bitmap); 516 texture.setOpaque(false); 517 entry.labelTexture = texture; 518 519 if (isActiveSlot(mSlotIndex)) { 520 mTextureUploader.addFgTexture(texture); 521 --mActiveRequestCount; 522 if (mActiveRequestCount == 0) requestNonactiveImages(); 523 if (mListener != null) mListener.onContentChanged(); 524 } else { 525 mTextureUploader.addBgTexture(texture); 526 } 527 } 528 } 529 530 public void onSlotSizeChanged(int width, int height) { 531 if (mSlotWidth == width) return; 532 533 mSlotWidth = width; 534 mLoadingLabel = null; 535 mLabelMaker.setLabelWidth(mSlotWidth); 536 537 if (!mIsActive) return; 538 539 for (int i = mContentStart, n = mContentEnd; i < n; ++i) { 540 AlbumSetEntry entry = mData[i % mData.length]; 541 if (entry.labelLoader != null) { 542 entry.labelLoader.recycle(); 543 entry.labelLoader = null; 544 entry.labelTexture = null; 545 } 546 if (entry.album != null) { 547 entry.labelLoader = new AlbumLabelLoader(i, 548 entry.title, entry.totalCount, entry.sourceType); 549 } 550 } 551 updateAllImageRequests(); 552 updateTextureUploadQueue(); 553 } 554 } 555