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