PrintPreviewController.java revision f3f963b0bebea91b17f7e60d9b826c458bfde38c
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.RecyclerView; 29import android.support.v7.widget.RecyclerView.ViewHolder; 30import android.support.v7.widget.RecyclerView.LayoutManager; 31import android.view.View; 32import com.android.internal.os.SomeArgs; 33import com.android.printspooler.R; 34import com.android.printspooler.model.MutexFileProvider; 35import com.android.printspooler.widget.PrintContentView; 36import com.android.printspooler.widget.EmbeddedContentContainer; 37import com.android.printspooler.widget.PrintOptionsLayout; 38 39import java.io.File; 40import java.io.FileNotFoundException; 41import java.util.ArrayList; 42import java.util.List; 43 44class 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 mPreloadController = new PreloadController(mRecyclerView); 81 mRecyclerView.setOnScrollListener(mPreloadController); 82 83 mContentView = (PrintContentView) activity.findViewById(R.id.options_content); 84 mEmbeddedContentContainer = (EmbeddedContentContainer) activity.findViewById( 85 R.id.embedded_content_container); 86 mEmbeddedContentContainer.setOnSizeChangeListener(this); 87 } 88 89 @Override 90 public void onSizeChanged(int width, int height) { 91 mPageAdapter.onPreviewAreaSizeChanged(); 92 } 93 94 public boolean isOptionsOpened() { 95 return mContentView.isOptionsOpened(); 96 } 97 98 public void closeOptions() { 99 mContentView.closeOptions(); 100 } 101 102 public void setUiShown(boolean shown) { 103 if (shown) { 104 mRecyclerView.setVisibility(View.VISIBLE); 105 } else { 106 mRecyclerView.setVisibility(View.GONE); 107 } 108 } 109 110 public void onOrientationChanged() { 111 // Adjust the print option column count. 112 final int optionColumnCount = mActivity.getResources().getInteger( 113 R.integer.print_option_column_count); 114 mPrintOptionsLayout.setColumnCount(optionColumnCount); 115 mPageAdapter.onOrientationChanged(); 116 } 117 118 public int getFilePageCount() { 119 return mPageAdapter.getFilePageCount(); 120 } 121 122 public PageRange[] getSelectedPages() { 123 return mPageAdapter.getSelectedPages(); 124 } 125 126 public PageRange[] getRequestedPages() { 127 return mPageAdapter.getRequestedPages(); 128 } 129 130 public void onContentUpdated(boolean documentChanged, int documentPageCount, 131 PageRange[] writtenPages, PageRange[] selectedPages, MediaSize mediaSize, 132 Margins minMargins) { 133 boolean contentChanged = false; 134 135 if (documentChanged) { 136 contentChanged = true; 137 } 138 139 if (documentPageCount != mDocumentPageCount) { 140 mDocumentPageCount = documentPageCount; 141 contentChanged = true; 142 } 143 144 if (contentChanged) { 145 // If not closed, close as we start over. 146 if (mPageAdapter.isOpened()) { 147 Message operation = mHandler.obtainMessage(MyHandler.MSG_CLOSE); 148 mHandler.enqueueOperation(operation); 149 } 150 } 151 152 // The content changed. In this case we have to invalidate 153 // all rendered pages and reopen the file... 154 if ((contentChanged || !mPageAdapter.isOpened()) && writtenPages != null) { 155 Message operation = mHandler.obtainMessage(MyHandler.MSG_OPEN); 156 mHandler.enqueueOperation(operation); 157 } 158 159 // Update the attributes before after closed to avoid flicker. 160 SomeArgs args = SomeArgs.obtain(); 161 args.arg1 = writtenPages; 162 args.arg2 = selectedPages; 163 args.arg3 = mediaSize; 164 args.arg4 = minMargins; 165 args.argi1 = documentPageCount; 166 167 Message operation = mHandler.obtainMessage(MyHandler.MSG_UPDATE, args); 168 mHandler.enqueueOperation(operation); 169 170 // If document changed and has pages we want to start preloading. 171 if (contentChanged && writtenPages != null) { 172 operation = mHandler.obtainMessage(MyHandler.MSG_START_PRELOAD); 173 mHandler.enqueueOperation(operation); 174 } 175 } 176 177 @Override 178 public void onReleaseRequested(final File file) { 179 // This is called from the async task's single threaded executor 180 // thread, i.e. not on the main thread - so post a message. 181 mHandler.post(new Runnable() { 182 @Override 183 public void run() { 184 // At this point the other end will write to the file, hence 185 // we have to close it and reopen after the write completes. 186 if (mPageAdapter.isOpened()) { 187 Message operation = mHandler.obtainMessage(MyHandler.MSG_CLOSE); 188 mHandler.enqueueOperation(operation); 189 } 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