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 17 package com.android.printspooler.ui; 18 19 import android.os.Handler; 20 import android.os.Looper; 21 import android.os.Message; 22 import android.os.ParcelFileDescriptor; 23 import android.print.PageRange; 24 import android.print.PrintAttributes.MediaSize; 25 import android.print.PrintAttributes.Margins; 26 import android.print.PrintDocumentInfo; 27 import android.support.v7.widget.GridLayoutManager; 28 import android.support.v7.widget.RecyclerView; 29 import android.support.v7.widget.RecyclerView.ViewHolder; 30 import android.support.v7.widget.RecyclerView.LayoutManager; 31 import android.view.View; 32 import com.android.internal.os.SomeArgs; 33 import com.android.printspooler.R; 34 import com.android.printspooler.model.MutexFileProvider; 35 import com.android.printspooler.widget.PrintContentView; 36 import com.android.printspooler.widget.EmbeddedContentContainer; 37 import com.android.printspooler.widget.PrintOptionsLayout; 38 39 import java.io.File; 40 import java.io.FileNotFoundException; 41 import java.util.ArrayList; 42 import java.util.List; 43 44 class 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 mRecyclerView.setItemViewCacheSize(0); 81 mPreloadController = new PreloadController(); 82 mRecyclerView.addOnScrollListener(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 || !mPageAdapter.isOpened()) && 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 if (mPageAdapter.isOpened()) { 188 Message operation = mHandler.obtainMessage(MyHandler.MSG_CLOSE); 189 mHandler.enqueueOperation(operation); 190 } 191 } 192 }); 193 } 194 195 public void destroy(Runnable callback) { 196 mHandler.cancelQueuedOperations(); 197 mRecyclerView.setAdapter(null); 198 mPageAdapter.destroy(callback); 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_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 cancelQueuedOperations() { 244 mPendingOperations.clear(); 245 } 246 247 public void enqueueOperation(Message message) { 248 mPendingOperations.add(message); 249 handleNextOperation(); 250 } 251 252 public void handleNextOperation() { 253 while (!mPendingOperations.isEmpty() && !mAsyncOperationInProgress) { 254 Message operation = mPendingOperations.remove(0); 255 handleMessage(operation); 256 } 257 } 258 259 @Override 260 public void handleMessage(Message message) { 261 switch (message.what) { 262 case MSG_OPEN: { 263 try { 264 File file = mFileProvider.acquireFile(PrintPreviewController.this); 265 ParcelFileDescriptor pfd = ParcelFileDescriptor.open(file, 266 ParcelFileDescriptor.MODE_READ_ONLY); 267 268 mAsyncOperationInProgress = true; 269 mPageAdapter.open(pfd, new Runnable() { 270 @Override 271 public void run() { 272 if (mDocumentPageCount == PrintDocumentInfo.PAGE_COUNT_UNKNOWN) { 273 mDocumentPageCount = mPageAdapter.getFilePageCount(); 274 mActivity.updateOptionsUi(); 275 } 276 mOnAsyncOperationDoneCallback.run(); 277 } 278 }); 279 } catch (FileNotFoundException fnfe) { 280 /* ignore - file guaranteed to be there */ 281 } 282 } break; 283 284 case MSG_CLOSE: { 285 mAsyncOperationInProgress = true; 286 mPageAdapter.close(new Runnable() { 287 @Override 288 public void run() { 289 mFileProvider.releaseFile(); 290 mOnAsyncOperationDoneCallback.run(); 291 } 292 }); 293 } break; 294 295 case MSG_UPDATE: { 296 SomeArgs args = (SomeArgs) message.obj; 297 PageRange[] writtenPages = (PageRange[]) args.arg1; 298 PageRange[] selectedPages = (PageRange[]) args.arg2; 299 MediaSize mediaSize = (MediaSize) args.arg3; 300 Margins margins = (Margins) args.arg4; 301 final int pageCount = args.argi1; 302 args.recycle(); 303 304 mPageAdapter.update(writtenPages, selectedPages, pageCount, 305 mediaSize, margins); 306 307 } break; 308 309 case MSG_START_PRELOAD: { 310 mPreloadController.startPreloadContent(); 311 } break; 312 } 313 } 314 } 315 316 private final class PreloadController extends RecyclerView.OnScrollListener { 317 private int mOldScrollState; 318 319 public PreloadController() { 320 mOldScrollState = mRecyclerView.getScrollState(); 321 } 322 323 @Override 324 public void onScrollStateChanged(RecyclerView recyclerView, int state) { 325 switch (mOldScrollState) { 326 case RecyclerView.SCROLL_STATE_SETTLING: { 327 if (state == RecyclerView.SCROLL_STATE_IDLE 328 || state == RecyclerView.SCROLL_STATE_DRAGGING){ 329 startPreloadContent(); 330 } 331 } break; 332 333 case RecyclerView.SCROLL_STATE_IDLE: 334 case RecyclerView.SCROLL_STATE_DRAGGING: { 335 if (state == RecyclerView.SCROLL_STATE_SETTLING) { 336 stopPreloadContent(); 337 } 338 } break; 339 } 340 mOldScrollState = state; 341 } 342 343 public void startPreloadContent() { 344 PageAdapter pageAdapter = (PageAdapter) mRecyclerView.getAdapter(); 345 if (pageAdapter != null && pageAdapter.isOpened()) { 346 PageRange shownPages = computeShownPages(); 347 if (shownPages != null) { 348 pageAdapter.startPreloadContent(shownPages); 349 } 350 } 351 } 352 353 public void stopPreloadContent() { 354 PageAdapter pageAdapter = (PageAdapter) mRecyclerView.getAdapter(); 355 if (pageAdapter != null && pageAdapter.isOpened()) { 356 pageAdapter.stopPreloadContent(); 357 } 358 } 359 360 private PageRange computeShownPages() { 361 final int childCount = mRecyclerView.getChildCount(); 362 if (childCount > 0) { 363 LayoutManager layoutManager = mRecyclerView.getLayoutManager(); 364 365 View firstChild = layoutManager.getChildAt(0); 366 ViewHolder firstHolder = mRecyclerView.getChildViewHolder(firstChild); 367 368 View lastChild = layoutManager.getChildAt(layoutManager.getChildCount() - 1); 369 ViewHolder lastHolder = mRecyclerView.getChildViewHolder(lastChild); 370 371 return new PageRange(firstHolder.getLayoutPosition(), 372 lastHolder.getLayoutPosition()); 373 } 374 return null; 375 } 376 } 377 } 378