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        mRecyclerView.setItemViewCacheSize(0);
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 || !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        if (mPageAdapter.isOpened()) {
197            Message operation = mHandler.obtainMessage(MyHandler.MSG_CLOSE);
198            mHandler.enqueueOperation(operation);
199        }
200
201        Message operation = mHandler.obtainMessage(MyHandler.MSG_DESTROY);
202        operation.obj = callback;
203        mHandler.enqueueOperation(operation);
204    }
205
206    @Override
207    public int getWidth() {
208        return mEmbeddedContentContainer.getWidth();
209    }
210
211    @Override
212    public int getHeight() {
213        return mEmbeddedContentContainer.getHeight();
214    }
215
216    @Override
217    public void setColumnCount(int columnCount) {
218        mLayoutManger.setSpanCount(columnCount);
219    }
220
221    @Override
222    public void setPadding(int left, int top , int right, int bottom) {
223        mRecyclerView.setPadding(left, top, right, bottom);
224    }
225
226    private final class MyHandler extends Handler {
227        public static final int MSG_OPEN = 1;
228        public static final int MSG_CLOSE = 2;
229        public static final int MSG_DESTROY = 3;
230        public static final int MSG_UPDATE = 4;
231        public static final int MSG_START_PRELOAD = 5;
232
233        private boolean mAsyncOperationInProgress;
234
235        private final Runnable mOnAsyncOperationDoneCallback = new Runnable() {
236            @Override
237            public void run() {
238                mAsyncOperationInProgress = false;
239                handleNextOperation();
240            }
241        };
242
243        private final List<Message> mPendingOperations = new ArrayList<>();
244
245        public MyHandler(Looper looper) {
246            super(looper, null, false);
247        }
248
249        public void enqueueOperation(Message message) {
250            mPendingOperations.add(message);
251            handleNextOperation();
252        }
253
254        public void handleNextOperation() {
255            while (!mPendingOperations.isEmpty() && !mAsyncOperationInProgress) {
256                Message operation = mPendingOperations.remove(0);
257                handleMessage(operation);
258            }
259        }
260
261        @Override
262        public void handleMessage(Message message) {
263            switch (message.what) {
264                case MSG_OPEN: {
265                    try {
266                        File file = mFileProvider.acquireFile(PrintPreviewController.this);
267                        ParcelFileDescriptor pfd = ParcelFileDescriptor.open(file,
268                                ParcelFileDescriptor.MODE_READ_ONLY);
269
270                        mAsyncOperationInProgress = true;
271                        mPageAdapter.open(pfd, new Runnable() {
272                            @Override
273                            public void run() {
274                                if (mDocumentPageCount == PrintDocumentInfo.PAGE_COUNT_UNKNOWN) {
275                                    mDocumentPageCount = mPageAdapter.getFilePageCount();
276                                    mActivity.updateOptionsUi();
277                                }
278                                mOnAsyncOperationDoneCallback.run();
279                            }
280                        });
281                    } catch (FileNotFoundException fnfe) {
282                        /* ignore - file guaranteed to be there */
283                    }
284                } break;
285
286                case MSG_CLOSE: {
287                    mAsyncOperationInProgress = true;
288                    mPageAdapter.close(new Runnable() {
289                        @Override
290                        public void run() {
291                            mFileProvider.releaseFile();
292                            mOnAsyncOperationDoneCallback.run();
293                        }
294                    });
295                } break;
296
297                case MSG_DESTROY: {
298                    Runnable callback = (Runnable) message.obj;
299                    mRecyclerView.setAdapter(null);
300                    mPageAdapter.destroy(callback);
301                    handleNextOperation();
302                } break;
303
304                case MSG_UPDATE: {
305                    SomeArgs args = (SomeArgs) message.obj;
306                    PageRange[] writtenPages = (PageRange[]) args.arg1;
307                    PageRange[] selectedPages = (PageRange[]) args.arg2;
308                    MediaSize mediaSize = (MediaSize) args.arg3;
309                    Margins margins = (Margins) args.arg4;
310                    final int pageCount = args.argi1;
311                    args.recycle();
312
313                    mPageAdapter.update(writtenPages, selectedPages, pageCount,
314                            mediaSize, margins);
315
316                } break;
317
318                case MSG_START_PRELOAD: {
319                    mPreloadController.startPreloadContent();
320                } break;
321            }
322        }
323    }
324
325    private final class PreloadController extends RecyclerView.OnScrollListener {
326        private final RecyclerView mRecyclerView;
327
328        private int mOldScrollState;
329
330        public PreloadController(RecyclerView recyclerView) {
331            mRecyclerView = recyclerView;
332            mOldScrollState = mRecyclerView.getScrollState();
333        }
334
335        @Override
336        public void onScrollStateChanged(RecyclerView recyclerView, int state) {
337            switch (mOldScrollState) {
338                case RecyclerView.SCROLL_STATE_SETTLING: {
339                    if (state == RecyclerView.SCROLL_STATE_IDLE
340                            || state == RecyclerView.SCROLL_STATE_DRAGGING){
341                        startPreloadContent();
342                    }
343                } break;
344
345                case RecyclerView.SCROLL_STATE_IDLE:
346                case RecyclerView.SCROLL_STATE_DRAGGING: {
347                    if (state == RecyclerView.SCROLL_STATE_SETTLING) {
348                        stopPreloadContent();
349                    }
350                } break;
351            }
352            mOldScrollState = state;
353        }
354
355        public void startPreloadContent() {
356            PageAdapter pageAdapter = (PageAdapter) mRecyclerView.getAdapter();
357            if (pageAdapter != null && pageAdapter.isOpened()) {
358                PageRange shownPages = computeShownPages();
359                if (shownPages != null) {
360                    pageAdapter.startPreloadContent(shownPages);
361                }
362            }
363        }
364
365        public void stopPreloadContent() {
366            PageAdapter pageAdapter = (PageAdapter) mRecyclerView.getAdapter();
367            if (pageAdapter != null && 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