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