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.app; 18 19import android.os.Handler; 20import android.os.Message; 21import android.os.Process; 22 23import com.android.gallery3d.common.Utils; 24import com.android.gallery3d.data.ContentListener; 25import com.android.gallery3d.data.MediaItem; 26import com.android.gallery3d.data.MediaObject; 27import com.android.gallery3d.data.MediaSet; 28import com.android.gallery3d.data.Path; 29import com.android.gallery3d.ui.SynchronizedHandler; 30 31import java.util.ArrayList; 32import java.util.Arrays; 33import java.util.concurrent.Callable; 34import java.util.concurrent.ExecutionException; 35import java.util.concurrent.FutureTask; 36 37public class AlbumDataLoader { 38 @SuppressWarnings("unused") 39 private static final String TAG = "AlbumDataAdapter"; 40 private static final int DATA_CACHE_SIZE = 1000; 41 42 private static final int MSG_LOAD_START = 1; 43 private static final int MSG_LOAD_FINISH = 2; 44 private static final int MSG_RUN_OBJECT = 3; 45 46 private static final int MIN_LOAD_COUNT = 32; 47 private static final int MAX_LOAD_COUNT = 64; 48 49 private final MediaItem[] mData; 50 private final long[] mItemVersion; 51 private final long[] mSetVersion; 52 53 public static interface DataListener { 54 public void onContentChanged(int index); 55 public void onSizeChanged(int size); 56 } 57 58 private int mActiveStart = 0; 59 private int mActiveEnd = 0; 60 61 private int mContentStart = 0; 62 private int mContentEnd = 0; 63 64 private final MediaSet mSource; 65 private long mSourceVersion = MediaObject.INVALID_DATA_VERSION; 66 67 private final Handler mMainHandler; 68 private int mSize = 0; 69 70 private DataListener mDataListener; 71 private MySourceListener mSourceListener = new MySourceListener(); 72 private LoadingListener mLoadingListener; 73 74 private ReloadTask mReloadTask; 75 // the data version on which last loading failed 76 private long mFailedVersion = MediaObject.INVALID_DATA_VERSION; 77 78 public AlbumDataLoader(AbstractGalleryActivity context, MediaSet mediaSet) { 79 mSource = mediaSet; 80 81 mData = new MediaItem[DATA_CACHE_SIZE]; 82 mItemVersion = new long[DATA_CACHE_SIZE]; 83 mSetVersion = new long[DATA_CACHE_SIZE]; 84 Arrays.fill(mItemVersion, MediaObject.INVALID_DATA_VERSION); 85 Arrays.fill(mSetVersion, MediaObject.INVALID_DATA_VERSION); 86 87 mMainHandler = new SynchronizedHandler(context.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) { 99 boolean loadingFailed = 100 (mFailedVersion != MediaObject.INVALID_DATA_VERSION); 101 mLoadingListener.onLoadingFinished(loadingFailed); 102 } 103 return; 104 } 105 } 106 }; 107 } 108 109 public void resume() { 110 mSource.addContentListener(mSourceListener); 111 mReloadTask = new ReloadTask(); 112 mReloadTask.start(); 113 } 114 115 public void pause() { 116 mReloadTask.terminate(); 117 mReloadTask = null; 118 mSource.removeContentListener(mSourceListener); 119 } 120 121 public MediaItem get(int index) { 122 if (!isActive(index)) { 123 return mSource.getMediaItem(index, 1).get(0); 124 } 125 return mData[index % mData.length]; 126 } 127 128 public int getActiveStart() { 129 return mActiveStart; 130 } 131 132 public boolean isActive(int index) { 133 return index >= mActiveStart && index < mActiveEnd; 134 } 135 136 public int size() { 137 return mSize; 138 } 139 140 // Returns the index of the MediaItem with the given path or 141 // -1 if the path is not cached 142 public int findItem(Path id) { 143 for (int i = mContentStart; i < mContentEnd; i++) { 144 MediaItem item = mData[i % DATA_CACHE_SIZE]; 145 if (item != null && id == item.getPath()) { 146 return i; 147 } 148 } 149 return -1; 150 } 151 152 private void clearSlot(int slotIndex) { 153 mData[slotIndex] = null; 154 mItemVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION; 155 mSetVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION; 156 } 157 158 private void setContentWindow(int contentStart, int contentEnd) { 159 if (contentStart == mContentStart && contentEnd == mContentEnd) return; 160 int end = mContentEnd; 161 int start = mContentStart; 162 163 // We need change the content window before calling reloadData(...) 164 synchronized (this) { 165 mContentStart = contentStart; 166 mContentEnd = contentEnd; 167 } 168 long[] itemVersion = mItemVersion; 169 long[] setVersion = mSetVersion; 170 if (contentStart >= end || start >= contentEnd) { 171 for (int i = start, n = end; i < n; ++i) { 172 clearSlot(i % DATA_CACHE_SIZE); 173 } 174 } else { 175 for (int i = start; i < contentStart; ++i) { 176 clearSlot(i % DATA_CACHE_SIZE); 177 } 178 for (int i = contentEnd, n = end; i < n; ++i) { 179 clearSlot(i % DATA_CACHE_SIZE); 180 } 181 } 182 if (mReloadTask != null) 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 <= mData.length && end <= mSize); 190 191 int length = mData.length; 192 mActiveStart = start; 193 mActiveEnd = end; 194 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 @Override 209 public void onContentDirty() { 210 if (mReloadTask != null) mReloadTask.notifyDirty(); 211 } 212 } 213 214 public void setDataListener(DataListener listener) { 215 mDataListener = listener; 216 } 217 218 public void setLoadingListener(LoadingListener listener) { 219 mLoadingListener = listener; 220 } 221 222 private <T> T executeAndWait(Callable<T> callable) { 223 FutureTask<T> task = new FutureTask<T>(callable); 224 mMainHandler.sendMessage( 225 mMainHandler.obtainMessage(MSG_RUN_OBJECT, task)); 226 try { 227 return task.get(); 228 } catch (InterruptedException e) { 229 return null; 230 } catch (ExecutionException e) { 231 throw new RuntimeException(e); 232 } 233 } 234 235 private static class UpdateInfo { 236 public long version; 237 public int reloadStart; 238 public int reloadCount; 239 240 public int size; 241 public ArrayList<MediaItem> items; 242 } 243 244 private class GetUpdateInfo implements Callable<UpdateInfo> { 245 private final long mVersion; 246 247 public GetUpdateInfo(long version) { 248 mVersion = version; 249 } 250 251 @Override 252 public UpdateInfo call() throws Exception { 253 if (mFailedVersion == mVersion) { 254 // previous loading failed, return null to pause loading 255 return null; 256 } 257 UpdateInfo info = new UpdateInfo(); 258 long version = mVersion; 259 info.version = mSourceVersion; 260 info.size = mSize; 261 long setVersion[] = mSetVersion; 262 for (int i = mContentStart, n = mContentEnd; i < n; ++i) { 263 int index = i % DATA_CACHE_SIZE; 264 if (setVersion[index] != version) { 265 info.reloadStart = i; 266 info.reloadCount = Math.min(MAX_LOAD_COUNT, n - i); 267 return info; 268 } 269 } 270 return mSourceVersion == mVersion ? null : info; 271 } 272 } 273 274 private class UpdateContent implements Callable<Void> { 275 276 private UpdateInfo mUpdateInfo; 277 278 public UpdateContent(UpdateInfo info) { 279 mUpdateInfo = info; 280 } 281 282 @Override 283 public Void call() throws Exception { 284 UpdateInfo info = mUpdateInfo; 285 mSourceVersion = info.version; 286 if (mSize != info.size) { 287 mSize = info.size; 288 if (mDataListener != null) mDataListener.onSizeChanged(mSize); 289 if (mContentEnd > mSize) mContentEnd = mSize; 290 if (mActiveEnd > mSize) mActiveEnd = mSize; 291 } 292 293 ArrayList<MediaItem> items = info.items; 294 295 mFailedVersion = MediaObject.INVALID_DATA_VERSION; 296 if ((items == null) || items.isEmpty()) { 297 if (info.reloadCount > 0) { 298 mFailedVersion = info.version; 299 Log.d(TAG, "loading failed: " + mFailedVersion); 300 } 301 return null; 302 } 303 int start = Math.max(info.reloadStart, mContentStart); 304 int end = Math.min(info.reloadStart + items.size(), mContentEnd); 305 306 for (int i = start; i < end; ++i) { 307 int index = i % DATA_CACHE_SIZE; 308 mSetVersion[index] = info.version; 309 MediaItem updateItem = items.get(i - info.reloadStart); 310 long itemVersion = updateItem.getDataVersion(); 311 if (mItemVersion[index] != itemVersion) { 312 mItemVersion[index] = itemVersion; 313 mData[index] = updateItem; 314 if (mDataListener != null && i >= mActiveStart && i < mActiveEnd) { 315 mDataListener.onContentChanged(i); 316 } 317 } 318 } 319 return null; 320 } 321 } 322 323 /* 324 * The thread model of ReloadTask 325 * * 326 * [Reload Task] [Main Thread] 327 * | | 328 * getUpdateInfo() --> | (synchronous call) 329 * (wait) <---- getUpdateInfo() 330 * | | 331 * Load Data | 332 * | | 333 * updateContent() --> | (synchronous call) 334 * (wait) updateContent() 335 * | | 336 * | | 337 */ 338 private class ReloadTask extends Thread { 339 340 private volatile boolean mActive = true; 341 private volatile boolean mDirty = true; 342 private boolean mIsLoading = false; 343 344 private void updateLoading(boolean loading) { 345 if (mIsLoading == loading) return; 346 mIsLoading = loading; 347 mMainHandler.sendEmptyMessage(loading ? MSG_LOAD_START : MSG_LOAD_FINISH); 348 } 349 350 @Override 351 public void run() { 352 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 353 354 boolean updateComplete = false; 355 while (mActive) { 356 synchronized (this) { 357 if (mActive && !mDirty && updateComplete) { 358 updateLoading(false); 359 if (mFailedVersion != MediaObject.INVALID_DATA_VERSION) { 360 Log.d(TAG, "reload pause"); 361 } 362 Utils.waitWithoutInterrupt(this); 363 if (mActive && (mFailedVersion != MediaObject.INVALID_DATA_VERSION)) { 364 Log.d(TAG, "reload resume"); 365 } 366 continue; 367 } 368 mDirty = false; 369 } 370 updateLoading(true); 371 long version = mSource.reload(); 372 UpdateInfo info = executeAndWait(new GetUpdateInfo(version)); 373 updateComplete = info == null; 374 if (updateComplete) continue; 375 if (info.version != version) { 376 info.size = mSource.getMediaItemCount(); 377 info.version = version; 378 } 379 if (info.reloadCount > 0) { 380 info.items = mSource.getMediaItem(info.reloadStart, info.reloadCount); 381 } 382 executeAndWait(new UpdateContent(info)); 383 } 384 updateLoading(false); 385 } 386 387 public synchronized void notifyDirty() { 388 mDirty = true; 389 notifyAll(); 390 } 391 392 public synchronized void terminate() { 393 mActive = false; 394 notifyAll(); 395 } 396 } 397} 398