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