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