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