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