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.DataManager; 26 import com.android.gallery3d.data.MediaItem; 27 import com.android.gallery3d.data.MediaObject; 28 import com.android.gallery3d.data.MediaSet; 29 import com.android.gallery3d.ui.AlbumSlotRenderer; 30 import com.android.gallery3d.ui.SynchronizedHandler; 31 32 import java.util.ArrayList; 33 import java.util.Arrays; 34 import java.util.concurrent.Callable; 35 import java.util.concurrent.ExecutionException; 36 import java.util.concurrent.FutureTask; 37 38 public class AlbumDataLoader { 39 @SuppressWarnings("unused") 40 private static final String TAG = "AlbumDataAdapter"; 41 private static final int DATA_CACHE_SIZE = 1000; 42 43 private static final int MSG_LOAD_START = 1; 44 private static final int MSG_LOAD_FINISH = 2; 45 private static final int MSG_RUN_OBJECT = 3; 46 47 private static final int MIN_LOAD_COUNT = 32; 48 private static final int MAX_LOAD_COUNT = 64; 49 50 private final MediaItem[] mData; 51 private final long[] mItemVersion; 52 private final long[] mSetVersion; 53 54 public static interface DataListener { 55 public void onContentChanged(int index); 56 public void onSizeChanged(int size); 57 } 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 68 private final Handler mMainHandler; 69 private int mSize = 0; 70 71 private DataListener mDataListener; 72 private MySourceListener mSourceListener = new MySourceListener(); 73 private LoadingListener mLoadingListener; 74 75 private ReloadTask mReloadTask; 76 77 public AlbumDataLoader(GalleryActivity context, MediaSet mediaSet) { 78 mSource = mediaSet; 79 80 mData = new MediaItem[DATA_CACHE_SIZE]; 81 mItemVersion = new long[DATA_CACHE_SIZE]; 82 mSetVersion = new long[DATA_CACHE_SIZE]; 83 Arrays.fill(mItemVersion, MediaObject.INVALID_DATA_VERSION); 84 Arrays.fill(mSetVersion, MediaObject.INVALID_DATA_VERSION); 85 86 mMainHandler = new SynchronizedHandler(context.getGLRoot()) { 87 @Override 88 public void handleMessage(Message message) { 89 switch (message.what) { 90 case MSG_RUN_OBJECT: 91 ((Runnable) message.obj).run(); 92 return; 93 case MSG_LOAD_START: 94 if (mLoadingListener != null) mLoadingListener.onLoadingStarted(); 95 return; 96 case MSG_LOAD_FINISH: 97 if (mLoadingListener != null) mLoadingListener.onLoadingFinished(); 98 return; 99 } 100 } 101 }; 102 } 103 104 public void resume() { 105 mSource.addContentListener(mSourceListener); 106 mReloadTask = new ReloadTask(); 107 mReloadTask.start(); 108 } 109 110 public void pause() { 111 mReloadTask.terminate(); 112 mReloadTask = null; 113 mSource.removeContentListener(mSourceListener); 114 } 115 116 public MediaItem get(int index) { 117 if (!isActive(index)) { 118 throw new IllegalArgumentException(String.format( 119 "%s not in (%s, %s)", index, mActiveStart, mActiveEnd)); 120 } 121 return mData[index % mData.length]; 122 } 123 124 public int getActiveStart() { 125 return mActiveStart; 126 } 127 128 public boolean isActive(int index) { 129 return index >= mActiveStart && index < mActiveEnd; 130 } 131 132 public int size() { 133 return mSize; 134 } 135 136 private void clearSlot(int slotIndex) { 137 mData[slotIndex] = null; 138 mItemVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION; 139 mSetVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION; 140 } 141 142 private void setContentWindow(int contentStart, int contentEnd) { 143 if (contentStart == mContentStart && contentEnd == mContentEnd) return; 144 int end = mContentEnd; 145 int start = mContentStart; 146 147 // We need change the content window before calling reloadData(...) 148 synchronized (this) { 149 mContentStart = contentStart; 150 mContentEnd = contentEnd; 151 } 152 long[] itemVersion = mItemVersion; 153 long[] setVersion = mSetVersion; 154 if (contentStart >= end || start >= contentEnd) { 155 for (int i = start, n = end; i < n; ++i) { 156 clearSlot(i % DATA_CACHE_SIZE); 157 } 158 } else { 159 for (int i = start; i < contentStart; ++i) { 160 clearSlot(i % DATA_CACHE_SIZE); 161 } 162 for (int i = contentEnd, n = end; i < n; ++i) { 163 clearSlot(i % DATA_CACHE_SIZE); 164 } 165 } 166 if (mReloadTask != null) mReloadTask.notifyDirty(); 167 } 168 169 public void setActiveWindow(int start, int end) { 170 if (start == mActiveStart && end == mActiveEnd) return; 171 172 Utils.assertTrue(start <= end 173 && end - start <= mData.length && end <= mSize); 174 175 int length = mData.length; 176 mActiveStart = start; 177 mActiveEnd = end; 178 179 // If no data is visible, keep the cache content 180 if (start == end) return; 181 182 int contentStart = Utils.clamp((start + end) / 2 - length / 2, 183 0, Math.max(0, mSize - length)); 184 int contentEnd = Math.min(contentStart + length, mSize); 185 if (mContentStart > start || mContentEnd < end 186 || Math.abs(contentStart - mContentStart) > MIN_LOAD_COUNT) { 187 setContentWindow(contentStart, contentEnd); 188 } 189 } 190 191 private class MySourceListener implements ContentListener { 192 public void onContentDirty() { 193 if (mReloadTask != null) mReloadTask.notifyDirty(); 194 } 195 } 196 197 public void setDataListener(DataListener listener) { 198 mDataListener = listener; 199 } 200 201 public void setLoadingListener(LoadingListener listener) { 202 mLoadingListener = listener; 203 } 204 205 private <T> T executeAndWait(Callable<T> callable) { 206 FutureTask<T> task = new FutureTask<T>(callable); 207 mMainHandler.sendMessage( 208 mMainHandler.obtainMessage(MSG_RUN_OBJECT, task)); 209 try { 210 return task.get(); 211 } catch (InterruptedException e) { 212 return null; 213 } catch (ExecutionException e) { 214 throw new RuntimeException(e); 215 } 216 } 217 218 private static class UpdateInfo { 219 public long version; 220 public int reloadStart; 221 public int reloadCount; 222 223 public int size; 224 public ArrayList<MediaItem> items; 225 } 226 227 private class GetUpdateInfo implements Callable<UpdateInfo> { 228 private final long mVersion; 229 230 public GetUpdateInfo(long version) { 231 mVersion = version; 232 } 233 234 public UpdateInfo call() throws Exception { 235 UpdateInfo info = new UpdateInfo(); 236 long version = mVersion; 237 info.version = mSourceVersion; 238 info.size = mSize; 239 long setVersion[] = mSetVersion; 240 for (int i = mContentStart, n = mContentEnd; i < n; ++i) { 241 int index = i % DATA_CACHE_SIZE; 242 if (setVersion[index] != version) { 243 info.reloadStart = i; 244 info.reloadCount = Math.min(MAX_LOAD_COUNT, n - i); 245 return info; 246 } 247 } 248 return mSourceVersion == mVersion ? null : info; 249 } 250 } 251 252 private class UpdateContent implements Callable<Void> { 253 254 private UpdateInfo mUpdateInfo; 255 256 public UpdateContent(UpdateInfo info) { 257 mUpdateInfo = info; 258 } 259 260 @Override 261 public Void call() throws Exception { 262 UpdateInfo info = mUpdateInfo; 263 mSourceVersion = info.version; 264 if (mSize != info.size) { 265 mSize = info.size; 266 if (mDataListener != null) mDataListener.onSizeChanged(mSize); 267 if (mContentEnd > mSize) mContentEnd = mSize; 268 if (mActiveEnd > mSize) mActiveEnd = mSize; 269 } 270 271 ArrayList<MediaItem> items = info.items; 272 273 if (items == null) return null; 274 int start = Math.max(info.reloadStart, mContentStart); 275 int end = Math.min(info.reloadStart + items.size(), mContentEnd); 276 277 for (int i = start; i < end; ++i) { 278 int index = i % DATA_CACHE_SIZE; 279 mSetVersion[index] = info.version; 280 MediaItem updateItem = items.get(i - info.reloadStart); 281 long itemVersion = updateItem.getDataVersion(); 282 if (mItemVersion[index] != itemVersion) { 283 mItemVersion[index] = itemVersion; 284 mData[index] = updateItem; 285 if (mDataListener != null && i >= mActiveStart && i < mActiveEnd) { 286 mDataListener.onContentChanged(i); 287 } 288 } 289 } 290 return null; 291 } 292 } 293 294 /* 295 * The thread model of ReloadTask 296 * * 297 * [Reload Task] [Main Thread] 298 * | | 299 * getUpdateInfo() --> | (synchronous call) 300 * (wait) <---- getUpdateInfo() 301 * | | 302 * Load Data | 303 * | | 304 * updateContent() --> | (synchronous call) 305 * (wait) updateContent() 306 * | | 307 * | | 308 */ 309 private class ReloadTask extends Thread { 310 311 private volatile boolean mActive = true; 312 private volatile boolean mDirty = true; 313 private boolean mIsLoading = false; 314 315 private void updateLoading(boolean loading) { 316 if (mIsLoading == loading) return; 317 mIsLoading = loading; 318 mMainHandler.sendEmptyMessage(loading ? MSG_LOAD_START : MSG_LOAD_FINISH); 319 } 320 321 @Override 322 public void run() { 323 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 324 325 boolean updateComplete = false; 326 while (mActive) { 327 synchronized (this) { 328 if (mActive && !mDirty && updateComplete) { 329 updateLoading(false); 330 Utils.waitWithoutInterrupt(this); 331 continue; 332 } 333 } 334 mDirty = false; 335 updateLoading(true); 336 long version; 337 synchronized (DataManager.LOCK) { 338 version = mSource.reload(); 339 } 340 UpdateInfo info = executeAndWait(new GetUpdateInfo(version)); 341 updateComplete = info == null; 342 if (updateComplete) continue; 343 synchronized (DataManager.LOCK) { 344 if (info.version != version) { 345 info.size = mSource.getMediaItemCount(); 346 info.version = version; 347 } 348 if (info.reloadCount > 0) { 349 info.items = mSource.getMediaItem(info.reloadStart, info.reloadCount); 350 } 351 } 352 executeAndWait(new UpdateContent(info)); 353 } 354 updateLoading(false); 355 } 356 357 public synchronized void notifyDirty() { 358 mDirty = true; 359 notifyAll(); 360 } 361 362 public synchronized void terminate() { 363 mActive = false; 364 notifyAll(); 365 } 366 } 367 } 368