AlbumSetSlidingWindow.java revision da071d27a1435cce080b5c609d0d833555e5a175
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 17package com.android.gallery3d.ui; 18 19import com.android.gallery3d.R; 20import com.android.gallery3d.app.GalleryActivity; 21import com.android.gallery3d.common.Utils; 22import com.android.gallery3d.data.MediaItem; 23import com.android.gallery3d.data.MediaSet; 24import com.android.gallery3d.data.Path; 25import com.android.gallery3d.ui.AlbumSetView.AlbumSetItem; 26import com.android.gallery3d.util.Future; 27import com.android.gallery3d.util.FutureListener; 28import com.android.gallery3d.util.GalleryUtils; 29import com.android.gallery3d.util.MediaSetUtils; 30import com.android.gallery3d.util.ThreadPool; 31 32import android.graphics.Bitmap; 33import android.graphics.Color; 34import android.os.Message; 35 36public class AlbumSetSlidingWindow implements AlbumSetView.ModelListener { 37 private static final String TAG = "GallerySlidingWindow"; 38 private static final int MSG_LOAD_BITMAP_DONE = 0; 39 private static final int PLACEHOLDER_COLOR = 0xFF222222; 40 41 public static interface Listener { 42 public void onSizeChanged(int size); 43 public void onContentInvalidated(); 44 public void onWindowContentChanged( 45 int slot, AlbumSetItem old, AlbumSetItem update); 46 } 47 48 private final AlbumSetView.Model mSource; 49 private int mSize; 50 private AlbumSetView.LabelSpec mLabelSpec; 51 52 private int mContentStart = 0; 53 private int mContentEnd = 0; 54 55 private int mActiveStart = 0; 56 private int mActiveEnd = 0; 57 58 private Listener mListener; 59 60 private final MyAlbumSetItem mData[]; 61 private SelectionDrawer mSelectionDrawer; 62 private final ColorTexture mWaitLoadingTexture; 63 64 private SynchronizedHandler mHandler; 65 private ThreadPool mThreadPool; 66 67 private int mActiveRequestCount = 0; 68 private String mLoadingLabel; 69 private boolean mIsActive = false; 70 71 private static class MyAlbumSetItem extends AlbumSetItem { 72 public Path setPath; 73 public int sourceType; 74 public int cacheFlag; 75 public int cacheStatus; 76 } 77 78 public AlbumSetSlidingWindow(GalleryActivity activity, 79 AlbumSetView.LabelSpec labelSpec, SelectionDrawer drawer, 80 AlbumSetView.Model source, int cacheSize) { 81 source.setModelListener(this); 82 mLabelSpec = labelSpec; 83 mLoadingLabel = activity.getAndroidContext().getString(R.string.loading); 84 mSource = source; 85 mSelectionDrawer = drawer; 86 mData = new MyAlbumSetItem[cacheSize]; 87 mSize = source.size(); 88 89 mWaitLoadingTexture = new ColorTexture(PLACEHOLDER_COLOR); 90 mWaitLoadingTexture.setSize(1, 1); 91 92 mHandler = new SynchronizedHandler(activity.getGLRoot()) { 93 @Override 94 public void handleMessage(Message message) { 95 Utils.assertTrue(message.what == MSG_LOAD_BITMAP_DONE); 96 ((GalleryDisplayItem) message.obj).onLoadBitmapDone(); 97 } 98 }; 99 100 mThreadPool = activity.getThreadPool(); 101 } 102 103 public void setSelectionDrawer(SelectionDrawer drawer) { 104 mSelectionDrawer = drawer; 105 } 106 107 public void setListener(Listener listener) { 108 mListener = listener; 109 } 110 111 public AlbumSetItem get(int slotIndex) { 112 Utils.assertTrue(isActiveSlot(slotIndex), 113 "invalid slot: %s outsides (%s, %s)", 114 slotIndex, mActiveStart, mActiveEnd); 115 return mData[slotIndex % mData.length]; 116 } 117 118 public int size() { 119 return mSize; 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 (contentStart >= mContentEnd || mContentStart >= contentEnd) { 130 for (int i = mContentStart, n = mContentEnd; i < n; ++i) { 131 freeSlotContent(i); 132 } 133 mSource.setActiveWindow(contentStart, contentEnd); 134 for (int i = contentStart; i < contentEnd; ++i) { 135 prepareSlotContent(i); 136 } 137 } else { 138 for (int i = mContentStart; i < contentStart; ++i) { 139 freeSlotContent(i); 140 } 141 for (int i = contentEnd, n = mContentEnd; i < n; ++i) { 142 freeSlotContent(i); 143 } 144 mSource.setActiveWindow(contentStart, contentEnd); 145 for (int i = contentStart, n = mContentStart; i < n; ++i) { 146 prepareSlotContent(i); 147 } 148 for (int i = mContentEnd; i < contentEnd; ++i) { 149 prepareSlotContent(i); 150 } 151 } 152 153 mContentStart = contentStart; 154 mContentEnd = contentEnd; 155 } 156 157 public void setActiveWindow(int start, int end) { 158 Utils.assertTrue( 159 start <= end && end - start <= mData.length && end <= mSize, 160 "start = %s, end = %s, length = %s, size = %s", 161 start, end, mData.length, mSize); 162 163 AlbumSetItem data[] = mData; 164 165 mActiveStart = start; 166 mActiveEnd = end; 167 168 // If no data is visible, keep the cache content 169 if (start == end) return; 170 171 int contentStart = Utils.clamp((start + end) / 2 - data.length / 2, 172 0, Math.max(0, mSize - data.length)); 173 int contentEnd = Math.min(contentStart + data.length, mSize); 174 setContentWindow(contentStart, contentEnd); 175 if (mIsActive) updateAllImageRequests(); 176 } 177 178 // We would like to request non active slots in the following order: 179 // Order: 8 6 4 2 1 3 5 7 180 // |---------|---------------|---------| 181 // |<- active ->| 182 // |<-------- cached range ----------->| 183 private void requestNonactiveImages() { 184 int range = Math.max( 185 mContentEnd - mActiveEnd, mActiveStart - mContentStart); 186 for (int i = 0 ;i < range; ++i) { 187 requestImagesInSlot(mActiveEnd + i); 188 requestImagesInSlot(mActiveStart - 1 - i); 189 } 190 } 191 192 private void cancelNonactiveImages() { 193 int range = Math.max( 194 mContentEnd - mActiveEnd, mActiveStart - mContentStart); 195 for (int i = 0 ;i < range; ++i) { 196 cancelImagesInSlot(mActiveEnd + i); 197 cancelImagesInSlot(mActiveStart - 1 - i); 198 } 199 } 200 201 private void requestImagesInSlot(int slotIndex) { 202 if (slotIndex < mContentStart || slotIndex >= mContentEnd) return; 203 AlbumSetItem items = mData[slotIndex % mData.length]; 204 for (DisplayItem item : items.covers) { 205 ((GalleryDisplayItem) item).requestImage(); 206 } 207 } 208 209 private void cancelImagesInSlot(int slotIndex) { 210 if (slotIndex < mContentStart || slotIndex >= mContentEnd) return; 211 AlbumSetItem items = mData[slotIndex % mData.length]; 212 for (DisplayItem item : items.covers) { 213 ((GalleryDisplayItem) item).cancelImageRequest(); 214 } 215 } 216 217 private void freeSlotContent(int slotIndex) { 218 AlbumSetItem data[] = mData; 219 int index = slotIndex % data.length; 220 AlbumSetItem original = data[index]; 221 if (original != null) { 222 data[index] = null; 223 for (DisplayItem item : original.covers) { 224 ((GalleryDisplayItem) item).recycle(); 225 } 226 } 227 } 228 229 private long getMediaSetDataVersion(MediaSet set) { 230 return set == null 231 ? MediaSet.INVALID_DATA_VERSION 232 : set.getDataVersion(); 233 } 234 235 private void prepareSlotContent(int slotIndex) { 236 MediaSet set = mSource.getMediaSet(slotIndex); 237 238 MyAlbumSetItem item = new MyAlbumSetItem(); 239 MediaItem[] coverItems = mSource.getCoverItems(slotIndex); 240 item.covers = new GalleryDisplayItem[coverItems.length]; 241 item.sourceType = identifySourceType(set); 242 item.cacheFlag = identifyCacheFlag(set); 243 item.cacheStatus = identifyCacheStatus(set); 244 item.setPath = set == null ? null : set.getPath(); 245 246 for (int i = 0; i < coverItems.length; ++i) { 247 item.covers[i] = new GalleryDisplayItem(slotIndex, i, coverItems[i]); 248 } 249 item.labelItem = new LabelDisplayItem(slotIndex); 250 item.setDataVersion = getMediaSetDataVersion(set); 251 mData[slotIndex % mData.length] = item; 252 } 253 254 private boolean isCoverItemsChanged(int slotIndex) { 255 AlbumSetItem original = mData[slotIndex % mData.length]; 256 if (original == null) return true; 257 MediaItem[] coverItems = mSource.getCoverItems(slotIndex); 258 259 if (original.covers.length != coverItems.length) return true; 260 for (int i = 0, n = coverItems.length; i < n; ++i) { 261 GalleryDisplayItem g = (GalleryDisplayItem) original.covers[i]; 262 if (g.mDataVersion != coverItems[i].getDataVersion()) return true; 263 } 264 return false; 265 } 266 267 private void updateSlotContent(final int slotIndex) { 268 269 MyAlbumSetItem data[] = mData; 270 int pos = slotIndex % data.length; 271 MyAlbumSetItem original = data[pos]; 272 273 if (!isCoverItemsChanged(slotIndex)) { 274 MediaSet set = mSource.getMediaSet(slotIndex); 275 original.sourceType = identifySourceType(set); 276 original.cacheFlag = identifyCacheFlag(set); 277 original.cacheStatus = identifyCacheStatus(set); 278 original.setPath = set == null ? null : set.getPath(); 279 ((LabelDisplayItem) original.labelItem).updateContent(); 280 if (mListener != null) mListener.onContentInvalidated(); 281 return; 282 } 283 284 prepareSlotContent(slotIndex); 285 AlbumSetItem update = data[pos]; 286 287 if (mListener != null && isActiveSlot(slotIndex)) { 288 mListener.onWindowContentChanged(slotIndex, original, update); 289 } 290 if (original != null) { 291 for (DisplayItem item : original.covers) { 292 ((GalleryDisplayItem) item).recycle(); 293 } 294 } 295 } 296 297 private void notifySlotChanged(int slotIndex) { 298 // If the updated content is not cached, ignore it 299 if (slotIndex < mContentStart || slotIndex >= mContentEnd) { 300 Log.w(TAG, String.format( 301 "invalid update: %s is outside (%s, %s)", 302 slotIndex, mContentStart, mContentEnd) ); 303 return; 304 } 305 updateSlotContent(slotIndex); 306 boolean isActiveSlot = isActiveSlot(slotIndex); 307 if (mActiveRequestCount == 0 || isActiveSlot) { 308 for (DisplayItem item : mData[slotIndex % mData.length].covers) { 309 GalleryDisplayItem galleryItem = (GalleryDisplayItem) item; 310 galleryItem.requestImage(); 311 if (isActiveSlot && galleryItem.isRequestInProgress()) { 312 ++mActiveRequestCount; 313 } 314 } 315 } 316 } 317 318 private void updateAllImageRequests() { 319 mActiveRequestCount = 0; 320 for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) { 321 for (DisplayItem item : mData[i % mData.length].covers) { 322 GalleryDisplayItem coverItem = (GalleryDisplayItem) item; 323 coverItem.requestImage(); 324 if (coverItem.isRequestInProgress()) ++mActiveRequestCount; 325 } 326 } 327 if (mActiveRequestCount == 0) { 328 requestNonactiveImages(); 329 } else { 330 cancelNonactiveImages(); 331 } 332 } 333 334 private class GalleryDisplayItem extends AbstractDisplayItem 335 implements FutureListener<Bitmap> { 336 private Future<Bitmap> mFuture; 337 private final int mSlotIndex; 338 private final int mCoverIndex; 339 private final int mMediaType; 340 private Texture mContent; 341 private final long mDataVersion; 342 private boolean mIsPanorama; 343 344 public GalleryDisplayItem(int slotIndex, int coverIndex, MediaItem item) { 345 super(item); 346 mSlotIndex = slotIndex; 347 mCoverIndex = coverIndex; 348 mMediaType = item.getMediaType(); 349 mDataVersion = item.getDataVersion(); 350 mIsPanorama = GalleryUtils.isPanorama(item); 351 updateContent(mWaitLoadingTexture); 352 } 353 354 @Override 355 protected void onBitmapAvailable(Bitmap bitmap) { 356 if (isActiveSlot(mSlotIndex)) { 357 --mActiveRequestCount; 358 if (mActiveRequestCount == 0) requestNonactiveImages(); 359 } 360 if (bitmap != null) { 361 BitmapTexture texture = new BitmapTexture(bitmap, true); 362 texture.setThrottled(true); 363 updateContent(new FadeInTexture(PLACEHOLDER_COLOR, texture)); 364 if (mListener != null) mListener.onContentInvalidated(); 365 } 366 } 367 368 private void updateContent(Texture content) { 369 mContent = content; 370 } 371 372 @Override 373 public int render(GLCanvas canvas, int pass) { 374 // Fit the content into the box 375 int width = mContent.getWidth(); 376 int height = mContent.getHeight(); 377 378 float scalex = mBoxWidth / (float) width; 379 float scaley = mBoxHeight / (float) height; 380 float scale = Math.min(scalex, scaley); 381 382 width = (int) Math.floor(width * scale); 383 height = (int) Math.floor(height * scale); 384 385 // Now draw it 386 int sourceType = SelectionDrawer.DATASOURCE_TYPE_NOT_CATEGORIZED; 387 int cacheFlag = MediaSet.CACHE_FLAG_NO; 388 int cacheStatus = MediaSet.CACHE_STATUS_NOT_CACHED; 389 MyAlbumSetItem set = mData[mSlotIndex % mData.length]; 390 Path path = set.setPath; 391 if (mCoverIndex == 0) { 392 sourceType = set.sourceType; 393 cacheFlag = set.cacheFlag; 394 cacheStatus = set.cacheStatus; 395 } 396 397 mSelectionDrawer.draw(canvas, mContent, width, height, 398 getRotation(), path, sourceType, mMediaType, 399 mIsPanorama, mLabelSpec.labelBackgroundHeight, 400 cacheFlag == MediaSet.CACHE_FLAG_FULL, 401 (cacheFlag == MediaSet.CACHE_FLAG_FULL) 402 && (cacheStatus != MediaSet.CACHE_STATUS_CACHED_FULL)); 403 404 if (mContent != mWaitLoadingTexture && 405 ((FadeInTexture) mContent).isAnimating()) { 406 return RENDER_MORE_FRAME; 407 } else { 408 return 0; 409 } 410 } 411 412 @Override 413 public void startLoadBitmap() { 414 mFuture = mThreadPool.submit(mMediaItem.requestImage( 415 MediaItem.TYPE_MICROTHUMBNAIL), this); 416 } 417 418 @Override 419 public void cancelLoadBitmap() { 420 mFuture.cancel(); 421 } 422 423 @Override 424 public void onFutureDone(Future<Bitmap> future) { 425 mHandler.sendMessage(mHandler.obtainMessage(MSG_LOAD_BITMAP_DONE, this)); 426 } 427 428 private void onLoadBitmapDone() { 429 Future<Bitmap> future = mFuture; 430 mFuture = null; 431 updateImage(future.get(), future.isCancelled()); 432 } 433 434 @Override 435 public String toString() { 436 return String.format("GalleryDisplayItem(%s, %s)", mSlotIndex, mCoverIndex); 437 } 438 } 439 440 private static int identifySourceType(MediaSet set) { 441 if (set == null) { 442 return SelectionDrawer.DATASOURCE_TYPE_NOT_CATEGORIZED; 443 } 444 445 Path path = set.getPath(); 446 if (MediaSetUtils.isCameraSource(path)) { 447 return SelectionDrawer.DATASOURCE_TYPE_CAMERA; 448 } 449 450 int type = SelectionDrawer.DATASOURCE_TYPE_NOT_CATEGORIZED; 451 String prefix = path.getPrefix(); 452 453 if (prefix.equals("picasa")) { 454 type = SelectionDrawer.DATASOURCE_TYPE_PICASA; 455 } else if (prefix.equals("local") || prefix.equals("merge")) { 456 type = SelectionDrawer.DATASOURCE_TYPE_LOCAL; 457 } else if (prefix.equals("mtp")) { 458 type = SelectionDrawer.DATASOURCE_TYPE_MTP; 459 } 460 461 return type; 462 } 463 464 private static int identifyCacheFlag(MediaSet set) { 465 if (set == null || (set.getSupportedOperations() 466 & MediaSet.SUPPORT_CACHE) == 0) { 467 return MediaSet.CACHE_FLAG_NO; 468 } 469 470 return set.getCacheFlag(); 471 } 472 473 private static int identifyCacheStatus(MediaSet set) { 474 if (set == null || (set.getSupportedOperations() 475 & MediaSet.SUPPORT_CACHE) == 0) { 476 return MediaSet.CACHE_STATUS_NOT_CACHED; 477 } 478 479 return set.getCacheStatus(); 480 } 481 482 private class LabelDisplayItem extends DisplayItem { 483 private static final int FONT_COLOR_TITLE = Color.WHITE; 484 private static final int FONT_COLOR_COUNT = 0x80FFFFFF; // 50% white 485 486 private StringTexture mTextureTitle; 487 private StringTexture mTextureCount; 488 private String mTitle; 489 private String mCount; 490 private int mLastWidth; 491 private final int mSlotIndex; 492 private boolean mHasIcon; 493 494 public LabelDisplayItem(int slotIndex) { 495 mSlotIndex = slotIndex; 496 } 497 498 public boolean updateContent() { 499 String title = mLoadingLabel; 500 String count = ""; 501 MediaSet set = mSource.getMediaSet(mSlotIndex); 502 if (set != null) { 503 title = Utils.ensureNotNull(set.getName()); 504 count = "" + set.getTotalMediaItemCount(); 505 } 506 if (Utils.equals(title, mTitle) 507 && Utils.equals(count, mCount) 508 && Utils.equals(mBoxWidth, mLastWidth)) { 509 return false; 510 } 511 mTitle = title; 512 mCount = count; 513 mLastWidth = mBoxWidth; 514 mHasIcon = (identifySourceType(set) != 515 SelectionDrawer.DATASOURCE_TYPE_NOT_CATEGORIZED); 516 517 AlbumSetView.LabelSpec s = mLabelSpec; 518 mTextureTitle = StringTexture.newInstance( 519 title, s.titleFontSize, FONT_COLOR_TITLE, 520 mBoxWidth - s.leftMargin, false); 521 mTextureCount = StringTexture.newInstance( 522 count, s.countFontSize, FONT_COLOR_COUNT, 523 mBoxWidth - s.leftMargin, true); 524 525 return true; 526 } 527 528 @Override 529 public int render(GLCanvas canvas, int pass) { 530 if (mBoxWidth != mLastWidth) { 531 updateContent(); 532 } 533 534 AlbumSetView.LabelSpec s = mLabelSpec; 535 int x = -mBoxWidth / 2; 536 int y = (mBoxHeight + 1) / 2 - s.labelBackgroundHeight; 537 y += s.titleOffset; 538 mTextureTitle.draw(canvas, x + s.leftMargin, y); 539 y += s.titleFontSize + s.countOffset; 540 x += mHasIcon ? s.iconSize : s.leftMargin; 541 mTextureCount.draw(canvas, x, y); 542 return 0; 543 } 544 545 @Override 546 public long getIdentity() { 547 return System.identityHashCode(this); 548 } 549 } 550 551 public void onSizeChanged(int size) { 552 if (mSize != size) { 553 mSize = size; 554 if (mListener != null && mIsActive) mListener.onSizeChanged(mSize); 555 } 556 } 557 558 public void onWindowContentChanged(int index) { 559 if (!mIsActive) { 560 // paused, ignore slot changed event 561 return; 562 } 563 notifySlotChanged(index); 564 } 565 566 public void pause() { 567 mIsActive = false; 568 for (int i = mContentStart, n = mContentEnd; i < n; ++i) { 569 freeSlotContent(i); 570 } 571 } 572 573 public void resume() { 574 mIsActive = true; 575 for (int i = mContentStart, n = mContentEnd; i < n; ++i) { 576 prepareSlotContent(i); 577 } 578 updateAllImageRequests(); 579 } 580} 581