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 
PrintPreviewController(PrintActivity activity, MutexFileProvider fileProvider)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
onSizeChanged(int width, int height)91     public void onSizeChanged(int width, int height) {
92         mPageAdapter.onPreviewAreaSizeChanged();
93     }
94 
isOptionsOpened()95     public boolean isOptionsOpened() {
96         return mContentView.isOptionsOpened();
97     }
98 
closeOptions()99     public void closeOptions() {
100         mContentView.closeOptions();
101     }
102 
setUiShown(boolean shown)103     public void setUiShown(boolean shown) {
104         if (shown) {
105             mRecyclerView.setVisibility(View.VISIBLE);
106         } else {
107             mRecyclerView.setVisibility(View.GONE);
108         }
109     }
110 
onOrientationChanged()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 
getFilePageCount()119     public int getFilePageCount() {
120         return mPageAdapter.getFilePageCount();
121     }
122 
getSelectedPages()123     public PageRange[] getSelectedPages() {
124         return mPageAdapter.getSelectedPages();
125     }
126 
getRequestedPages()127     public PageRange[] getRequestedPages() {
128         return mPageAdapter.getRequestedPages();
129     }
130 
onContentUpdated(boolean documentChanged, int documentPageCount, PageRange[] writtenPages, PageRange[] selectedPages, MediaSize mediaSize, Margins minMargins)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
onReleaseRequested(final File file)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 
destroy(Runnable callback)195     public void destroy(Runnable callback) {
196         mHandler.cancelQueuedOperations();
197         mRecyclerView.setAdapter(null);
198         mPageAdapter.destroy(callback);
199     }
200 
201     @Override
getWidth()202     public int getWidth() {
203         return mEmbeddedContentContainer.getWidth();
204     }
205 
206     @Override
getHeight()207     public int getHeight() {
208         return mEmbeddedContentContainer.getHeight();
209     }
210 
211     @Override
setColumnCount(int columnCount)212     public void setColumnCount(int columnCount) {
213         mLayoutManger.setSpanCount(columnCount);
214     }
215 
216     @Override
setPadding(int left, int top , int right, int bottom)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 
MyHandler(Looper looper)239         public MyHandler(Looper looper) {
240             super(looper, null, false);
241         }
242 
cancelQueuedOperations()243         public void cancelQueuedOperations() {
244             mPendingOperations.clear();
245         }
246 
enqueueOperation(Message message)247         public void enqueueOperation(Message message) {
248             mPendingOperations.add(message);
249             handleNextOperation();
250         }
251 
handleNextOperation()252         public void handleNextOperation() {
253             while (!mPendingOperations.isEmpty() && !mAsyncOperationInProgress) {
254                 Message operation = mPendingOperations.remove(0);
255                 handleMessage(operation);
256             }
257         }
258 
259         @Override
handleMessage(Message message)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 
PreloadController()319         public PreloadController() {
320             mOldScrollState = mRecyclerView.getScrollState();
321         }
322 
323         @Override
onScrollStateChanged(RecyclerView recyclerView, int state)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 
startPreloadContent()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 
stopPreloadContent()353         public void stopPreloadContent() {
354             PageAdapter pageAdapter = (PageAdapter) mRecyclerView.getAdapter();
355             if (pageAdapter != null && pageAdapter.isOpened()) {
356                 pageAdapter.stopPreloadContent();
357             }
358         }
359 
computeShownPages()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