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 throw new IllegalArgumentException(String.format( 124 "%s not in (%s, %s)", index, mActiveStart, mActiveEnd)); 125 } 126 return mData[index % mData.length]; 127 } 128 129 public int getActiveStart() { 130 return mActiveStart; 131 } 132 133 public boolean isActive(int index) { 134 return index >= mActiveStart && index < mActiveEnd; 135 } 136 137 public int size() { 138 return mSize; 139 } 140 141 // Returns the index of the MediaItem with the given path or 142 // -1 if the path is not cached 143 public int findItem(Path id) { 144 for (int i = mContentStart; i < mContentEnd; i++) { 145 MediaItem item = mData[i % DATA_CACHE_SIZE]; 146 if (item != null && id == item.getPath()) { 147 return i; 148 } 149 } 150 return -1; 151 } 152 153 private void clearSlot(int slotIndex) { 154 mData[slotIndex] = null; 155 mItemVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION; 156 mSetVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION; 157 } 158 159 private void setContentWindow(int contentStart, int contentEnd) { 160 if (contentStart == mContentStart && contentEnd == mContentEnd) return; 161 int end = mContentEnd; 162 int start = mContentStart; 163 164 // We need change the content window before calling reloadData(...) 165 synchronized (this) { 166 mContentStart = contentStart; 167 mContentEnd = contentEnd; 168 } 169 long[] itemVersion = mItemVersion; 170 long[] setVersion = mSetVersion; 171 if (contentStart >= end || start >= contentEnd) { 172 for (int i = start, n = end; i < n; ++i) { 173 clearSlot(i % DATA_CACHE_SIZE); 174 } 175 } else { 176 for (int i = start; i < contentStart; ++i) { 177 clearSlot(i % DATA_CACHE_SIZE); 178 } 179 for (int i = contentEnd, n = end; i < n; ++i) { 180 clearSlot(i % DATA_CACHE_SIZE); 181 } 182 } 183 if (mReloadTask != null) mReloadTask.notifyDirty(); 184 } 185 186 public void setActiveWindow(int start, int end) { 187 if (start == mActiveStart && end == mActiveEnd) return; 188 189 Utils.assertTrue(start <= end 190 && end - start <= mData.length && end <= mSize); 191 192 int length = mData.length; 193 mActiveStart = start; 194 mActiveEnd = end; 195 196 // If no data is visible, keep the cache content 197 if (start == end) return; 198 199 int contentStart = Utils.clamp((start + end) / 2 - length / 2, 200 0, Math.max(0, mSize - length)); 201 int contentEnd = Math.min(contentStart + length, mSize); 202 if (mContentStart > start || mContentEnd < end 203 || Math.abs(contentStart - mContentStart) > MIN_LOAD_COUNT) { 204 setContentWindow(contentStart, contentEnd); 205 } 206 } 207 208 private class MySourceListener implements ContentListener { 209 @Override 210 public void onContentDirty() { 211 if (mReloadTask != null) mReloadTask.notifyDirty(); 212 } 213 } 214 215 public void setDataListener(DataListener listener) { 216 mDataListener = listener; 217 } 218 219 public void setLoadingListener(LoadingListener listener) { 220 mLoadingListener = listener; 221 } 222 223 private <T> T executeAndWait(Callable<T> callable) { 224 FutureTask<T> task = new FutureTask<T>(callable); 225 mMainHandler.sendMessage( 226 mMainHandler.obtainMessage(MSG_RUN_OBJECT, task)); 227 try { 228 return task.get(); 229 } catch (InterruptedException e) { 230 return null; 231 } catch (ExecutionException e) { 232 throw new RuntimeException(e); 233 } 234 } 235 236 private static class UpdateInfo { 237 public long version; 238 public int reloadStart; 239 public int reloadCount; 240 241 public int size; 242 public ArrayList<MediaItem> items; 243 } 244 245 private class GetUpdateInfo implements Callable<UpdateInfo> { 246 private final long mVersion; 247 248 public GetUpdateInfo(long version) { 249 mVersion = version; 250 } 251 252 @Override 253 public UpdateInfo call() throws Exception { 254 if (mFailedVersion == mVersion) { 255 // previous loading failed, return null to pause loading 256 return null; 257 } 258 UpdateInfo info = new UpdateInfo(); 259 long version = mVersion; 260 info.version = mSourceVersion; 261 info.size = mSize; 262 long setVersion[] = mSetVersion; 263 for (int i = mContentStart, n = mContentEnd; i < n; ++i) { 264 int index = i % DATA_CACHE_SIZE; 265 if (setVersion[index] != version) { 266 info.reloadStart = i; 267 info.reloadCount = Math.min(MAX_LOAD_COUNT, n - i); 268 return info; 269 } 270 } 271 return mSourceVersion == mVersion ? null : info; 272 } 273 } 274 275 private class UpdateContent implements Callable<Void> { 276 277 private UpdateInfo mUpdateInfo; 278 279 public UpdateContent(UpdateInfo info) { 280 mUpdateInfo = info; 281 } 282 283 @Override 284 public Void call() throws Exception { 285 UpdateInfo info = mUpdateInfo; 286 mSourceVersion = info.version; 287 if (mSize != info.size) { 288 mSize = info.size; 289 if (mDataListener != null) mDataListener.onSizeChanged(mSize); 290 if (mContentEnd > mSize) mContentEnd = mSize; 291 if (mActiveEnd > mSize) mActiveEnd = mSize; 292 } 293 294 ArrayList<MediaItem> items = info.items; 295 296 mFailedVersion = MediaObject.INVALID_DATA_VERSION; 297 if ((items == null) || items.isEmpty()) { 298 if (info.reloadCount > 0) { 299 mFailedVersion = info.version; 300 Log.d(TAG, "loading failed: " + mFailedVersion); 301 } 302 return null; 303 } 304 int start = Math.max(info.reloadStart, mContentStart); 305 int end = Math.min(info.reloadStart + items.size(), mContentEnd); 306 307 for (int i = start; i < end; ++i) { 308 int index = i % DATA_CACHE_SIZE; 309 mSetVersion[index] = info.version; 310 MediaItem updateItem = items.get(i - info.reloadStart); 311 long itemVersion = updateItem.getDataVersion(); 312 if (mItemVersion[index] != itemVersion) { 313 mItemVersion[index] = itemVersion; 314 mData[index] = updateItem; 315 if (mDataListener != null && i >= mActiveStart && i < mActiveEnd) { 316 mDataListener.onContentChanged(i); 317 } 318 } 319 } 320 return null; 321 } 322 } 323 324 /* 325 * The thread model of ReloadTask 326 * * 327 * [Reload Task] [Main Thread] 328 * | | 329 * getUpdateInfo() --> | (synchronous call) 330 * (wait) <---- getUpdateInfo() 331 * | | 332 * Load Data | 333 * | | 334 * updateContent() --> | (synchronous call) 335 * (wait) updateContent() 336 * | | 337 * | | 338 */ 339 private class ReloadTask extends Thread { 340 341 private volatile boolean mActive = true; 342 private volatile boolean mDirty = true; 343 private boolean mIsLoading = false; 344 345 private void updateLoading(boolean loading) { 346 if (mIsLoading == loading) return; 347 mIsLoading = loading; 348 mMainHandler.sendEmptyMessage(loading ? MSG_LOAD_START : MSG_LOAD_FINISH); 349 } 350 351 @Override 352 public void run() { 353 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 354 355 boolean updateComplete = false; 356 while (mActive) { 357 synchronized (this) { 358 if (mActive && !mDirty && updateComplete) { 359 updateLoading(false); 360 if (mFailedVersion != MediaObject.INVALID_DATA_VERSION) { 361 Log.d(TAG, "reload pause"); 362 } 363 Utils.waitWithoutInterrupt(this); 364 if (mActive && (mFailedVersion != MediaObject.INVALID_DATA_VERSION)) { 365 Log.d(TAG, "reload resume"); 366 } 367 continue; 368 } 369 mDirty = false; 370 } 371 updateLoading(true); 372 long version = mSource.reload(); 373 UpdateInfo info = executeAndWait(new GetUpdateInfo(version)); 374 updateComplete = info == null; 375 if (updateComplete) continue; 376 if (info.version != version) { 377 info.size = mSource.getMediaItemCount(); 378 info.version = version; 379 } 380 if (info.reloadCount > 0) { 381 info.items = mSource.getMediaItem(info.reloadStart, info.reloadCount); 382 } 383 executeAndWait(new UpdateContent(info)); 384 } 385 updateLoading(false); 386 } 387 388 public synchronized void notifyDirty() { 389 mDirty = true; 390 notifyAll(); 391 } 392 393 public synchronized void terminate() { 394 mActive = false; 395 notifyAll(); 396 } 397 } 398} 399