1 /* 2 * Copyright (C) 2014 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.printspooler.ui; 18 19 import android.os.Handler; 20 import android.os.Looper; 21 import android.os.Message; 22 import android.os.ParcelFileDescriptor; 23 import android.print.PageRange; 24 import android.print.PrintAttributes.MediaSize; 25 import android.print.PrintAttributes.Margins; 26 import android.print.PrintDocumentInfo; 27 import android.support.v7.widget.GridLayoutManager; 28 import android.support.v7.widget.RecyclerView; 29 import android.support.v7.widget.RecyclerView.ViewHolder; 30 import android.support.v7.widget.RecyclerView.LayoutManager; 31 import android.view.View; 32 import com.android.internal.os.SomeArgs; 33 import com.android.printspooler.R; 34 import com.android.printspooler.model.MutexFileProvider; 35 import com.android.printspooler.widget.PrintContentView; 36 import com.android.printspooler.widget.EmbeddedContentContainer; 37 import com.android.printspooler.widget.PrintOptionsLayout; 38 39 import java.io.File; 40 import java.io.FileNotFoundException; 41 import java.util.ArrayList; 42 import java.util.List; 43 44 class PrintPreviewController implements MutexFileProvider.OnReleaseRequestCallback, 45 PageAdapter.PreviewArea, EmbeddedContentContainer.OnSizeChangeListener { 46 47 private final PrintActivity mActivity; 48 49 private final MutexFileProvider mFileProvider; 50 private final MyHandler mHandler; 51 52 private final PageAdapter mPageAdapter; 53 private final GridLayoutManager mLayoutManger; 54 55 private final PrintOptionsLayout mPrintOptionsLayout; 56 private final RecyclerView mRecyclerView; 57 private final PrintContentView mContentView; 58 private final EmbeddedContentContainer mEmbeddedContentContainer; 59 60 private final PreloadController mPreloadController; 61 62 private int mDocumentPageCount; 63 64 public PrintPreviewController(PrintActivity activity, MutexFileProvider fileProvider) { 65 mActivity = activity; 66 mHandler = new MyHandler(activity.getMainLooper()); 67 mFileProvider = fileProvider; 68 69 mPrintOptionsLayout = (PrintOptionsLayout) activity.findViewById(R.id.options_container); 70 mPageAdapter = new PageAdapter(activity, activity, this); 71 72 final int columnCount = mActivity.getResources().getInteger( 73 R.integer.preview_page_per_row_count); 74 75 mLayoutManger = new GridLayoutManager(mActivity, columnCount); 76 77 mRecyclerView = (RecyclerView) activity.findViewById(R.id.preview_content); 78 mRecyclerView.setLayoutManager(mLayoutManger); 79 mRecyclerView.setAdapter(mPageAdapter); 80 mRecyclerView.setItemViewCacheSize(0); 81 mPreloadController = new PreloadController(mRecyclerView); 82 mRecyclerView.setOnScrollListener(mPreloadController); 83 84 mContentView = (PrintContentView) activity.findViewById(R.id.options_content); 85 mEmbeddedContentContainer = (EmbeddedContentContainer) activity.findViewById( 86 R.id.embedded_content_container); 87 mEmbeddedContentContainer.setOnSizeChangeListener(this); 88 } 89 90 @Override 91 public void onSizeChanged(int width, int height) { 92 mPageAdapter.onPreviewAreaSizeChanged(); 93 } 94 95 public boolean isOptionsOpened() { 96 return mContentView.isOptionsOpened(); 97 } 98 99 public void closeOptions() { 100 mContentView.closeOptions(); 101 } 102 103 public void setUiShown(boolean shown) { 104 if (shown) { 105 mRecyclerView.setVisibility(View.VISIBLE); 106 } else { 107 mRecyclerView.setVisibility(View.GONE); 108 } 109 } 110 111 public void onOrientationChanged() { 112 // Adjust the print option column count. 113 final int optionColumnCount = mActivity.getResources().getInteger( 114 R.integer.print_option_column_count); 115 mPrintOptionsLayout.setColumnCount(optionColumnCount); 116 mPageAdapter.onOrientationChanged(); 117 } 118 119 public int getFilePageCount() { 120 return mPageAdapter.getFilePageCount(); 121 } 122 123 public PageRange[] getSelectedPages() { 124 return mPageAdapter.getSelectedPages(); 125 } 126 127 public PageRange[] getRequestedPages() { 128 return mPageAdapter.getRequestedPages(); 129 } 130 131 public void onContentUpdated(boolean documentChanged, int documentPageCount, 132 PageRange[] writtenPages, PageRange[] selectedPages, MediaSize mediaSize, 133 Margins minMargins) { 134 boolean contentChanged = false; 135 136 if (documentChanged) { 137 contentChanged = true; 138 } 139 140 if (documentPageCount != mDocumentPageCount) { 141 mDocumentPageCount = documentPageCount; 142 contentChanged = true; 143 } 144 145 if (contentChanged) { 146 // If not closed, close as we start over. 147 if (mPageAdapter.isOpened()) { 148 Message operation = mHandler.obtainMessage(MyHandler.MSG_CLOSE); 149 mHandler.enqueueOperation(operation); 150 } 151 } 152 153 // The content changed. In this case we have to invalidate 154 // all rendered pages and reopen the file... 155 if ((contentChanged || !mPageAdapter.isOpened()) && writtenPages != null) { 156 Message operation = mHandler.obtainMessage(MyHandler.MSG_OPEN); 157 mHandler.enqueueOperation(operation); 158 } 159 160 // Update the attributes before after closed to avoid flicker. 161 SomeArgs args = SomeArgs.obtain(); 162 args.arg1 = writtenPages; 163 args.arg2 = selectedPages; 164 args.arg3 = mediaSize; 165 args.arg4 = minMargins; 166 args.argi1 = documentPageCount; 167 168 Message operation = mHandler.obtainMessage(MyHandler.MSG_UPDATE, args); 169 mHandler.enqueueOperation(operation); 170 171 // If document changed and has pages we want to start preloading. 172 if (contentChanged && writtenPages != null) { 173 operation = mHandler.obtainMessage(MyHandler.MSG_START_PRELOAD); 174 mHandler.enqueueOperation(operation); 175 } 176 } 177 178 @Override 179 public void onReleaseRequested(final File file) { 180 // This is called from the async task's single threaded executor 181 // thread, i.e. not on the main thread - so post a message. 182 mHandler.post(new Runnable() { 183 @Override 184 public void run() { 185 // At this point the other end will write to the file, hence 186 // we have to close it and reopen after the write completes. 187 if (mPageAdapter.isOpened()) { 188 Message operation = mHandler.obtainMessage(MyHandler.MSG_CLOSE); 189 mHandler.enqueueOperation(operation); 190 } 191 } 192 }); 193 } 194 195 public void destroy(Runnable callback) { 196 if (mPageAdapter.isOpened()) { 197 Message operation = mHandler.obtainMessage(MyHandler.MSG_CLOSE); 198 mHandler.enqueueOperation(operation); 199 } 200 201 Message operation = mHandler.obtainMessage(MyHandler.MSG_DESTROY); 202 operation.obj = callback; 203 mHandler.enqueueOperation(operation); 204 } 205 206 @Override 207 public int getWidth() { 208 return mEmbeddedContentContainer.getWidth(); 209 } 210 211 @Override 212 public int getHeight() { 213 return mEmbeddedContentContainer.getHeight(); 214 } 215 216 @Override 217 public void setColumnCount(int columnCount) { 218 mLayoutManger.setSpanCount(columnCount); 219 } 220 221 @Override 222 public void setPadding(int left, int top , int right, int bottom) { 223 mRecyclerView.setPadding(left, top, right, bottom); 224 } 225 226 private final class MyHandler extends Handler { 227 public static final int MSG_OPEN = 1; 228 public static final int MSG_CLOSE = 2; 229 public static final int MSG_DESTROY = 3; 230 public static final int MSG_UPDATE = 4; 231 public static final int MSG_START_PRELOAD = 5; 232 233 private boolean mAsyncOperationInProgress; 234 235 private final Runnable mOnAsyncOperationDoneCallback = new Runnable() { 236 @Override 237 public void run() { 238 mAsyncOperationInProgress = false; 239 handleNextOperation(); 240 } 241 }; 242 243 private final List<Message> mPendingOperations = new ArrayList<>(); 244 245 public MyHandler(Looper looper) { 246 super(looper, null, false); 247 } 248 249 public void enqueueOperation(Message message) { 250 mPendingOperations.add(message); 251 handleNextOperation(); 252 } 253 254 public void handleNextOperation() { 255 while (!mPendingOperations.isEmpty() && !mAsyncOperationInProgress) { 256 Message operation = mPendingOperations.remove(0); 257 handleMessage(operation); 258 } 259 } 260 261 @Override 262 public void handleMessage(Message message) { 263 switch (message.what) { 264 case MSG_OPEN: { 265 try { 266 File file = mFileProvider.acquireFile(PrintPreviewController.this); 267 ParcelFileDescriptor pfd = ParcelFileDescriptor.open(file, 268 ParcelFileDescriptor.MODE_READ_ONLY); 269 270 mAsyncOperationInProgress = true; 271 mPageAdapter.open(pfd, new Runnable() { 272 @Override 273 public void run() { 274 if (mDocumentPageCount == PrintDocumentInfo.PAGE_COUNT_UNKNOWN) { 275 mDocumentPageCount = mPageAdapter.getFilePageCount(); 276 mActivity.updateOptionsUi(); 277 } 278 mOnAsyncOperationDoneCallback.run(); 279 } 280 }); 281 } catch (FileNotFoundException fnfe) { 282 /* ignore - file guaranteed to be there */ 283 } 284 } break; 285 286 case MSG_CLOSE: { 287 mAsyncOperationInProgress = true; 288 mPageAdapter.close(new Runnable() { 289 @Override 290 public void run() { 291 mFileProvider.releaseFile(); 292 mOnAsyncOperationDoneCallback.run(); 293 } 294 }); 295 } break; 296 297 case MSG_DESTROY: { 298 Runnable callback = (Runnable) message.obj; 299 mRecyclerView.setAdapter(null); 300 mPageAdapter.destroy(callback); 301 handleNextOperation(); 302 } break; 303 304 case MSG_UPDATE: { 305 SomeArgs args = (SomeArgs) message.obj; 306 PageRange[] writtenPages = (PageRange[]) args.arg1; 307 PageRange[] selectedPages = (PageRange[]) args.arg2; 308 MediaSize mediaSize = (MediaSize) args.arg3; 309 Margins margins = (Margins) args.arg4; 310 final int pageCount = args.argi1; 311 args.recycle(); 312 313 mPageAdapter.update(writtenPages, selectedPages, pageCount, 314 mediaSize, margins); 315 316 } break; 317 318 case MSG_START_PRELOAD: { 319 mPreloadController.startPreloadContent(); 320 } break; 321 } 322 } 323 } 324 325 private final class PreloadController extends RecyclerView.OnScrollListener { 326 private final RecyclerView mRecyclerView; 327 328 private int mOldScrollState; 329 330 public PreloadController(RecyclerView recyclerView) { 331 mRecyclerView = recyclerView; 332 mOldScrollState = mRecyclerView.getScrollState(); 333 } 334 335 @Override 336 public void onScrollStateChanged(RecyclerView recyclerView, int state) { 337 switch (mOldScrollState) { 338 case RecyclerView.SCROLL_STATE_SETTLING: { 339 if (state == RecyclerView.SCROLL_STATE_IDLE 340 || state == RecyclerView.SCROLL_STATE_DRAGGING){ 341 startPreloadContent(); 342 } 343 } break; 344 345 case RecyclerView.SCROLL_STATE_IDLE: 346 case RecyclerView.SCROLL_STATE_DRAGGING: { 347 if (state == RecyclerView.SCROLL_STATE_SETTLING) { 348 stopPreloadContent(); 349 } 350 } break; 351 } 352 mOldScrollState = state; 353 } 354 355 public void startPreloadContent() { 356 PageAdapter pageAdapter = (PageAdapter) mRecyclerView.getAdapter(); 357 if (pageAdapter != null && pageAdapter.isOpened()) { 358 PageRange shownPages = computeShownPages(); 359 if (shownPages != null) { 360 pageAdapter.startPreloadContent(shownPages); 361 } 362 } 363 } 364 365 public void stopPreloadContent() { 366 PageAdapter pageAdapter = (PageAdapter) mRecyclerView.getAdapter(); 367 if (pageAdapter != null && pageAdapter.isOpened()) { 368 pageAdapter.stopPreloadContent(); 369 } 370 } 371 372 private PageRange computeShownPages() { 373 final int childCount = mRecyclerView.getChildCount(); 374 if (childCount > 0) { 375 LayoutManager layoutManager = mRecyclerView.getLayoutManager(); 376 377 View firstChild = layoutManager.getChildAt(0); 378 ViewHolder firstHolder = mRecyclerView.getChildViewHolder(firstChild); 379 380 View lastChild = layoutManager.getChildAt(layoutManager.getChildCount() - 1); 381 ViewHolder lastHolder = mRecyclerView.getChildViewHolder(lastChild); 382 383 return new PageRange(firstHolder.getPosition(), lastHolder.getPosition()); 384 } 385 return null; 386 } 387 } 388 } 389