PrintPreviewController.java revision 525a66b2bb5abf844aff2109bdc9ed819566bece
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.OrientationHelper; 28import android.support.v7.widget.RecyclerView; 29import android.support.v7.widget.RecyclerView.ViewHolder; 30import android.support.v7.widget.RecyclerView.LayoutManager; 31import android.support.v7.widget.StaggeredGridLayoutManager; 32import android.view.View; 33import com.android.internal.os.SomeArgs; 34import com.android.printspooler.R; 35import com.android.printspooler.model.MutexFileProvider; 36import com.android.printspooler.widget.PrintContentView; 37import com.android.printspooler.widget.EmbeddedContentContainer; 38import com.android.printspooler.widget.PrintOptionsLayout; 39 40import java.io.File; 41import java.io.FileNotFoundException; 42import java.util.ArrayList; 43import java.util.List; 44 45class PrintPreviewController implements MutexFileProvider.OnReleaseRequestCallback, 46 PageAdapter.PreviewArea, EmbeddedContentContainer.OnSizeChangeListener { 47 48 private PrintActivity mActivity; 49 50 private final MutexFileProvider mFileProvider; 51 52 private final MyHandler mHandler; 53 54 private final PageAdapter mPageAdapter; 55 56 private final StaggeredGridLayoutManager mLayoutManger; 57 58 private PrintOptionsLayout mPrintOptionsLayout; 59 60 private final RecyclerView mRecyclerView; 61 62 private final PrintContentView mContentView; 63 64 private final EmbeddedContentContainer mEmbeddedContentContainer; 65 66 private final PreloadController mPreloadController; 67 68 private int mDocumentPageCount; 69 70 public PrintPreviewController(PrintActivity activity, MutexFileProvider fileProvider) { 71 mActivity = activity; 72 mHandler = new MyHandler(activity.getMainLooper()); 73 mFileProvider = fileProvider; 74 75 mPrintOptionsLayout = (PrintOptionsLayout) activity.findViewById(R.id.options_container); 76 mPageAdapter = new PageAdapter(activity, activity, this); 77 78 final int columnCount = mActivity.getResources().getInteger( 79 R.integer.preview_page_per_row_count); 80 81 mLayoutManger = new StaggeredGridLayoutManager(columnCount, OrientationHelper.VERTICAL); 82 mRecyclerView = (RecyclerView) activity.findViewById(R.id.preview_content); 83 mRecyclerView.setLayoutManager(mLayoutManger); 84 mRecyclerView.setAdapter(mPageAdapter); 85 mPreloadController = new PreloadController(mRecyclerView); 86 mRecyclerView.setOnScrollListener(mPreloadController); 87 88 mContentView = (PrintContentView) activity.findViewById(R.id.options_content); 89 mEmbeddedContentContainer = (EmbeddedContentContainer) activity.findViewById( 90 R.id.embedded_content_container); 91 mEmbeddedContentContainer.setOnSizeChangeListener(this); 92 } 93 94 @Override 95 public void onSizeChanged(int width, int height) { 96 mPageAdapter.onPreviewAreaSizeChanged(); 97 } 98 99 public boolean isOptionsOpened() { 100 return mContentView.isOptionsOpened(); 101 } 102 103 public void closeOptions() { 104 mContentView.closeOptions(); 105 } 106 107 public void setUiShown(boolean shown) { 108 if (shown) { 109 mRecyclerView.setVisibility(View.VISIBLE); 110 } else { 111 mRecyclerView.setVisibility(View.GONE); 112 } 113 } 114 115 public void onOrientationChanged() { 116 // Adjust the print option column count. 117 final int optionColumnCount = mActivity.getResources().getInteger( 118 R.integer.print_option_column_count); 119 mPrintOptionsLayout.setColumnCount(optionColumnCount); 120 mPageAdapter.onOrientationChanged(); 121 } 122 123 public int getFilePageCount() { 124 return mPageAdapter.getFilePageCount(); 125 } 126 127 public PageRange[] getSelectedPages() { 128 return mPageAdapter.getSelectedPages(); 129 } 130 131 public PageRange[] getRequestedPages() { 132 return mPageAdapter.getRequestedPages(); 133 } 134 135 public void onContentUpdated(boolean documentChanged, int documentPageCount, 136 PageRange[] writtenPages, PageRange[] selectedPages, MediaSize mediaSize, 137 Margins minMargins) { 138 boolean contentChanged = false; 139 140 if (documentChanged) { 141 contentChanged = true; 142 } 143 144 if (documentPageCount != mDocumentPageCount) { 145 mDocumentPageCount = documentPageCount; 146 contentChanged = true; 147 } 148 149 if (contentChanged) { 150 // If not closed, close as we start over. 151 if (mPageAdapter.isOpened()) { 152 Message operation = mHandler.obtainMessage(MyHandler.MSG_CLOSE); 153 mHandler.enqueueOperation(operation); 154 } 155 } 156 157 // The content changed. In this case we have to invalidate 158 // all rendered pages and reopen the file... 159 if (contentChanged && writtenPages != null) { 160 Message operation = mHandler.obtainMessage(MyHandler.MSG_OPEN); 161 mHandler.enqueueOperation(operation); 162 } 163 164 // Update the attributes before after closed to avoid flicker. 165 SomeArgs args = SomeArgs.obtain(); 166 args.arg1 = writtenPages; 167 args.arg2 = selectedPages; 168 args.arg3 = mediaSize; 169 args.arg4 = minMargins; 170 args.argi1 = documentPageCount; 171 172 Message operation = mHandler.obtainMessage(MyHandler.MSG_UPDATE, args); 173 mHandler.enqueueOperation(operation); 174 175 // If document changed and has pages we want to start preloading. 176 if (contentChanged && writtenPages != null) { 177 operation = mHandler.obtainMessage(MyHandler.MSG_START_PRELOAD); 178 mHandler.enqueueOperation(operation); 179 } 180 } 181 182 @Override 183 public void onReleaseRequested(final File file) { 184 // This is called from the async task's single threaded executor 185 // thread, i.e. not on the main thread - so post a message. 186 mHandler.post(new Runnable() { 187 @Override 188 public void run() { 189 // At this point the other end will write to the file, hence 190 // we have to close it and reopen after the write completes. 191 Message operation = mHandler.obtainMessage(MyHandler.MSG_CLOSE); 192 mHandler.enqueueOperation(operation); 193 } 194 }); 195 } 196 197 public void destroy() { 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 implements 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(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 @Override 349 public void onScrolled(int dx, int dy) { 350 /* do nothing */ 351 } 352 353 public void startPreloadContent() { 354 PageAdapter pageAdapter = (PageAdapter) mRecyclerView.getAdapter(); 355 356 if (pageAdapter.isOpened()) { 357 PageRange shownPages = computeShownPages(); 358 if (shownPages != null) { 359 pageAdapter.startPreloadContent(shownPages); 360 } 361 } 362 } 363 364 public void stopPreloadContent() { 365 PageAdapter pageAdapter = (PageAdapter) mRecyclerView.getAdapter(); 366 367 if (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