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.app; 18 19 import android.os.Handler; 20 import android.os.Message; 21 import android.os.Process; 22 23 import com.android.gallery3d.common.Utils; 24 import com.android.gallery3d.data.ContentListener; 25 import com.android.gallery3d.data.MediaItem; 26 import com.android.gallery3d.data.MediaObject; 27 import com.android.gallery3d.data.MediaSet; 28 import com.android.gallery3d.data.Path; 29 import com.android.gallery3d.ui.SynchronizedHandler; 30 31 import java.util.Arrays; 32 import java.util.concurrent.Callable; 33 import java.util.concurrent.ExecutionException; 34 import java.util.concurrent.FutureTask; 35 36 public class AlbumSetDataLoader { 37 @SuppressWarnings("unused") 38 private static final String TAG = "AlbumSetDataAdapter"; 39 40 private static final int INDEX_NONE = -1; 41 42 private static final int MIN_LOAD_COUNT = 4; 43 44 private static final int MSG_LOAD_START = 1; 45 private static final int MSG_LOAD_FINISH = 2; 46 private static final int MSG_RUN_OBJECT = 3; 47 48 public static interface DataListener { 49 public void onContentChanged(int index); 50 public void onSizeChanged(int size); 51 } 52 53 private final MediaSet[] mData; 54 private final MediaItem[] mCoverItem; 55 private final int[] mTotalCount; 56 private final long[] mItemVersion; 57 private final long[] mSetVersion; 58 59 private int mActiveStart = 0; 60 private int mActiveEnd = 0; 61 62 private int mContentStart = 0; 63 private int mContentEnd = 0; 64 65 private final MediaSet mSource; 66 private long mSourceVersion = MediaObject.INVALID_DATA_VERSION; 67 private int mSize; 68 69 private DataListener mDataListener; 70 private LoadingListener mLoadingListener; 71 private ReloadTask mReloadTask; 72 73 private final Handler mMainHandler; 74 75 private final MySourceListener mSourceListener = new MySourceListener(); 76 77 public AlbumSetDataLoader(AbstractGalleryActivity activity, MediaSet albumSet, int cacheSize) { 78 mSource = Utils.checkNotNull(albumSet); 79 mCoverItem = new MediaItem[cacheSize]; 80 mData = new MediaSet[cacheSize]; 81 mTotalCount = new int[cacheSize]; 82 mItemVersion = new long[cacheSize]; 83 mSetVersion = new long[cacheSize]; 84 Arrays.fill(mItemVersion, MediaObject.INVALID_DATA_VERSION); 85 Arrays.fill(mSetVersion, MediaObject.INVALID_DATA_VERSION); 86 87 mMainHandler = new SynchronizedHandler(activity.getGLRoot()) { 88 @Override 89 public void handleMessage(Message message) { 90 switch (message.what) { 91 case MSG_RUN_OBJECT: 92 ((Runnable) message.obj).run(); 93 return; 94 case MSG_LOAD_START: 95 if (mLoadingListener != null) mLoadingListener.onLoadingStarted(); 96 return; 97 case MSG_LOAD_FINISH: 98 if (mLoadingListener != null) mLoadingListener.onLoadingFinished(false); 99 return; 100 } 101 } 102 }; 103 } 104 105 public void pause() { 106 mReloadTask.terminate(); 107 mReloadTask = null; 108 mSource.removeContentListener(mSourceListener); 109 } 110 111 public void resume() { 112 mSource.addContentListener(mSourceListener); 113 mReloadTask = new ReloadTask(); 114 mReloadTask.start(); 115 } 116 117 private void assertIsActive(int index) { 118 if (index < mActiveStart && index >= mActiveEnd) { 119 throw new IllegalArgumentException(String.format( 120 "%s not in (%s, %s)", index, mActiveStart, mActiveEnd)); 121 } 122 } 123 124 public MediaSet getMediaSet(int index) { 125 assertIsActive(index); 126 return mData[index % mData.length]; 127 } 128 129 public MediaItem getCoverItem(int index) { 130 assertIsActive(index); 131 return mCoverItem[index % mCoverItem.length]; 132 } 133 134 public int getTotalCount(int index) { 135 assertIsActive(index); 136 return mTotalCount[index % mTotalCount.length]; 137 } 138 139 public int getActiveStart() { 140 return mActiveStart; 141 } 142 143 public boolean isActive(int index) { 144 return index >= mActiveStart && index < mActiveEnd; 145 } 146 147 public int size() { 148 return mSize; 149 } 150 151 // Returns the index of the MediaSet with the given path or 152 // -1 if the path is not cached 153 public int findSet(Path id) { 154 int length = mData.length; 155 for (int i = mContentStart; i < mContentEnd; i++) { 156 MediaSet set = mData[i % length]; 157 if (set != null && id == set.getPath()) { 158 return i; 159 } 160 } 161 return -1; 162 } 163 164 private void clearSlot(int slotIndex) { 165 mData[slotIndex] = null; 166 mCoverItem[slotIndex] = null; 167 mTotalCount[slotIndex] = 0; 168 mItemVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION; 169 mSetVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION; 170 } 171 172 private void setContentWindow(int contentStart, int contentEnd) { 173 if (contentStart == mContentStart && contentEnd == mContentEnd) return; 174 int length = mCoverItem.length; 175 176 int start = this.mContentStart; 177 int end = this.mContentEnd; 178 179 mContentStart = contentStart; 180 mContentEnd = contentEnd; 181 182 if (contentStart >= end || start >= contentEnd) { 183 for (int i = start, n = end; i < n; ++i) { 184 clearSlot(i % length); 185 } 186 } else { 187 for (int i = start; i < contentStart; ++i) { 188 clearSlot(i % length); 189 } 190 for (int i = contentEnd, n = end; i < n; ++i) { 191 clearSlot(i % length); 192 } 193 } 194 mReloadTask.notifyDirty(); 195 } 196 197 public void setActiveWindow(int start, int end) { 198 if (start == mActiveStart && end == mActiveEnd) return; 199 200 Utils.assertTrue(start <= end 201 && end - start <= mCoverItem.length && end <= mSize); 202 203 mActiveStart = start; 204 mActiveEnd = end; 205 206 int length = mCoverItem.length; 207 // If no data is visible, keep the cache content 208 if (start == end) return; 209 210 int contentStart = Utils.clamp((start + end) / 2 - length / 2, 211 0, Math.max(0, mSize - length)); 212 int contentEnd = Math.min(contentStart + length, mSize); 213 if (mContentStart > start || mContentEnd < end 214 || Math.abs(contentStart - mContentStart) > MIN_LOAD_COUNT) { 215 setContentWindow(contentStart, contentEnd); 216 } 217 } 218 219 private class MySourceListener implements ContentListener { 220 @Override 221 public void onContentDirty() { 222 mReloadTask.notifyDirty(); 223 } 224 } 225 226 public void setModelListener(DataListener listener) { 227 mDataListener = listener; 228 } 229 230 public void setLoadingListener(LoadingListener listener) { 231 mLoadingListener = listener; 232 } 233 234 private static class UpdateInfo { 235 public long version; 236 public int index; 237 238 public int size; 239 public MediaSet item; 240 public MediaItem cover; 241 public int totalCount; 242 } 243 244 private class GetUpdateInfo implements Callable<UpdateInfo> { 245 246 private final long mVersion; 247 248 public GetUpdateInfo(long version) { 249 mVersion = version; 250 } 251 252 private int getInvalidIndex(long version) { 253 long setVersion[] = mSetVersion; 254 int length = setVersion.length; 255 for (int i = mContentStart, n = mContentEnd; i < n; ++i) { 256 int index = i % length; 257 if (setVersion[i % length] != version) return i; 258 } 259 return INDEX_NONE; 260 } 261 262 @Override 263 public UpdateInfo call() throws Exception { 264 int index = getInvalidIndex(mVersion); 265 if (index == INDEX_NONE && mSourceVersion == mVersion) return null; 266 UpdateInfo info = new UpdateInfo(); 267 info.version = mSourceVersion; 268 info.index = index; 269 info.size = mSize; 270 return info; 271 } 272 } 273 274 private class UpdateContent implements Callable<Void> { 275 private final UpdateInfo mUpdateInfo; 276 277 public UpdateContent(UpdateInfo info) { 278 mUpdateInfo = info; 279 } 280 281 @Override 282 public Void call() { 283 // Avoid notifying listeners of status change after pause 284 // Otherwise gallery will be in inconsistent state after resume. 285 if (mReloadTask == null) return null; 286 UpdateInfo info = mUpdateInfo; 287 mSourceVersion = info.version; 288 if (mSize != info.size) { 289 mSize = info.size; 290 if (mDataListener != null) mDataListener.onSizeChanged(mSize); 291 if (mContentEnd > mSize) mContentEnd = mSize; 292 if (mActiveEnd > mSize) mActiveEnd = mSize; 293 } 294 // Note: info.index could be INDEX_NONE, i.e., -1 295 if (info.index >= mContentStart && info.index < mContentEnd) { 296 int pos = info.index % mCoverItem.length; 297 mSetVersion[pos] = info.version; 298 long itemVersion = info.item.getDataVersion(); 299 if (mItemVersion[pos] == itemVersion) return null; 300 mItemVersion[pos] = itemVersion; 301 mData[pos] = info.item; 302 mCoverItem[pos] = info.cover; 303 mTotalCount[pos] = info.totalCount; 304 if (mDataListener != null 305 && info.index >= mActiveStart && info.index < mActiveEnd) { 306 mDataListener.onContentChanged(info.index); 307 } 308 } 309 return null; 310 } 311 } 312 313 private <T> T executeAndWait(Callable<T> callable) { 314 FutureTask<T> task = new FutureTask<T>(callable); 315 mMainHandler.sendMessage( 316 mMainHandler.obtainMessage(MSG_RUN_OBJECT, task)); 317 try { 318 return task.get(); 319 } catch (InterruptedException e) { 320 return null; 321 } catch (ExecutionException e) { 322 throw new RuntimeException(e); 323 } 324 } 325 326 // TODO: load active range first 327 private class ReloadTask extends Thread { 328 private volatile boolean mActive = true; 329 private volatile boolean mDirty = true; 330 private volatile boolean mIsLoading = false; 331 332 private void updateLoading(boolean loading) { 333 if (mIsLoading == loading) return; 334 mIsLoading = loading; 335 mMainHandler.sendEmptyMessage(loading ? MSG_LOAD_START : MSG_LOAD_FINISH); 336 } 337 338 @Override 339 public void run() { 340 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 341 342 boolean updateComplete = false; 343 while (mActive) { 344 synchronized (this) { 345 if (mActive && !mDirty && updateComplete) { 346 if (!mSource.isLoading()) updateLoading(false); 347 Utils.waitWithoutInterrupt(this); 348 continue; 349 } 350 } 351 mDirty = false; 352 updateLoading(true); 353 354 long version = mSource.reload(); 355 UpdateInfo info = executeAndWait(new GetUpdateInfo(version)); 356 updateComplete = info == null; 357 if (updateComplete) continue; 358 if (info.version != version) { 359 info.version = version; 360 info.size = mSource.getSubMediaSetCount(); 361 362 // If the size becomes smaller after reload(), we may 363 // receive from GetUpdateInfo an index which is too 364 // big. Because the main thread is not aware of the size 365 // change until we call UpdateContent. 366 if (info.index >= info.size) { 367 info.index = INDEX_NONE; 368 } 369 } 370 if (info.index != INDEX_NONE) { 371 info.item = mSource.getSubMediaSet(info.index); 372 if (info.item == null) continue; 373 info.cover = info.item.getCoverMediaItem(); 374 info.totalCount = info.item.getTotalMediaItemCount(); 375 } 376 executeAndWait(new UpdateContent(info)); 377 } 378 updateLoading(false); 379 } 380 381 public synchronized void notifyDirty() { 382 mDirty = true; 383 notifyAll(); 384 } 385 386 public synchronized void terminate() { 387 mActive = false; 388 notifyAll(); 389 } 390 } 391 } 392 393 394