PrintPreviewController.java revision 6f249835a4ff9e7e7e3ca0190b7ecf72e689656d
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 mPageAdapter.destroy(); 199 } 200 201 @Override 202 public int getWidth() { 203 return mEmbeddedContentContainer.getWidth(); 204 } 205 206 @Override 207 public int getHeight() { 208 return mEmbeddedContentContainer.getHeight(); 209 } 210 211 @Override 212 public void setColumnCount(int columnCount) { 213 mLayoutManger.setSpanCount(columnCount); 214 } 215 216 @Override 217 public void setPadding(int left, int top , int right, int bottom) { 218 mRecyclerView.setPadding(left, top, right, bottom); 219 } 220 221 private final class MyHandler extends Handler { 222 public static final int MSG_OPEN = 1; 223 public static final int MSG_CLOSE = 2; 224 public static final int MSG_DESTROY = 3; 225 public static final int MSG_UPDATE = 4; 226 public static final int MSG_START_PRELOAD = 5; 227 228 private boolean mAsyncOperationInProgress; 229 230 private final Runnable mOnAsyncOperationDoneCallback = new Runnable() { 231 @Override 232 public void run() { 233 mAsyncOperationInProgress = false; 234 handleNextOperation(); 235 } 236 }; 237 238 private final List<Message> mPendingOperations = new ArrayList<>(); 239 240 public MyHandler(Looper looper) { 241 super(looper, null, false); 242 } 243 244 public void enqueueOperation(Message message) { 245 mPendingOperations.add(message); 246 handleNextOperation(); 247 } 248 249 public void handleNextOperation() { 250 while (!mPendingOperations.isEmpty() && !mAsyncOperationInProgress) { 251 Message operation = mPendingOperations.remove(0); 252 handleMessage(operation); 253 } 254 } 255 256 @Override 257 public void handleMessage(Message message) { 258 switch (message.what) { 259 case MSG_OPEN: { 260 try { 261 File file = mFileProvider.acquireFile(PrintPreviewController.this); 262 ParcelFileDescriptor pfd = ParcelFileDescriptor.open(file, 263 ParcelFileDescriptor.MODE_READ_ONLY); 264 265 mAsyncOperationInProgress = true; 266 mPageAdapter.open(pfd, new Runnable() { 267 @Override 268 public void run() { 269 if (mDocumentPageCount == PrintDocumentInfo.PAGE_COUNT_UNKNOWN) { 270 mDocumentPageCount = mPageAdapter.getFilePageCount(); 271 mActivity.updateOptionsUi(); 272 } 273 mOnAsyncOperationDoneCallback.run(); 274 } 275 }); 276 } catch (FileNotFoundException fnfe) { 277 /* ignore - file guaranteed to be there */ 278 } 279 } break; 280 281 case MSG_CLOSE: { 282 mAsyncOperationInProgress = true; 283 mPageAdapter.close(new Runnable() { 284 @Override 285 public void run() { 286 mFileProvider.releaseFile(); 287 mOnAsyncOperationDoneCallback.run(); 288 } 289 }); 290 } break; 291 292 case MSG_DESTROY: { 293 mPageAdapter.destroy(); 294 handleNextOperation(); 295 } break; 296 297 case MSG_UPDATE: { 298 SomeArgs args = (SomeArgs) message.obj; 299 PageRange[] writtenPages = (PageRange[]) args.arg1; 300 PageRange[] selectedPages = (PageRange[]) args.arg2; 301 MediaSize mediaSize = (MediaSize) args.arg3; 302 Margins margins = (Margins) args.arg4; 303 final int pageCount = args.argi1; 304 args.recycle(); 305 306 mPageAdapter.update(writtenPages, selectedPages, pageCount, 307 mediaSize, margins); 308 309 } break; 310 311 case MSG_START_PRELOAD: { 312 mPreloadController.startPreloadContent(); 313 } break; 314 } 315 } 316 } 317 318 private final class PreloadController extends RecyclerView.OnScrollListener { 319 private final RecyclerView mRecyclerView; 320 321 private int mOldScrollState; 322 323 public PreloadController(RecyclerView recyclerView) { 324 mRecyclerView = recyclerView; 325 mOldScrollState = mRecyclerView.getScrollState(); 326 } 327 328 @Override 329 public void onScrollStateChanged(RecyclerView recyclerView, int state) { 330 switch (mOldScrollState) { 331 case RecyclerView.SCROLL_STATE_SETTLING: { 332 if (state == RecyclerView.SCROLL_STATE_IDLE 333 || state == RecyclerView.SCROLL_STATE_DRAGGING){ 334 startPreloadContent(); 335 } 336 } break; 337 338 case RecyclerView.SCROLL_STATE_IDLE: 339 case RecyclerView.SCROLL_STATE_DRAGGING: { 340 if (state == RecyclerView.SCROLL_STATE_SETTLING) { 341 stopPreloadContent(); 342 } 343 } break; 344 } 345 mOldScrollState = state; 346 } 347 348 public void startPreloadContent() { 349 PageAdapter pageAdapter = (PageAdapter) mRecyclerView.getAdapter(); 350 351 if (pageAdapter.isOpened()) { 352 PageRange shownPages = computeShownPages(); 353 if (shownPages != null) { 354 pageAdapter.startPreloadContent(shownPages); 355 } 356 } 357 } 358 359 public void stopPreloadContent() { 360 PageAdapter pageAdapter = (PageAdapter) mRecyclerView.getAdapter(); 361 362 if (pageAdapter.isOpened()) { 363 pageAdapter.stopPreloadContent(); 364 } 365 } 366 367 private PageRange computeShownPages() { 368 final int childCount = mRecyclerView.getChildCount(); 369 if (childCount > 0) { 370 LayoutManager layoutManager = mRecyclerView.getLayoutManager(); 371 372 View firstChild = layoutManager.getChildAt(0); 373 ViewHolder firstHolder = mRecyclerView.getChildViewHolder(firstChild); 374 375 View lastChild = layoutManager.getChildAt(layoutManager.getChildCount() - 1); 376 ViewHolder lastHolder = mRecyclerView.getChildViewHolder(lastChild); 377 378 return new PageRange(firstHolder.getPosition(), lastHolder.getPosition()); 379 } 380 return null; 381 } 382 } 383} 384