diff options
32 files changed, 3546 insertions, 775 deletions
diff --git a/core/java/android/print/PageRange.java b/core/java/android/print/PageRange.java index d6320f0..8c229c5 100644 --- a/core/java/android/print/PageRange.java +++ b/core/java/android/print/PageRange.java @@ -78,6 +78,30 @@ public final class PageRange implements Parcelable { return mEnd; } + /** + * Gets whether a page range contains a a given page. + * + * @param pageIndex The page index. + * @return True if the page is within this range. + * + * @hide + */ + public boolean contains(int pageIndex) { + return pageIndex >= mStart && pageIndex <= mEnd; + } + + /** + * Get the size of this range which is the number of + * pages it contains. + * + * @return The size of the range. + * + * @hide + */ + public int getSize() { + return mEnd - mStart + 1; + } + @Override public int describeContents() { return 0; diff --git a/core/java/android/print/PrintAttributes.java b/core/java/android/print/PrintAttributes.java index 2810d55..30f0c6a 100644 --- a/core/java/android/print/PrintAttributes.java +++ b/core/java/android/print/PrintAttributes.java @@ -105,6 +105,13 @@ public final class PrintAttributes implements Parcelable { /** * Gets the minimal margins. If the content does not fit * these margins it will be clipped. + * <p> + * <strong>These margins are physically imposed by the printer and they + * are <em>not</em> rotated, i.e. they are the same for both portrait and + * landscape. For example, a printer may not be able to print in a stripe + * on both left and right sides of the page. + * </strong> + * </p> * * @return The margins or <code>null</code> if not set. */ @@ -115,6 +122,13 @@ public final class PrintAttributes implements Parcelable { /** * Sets the minimal margins. If the content does not fit * these margins it will be clipped. + * <p> + * <strong>These margins are physically imposed by the printer and they + * are <em>not</em> rotated, i.e. they are the same for both portrait and + * landscape. For example, a printer may not be able to print in a stripe + * on both left and right sides of the page. + * </strong> + * </p> * * @param The margins. * @@ -193,14 +207,8 @@ public final class PrintAttributes implements Parcelable { oldResolution.getHorizontalDpi()); attributes.setResolution(newResolution); - // Rotate the physical margins. - Margins oldMinMargins = getMinMargins(); - Margins newMinMargins = new Margins( - oldMinMargins.getBottomMils(), - oldMinMargins.getLeftMils(), - oldMinMargins.getTopMils(), - oldMinMargins.getRightMils()); - attributes.setMinMargins(newMinMargins); + // Do not rotate the physical margins. + attributes.setMinMargins(getMinMargins()); attributes.setColorMode(getColorMode()); @@ -236,14 +244,8 @@ public final class PrintAttributes implements Parcelable { oldResolution.getHorizontalDpi()); attributes.setResolution(newResolution); - // Rotate the physical margins. - Margins oldMinMargins = getMinMargins(); - Margins newMargins = new Margins( - oldMinMargins.getTopMils(), - oldMinMargins.getRightMils(), - oldMinMargins.getBottomMils(), - oldMinMargins.getLeftMils()); - attributes.setMinMargins(newMargins); + // Do not rotate the physical margins. + attributes.setMinMargins(getMinMargins()); attributes.setColorMode(getColorMode()); diff --git a/core/java/android/print/PrintManager.java b/core/java/android/print/PrintManager.java index 9361286..7ec838e 100644 --- a/core/java/android/print/PrintManager.java +++ b/core/java/android/print/PrintManager.java @@ -105,7 +105,7 @@ public final class PrintManager { private static final String LOG_TAG = "PrintManager"; - private static final boolean DEBUG = false; + private static final boolean DEBUG = true; private static final int MSG_NOTIFY_PRINT_JOB_STATE_CHANGED = 1; diff --git a/core/jni/android/graphics/pdf/PdfRenderer.cpp b/core/jni/android/graphics/pdf/PdfRenderer.cpp index 15de24a..303ddea 100644 --- a/core/jni/android/graphics/pdf/PdfRenderer.cpp +++ b/core/jni/android/graphics/pdf/PdfRenderer.cpp @@ -204,9 +204,13 @@ static void renderPageBitmap(FPDF_BITMAP bitmap, FPDF_PAGE page, int destLeft, i // PDF's coordinate system origin is left-bottom while // in graphics it is the top-left, so remap the origin. matrix.Set(1, 0, 0, -1, 0, pPage->GetPageHeight()); - matrix.Scale(transform->getScaleX(), transform->getScaleY()); - matrix.Rotate(transform->getSkewX(), transform->getSkewY()); - matrix.Translate(transform->getTranslateX(), transform->getTranslateY()); + + SkScalar transformValues[6]; + transform->asAffine(transformValues); + + matrix.Concat(transformValues[SkMatrix::kAScaleX], transformValues[SkMatrix::kASkewY], + transformValues[SkMatrix::kASkewX], transformValues[SkMatrix::kAScaleY], + transformValues[SkMatrix::kATransX], transformValues[SkMatrix::kATransY]); } pageContext->AppendObjectList(pPage, &matrix); @@ -251,6 +255,7 @@ static void nativeRenderPage(JNIEnv* env, jclass thiz, jlong documentPtr, jlong renderPageBitmap(bitmap, page, destLeft, destTop, destRight, destBottom, skMatrix, renderFlags); + skBitmap->notifyPixelsChanged(); skBitmap->unlockPixels(); } diff --git a/graphics/java/android/graphics/pdf/PdfRenderer.java b/graphics/java/android/graphics/pdf/PdfRenderer.java index b63edce..b5d9729 100644 --- a/graphics/java/android/graphics/pdf/PdfRenderer.java +++ b/graphics/java/android/graphics/pdf/PdfRenderer.java @@ -70,6 +70,32 @@ import java.lang.annotation.RetentionPolicy; * renderer.close(); * </pre> * + * <h3>Print preview and print output</h3> + * <p> + * If you are using this class to rasterize a PDF for printing or show a print + * preview, it is recommended that you respect the following contract in order + * to provide a consistent user experience when seeing a preview and printing, + * i.e. the user sees a preview that is the same as the printout. + * </p> + * <ul> + * <li> + * Respect the property whether the document would like to be scaled for printing + * as per {@link #shouldScaleForPrinting()}. + * </li> + * <li> + * When scaling a document for printing the aspect ratio should be preserved. + * </li> + * <li> + * Do not inset the content with any margins from the {@link android.print.PrintAttributes} + * as the application is responsible to render it such that the margins are respected. + * </li> + * <li> + * If document page size is greater than the printed media size the content should + * be anchored to the upper left corner of the page for left-to-right locales and + * top right corner for right-to-left locales. + * </li> + * </ul> + * * @see #close() */ public final class PdfRenderer implements AutoCloseable { @@ -188,7 +214,6 @@ public final class PdfRenderer implements AutoCloseable { private void doClose() { if (mCurrentPage != null) { mCurrentPage.close(); - mCurrentPage = null; } nativeClose(mNativeDocument); try { @@ -280,7 +305,7 @@ public final class PdfRenderer implements AutoCloseable { * </p> * <p> * You may optionally specify a matrix to transform the content from page coordinates - * which are in points (1/72") to bitmap coordintates which are in pixels. If this + * which are in points (1/72") to bitmap coordinates which are in pixels. If this * matrix is not provided this method will apply a transformation that will fit the * whole page to the destination clip if provided or the destination bitmap if no * clip is provided. @@ -375,6 +400,7 @@ public final class PdfRenderer implements AutoCloseable { nativeClosePage(mNativePage); mNativePage = 0; mCloseGuard.close(); + mCurrentPage = null; } private void throwIfClosed() { diff --git a/packages/PrintSpooler/Android.mk b/packages/PrintSpooler/Android.mk index 96592b4..a3a1f8c 100644 --- a/packages/PrintSpooler/Android.mk +++ b/packages/PrintSpooler/Android.mk @@ -24,6 +24,6 @@ LOCAL_PACKAGE_NAME := PrintSpooler LOCAL_JAVA_LIBRARIES := framework-base -LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4 +LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4 android-support-v7-recyclerview include $(BUILD_PACKAGE) diff --git a/packages/PrintSpooler/AndroidManifest.xml b/packages/PrintSpooler/AndroidManifest.xml index 4c0bbb8..223013f 100644 --- a/packages/PrintSpooler/AndroidManifest.xml +++ b/packages/PrintSpooler/AndroidManifest.xml @@ -17,9 +17,9 @@ */ --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.android.printspooler" - android:versionName="1" - android:versionCode="1"> + package="com.android.printspooler" + android:versionName="1" + android:versionCode="1"> <!-- Allows an application to call APIs that give it access to all print jobs on the device. Usually an app can access only the print jobs it created. --> @@ -58,7 +58,7 @@ android:name=".ui.PrintActivity" android:configChanges="orientation|screenSize" android:permission="android.permission.BIND_PRINT_SPOOLER_SERVICE" - android:theme="@android:style/Theme.DeviceDefault.NoActionBar"> + android:theme="@style/PrintActivity"> <intent-filter> <action android:name="android.print.PRINT_DIALOG" /> <category android:name="android.intent.category.DEFAULT" /> diff --git a/packages/PrintSpooler/res/layout/preview_page.xml b/packages/PrintSpooler/res/layout/preview_page.xml new file mode 100644 index 0000000..0e314d1 --- /dev/null +++ b/packages/PrintSpooler/res/layout/preview_page.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2014 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/preview_page" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="@dimen/preview_page_margin" + android:orientation="vertical" + android:background="?android:attr/colorForeground"> + + <com.android.printspooler.widget.PageContentView + android:id="@+id/page_content" + android:layout_width="fill_parent" + android:layout_height="fill_parent"> + </com.android.printspooler.widget.PageContentView> + + <RelativeLayout + android:id="@+id/page_footer" + android:layout_width="fill_parent" + android:layout_height="?android:attr/listPreferredItemHeightSmall" + android:background="@*android:color/material_grey_500" + android:orientation="horizontal"> + + <TextView + android:id="@+id/page_number" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerInParent="true" + android:textAppearance="?android:attr/textAppearanceMedium" + android:textColor="?android:attr/textColorPrimary"> + </TextView> + + <CheckBox + android:id="@+id/page_selector" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginRight="8dip" + android:layout_alignParentEnd="true" + android:layout_centerVertical="true"> + </CheckBox> + + </RelativeLayout> + +</LinearLayout> diff --git a/packages/PrintSpooler/res/layout/print_activity.xml b/packages/PrintSpooler/res/layout/print_activity.xml index 9715322..01cf9c1 100644 --- a/packages/PrintSpooler/res/layout/print_activity.xml +++ b/packages/PrintSpooler/res/layout/print_activity.xml @@ -14,24 +14,23 @@ limitations under the License. --> -<com.android.printspooler.widget.ContentView +<com.android.printspooler.widget.PrintContentView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:printspooler="http://schemas.android.com/apk/res/com.android.printspooler" android:id="@+id/options_content" android:layout_width="fill_parent" - android:layout_height="fill_parent" - android:visibility="invisible" - android:background="?android:attr/colorForeground"> + android:layout_height="fill_parent"> + + <!-- Destination --> <FrameLayout android:id="@+id/static_content" android:layout_width="fill_parent" android:layout_height="wrap_content" android:padding="16dip" + android:elevation="8dip" android:background="?android:attr/colorForegroundInverse"> - <!-- Destination --> - <Spinner android:id="@+id/destination_spinner" android:layout_width="wrap_content" @@ -51,6 +50,7 @@ android:paddingStart="16dip" android:paddingEnd="16dip" android:orientation="horizontal" + android:elevation="8dip" android:background="?android:attr/colorForegroundInverse"> <TextView @@ -69,7 +69,8 @@ android:layout_height="wrap_content" android:layout_marginTop="8dip" android:layout_marginStart="16dip" - android:textAppearance="?android:attr/textAppearanceMedium"> + android:textAppearance="?android:attr/textAppearanceMedium" + android:textColor="?android:attr/textColorPrimary"> </TextView> <TextView @@ -88,302 +89,317 @@ android:layout_height="wrap_content" android:layout_marginTop="8dip" android:layout_marginStart="16dip" - android:textAppearance="?android:attr/textAppearanceMedium"> + android:textAppearance="?android:attr/textAppearanceMedium" + android:textColor="?android:attr/textColorPrimary"> </TextView> </LinearLayout> - <FrameLayout + <!-- Print button --> + + <ImageButton + android:id="@+id/print_button" + style="?android:attr/buttonStyleSmall" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="16dip" + android:elevation="8dip" + android:background="@drawable/print_button"> + </ImageButton> + + <!-- Controls --> + + <LinearLayout android:id="@+id/dynamic_content" android:layout_width="fill_parent" android:layout_height="wrap_content" - android:paddingBottom="16dip"> + android:orientation="vertical" + android:elevation="8dip" + android:background="?android:attr/colorForegroundInverse"> <LinearLayout + android:id="@+id/draggable_content" android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="vertical"> - <LinearLayout - android:id="@+id/draggable_content" + <com.android.printspooler.widget.PrintOptionsLayout + android:id="@+id/options_container" android:layout_width="fill_parent" android:layout_height="wrap_content" - android:orientation="vertical"> + printspooler:columnCount="@integer/print_option_column_count"> - <com.android.printspooler.widget.PrintOptionsLayout - android:id="@+id/options_container" - android:layout_width="fill_parent" + <LinearLayout + android:layout_width="wrap_content" android:layout_height="wrap_content" - android:background="?android:attr/colorForegroundInverse" - printspooler:columnCount="@integer/print_option_column_count"> + android:layout_marginStart="16dip" + android:layout_marginEnd="16dip" + android:orientation="vertical"> + + <!-- Copies --> - <LinearLayout + <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="16dip" - android:layout_marginEnd="16dip" - android:orientation="vertical"> - - <!-- Copies --> - - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginTop="8dip" - android:layout_marginStart="12dip" - android:textAppearance="?android:attr/textAppearanceSmall" - android:labelFor="@+id/copies_edittext" - android:text="@string/label_copies"> - </TextView> - - <view - class="com.android.printspooler.widget.FirstFocusableEditText" - android:id="@+id/copies_edittext" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - style="?android:attr/editTextStyle" - android:inputType="numberDecimal"> - </view> - - </LinearLayout> - - <LinearLayout - android:layout_width="wrap_content" + android:layout_marginTop="8dip" + android:layout_marginStart="12dip" + android:textAppearance="?android:attr/textAppearanceSmall" + android:labelFor="@+id/copies_edittext" + android:text="@string/label_copies"> + </TextView> + + <view + class="com.android.printspooler.widget.FirstFocusableEditText" + android:id="@+id/copies_edittext" + android:layout_width="fill_parent" android:layout_height="wrap_content" - android:layout_marginStart="16dip" - android:layout_marginEnd="16dip" - android:orientation="vertical"> - - <!-- Paper size --> - - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginTop="8dip" - android:layout_marginStart="12dip" - android:textAppearance="?android:attr/textAppearanceSmall" - android:labelFor="@+id/paper_size_spinner" - android:text="@string/label_paper_size"> - </TextView> - - <Spinner - android:id="@+id/paper_size_spinner" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - style="@style/PrintOptionSpinnerStyle"> - </Spinner> - - </LinearLayout> - - <LinearLayout + style="?android:attr/editTextStyle" + android:inputType="numberDecimal"> + </view> + + </LinearLayout> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="16dip" + android:layout_marginEnd="16dip" + android:orientation="vertical"> + + <!-- Paper size --> + + <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="16dip" - android:layout_marginEnd="16dip" - android:orientation="vertical"> - - <!-- Color --> - - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginTop="8dip" - android:layout_marginStart="12dip" - android:textAppearance="?android:attr/textAppearanceSmall" - android:labelFor="@+id/color_spinner" - android:text="@string/label_color"> - </TextView> - - <Spinner - android:id="@+id/color_spinner" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - style="@style/PrintOptionSpinnerStyle"> - </Spinner> - - </LinearLayout> - - <LinearLayout - android:layout_width="wrap_content" + android:layout_marginTop="8dip" + android:layout_marginStart="12dip" + android:textAppearance="?android:attr/textAppearanceSmall" + android:labelFor="@+id/paper_size_spinner" + android:text="@string/label_paper_size"> + </TextView> + + <Spinner + android:id="@+id/paper_size_spinner" + android:layout_width="fill_parent" android:layout_height="wrap_content" - android:layout_marginStart="16dip" - android:layout_marginEnd="16dip" - android:orientation="vertical"> - - <!-- Orientation --> - - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginTop="8dip" - android:layout_marginStart="12dip" - android:textAppearance="?android:attr/textAppearanceSmall" - android:labelFor="@+id/orientation_spinner" - android:text="@string/label_orientation"> - </TextView> - - <Spinner - android:id="@+id/orientation_spinner" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - style="@style/PrintOptionSpinnerStyle"> - </Spinner> - - </LinearLayout> - - <LinearLayout + style="@style/PrintOptionSpinnerStyle"> + </Spinner> + + </LinearLayout> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="16dip" + android:layout_marginEnd="16dip" + android:orientation="vertical"> + + <!-- Color --> + + <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="16dip" - android:layout_marginEnd="16dip" - android:orientation="vertical"> - - <!-- Range options --> - - <TextView - android:id="@+id/range_options_title" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginTop="8dip" - android:layout_marginStart="12dip" - android:textAppearance="?android:attr/textAppearanceSmall" - android:labelFor="@+id/range_options_spinner" - android:text="@string/page_count_unknown"> - </TextView> - - <Spinner - android:id="@+id/range_options_spinner" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - style="@style/PrintOptionSpinnerStyle"> - </Spinner> - - </LinearLayout> - - <LinearLayout - android:layout_width="wrap_content" + android:layout_marginTop="8dip" + android:layout_marginStart="12dip" + android:textAppearance="?android:attr/textAppearanceSmall" + android:labelFor="@+id/color_spinner" + android:text="@string/label_color"> + </TextView> + + <Spinner + android:id="@+id/color_spinner" + android:layout_width="fill_parent" android:layout_height="wrap_content" - android:layout_marginStart="16dip" - android:layout_marginEnd="16dip" - android:orientation="vertical"> - - <!-- Pages --> - - <TextView - android:id="@+id/page_range_title" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginTop="8dip" - android:layout_marginStart="12dip" - android:textAppearance="?android:attr/textAppearanceSmall" - android:text="@string/pages_range_example" - android:labelFor="@+id/page_range_edittext" - android:textAllCaps="false" - android:visibility="visible"> - </TextView> - - <view - class="com.android.printspooler.widget.FirstFocusableEditText" - android:id="@+id/page_range_edittext" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:layout_gravity="bottom|fill_horizontal" - style="@style/PrintOptionEditTextStyle" - android:visibility="visible" - android:inputType="textNoSuggestions"> - </view> - - </LinearLayout> - - </com.android.printspooler.widget.PrintOptionsLayout> - - <!-- More options --> + style="@style/PrintOptionSpinnerStyle"> + </Spinner> + + </LinearLayout> <LinearLayout - android:id="@+id/more_options_container" - android:layout_width="fill_parent" + android:layout_width="wrap_content" android:layout_height="wrap_content" - android:paddingStart="28dip" - android:paddingEnd="28dip" - android:orientation="vertical" - android:visibility="visible" - android:background="?android:attr/colorForegroundInverse"> + android:layout_marginStart="16dip" + android:layout_marginEnd="16dip" + android:orientation="vertical"> + + <!-- Orientation --> - <ImageView + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dip" + android:layout_marginStart="12dip" + android:textAppearance="?android:attr/textAppearanceSmall" + android:labelFor="@+id/orientation_spinner" + android:text="@string/label_orientation"> + </TextView> + + <Spinner + android:id="@+id/orientation_spinner" android:layout_width="fill_parent" - android:layout_height="1dip" - android:layout_gravity="fill_horizontal" - android:background="?android:attr/colorControlNormal" - android:contentDescription="@null"> - </ImageView> - - <Button - android:id="@+id/more_options_button" - style="?android:attr/borderlessButtonStyle" + android:layout_height="wrap_content" + style="@style/PrintOptionSpinnerStyle"> + </Spinner> + + </LinearLayout> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="16dip" + android:layout_marginEnd="16dip" + android:orientation="vertical"> + + <!-- Range options --> + + <TextView + android:id="@+id/range_options_title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dip" + android:layout_marginStart="12dip" + android:textAppearance="?android:attr/textAppearanceSmall" + android:labelFor="@+id/range_options_spinner" + android:text="@string/page_count_unknown"> + </TextView> + + <Spinner + android:id="@+id/range_options_spinner" android:layout_width="fill_parent" android:layout_height="wrap_content" - android:layout_gravity="fill_horizontal" - android:text="@string/more_options_button" - android:gravity="start|center_vertical" - android:textAllCaps="false"> - </Button> + style="@style/PrintOptionSpinnerStyle"> + </Spinner> + + </LinearLayout> - <ImageView + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="16dip" + android:layout_marginEnd="16dip" + android:orientation="vertical"> + + <!-- Pages --> + + <TextView + android:id="@+id/page_range_title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dip" + android:layout_marginStart="12dip" + android:textAppearance="?android:attr/textAppearanceSmall" + android:text="@string/pages_range_example" + android:labelFor="@+id/page_range_edittext" + android:textAllCaps="false" + android:visibility="visible"> + </TextView> + + <view + class="com.android.printspooler.widget.FirstFocusableEditText" + android:id="@+id/page_range_edittext" android:layout_width="fill_parent" - android:layout_height="1dip" - android:layout_gravity="fill_horizontal" - android:background="?android:attr/colorControlNormal" - android:contentDescription="@null"> - </ImageView> + android:layout_height="wrap_content" + android:layout_gravity="bottom|fill_horizontal" + style="@style/PrintOptionEditTextStyle" + android:visibility="visible" + android:inputType="textNoSuggestions"> + </view> </LinearLayout> - </LinearLayout> + </com.android.printspooler.widget.PrintOptionsLayout> - <!-- Expand/collapse handle --> + <!-- More options --> - <FrameLayout - android:id="@+id/expand_collapse_handle" + <LinearLayout + android:id="@+id/more_options_container" android:layout_width="fill_parent" - android:layout_height="?android:attr/listPreferredItemHeightSmall" - android:layout_marginBottom="28dip" - android:background="?android:attr/colorForegroundInverse" - android:elevation="12dip"> + android:layout_height="wrap_content" + android:paddingStart="28dip" + android:paddingEnd="28dip" + android:orientation="vertical" + android:visibility="visible"> - <ImageButton - android:id="@+id/expand_collapse_icon" - android:layout_width="wrap_content" + <ImageView + android:layout_width="fill_parent" + android:layout_height="1dip" + android:layout_gravity="fill_horizontal" + android:background="?android:attr/colorControlNormal" + android:contentDescription="@null"> + </ImageView> + + <Button + android:id="@+id/more_options_button" + style="?android:attr/borderlessButtonStyle" + android:layout_width="fill_parent" android:layout_height="wrap_content" - android:layout_gravity="center" - android:background="@drawable/ic_expand_more"> - </ImageButton> + android:layout_gravity="fill_horizontal" + android:text="@string/more_options_button" + android:gravity="start|center_vertical" + android:textAllCaps="false"> + </Button> + + <ImageView + android:layout_width="fill_parent" + android:layout_height="1dip" + android:layout_gravity="fill_horizontal" + android:background="?android:attr/colorControlNormal" + android:contentDescription="@null"> + </ImageView> - </FrameLayout> + </LinearLayout> </LinearLayout> - <!-- Print button --> + <!-- Expand/collapse handle --> - <ImageButton - android:id="@+id/print_button" - style="?android:attr/buttonStyleSmall" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="end|bottom" - android:layout_marginEnd="16dip" - android:elevation="12dip" - android:background="@drawable/print_button" - android:src="@*android:drawable/ic_print"> - </ImageButton> + <FrameLayout + android:id="@+id/expand_collapse_handle" + android:layout_width="fill_parent" + android:layout_height="wrap_content"> - </FrameLayout> + <ImageView + android:id="@+id/expand_collapse_icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dip" + android:layout_marginBottom="8dip" + android:layout_gravity="center" + android:background="@drawable/ic_expand_more"> + </ImageView> + </FrameLayout> - <FrameLayout + </LinearLayout> + + <!-- Content --> + + <com.android.printspooler.widget.EmbeddedContentContainer android:id="@+id/embedded_content_container" android:layout_width="fill_parent" - android:layout_height="0dip" - android:animateLayoutChanges="true"> - </FrameLayout> + android:layout_height="fill_parent" + android:animateLayoutChanges="true" + android:background="@color/print_preview_background_color" + android:gravity="center"> + + <!-- Error message added here --> + + <android.support.v7.widget.RecyclerView + android:id="@+id/preview_content" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:clipToPadding="false" + android:orientation="vertical"> + </android.support.v7.widget.RecyclerView> + + <!-- Scrim --> + + <View + android:id="@+id/embedded_content_scrim" + android:layout_width="fill_parent" + android:layout_height="fill_parent"> + </View> + + </com.android.printspooler.widget.EmbeddedContentContainer> -</com.android.printspooler.widget.ContentView> +</com.android.printspooler.widget.PrintContentView> diff --git a/packages/PrintSpooler/res/layout/print_progress_fragment.xml b/packages/PrintSpooler/res/layout/print_progress_fragment.xml index 212da9e..3b010f8 100644 --- a/packages/PrintSpooler/res/layout/print_progress_fragment.xml +++ b/packages/PrintSpooler/res/layout/print_progress_fragment.xml @@ -36,30 +36,13 @@ style="?android:attr/progressBarStyleHorizontal"> </ProgressBar> - <FrameLayout + <TextView + android:id="@+id/message" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:minHeight="?android:attr/listPreferredItemHeight" - android:gravity="center" - android:animateLayoutChanges="true"> - - <TextView - android:id="@+id/message" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:textAppearance="?android:attr/textAppearanceLargeInverse" - android:text="@string/print_operation_canceling" - android:visibility="gone"> - </TextView> - - <Button - android:id="@+id/cancel_button" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@android:string/cancel"> - </Button> - - </FrameLayout> + android:textAppearance="?android:attr/textAppearanceLargeInverse" + android:text="@string/print_preparing_preview"> + </TextView> </LinearLayout> diff --git a/packages/PrintSpooler/res/values-land/constants.xml b/packages/PrintSpooler/res/values-land/constants.xml index 0db7513..e84a32b 100644 --- a/packages/PrintSpooler/res/values-land/constants.xml +++ b/packages/PrintSpooler/res/values-land/constants.xml @@ -18,6 +18,6 @@ <dimen name="printer_list_view_padding_start">48dip</dimen> <dimen name="printer_list_view_padding_end">48dip</dimen> - <integer name="print_option_column_count">3</integer> + <integer name="preview_page_per_row_count">2</integer> </resources> diff --git a/packages/PrintSpooler/res/values-sw600dp-land/constants.xml b/packages/PrintSpooler/res/values-sw600dp-land/constants.xml index cacdf98..f60d8e4 100644 --- a/packages/PrintSpooler/res/values-sw600dp-land/constants.xml +++ b/packages/PrintSpooler/res/values-sw600dp-land/constants.xml @@ -17,5 +17,6 @@ <resources> <integer name="print_option_column_count">6</integer> + <integer name="preview_page_per_row_count">4</integer> </resources> diff --git a/packages/PrintSpooler/res/values-sw600dp/constants.xml b/packages/PrintSpooler/res/values-sw600dp/constants.xml index 14c099c..44de7bf 100644 --- a/packages/PrintSpooler/res/values-sw600dp/constants.xml +++ b/packages/PrintSpooler/res/values-sw600dp/constants.xml @@ -17,5 +17,6 @@ <resources> <integer name="print_option_column_count">3</integer> + <integer name="preview_page_per_row_count">2</integer> </resources> diff --git a/packages/PrintSpooler/res/values/colors.xml b/packages/PrintSpooler/res/values/colors.xml index fd6ae19..677fda7 100644 --- a/packages/PrintSpooler/res/values/colors.xml +++ b/packages/PrintSpooler/res/values/colors.xml @@ -18,4 +18,8 @@ <color name="print_button_tint_color">#EEFF41</color> + <color name="print_preview_scrim_color">#99000000</color> + + <color name="print_preview_background_color">#F2F1F2</color> + </resources> diff --git a/packages/PrintSpooler/res/values/constants.xml b/packages/PrintSpooler/res/values/constants.xml index 9a2c14e..a19cd65 100644 --- a/packages/PrintSpooler/res/values/constants.xml +++ b/packages/PrintSpooler/res/values/constants.xml @@ -19,9 +19,9 @@ <integer name="page_option_value_all">0</integer> <integer name="page_option_value_page_range">1</integer> - <integer name="print_option_column_count">2</integer> + <integer name="preview_page_per_row_count">1</integer> - <integer-array name="page_options_values" translatable="false"> + <integer-array name="page_options_values"> <item>@integer/page_option_value_all</item> <item>@integer/page_option_value_page_range</item> </integer-array> @@ -31,4 +31,11 @@ <dimen name="printer_list_view_padding_start">16dip</dimen> <dimen name="printer_list_view_padding_end">16dip</dimen> + <dimen name="selected_page_elevation">6dip</dimen> + <dimen name="unselected_page_elevation">2dip</dimen> + + <dimen name="preview_page_margin">8dip</dimen> + + <dimen name="preview_list_padding">24dip</dimen> + </resources> diff --git a/packages/PrintSpooler/res/values/strings.xml b/packages/PrintSpooler/res/values/strings.xml index d85529c..dd90bec 100644 --- a/packages/PrintSpooler/res/values/strings.xml +++ b/packages/PrintSpooler/res/values/strings.xml @@ -73,6 +73,11 @@ <!-- Title for the print dialog announced to the user for accessibility. Not shown in the UI. [CHAR LIMIT=none] --> <string name="print_dialog">Print dialog</string> + + <!-- Template for the message that shows the current page out of the total number of pages --> + <string name="current_page_template"><xliff:g id="current_page">%1$d</xliff:g> + /<xliff:g id="page_count">%2$d</xliff:g></string> + <!-- Select printer activity --> <!-- Title for the share action bar menu item. [CHAR LIMIT=20] --> @@ -203,17 +208,17 @@ <string name="print_write_error_message">Couldn\'t write to file</string> <!-- Default message for an error while generating a print job. [CHAR LIMIT=50] --> - <string name="print_error_default_message">Couldn\'t generate print job</string> + <string name="print_error_default_message">Sorry, that didn\'t work. Try again.</string> <!-- Label for the retry button in the error message. [CHAR LIMIT=50] --> <string name="print_error_retry">Retry</string> - <!-- Message for the currently selected printer becoming unavailable. [CHAR LIMIT=50] --> - <string name="print_error_printer_unavailable">Printer unavailable</string> + <!-- Message for the currently selected printer being unavailable. [CHAR LIMIT=100] --> + <string name="print_error_printer_unavailable">This printer isn\'t available right now.</string> <!-- Long running operations --> - <!-- Message for the cancel print operation UI while waiting for cancel to be performed. [CHAR LIMIT=50] --> - <string name="print_operation_canceling">Cancelling\u2026</string> + <!-- Message long running operation when preparing print preview. [CHAR LIMIT=50] --> + <string name="print_preparing_preview">Preparing preview\u2026</string> </resources> diff --git a/packages/PrintSpooler/res/values/themes.xml b/packages/PrintSpooler/res/values/themes.xml index 40bf725..e1e6c44 100644 --- a/packages/PrintSpooler/res/values/themes.xml +++ b/packages/PrintSpooler/res/values/themes.xml @@ -16,6 +16,13 @@ <resources> + <style name="PrintActivity" parent="@android:style/Theme.DeviceDefault.NoActionBar"> + <item name="android:windowIsTranslucent">true</item> + <item name="android:windowBackground">@android:color/transparent</item> + <item name="android:windowContentOverlay">@null</item> + <item name="android:backgroundDimEnabled">false</item> + </style> + <style name="SelectPrinterActivityTheme" parent="@android:style/Theme.DeviceDefault.Light"> <item name="android:actionBarStyle">@style/SelectPrinterActivityActionBarStyle</item> </style> diff --git a/packages/PrintSpooler/src/com/android/printspooler/model/MutexFileProvider.java b/packages/PrintSpooler/src/com/android/printspooler/model/MutexFileProvider.java new file mode 100644 index 0000000..1f48638 --- /dev/null +++ b/packages/PrintSpooler/src/com/android/printspooler/model/MutexFileProvider.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.printspooler.model; + +import android.util.Log; + +import java.io.File; +import java.io.IOException; + +/** + * This class provides a shared file to several threads. Only one thread + * at a time can use the file. To acquire the file a thread has to + * request it in a blocking call to {@link #acquireFile(OnReleaseRequestCallback)}. + * The provided callback is optional and is used to notify the owning thread + * when another one wants to acquire the file. In case a release is requested + * the thread owning the file must release it as soon as possible. If no + * callback is provided a thread that acquires the file must release it + * as soon as possible, i.e. even if callback was provided the thread cannot + * have the file for less time. + */ +public final class MutexFileProvider { + private static final String LOG_TAG = "MutexFileProvider"; + + private static final boolean DEBUG = true; + + private final Object mLock = new Object(); + + private final File mFile; + + private Thread mOwnerThread; + + private OnReleaseRequestCallback mOnReleaseRequestCallback; + + public interface OnReleaseRequestCallback { + public void onReleaseRequested(File file); + } + + public MutexFileProvider(File file) throws IOException { + mFile = file; + if (file.exists()) { + file.delete(); + } + file.createNewFile(); + } + + public File acquireFile(OnReleaseRequestCallback callback) { + synchronized (mLock) { + // If this thread has the file, nothing to do. + if (mOwnerThread == Thread.currentThread()) { + return mFile; + } + + // Another thread wants file ask for a release. + if (mOwnerThread != null && mOnReleaseRequestCallback != null) { + mOnReleaseRequestCallback.onReleaseRequested(mFile); + } + + // Wait until the file is released. + while (mOwnerThread != null) { + try { + mLock.wait(); + } catch (InterruptedException ie) { + /* ignore */ + } + } + + // Update the owner and the callback. + mOwnerThread = Thread.currentThread(); + mOnReleaseRequestCallback = callback; + + if (DEBUG) { + Log.i(LOG_TAG, "Acquired file: " + mFile + " by thread: " + mOwnerThread); + } + + return mFile; + } + } + + public void releaseFile() { + synchronized (mLock) { + if (mOwnerThread != Thread.currentThread()) { + throw new IllegalStateException("Not acquired"); + } + + if (DEBUG) { + Log.i(LOG_TAG, "Released file: " + mFile + " from thread: " + mOwnerThread); + } + + // Update the owner and the callback. + mOwnerThread = null; + mOnReleaseRequestCallback = null; + + mLock.notifyAll(); + } + } +} diff --git a/packages/PrintSpooler/src/com/android/printspooler/model/NotificationController.java b/packages/PrintSpooler/src/com/android/printspooler/model/NotificationController.java index 929f0fc..d37ccc0 100644 --- a/packages/PrintSpooler/src/com/android/printspooler/model/NotificationController.java +++ b/packages/PrintSpooler/src/com/android/printspooler/model/NotificationController.java @@ -342,7 +342,7 @@ final class NotificationController { printManager.cancelPrintJob(printJobId, PrintManager.APP_ID_ANY, UserHandle.myUserId()); } catch (RemoteException re) { - Log.i(LOG_TAG, "Error requestion print job cancellation", re); + Log.i(LOG_TAG, "Error requesting print job cancellation", re); } finally { wakeLock.release(); } @@ -379,7 +379,7 @@ final class NotificationController { printManager.restartPrintJob(printJobId, PrintManager.APP_ID_ANY, UserHandle.myUserId()); } catch (RemoteException re) { - Log.i(LOG_TAG, "Error requestion print job restart", re); + Log.i(LOG_TAG, "Error requesting print job restart", re); } finally { wakeLock.release(); } diff --git a/packages/PrintSpooler/src/com/android/printspooler/model/PageContentRepository.java b/packages/PrintSpooler/src/com/android/printspooler/model/PageContentRepository.java new file mode 100644 index 0000000..63b4d96 --- /dev/null +++ b/packages/PrintSpooler/src/com/android/printspooler/model/PageContentRepository.java @@ -0,0 +1,871 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.printspooler.model; + +import android.app.ActivityManager; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.pdf.PdfRenderer; +import android.os.AsyncTask; +import android.os.Debug; +import android.os.ParcelFileDescriptor; +import android.print.PrintAttributes.MediaSize; +import android.print.PrintAttributes.Margins; +import android.print.PrintDocumentInfo; +import android.util.ArrayMap; +import android.util.Log; +import android.view.View; +import dalvik.system.CloseGuard; + +import java.io.IOException; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; + +public final class PageContentRepository { + private static final String LOG_TAG = "PageContentRepository"; + + private static final boolean DEBUG = true; + + private static final int INVALID_PAGE_INDEX = -1; + + private static final int STATE_CLOSED = 0; + private static final int STATE_OPENED = 1; + private static final int STATE_DESTROYED = 2; + + private static final int BYTES_PER_PIXEL = 4; + + private static final int BYTES_PER_MEGABYTE = 1048576; + + private static final int MILS_PER_INCH = 1000; + private static final int POINTS_IN_INCH = 72; + + private final CloseGuard mCloseGuard = CloseGuard.get(); + + private final ArrayMap<Integer, PageContentProvider> mPageContentProviders = + new ArrayMap<>(); + + private final AsyncRenderer mRenderer; + + private RenderSpec mLastRenderSpec; + + private int mScheduledPreloadFirstShownPage = INVALID_PAGE_INDEX; + + private int mScheduledPreloadLastShownPage = INVALID_PAGE_INDEX; + + private int mState; + + public interface OnPageContentAvailableCallback { + public void onPageContentAvailable(BitmapDrawable content); + } + + public PageContentRepository(Context context) { + mRenderer = new AsyncRenderer(context); + mState = STATE_CLOSED; + if (DEBUG) { + Log.i(LOG_TAG, "STATE_CLOSED"); + } + mCloseGuard.open("destroy"); + } + + public void open(ParcelFileDescriptor source, final Runnable callback) { + throwIfNotClosed(); + mState = STATE_OPENED; + if (DEBUG) { + Log.i(LOG_TAG, "STATE_OPENED"); + } + mRenderer.open(source, callback); + } + + public void close(Runnable callback) { + throwIfNotOpened(); + mState = STATE_CLOSED; + if (DEBUG) { + Log.i(LOG_TAG, "STATE_CLOSED"); + } + + mRenderer.close(callback); + } + + public void destroy() { + throwIfNotClosed(); + mState = STATE_DESTROYED; + if (DEBUG) { + Log.i(LOG_TAG, "STATE_DESTROYED"); + } + throwIfNotClosed(); + doDestroy(); + } + + public void startPreload(int firstShownPage, int lastShownPage) { + // If we do not have a render spec we have no clue what size the + // preloaded bitmaps should be, so just take a note for what to do. + if (mLastRenderSpec == null) { + mScheduledPreloadFirstShownPage = firstShownPage; + mScheduledPreloadLastShownPage = lastShownPage; + } else { + mRenderer.startPreload(firstShownPage, lastShownPage, mLastRenderSpec); + } + } + + public void stopPreload() { + mRenderer.stopPreload(); + } + + public int getFilePageCount() { + return mRenderer.getPageCount(); + } + + public PageContentProvider peekPageContentProvider(int pageIndex) { + return mPageContentProviders.get(pageIndex); + } + + public PageContentProvider acquirePageContentProvider(int pageIndex, View owner) { + throwIfDestroyed(); + + if (DEBUG) { + Log.i(LOG_TAG, "Acquiring provider for page: " + pageIndex); + } + + if (mPageContentProviders.get(pageIndex)!= null) { + throw new IllegalStateException("Already acquired for page: " + pageIndex); + } + + PageContentProvider provider = new PageContentProvider(pageIndex, owner); + + mPageContentProviders.put(pageIndex, provider); + + return provider; + } + + public void releasePageContentProvider(PageContentProvider provider) { + throwIfDestroyed(); + + if (DEBUG) { + Log.i(LOG_TAG, "Releasing provider for page: " + provider.mPageIndex); + } + + if (mPageContentProviders.remove(provider.mPageIndex) == null) { + throw new IllegalStateException("Not acquired"); + } + + provider.cancelLoad(); + } + + @Override + protected void finalize() throws Throwable { + try { + if (mState != STATE_DESTROYED) { + mCloseGuard.warnIfOpen(); + doDestroy(); + } + } finally { + super.finalize(); + } + } + + private void doDestroy() { + mState = STATE_DESTROYED; + if (DEBUG) { + Log.i(LOG_TAG, "STATE_DESTROYED"); + } + mRenderer.destroy(); + } + + private void throwIfNotOpened() { + if (mState != STATE_OPENED) { + throw new IllegalStateException("Not opened"); + } + } + + private void throwIfNotClosed() { + if (mState != STATE_CLOSED) { + throw new IllegalStateException("Not closed"); + } + } + + private void throwIfDestroyed() { + if (mState == STATE_DESTROYED) { + throw new IllegalStateException("Destroyed"); + } + } + + public final class PageContentProvider { + private final int mPageIndex; + private View mOwner; + + public PageContentProvider(int pageIndex, View owner) { + mPageIndex = pageIndex; + mOwner = owner; + } + + public View getOwner() { + return mOwner; + } + + public int getPageIndex() { + return mPageIndex; + } + + public void getPageContent(RenderSpec renderSpec, OnPageContentAvailableCallback callback) { + throwIfDestroyed(); + + mLastRenderSpec = renderSpec; + + // We tired to preload but didn't know the bitmap size, now + // that we know let us do the work. + if (mScheduledPreloadFirstShownPage != INVALID_PAGE_INDEX + && mScheduledPreloadLastShownPage != INVALID_PAGE_INDEX) { + startPreload(mScheduledPreloadFirstShownPage, mScheduledPreloadLastShownPage); + mScheduledPreloadFirstShownPage = INVALID_PAGE_INDEX; + mScheduledPreloadLastShownPage = INVALID_PAGE_INDEX; + } + + if (mState == STATE_OPENED) { + mRenderer.renderPage(mPageIndex, renderSpec, callback); + } else { + mRenderer.getCachedPage(mPageIndex, renderSpec, callback); + } + } + + void cancelLoad() { + throwIfDestroyed(); + + if (mState == STATE_OPENED) { + mRenderer.cancelRendering(mPageIndex); + } + } + } + + private static final class PageContentLruCache { + private final LinkedHashMap<Integer, RenderedPage> mRenderedPages = + new LinkedHashMap<>(); + + private final int mMaxSizeInBytes; + + private int mSizeInBytes; + + public PageContentLruCache(int maxSizeInBytes) { + mMaxSizeInBytes = maxSizeInBytes; + } + + public RenderedPage getRenderedPage(int pageIndex) { + return mRenderedPages.get(pageIndex); + } + + public RenderedPage removeRenderedPage(int pageIndex) { + RenderedPage page = mRenderedPages.remove(pageIndex); + if (page != null) { + mSizeInBytes -= page.getSizeInBytes(); + } + return page; + } + + public RenderedPage putRenderedPage(int pageIndex, RenderedPage renderedPage) { + RenderedPage oldRenderedPage = mRenderedPages.remove(pageIndex); + if (oldRenderedPage != null) { + if (!oldRenderedPage.renderSpec.equals(renderedPage.renderSpec)) { + throw new IllegalStateException("Wrong page size"); + } + } else { + final int contentSizeInBytes = renderedPage.getSizeInBytes(); + if (mSizeInBytes + contentSizeInBytes > mMaxSizeInBytes) { + throw new IllegalStateException("Client didn't free space"); + } + + mSizeInBytes += contentSizeInBytes; + } + return mRenderedPages.put(pageIndex, renderedPage); + } + + public void invalidate() { + for (Map.Entry<Integer, RenderedPage> entry : mRenderedPages.entrySet()) { + entry.getValue().state = RenderedPage.STATE_SCRAP; + } + } + + public RenderedPage removeLeastNeeded() { + if (mRenderedPages.isEmpty()) { + return null; + } + + // First try to remove a rendered page that holds invalidated + // or incomplete content, i.e. its render spec is null. + for (Map.Entry<Integer, RenderedPage> entry : mRenderedPages.entrySet()) { + RenderedPage renderedPage = entry.getValue(); + if (renderedPage.state == RenderedPage.STATE_SCRAP) { + Integer pageIndex = entry.getKey(); + mRenderedPages.remove(pageIndex); + mSizeInBytes -= renderedPage.getSizeInBytes(); + return renderedPage; + } + } + + // If all rendered pages contain rendered content, then use the oldest. + final int pageIndex = mRenderedPages.eldest().getKey(); + RenderedPage renderedPage = mRenderedPages.remove(pageIndex); + mSizeInBytes -= renderedPage.getSizeInBytes(); + return renderedPage; + } + + public int getSizeInBytes() { + return mSizeInBytes; + } + + public int getMaxSizeInBytes() { + return mMaxSizeInBytes; + } + + public void clear() { + Iterator<Map.Entry<Integer, RenderedPage>> iterator = + mRenderedPages.entrySet().iterator(); + while (iterator.hasNext()) { + iterator.next().getValue().recycle(); + iterator.remove(); + } + } + } + + public static final class RenderSpec { + final int bitmapWidth; + final int bitmapHeight; + final MediaSize mediaSize; + final Margins minMargins; + + public RenderSpec(int bitmapWidth, int bitmapHeight, + MediaSize mediaSize, Margins minMargins) { + this.bitmapWidth = bitmapWidth; + this.bitmapHeight = bitmapHeight; + this.mediaSize = mediaSize; + this.minMargins = minMargins; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + RenderSpec other = (RenderSpec) obj; + if (bitmapHeight != other.bitmapHeight) { + return false; + } + if (bitmapWidth != other.bitmapWidth) { + return false; + } + if (mediaSize != null) { + if (!mediaSize.equals(other.mediaSize)) { + return false; + } + } else if (other.mediaSize != null) { + return false; + } + if (minMargins != null) { + if (!minMargins.equals(other.minMargins)) { + return false; + } + } else if (other.minMargins != null) { + return false; + } + return true; + } + + public boolean hasSameSize(RenderedPage page) { + Bitmap bitmap = page.content.getBitmap(); + return bitmap.getWidth() == bitmapWidth + && bitmap.getHeight() == bitmapHeight; + } + + @Override + public int hashCode() { + int result = bitmapWidth; + result = 31 * result + bitmapHeight; + result = 31 * result + (mediaSize != null ? mediaSize.hashCode() : 0); + result = 31 * result + (minMargins != null ? minMargins.hashCode() : 0); + return result; + } + } + + private static final class RenderedPage { + public static final int STATE_RENDERED = 0; + public static final int STATE_RENDERING = 1; + public static final int STATE_SCRAP = 2; + + final BitmapDrawable content; + RenderSpec renderSpec; + + int state = STATE_SCRAP; + + RenderedPage(BitmapDrawable content) { + this.content = content; + } + + public int getSizeInBytes() { + return content.getBitmap().getByteCount(); + } + + public void recycle() { + content.getBitmap().recycle(); + } + + public void erase() { + content.getBitmap().eraseColor(Color.WHITE); + } + } + + private static int pointsFromMils(int mils) { + return (int) (((float) mils / MILS_PER_INCH) * POINTS_IN_INCH); + } + + private static class AsyncRenderer { + private final Context mContext; + + private final PageContentLruCache mPageContentCache; + + private final ArrayMap<Integer, RenderPageTask> mPageToRenderTaskMap = new ArrayMap<>(); + + private int mPageCount = PrintDocumentInfo.PAGE_COUNT_UNKNOWN; + + // Accessed only by the executor thread. + private PdfRenderer mRenderer; + + public AsyncRenderer(Context context) { + mContext = context; + + ActivityManager activityManager = (ActivityManager) + mContext.getSystemService(Context.ACTIVITY_SERVICE); + final int cacheSizeInBytes = activityManager.getMemoryClass() * BYTES_PER_MEGABYTE / 4; + mPageContentCache = new PageContentLruCache(cacheSizeInBytes); + } + + public void open(final ParcelFileDescriptor source, final Runnable callback) { + // Opening a new document invalidates the cache as it has pages + // from the last document. We keep the cache even when the document + // is closed to show pages while the other side is writing the new + // document. + mPageContentCache.invalidate(); + + new AsyncTask<Void, Void, Integer>() { + @Override + protected Integer doInBackground(Void... params) { + try { + mRenderer = new PdfRenderer(source); + return mRenderer.getPageCount(); + } catch (IOException ioe) { + throw new IllegalStateException("Cannot open PDF document"); + } + } + + @Override + public void onPostExecute(Integer pageCount) { + mPageCount = pageCount; + if (callback != null) { + callback.run(); + } + } + }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null); + } + + public void close(final Runnable callback) { + cancelAllRendering(); + + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... params) { + mRenderer.close(); + return null; + } + + @Override + public void onPostExecute(Void result) { + mPageCount = PrintDocumentInfo.PAGE_COUNT_UNKNOWN; + if (callback != null) { + callback.run(); + } + } + }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null); + } + + public void destroy() { + mPageContentCache.invalidate(); + mPageContentCache.clear(); + } + + public void startPreload(int firstShownPage, int lastShownPage, RenderSpec renderSpec) { + if (DEBUG) { + Log.i(LOG_TAG, "Preloading pages around [" + firstShownPage + + "-" + lastShownPage + "]"); + } + + final int bitmapSizeInBytes = renderSpec.bitmapWidth * renderSpec.bitmapHeight + * BYTES_PER_PIXEL; + final int maxCachedPageCount = mPageContentCache.getMaxSizeInBytes() + / bitmapSizeInBytes; + final int halfPreloadCount = (maxCachedPageCount - (lastShownPage - firstShownPage)) /2; + + final int excessFromStart; + if (firstShownPage - halfPreloadCount < 0) { + excessFromStart = halfPreloadCount - firstShownPage; + } else { + excessFromStart = 0; + } + + final int excessFromEnd; + if (lastShownPage + halfPreloadCount >= mPageCount) { + excessFromEnd = (lastShownPage + halfPreloadCount) - mPageCount; + } else { + excessFromEnd = 0; + } + + final int fromIndex = Math.max(firstShownPage - halfPreloadCount - excessFromEnd, 0); + final int toIndex = Math.min(lastShownPage + halfPreloadCount + excessFromStart, + mPageCount - 1); + + for (int i = fromIndex; i <= toIndex; i++) { + renderPage(i, renderSpec, null); + } + } + + public void stopPreload() { + final int taskCount = mPageToRenderTaskMap.size(); + for (int i = 0; i < taskCount; i++) { + RenderPageTask task = mPageToRenderTaskMap.valueAt(i); + if (task.isPreload() && !task.isCancelled()) { + task.cancel(true); + } + } + } + + public int getPageCount() { + return mPageCount; + } + + public void getCachedPage(int pageIndex, RenderSpec renderSpec, + OnPageContentAvailableCallback callback) { + RenderedPage renderedPage = mPageContentCache.getRenderedPage(pageIndex); + if (renderedPage != null && renderedPage.state == RenderedPage.STATE_RENDERED + && renderedPage.renderSpec.equals(renderSpec)) { + if (DEBUG) { + Log.i(LOG_TAG, "Cache hit for page: " + pageIndex); + } + + // Announce if needed. + if (callback != null) { + callback.onPageContentAvailable(renderedPage.content); + } + } + } + + public void renderPage(int pageIndex, RenderSpec renderSpec, + OnPageContentAvailableCallback callback) { + // First, check if we have a rendered page for this index. + RenderedPage renderedPage = mPageContentCache.getRenderedPage(pageIndex); + if (renderedPage != null && renderedPage.state == RenderedPage.STATE_RENDERED) { + // If we have rendered page with same constraints - done. + if (renderedPage.renderSpec.equals(renderSpec)) { + if (DEBUG) { + Log.i(LOG_TAG, "Cache hit for page: " + pageIndex); + } + + // Announce if needed. + if (callback != null) { + callback.onPageContentAvailable(renderedPage.content); + } + return; + } else { + // If the constraints changed, mark the page obsolete. + renderedPage.state = RenderedPage.STATE_SCRAP; + } + } + + // Next, check if rendering this page is scheduled. + RenderPageTask renderTask = mPageToRenderTaskMap.get(pageIndex); + if (renderTask != null && !renderTask.isCancelled()) { + // If not rendered and constraints same.... + if (renderTask.mRenderSpec.equals(renderSpec)) { + if (renderTask.mCallback != null) { + // If someone else is already waiting for this page - bad state. + if (callback != null && renderTask.mCallback != callback) { + throw new IllegalStateException("Page rendering not cancelled"); + } + } else { + // No callback means we are preloading so just let the argument + // callback be attached to our work in progress. + renderTask.mCallback = callback; + } + return; + } else { + // If not rendered and constraints changed - cancel rendering. + renderTask.cancel(true); + } + } + + // Oh well, we will have work to do... + renderTask = new RenderPageTask(pageIndex, renderSpec, callback); + mPageToRenderTaskMap.put(pageIndex, renderTask); + renderTask.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null); + } + + public void cancelRendering(int pageIndex) { + RenderPageTask task = mPageToRenderTaskMap.get(pageIndex); + if (task != null && !task.isCancelled()) { + task.cancel(true); + } + } + + private void cancelAllRendering() { + final int taskCount = mPageToRenderTaskMap.size(); + for (int i = 0; i < taskCount; i++) { + RenderPageTask task = mPageToRenderTaskMap.valueAt(i); + if (!task.isCancelled()) { + task.cancel(true); + } + } + } + + private final class RenderPageTask extends AsyncTask<Void, Void, RenderedPage> { + final int mPageIndex; + final RenderSpec mRenderSpec; + OnPageContentAvailableCallback mCallback; + RenderedPage mRenderedPage; + + public RenderPageTask(int pageIndex, RenderSpec renderSpec, + OnPageContentAvailableCallback callback) { + mPageIndex = pageIndex; + mRenderSpec = renderSpec; + mCallback = callback; + } + + @Override + protected void onPreExecute() { + mRenderedPage = mPageContentCache.getRenderedPage(mPageIndex); + if (mRenderedPage != null && mRenderedPage.state == RenderedPage.STATE_RENDERED) { + throw new IllegalStateException("Trying to render a rendered page"); + } + + // Reuse bitmap for the page only if the right size. + if (mRenderedPage != null && !mRenderSpec.hasSameSize(mRenderedPage)) { + if (DEBUG) { + Log.i(LOG_TAG, "Recycling bitmap for page: " + mPageIndex + + " with different size."); + } + mPageContentCache.removeRenderedPage(mPageIndex); + mRenderedPage.recycle(); + mRenderedPage = null; + } + + final int bitmapSizeInBytes = mRenderSpec.bitmapWidth + * mRenderSpec.bitmapHeight * BYTES_PER_PIXEL; + + // Try to find a bitmap to reuse. + while (mRenderedPage == null) { + + // Fill the cache greedily. + if (mPageContentCache.getSizeInBytes() <= 0 + || mPageContentCache.getSizeInBytes() + bitmapSizeInBytes + <= mPageContentCache.getMaxSizeInBytes()) { + break; + } + + RenderedPage renderedPage = mPageContentCache.removeLeastNeeded(); + + if (!mRenderSpec.hasSameSize(renderedPage)) { + if (DEBUG) { + Log.i(LOG_TAG, "Recycling bitmap for page: " + mPageIndex + + " with different size."); + } + renderedPage.recycle(); + continue; + } + + mRenderedPage = renderedPage; + renderedPage.erase(); + + if (DEBUG) { + Log.i(LOG_TAG, "Reused bitmap for page: " + mPageIndex + " cache size: " + + mPageContentCache.getSizeInBytes() + " bytes"); + } + + break; + } + +// if (mRenderedPage == null) { +// final int bitmapSizeInBytes = mRenderSpec.bitmapWidth +// * mRenderSpec.bitmapHeight * BYTES_PER_PIXEL; +// +// while (mPageContentCache.getSizeInBytes() > 0 +// && mPageContentCache.getSizeInBytes() + bitmapSizeInBytes +// > mPageContentCache.getMaxSizeInBytes()) { +// RenderedPage renderedPage = mPageContentCache.removeLeastNeeded(); +// +// // If not the right size - recycle and keep freeing space. +// Bitmap bitmap = renderedPage.content.getBitmap(); +// if (!mRenderSpec.hasSameSize(bitmap.getWidth(), bitmap.getHeight())) { +// if (DEBUG) { +// Log.i(LOG_TAG, "Recycling bitmap for page: " + mPageIndex +// + " with different size."); +// } +// bitmap.recycle(); +// continue; +// } +// +// mRenderedPage = renderedPage; +// bitmap.eraseColor(Color.WHITE); +// +// if (DEBUG) { +// Log.i(LOG_TAG, "Reused bitmap for page: " + mPageIndex + " cache size: " +// + mPageContentCache.getSizeInBytes() + " bytes"); +// } +// } +// } + + if (mRenderedPage == null) { + if (DEBUG) { + Log.i(LOG_TAG, "Created bitmap for page: " + mPageIndex + " cache size: " + + mPageContentCache.getSizeInBytes() + " bytes"); + } + Bitmap bitmap = Bitmap.createBitmap(mRenderSpec.bitmapWidth, + mRenderSpec.bitmapHeight, Bitmap.Config.ARGB_8888); + bitmap.eraseColor(Color.WHITE); + BitmapDrawable content = new BitmapDrawable(mContext.getResources(), bitmap); + mRenderedPage = new RenderedPage(content); + } + + mRenderedPage.renderSpec = mRenderSpec; + mRenderedPage.state = RenderedPage.STATE_RENDERING; + + mPageContentCache.putRenderedPage(mPageIndex, mRenderedPage); + } + + @Override + protected RenderedPage doInBackground(Void... params) { + if (isCancelled()) { + return mRenderedPage; + } + + PdfRenderer.Page page = mRenderer.openPage(mPageIndex); + + if (isCancelled()) { + page.close(); + return mRenderedPage; + } + + Bitmap bitmap = mRenderedPage.content.getBitmap(); + + final int srcWidthPts = page.getWidth(); + final int srcHeightPts = page.getHeight(); + + final int dstWidthPts = pointsFromMils(mRenderSpec.mediaSize.getWidthMils()); + final int dstHeightPts = pointsFromMils(mRenderSpec.mediaSize.getHeightMils()); + + final boolean scaleContent = mRenderer.shouldScaleForPrinting(); + final boolean contentLandscape = !mRenderSpec.mediaSize.isPortrait(); + + final float displayScale; + Matrix matrix = new Matrix(); + + if (scaleContent) { + displayScale = Math.min((float) bitmap.getWidth() / srcWidthPts, + (float) bitmap.getHeight() / srcHeightPts); + } else { + if (contentLandscape) { + displayScale = (float) bitmap.getHeight() / dstHeightPts; + } else { + displayScale = (float) bitmap.getWidth() / dstWidthPts; + } + } + matrix.postScale(displayScale, displayScale); + + Configuration configuration = mContext.getResources().getConfiguration(); + if (configuration.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { + matrix.postTranslate(bitmap.getWidth() - srcWidthPts * displayScale, 0); + } + + final int paddingLeftPts = pointsFromMils(mRenderSpec.minMargins.getLeftMils()); + final int paddingTopPts = pointsFromMils(mRenderSpec.minMargins.getTopMils()); + final int paddingRightPts = pointsFromMils(mRenderSpec.minMargins.getRightMils()); + final int paddingBottomPts = pointsFromMils(mRenderSpec.minMargins.getBottomMils()); + + Rect clip = new Rect(); + clip.left = (int) (paddingLeftPts * displayScale); + clip.top = (int) (paddingTopPts * displayScale); + clip.right = (int) (bitmap.getWidth() - paddingRightPts * displayScale); + clip.bottom = (int) (bitmap.getHeight() - paddingBottomPts * displayScale); + + if (DEBUG) { + Log.i(LOG_TAG, "Rendering page:" + mPageIndex + " of " + mPageCount); + } + + page.render(bitmap, clip, matrix, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY); + + page.close(); + + return mRenderedPage; + } + + @Override + public void onPostExecute(RenderedPage renderedPage) { + if (DEBUG) { + Log.i(LOG_TAG, "Completed rendering page: " + mPageIndex); + } + + // This task is done. + mPageToRenderTaskMap.remove(mPageIndex); + + // Take a note that the content is rendered. + renderedPage.state = RenderedPage.STATE_RENDERED; + + // Announce success if needed. + if (mCallback != null) { + mCallback.onPageContentAvailable(renderedPage.content); + } + } + + @Override + protected void onCancelled(RenderedPage renderedPage) { + if (DEBUG) { + Log.i(LOG_TAG, "Cancelled rendering page: " + mPageIndex); + } + + // This task is done. + mPageToRenderTaskMap.remove(mPageIndex); + + // If canceled before on pre-execute. + if (renderedPage == null) { + return; + } + + // Take a note that the content is not rendered. + renderedPage.state = RenderedPage.STATE_SCRAP; + } + + public boolean isPreload() { + return mCallback == null; + } + } + } +} diff --git a/packages/PrintSpooler/src/com/android/printspooler/model/RemotePrintDocument.java b/packages/PrintSpooler/src/com/android/printspooler/model/RemotePrintDocument.java index e70c361..9351078 100644 --- a/packages/PrintSpooler/src/com/android/printspooler/model/RemotePrintDocument.java +++ b/packages/PrintSpooler/src/com/android/printspooler/model/RemotePrintDocument.java @@ -55,7 +55,7 @@ import java.util.Arrays; public final class RemotePrintDocument { private static final String LOG_TAG = "RemotePrintDocument"; - private static final boolean DEBUG = false; + private static final boolean DEBUG = true; private static final int STATE_INITIAL = 0; private static final int STATE_STARTED = 1; @@ -67,10 +67,6 @@ public final class RemotePrintDocument { private static final int STATE_CANCELED = 7; private static final int STATE_DESTROYED = 8; - private static final PageRange[] ALL_PAGES_ARRAY = new PageRange[] { - PageRange.ALL_PAGES - }; - private final Context mContext; private final RemotePrintDocumentInfo mDocumentInfo; @@ -93,18 +89,23 @@ public final class RemotePrintDocument { // do nothing. However, if there is no next command we may need to // ask for some pages given we do not already have them or we do // but the content has changed. - LayoutCommand layoutCommand = (LayoutCommand) mCurrentCommand; if (mNextCommand == null) { - if (layoutCommand.isDocumentChanged() || !PageRangeUtils.contains( - mDocumentInfo.writtenPages, mUpdateSpec.pages)) { + if (mUpdateSpec.pages != null && (mDocumentInfo.changed + || (mDocumentInfo.info.getPageCount() + != PrintDocumentInfo.PAGE_COUNT_UNKNOWN + && !PageRangeUtils.contains(mDocumentInfo.writtenPages, + mUpdateSpec.pages, mDocumentInfo.info.getPageCount())))) { mNextCommand = new WriteCommand(mContext, mLooper, mPrintDocumentAdapter, mDocumentInfo, mDocumentInfo.info.getPageCount(), mUpdateSpec.pages, - mDocumentInfo.file, mCommandResultCallback); + mDocumentInfo.fileProvider, mCommandResultCallback); } else { - // If we have the requested pages just update that ones to be printed. - mDocumentInfo.printedPages = computePrintedPages(mUpdateSpec.pages, - mDocumentInfo.writtenPages, mDocumentInfo.info.getPageCount()); + if (mUpdateSpec.pages != null) { + // If we have the requested pages, update which ones to be printed. + mDocumentInfo.printedPages = PageRangeUtils.computePrintedPages( + mUpdateSpec.pages, mDocumentInfo.writtenPages, + mDocumentInfo.info.getPageCount()); + } // Notify we are done. notifyUpdateCompleted(); } @@ -154,13 +155,14 @@ public final class RemotePrintDocument { } public RemotePrintDocument(Context context, IPrintDocumentAdapter adapter, - File file, DocumentObserver destroyListener, UpdateResultCallbacks callbacks) { + MutexFileProvider fileProvider, DocumentObserver destroyListener, + UpdateResultCallbacks callbacks) { mPrintDocumentAdapter = adapter; mLooper = context.getMainLooper(); mContext = context; mDocumentObserver = destroyListener; mDocumentInfo = new RemotePrintDocumentInfo(); - mDocumentInfo.file = file; + mDocumentInfo.fileProvider = fileProvider; mUpdateCallbacks = callbacks; connectToRemoteDocument(); } @@ -219,7 +221,8 @@ public final class RemotePrintDocument { // If no layout in progress and we don't have all pages - schedule a write. } else if ((!(mCurrentCommand instanceof LayoutCommand) || (!mCurrentCommand.isPending() && !mCurrentCommand.isRunning())) - && !PageRangeUtils.contains(mUpdateSpec.pages, pages)) { + && pages != null && !PageRangeUtils.contains(mUpdateSpec.pages, pages, + mDocumentInfo.info.getPageCount())) { willUpdate = true; // Cancel the current write as a new one is to be scheduled. @@ -231,7 +234,7 @@ public final class RemotePrintDocument { // Schedule a write command. AsyncCommand command = new WriteCommand(mContext, mLooper, mPrintDocumentAdapter, mDocumentInfo, mDocumentInfo.info.getPageCount(), pages, - mDocumentInfo.file, mCommandResultCallback); + mDocumentInfo.fileProvider, mCommandResultCallback); scheduleCommand(command); mState = STATE_UPDATING; @@ -325,10 +328,12 @@ public final class RemotePrintDocument { } public void writeContent(ContentResolver contentResolver, Uri uri) { + File file = null; InputStream in = null; OutputStream out = null; try { - in = new FileInputStream(mDocumentInfo.file); + file = mDocumentInfo.fileProvider.acquireFile(null); + in = new FileInputStream(file); out = contentResolver.openOutputStream(uri); final byte[] buffer = new byte[8192]; while (true) { @@ -343,6 +348,9 @@ public final class RemotePrintDocument { } finally { IoUtils.closeQuietly(in); IoUtils.closeQuietly(out); + if (file != null) { + mDocumentInfo.fileProvider.releaseFile(); + } } } @@ -453,42 +461,6 @@ public final class RemotePrintDocument { } } - private static PageRange[] computePrintedPages(PageRange[] requestedPages, - PageRange[] writtenPages, int pageCount) { - // Adjust the print job pages based on what was requested and written. - // The cases are ordered in the most expected to the least expected. - if (Arrays.equals(writtenPages, requestedPages)) { - // We got a document with exactly the pages we wanted. Hence, - // the printer has to print all pages in the data. - return ALL_PAGES_ARRAY; - } else if (Arrays.equals(writtenPages, ALL_PAGES_ARRAY)) { - // We requested specific pages but got all of them. Hence, - // the printer has to print only the requested pages. - return requestedPages; - } else if (PageRangeUtils.contains(writtenPages, requestedPages)) { - // We requested specific pages and got more but not all pages. - // Hence, we have to offset appropriately the printed pages to - // be based off the start of the written ones instead of zero. - // The written pages are always non-null and not empty. - final int offset = -writtenPages[0].getStart(); - PageRangeUtils.offset(requestedPages, offset); - return requestedPages; - } else if (Arrays.equals(requestedPages, ALL_PAGES_ARRAY) - && isAllPages(writtenPages, pageCount)) { - // We requested all pages via the special constant and got all - // of them as an explicit enumeration. Hence, the printer has - // to print only the requested pages. - return ALL_PAGES_ARRAY; - } - - return null; - } - - private static boolean isAllPages(PageRange[] pageRanges, int pageCount) { - return pageRanges.length > 0 && pageRanges[0].getStart() == 0 - && pageRanges[pageRanges.length - 1].getEnd() == pageCount - 1; - } - static final class UpdateSpec { final PrintAttributes attributes = new PrintAttributes.Builder().build(); boolean preview; @@ -498,7 +470,7 @@ public final class RemotePrintDocument { PageRange[] pages) { this.attributes.copyFrom(attributes); this.preview = preview; - this.pages = Arrays.copyOf(pages, pages.length); + this.pages = (pages != null) ? Arrays.copyOf(pages, pages.length) : null; } public void reset() { @@ -518,7 +490,10 @@ public final class RemotePrintDocument { public PrintDocumentInfo info; public PageRange[] printedPages; public PageRange[] writtenPages; - public File file; + public MutexFileProvider fileProvider; + public boolean changed; + public boolean updated; + public boolean laidout; } private interface CommandDoneCallback { @@ -647,8 +622,6 @@ public final class RemotePrintDocument { private final Handler mHandler; - private boolean mDocumentChanged; - public LayoutCommand(Looper looper, IPrintDocumentAdapter adapter, RemotePrintDocumentInfo document, PrintAttributes oldAttributes, PrintAttributes newAttributes, boolean preview, CommandDoneCallback callback) { @@ -660,10 +633,6 @@ public final class RemotePrintDocument { mMetadata.putBoolean(PrintDocumentAdapter.EXTRA_PRINT_PREVIEW, preview); } - public boolean isDocumentChanged() { - return mDocumentChanged; - } - @Override public void run() { running(); @@ -672,6 +641,7 @@ public final class RemotePrintDocument { if (DEBUG) { Log.i(LOG_TAG, "[PERFORMING] layout"); } + mDocument.changed = false; mAdapter.layout(mOldAttributes, mNewAttributes, mRemoteResultCallback, mMetadata, mSequence); } catch (RemoteException re) { @@ -720,12 +690,13 @@ public final class RemotePrintDocument { // we will request them again with the new content. mDocument.writtenPages = null; mDocument.printedPages = null; - mDocumentChanged = true; + mDocument.changed = true; } // Update the document with data from the layout pass. mDocument.attributes = mNewAttributes; mDocument.metadata = mMetadata; + mDocument.laidout = true; mDocument.info = info; // Release the remote cancellation interface. @@ -744,6 +715,8 @@ public final class RemotePrintDocument { Log.i(LOG_TAG, "[CALLBACK] onLayoutFailed"); } + mDocument.laidout = false; + failed(error); // Release the remote cancellation interface. @@ -877,7 +850,7 @@ public final class RemotePrintDocument { private static final class WriteCommand extends AsyncCommand { private final int mPageCount; private final PageRange[] mPages; - private final File mContentFile; + private final MutexFileProvider mFileProvider; private final IWriteResultCallback mRemoteResultCallback; private final CommandDoneCallback mDoneCallback; @@ -887,14 +860,14 @@ public final class RemotePrintDocument { public WriteCommand(Context context, Looper looper, IPrintDocumentAdapter adapter, RemotePrintDocumentInfo document, int pageCount, PageRange[] pages, - File contentFile, CommandDoneCallback callback) { + MutexFileProvider fileProvider, CommandDoneCallback callback) { super(adapter, document, callback); mContext = context; mHandler = new WriteHandler(looper); mRemoteResultCallback = new WriteResultCallback(mHandler); mPageCount = pageCount; mPages = Arrays.copyOf(pages, pages.length); - mContentFile = contentFile; + mFileProvider = fileProvider; mDoneCallback = callback; } @@ -909,17 +882,19 @@ public final class RemotePrintDocument { new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { + File file = null; InputStream in = null; OutputStream out = null; ParcelFileDescriptor source = null; ParcelFileDescriptor sink = null; try { + file = mFileProvider.acquireFile(null); ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); source = pipe[0]; sink = pipe[1]; in = new FileInputStream(source.getFileDescriptor()); - out = new FileOutputStream(mContentFile); + out = new FileOutputStream(file); // Async call to initiate the other process writing the data. if (DEBUG) { @@ -947,6 +922,9 @@ public final class RemotePrintDocument { IoUtils.closeQuietly(out); IoUtils.closeQuietly(sink); IoUtils.closeQuietly(source); + if (file != null) { + mFileProvider.releaseFile(); + } } return null; } @@ -984,7 +962,8 @@ public final class RemotePrintDocument { } PageRange[] writtenPages = PageRangeUtils.normalize(pages); - PageRange[] printedPages = computePrintedPages(mPages, writtenPages, mPageCount); + PageRange[] printedPages = PageRangeUtils.computePrintedPages( + mPages, writtenPages, mPageCount); // Handle if we got invalid pages if (printedPages != null) { diff --git a/packages/PrintSpooler/src/com/android/printspooler/ui/PageAdapter.java b/packages/PrintSpooler/src/com/android/printspooler/ui/PageAdapter.java new file mode 100644 index 0000000..09ce4e1 --- /dev/null +++ b/packages/PrintSpooler/src/com/android/printspooler/ui/PageAdapter.java @@ -0,0 +1,783 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.printspooler.ui; + +import android.content.Context; +import android.os.ParcelFileDescriptor; +import android.print.PageRange; +import android.print.PrintAttributes.MediaSize; +import android.print.PrintAttributes.Margins; +import android.print.PrintDocumentInfo; +import android.support.v7.widget.RecyclerView.Adapter; +import android.support.v7.widget.RecyclerView.ViewHolder; +import android.util.Log; +import android.util.SparseArray; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import android.widget.CheckBox; +import android.widget.TextView; +import com.android.printspooler.R; +import com.android.printspooler.model.PageContentRepository; +import com.android.printspooler.model.PageContentRepository.PageContentProvider; +import com.android.printspooler.util.PageRangeUtils; +import com.android.printspooler.widget.PageContentView; +import dalvik.system.CloseGuard; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * This class represents the adapter for the pages in the print preview list. + */ +public final class PageAdapter extends Adapter { + private static final String LOG_TAG = "PageAdapter"; + + private static final int MAX_PREVIEW_PAGES_BATCH = 50; + + private static final boolean DEBUG = true; + + private static final PageRange[] ALL_PAGES_ARRAY = new PageRange[] { + PageRange.ALL_PAGES + }; + + private static final int INVALID_PAGE_INDEX = -1; + + private static final int STATE_CLOSED = 0; + private static final int STATE_OPENED = 1; + private static final int STATE_DESTROYED = 2; + + private final CloseGuard mCloseGuard = CloseGuard.get(); + + private final SparseArray<Void> mBoundPagesInAdapter = new SparseArray<>(); + + private final SparseArray<Void> mConfirmedPagesInDocument = new SparseArray<>(); + + private final PageClickListener mPageClickListener = new PageClickListener(); + + private final LayoutInflater mLayoutInflater; + + private final Context mContext; + + private final ContentUpdateRequestCallback mContentUpdateRequestCallback; + + private final PageContentRepository mPageContentRepository; + + private final PreviewArea mPreviewArea; + + private int mDocumentPageCount = PrintDocumentInfo.PAGE_COUNT_UNKNOWN; + + private int mSelectedPageCount; + + // Which document pages to be written. + private PageRange[] mRequestedPages; + + // Pages written in the current file. + private PageRange[] mWrittenPages; + + // Pages the user selected in the UI. + private PageRange[] mSelectedPages; + + private float mSelectedPageElevation; + + private float mUnselectedPageElevation; + + private int mPreviewPageMargin; + + private int mPreviewListPadding; + + private int mFooterHeight; + + private int mColumnCount; + + private MediaSize mMediaSize; + + private Margins mMinMargins; + + private int mState; + + private int mPageContentWidth; + + private int mPageContentHeight; + + public interface ContentUpdateRequestCallback { + public void requestContentUpdate(); + } + + public interface PreviewArea { + public int getWidth(); + public int getHeight(); + public void setColumnCount(int columnCount); + public void setPadding(int left, int top, int right, int bottom); + } + + public PageAdapter(Context context, ContentUpdateRequestCallback updateRequestCallback, + PreviewArea previewArea) { + mContext = context; + mContentUpdateRequestCallback = updateRequestCallback; + mLayoutInflater = (LayoutInflater) context.getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + mPageContentRepository = new PageContentRepository(context); + + mSelectedPageElevation = mContext.getResources().getDimension( + R.dimen.selected_page_elevation); + mUnselectedPageElevation = mContext.getResources().getDimension( + R.dimen.unselected_page_elevation); + + mPreviewPageMargin = mContext.getResources().getDimensionPixelSize( + R.dimen.preview_page_margin); + + mPreviewListPadding = mContext.getResources().getDimensionPixelSize( + R.dimen.preview_list_padding); + + mColumnCount = mContext.getResources().getInteger( + R.integer.preview_page_per_row_count); + + TypedValue outValue = new TypedValue(); + mContext.getTheme().resolveAttribute( + com.android.internal.R.attr.listPreferredItemHeightSmall, outValue, true); + mFooterHeight = TypedValue.complexToDimensionPixelSize(outValue.data, + mContext.getResources().getDisplayMetrics()); + + mPreviewArea = previewArea; + + mCloseGuard.open("destroy"); + + setHasStableIds(true); + + mState = STATE_CLOSED; + if (DEBUG) { + Log.i(LOG_TAG, "STATE_CLOSED"); + } + } + + public void onOrientationChanged() { + mColumnCount = mContext.getResources().getInteger( + R.integer.preview_page_per_row_count); + } + + public boolean isOpened() { + return mState == STATE_OPENED; + } + + public int getFilePageCount() { + return mPageContentRepository.getFilePageCount(); + } + + public void open(ParcelFileDescriptor source, Runnable callback) { + throwIfNotClosed(); + mState = STATE_OPENED; + if (DEBUG) { + Log.i(LOG_TAG, "STATE_OPENED"); + } + mPageContentRepository.open(source, callback); + } + + public void update(PageRange[] writtenPages, PageRange[] selectedPages, + int documentPageCount, MediaSize mediaSize, Margins minMargins) { + boolean documentChanged = false; + boolean updatePreviewAreaAndPageSize = false; + + // If the app does not tell how many pages are in the document we cannot + // optimize and ask for all pages whose count we get from the renderer. + if (documentPageCount == PrintDocumentInfo.PAGE_COUNT_UNKNOWN) { + if (writtenPages == null) { + // If we already requested all pages, just wait. + if (!Arrays.equals(ALL_PAGES_ARRAY, mRequestedPages)) { + mRequestedPages = ALL_PAGES_ARRAY; + mContentUpdateRequestCallback.requestContentUpdate(); + } + return; + } else { + documentPageCount = mPageContentRepository.getFilePageCount(); + if (documentPageCount <= 0) { + return; + } + } + } + + if (!Arrays.equals(mSelectedPages, selectedPages)) { + mSelectedPages = selectedPages; + mSelectedPageCount = PageRangeUtils.getNormalizedPageCount( + mSelectedPages, documentPageCount); + setConfirmedPages(mSelectedPages, documentPageCount); + updatePreviewAreaAndPageSize = true; + documentChanged = true; + } + + if (mDocumentPageCount != documentPageCount) { + mDocumentPageCount = documentPageCount; + documentChanged = true; + } + + if (mMediaSize == null || !mMediaSize.equals(mediaSize)) { + mMediaSize = mediaSize; + updatePreviewAreaAndPageSize = true; + documentChanged = true; + } + + if (mMinMargins == null || !mMinMargins.equals(minMargins)) { + mMinMargins = minMargins; + updatePreviewAreaAndPageSize = true; + documentChanged = true; + } + + // If *all pages* is selected we need to convert that to absolute + // range as we will be checking if some pages are written or not. + if (writtenPages != null) { + // If we get all pages, this means all pages that we requested. + if (PageRangeUtils.isAllPages(writtenPages)) { + writtenPages = mRequestedPages; + } + if (!Arrays.equals(mWrittenPages, writtenPages)) { + // TODO: Do a surgical invalidation of only written pages changed. + mWrittenPages = writtenPages; + documentChanged = true; + } + } + + if (updatePreviewAreaAndPageSize) { + updatePreviewAreaAndPageSize(); + } + + if (documentChanged) { + notifyDataSetChanged(); + } + } + + public void close(Runnable callback) { + throwIfNotOpened(); + mState = STATE_CLOSED; + if (DEBUG) { + Log.i(LOG_TAG, "STATE_CLOSED"); + } + mPageContentRepository.close(callback); + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View page = mLayoutInflater.inflate(R.layout.preview_page, parent, false); + ViewHolder holder = new MyViewHolder(page); + holder.setIsRecyclable(true); + return holder; + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + if (DEBUG) { + Log.i(LOG_TAG, "Binding holder: " + holder + " with id: " + getItemId(position) + + " for position: " + position); + } + + final int pageCount = getItemCount(); + MyViewHolder myHolder = (MyViewHolder) holder; + + View page = holder.itemView; + if (pageCount > 1) { + page.setOnClickListener(mPageClickListener); + } else { + page.setOnClickListener(null); + } + page.setTag(holder); + + myHolder.mPageInAdapter = position; + + final int pageInDocument = computePageIndexInDocument(position); + final int pageIndexInFile = computePageIndexInFile(pageInDocument); + + PageContentView content = (PageContentView) page.findViewById(R.id.page_content); + + LayoutParams params = content.getLayoutParams(); + params.width = mPageContentWidth; + params.height = mPageContentHeight; + + PageContentProvider provider = content.getPageContentProvider(); + + if (pageIndexInFile != INVALID_PAGE_INDEX) { + if (DEBUG) { + Log.i(LOG_TAG, "Binding provider:" + + " pageIndexInAdapter: " + position + + ", pageInDocument: " + pageInDocument + + ", pageIndexInFile: " + pageIndexInFile); + } + + // OK, there are bugs in recycler view which tries to bind views + // without recycling them which would give us a chane to clean up. + PageContentProvider boundProvider = mPageContentRepository + .peekPageContentProvider(pageIndexInFile); + if (boundProvider != null) { + PageContentView owner = (PageContentView) boundProvider.getOwner(); + owner.init(null, mMediaSize, mMinMargins); + mPageContentRepository.releasePageContentProvider(boundProvider); + } + + provider = mPageContentRepository.acquirePageContentProvider( + pageIndexInFile, content); + mBoundPagesInAdapter.put(position, null); + } else { + onSelectedPageNotInFile(pageInDocument); + } + content.init(provider, mMediaSize, mMinMargins); + + + CheckBox checkbox = (CheckBox) page.findViewById(R.id.page_selector); + checkbox.setTag(myHolder); + if (pageCount > 1) { + checkbox.setOnClickListener(mPageClickListener); + checkbox.setVisibility(View.VISIBLE); + } else { + checkbox.setOnClickListener(null); + checkbox.setVisibility(View.GONE); + } + + if (mConfirmedPagesInDocument.indexOfKey(pageInDocument) >= 0) { + checkbox.setChecked(true); + page.setTranslationZ(mSelectedPageElevation); + } else { + checkbox.setChecked(false); + page.setTranslationZ(mUnselectedPageElevation); + } + + TextView pageNumberView = (TextView) page.findViewById(R.id.page_number); + String text = mContext.getString(R.string.current_page_template, + pageInDocument + 1, mDocumentPageCount); + pageNumberView.setText(text); + } + + @Override + public int getItemCount() { + return mSelectedPageCount; + } + + @Override + public long getItemId(int position) { + return computePageIndexInDocument(position); + } + + @Override + public void onViewRecycled(ViewHolder holder) { + MyViewHolder myHolder = (MyViewHolder) holder; + PageContentView content = (PageContentView) holder.itemView + .findViewById(R.id.page_content); + recyclePageView(content, myHolder.mPageInAdapter); + myHolder.mPageInAdapter = INVALID_PAGE_INDEX; + } + + public PageRange[] getRequestedPages() { + return mRequestedPages; + } + + public PageRange[] getSelectedPages() { + PageRange[] selectedPages = computeSelectedPages(); + if (!Arrays.equals(mSelectedPages, selectedPages)) { + mSelectedPages = selectedPages; + mSelectedPageCount = PageRangeUtils.getNormalizedPageCount( + mSelectedPages, mDocumentPageCount); + notifyDataSetChanged(); + } + return mSelectedPages; + } + + public void onPreviewAreaSizeChanged() { + if (mMediaSize != null) { + updatePreviewAreaAndPageSize(); + notifyDataSetChanged(); + } + } + + private void updatePreviewAreaAndPageSize() { + final int availableWidth = mPreviewArea.getWidth(); + final int availableHeight = mPreviewArea.getHeight(); + + // Page aspect ratio to keep. + final float pageAspectRatio = (float) mMediaSize.getWidthMils() + / mMediaSize.getHeightMils(); + + // Make sure we have no empty columns. + final int columnCount = Math.min(mSelectedPageCount, mColumnCount); + mPreviewArea.setColumnCount(columnCount); + + // Compute max page width. + final int horizontalMargins = 2 * columnCount * mPreviewPageMargin; + final int horizontalPaddingAndMargins = horizontalMargins + 2 * mPreviewListPadding; + final int pageContentDesiredWidth = (int) ((((float) availableWidth + - horizontalPaddingAndMargins) / columnCount) + 0.5f); + + // Compute max page height. + final int pageContentDesiredHeight = (int) (((float) pageContentDesiredWidth + / pageAspectRatio) + 0.5f); + final int pageContentMaxHeight = availableHeight - 2 * (mPreviewListPadding + + mPreviewPageMargin) - mFooterHeight; + + mPageContentHeight = Math.min(pageContentDesiredHeight, pageContentMaxHeight); + mPageContentWidth = (int) ((mPageContentHeight * pageAspectRatio) + 0.5f); + + final int totalContentWidth = columnCount * mPageContentWidth + horizontalMargins; + final int horizontalPadding = (availableWidth - totalContentWidth) / 2; + + final int rowCount = mSelectedPageCount / columnCount + + ((mSelectedPageCount % columnCount) > 0 ? 1 : 0); + final int totalContentHeight = rowCount* (mPageContentHeight + mFooterHeight + 2 + * mPreviewPageMargin); + final int verticalPadding = Math.max(mPreviewListPadding, + (availableHeight - totalContentHeight) / 2); + + mPreviewArea.setPadding(horizontalPadding, verticalPadding, + horizontalPadding, verticalPadding); + } + + private PageRange[] computeSelectedPages() { + ArrayList<PageRange> selectedPagesList = new ArrayList<>(); + + int startPageIndex = INVALID_PAGE_INDEX; + int endPageIndex = INVALID_PAGE_INDEX; + + final int pageCount = mConfirmedPagesInDocument.size(); + for (int i = 0; i < pageCount; i++) { + final int pageIndex = mConfirmedPagesInDocument.keyAt(i); + if (startPageIndex == INVALID_PAGE_INDEX) { + startPageIndex = endPageIndex = pageIndex; + } + if (endPageIndex + 1 < pageIndex) { + PageRange pageRange = new PageRange(startPageIndex, endPageIndex); + selectedPagesList.add(pageRange); + startPageIndex = pageIndex; + } + endPageIndex = pageIndex; + } + + if (startPageIndex != INVALID_PAGE_INDEX + && endPageIndex != INVALID_PAGE_INDEX) { + PageRange pageRange = new PageRange(startPageIndex, endPageIndex); + selectedPagesList.add(pageRange); + } + + PageRange[] selectedPages = new PageRange[selectedPagesList.size()]; + selectedPagesList.toArray(selectedPages); + + return selectedPages; + } + + public void destroy() { + throwIfNotClosed(); + doDestroy(); + } + + @Override + protected void finalize() throws Throwable { + try { + if (mState != STATE_DESTROYED) { + mCloseGuard.warnIfOpen(); + doDestroy(); + } + } finally { + super.finalize(); + } + } + + private int computePageIndexInDocument(int indexInAdapter) { + int skippedAdapterPages = 0; + final int selectedPagesCount = mSelectedPages.length; + for (int i = 0; i < selectedPagesCount; i++) { + PageRange pageRange = PageRangeUtils.asAbsoluteRange( + mSelectedPages[i], mDocumentPageCount); + skippedAdapterPages += pageRange.getSize(); + if (skippedAdapterPages > indexInAdapter) { + final int overshoot = skippedAdapterPages - indexInAdapter - 1; + return pageRange.getEnd() - overshoot; + } + } + return INVALID_PAGE_INDEX; + } + + private int computePageIndexInFile(int pageIndexInDocument) { + if (!PageRangeUtils.contains(mSelectedPages, pageIndexInDocument)) { + return INVALID_PAGE_INDEX; + } + if (mWrittenPages == null) { + return INVALID_PAGE_INDEX; + } + + int indexInFile = INVALID_PAGE_INDEX; + final int rangeCount = mWrittenPages.length; + for (int i = 0; i < rangeCount; i++) { + PageRange pageRange = mWrittenPages[i]; + if (!pageRange.contains(pageIndexInDocument)) { + indexInFile += pageRange.getSize(); + } else { + indexInFile += pageIndexInDocument - pageRange.getStart() + 1; + return indexInFile; + } + } + return INVALID_PAGE_INDEX; + } + + private void setConfirmedPages(PageRange[] pagesInDocument, int documentPageCount) { + mConfirmedPagesInDocument.clear(); + final int rangeCount = pagesInDocument.length; + for (int i = 0; i < rangeCount; i++) { + PageRange pageRange = PageRangeUtils.asAbsoluteRange(pagesInDocument[i], + documentPageCount); + for (int j = pageRange.getStart(); j <= pageRange.getEnd(); j++) { + mConfirmedPagesInDocument.put(j, null); + } + } + } + + private void onSelectedPageNotInFile(int pageInDocument) { + PageRange[] requestedPages = computeRequestedPages(pageInDocument); + if (!Arrays.equals(mRequestedPages, requestedPages)) { + mRequestedPages = requestedPages; + if (DEBUG) { + Log.i(LOG_TAG, "Requesting pages: " + Arrays.toString(mRequestedPages)); + } + mContentUpdateRequestCallback.requestContentUpdate(); + } + } + + private PageRange[] computeRequestedPages(int pageInDocument) { + if (mRequestedPages != null && + PageRangeUtils.contains(mRequestedPages, pageInDocument)) { + return mRequestedPages; + } + + List<PageRange> pageRangesList = new ArrayList<>(); + + int remainingPagesToRequest = MAX_PREVIEW_PAGES_BATCH; + final int selectedPagesCount = mSelectedPages.length; + + // We always request the pages that are bound, i.e. shown on screen. + PageRange[] boundPagesInDocument = computeBoundPagesInDocument(); + + final int boundRangeCount = boundPagesInDocument.length; + for (int i = 0; i < boundRangeCount; i++) { + PageRange boundRange = boundPagesInDocument[i]; + pageRangesList.add(boundRange); + } + remainingPagesToRequest -= PageRangeUtils.getNormalizedPageCount( + boundPagesInDocument, mDocumentPageCount); + + final boolean requestFromStart = mRequestedPages == null + || pageInDocument > mRequestedPages[mRequestedPages.length - 1].getEnd(); + + if (!requestFromStart) { + if (DEBUG) { + Log.i(LOG_TAG, "Requesting from end"); + } + + // Reminder that ranges are always normalized. + for (int i = selectedPagesCount - 1; i >= 0; i--) { + if (remainingPagesToRequest <= 0) { + break; + } + + PageRange selectedRange = PageRangeUtils.asAbsoluteRange(mSelectedPages[i], + mDocumentPageCount); + if (pageInDocument < selectedRange.getStart()) { + continue; + } + + PageRange pagesInRange; + int rangeSpan; + + if (selectedRange.contains(pageInDocument)) { + rangeSpan = pageInDocument - selectedRange.getStart() + 1; + rangeSpan = Math.min(rangeSpan, remainingPagesToRequest); + final int fromPage = Math.max(pageInDocument - rangeSpan - 1, 0); + rangeSpan = Math.max(rangeSpan, 0); + pagesInRange = new PageRange(fromPage, pageInDocument); + } else { + rangeSpan = selectedRange.getSize(); + rangeSpan = Math.min(rangeSpan, remainingPagesToRequest); + rangeSpan = Math.max(rangeSpan, 0); + final int fromPage = Math.max(selectedRange.getEnd() - rangeSpan - 1, 0); + final int toPage = selectedRange.getEnd(); + pagesInRange = new PageRange(fromPage, toPage); + } + + pageRangesList.add(pagesInRange); + remainingPagesToRequest -= rangeSpan; + } + } else { + if (DEBUG) { + Log.i(LOG_TAG, "Requesting from start"); + } + + // Reminder that ranges are always normalized. + for (int i = 0; i < selectedPagesCount; i++) { + if (remainingPagesToRequest <= 0) { + break; + } + + PageRange selectedRange = PageRangeUtils.asAbsoluteRange(mSelectedPages[i], + mDocumentPageCount); + if (pageInDocument > selectedRange.getEnd()) { + continue; + } + + PageRange pagesInRange; + int rangeSpan; + + if (selectedRange.contains(pageInDocument)) { + rangeSpan = selectedRange.getEnd() - pageInDocument + 1; + rangeSpan = Math.min(rangeSpan, remainingPagesToRequest); + final int toPage = Math.min(pageInDocument + rangeSpan - 1, + mDocumentPageCount - 1); + pagesInRange = new PageRange(pageInDocument, toPage); + } else { + rangeSpan = selectedRange.getSize(); + rangeSpan = Math.min(rangeSpan, remainingPagesToRequest); + final int fromPage = selectedRange.getStart(); + final int toPage = Math.min(selectedRange.getStart() + rangeSpan - 1, + mDocumentPageCount - 1); + pagesInRange = new PageRange(fromPage, toPage); + } + + if (DEBUG) { + Log.i(LOG_TAG, "computeRequestedPages() Adding range:" + pagesInRange); + } + pageRangesList.add(pagesInRange); + remainingPagesToRequest -= rangeSpan; + } + } + + PageRange[] pageRanges = new PageRange[pageRangesList.size()]; + pageRangesList.toArray(pageRanges); + + return PageRangeUtils.normalize(pageRanges); + } + + private PageRange[] computeBoundPagesInDocument() { + List<PageRange> pagesInDocumentList = new ArrayList<>(); + + int fromPage = INVALID_PAGE_INDEX; + int toPage = INVALID_PAGE_INDEX; + + final int boundPageCount = mBoundPagesInAdapter.size(); + for (int i = 0; i < boundPageCount; i++) { + // The container is a sparse array, so keys are sorted in ascending order. + final int boundPageInAdapter = mBoundPagesInAdapter.keyAt(i); + final int boundPageInDocument = computePageIndexInDocument(boundPageInAdapter); + + if (fromPage == INVALID_PAGE_INDEX) { + fromPage = boundPageInDocument; + } + + if (toPage == INVALID_PAGE_INDEX) { + toPage = boundPageInDocument; + } + + if (boundPageInDocument > toPage + 1) { + PageRange pageRange = new PageRange(fromPage, toPage); + pagesInDocumentList.add(pageRange); + fromPage = toPage = boundPageInDocument; + } else { + toPage = boundPageInDocument; + } + } + + if (fromPage != INVALID_PAGE_INDEX && toPage != INVALID_PAGE_INDEX) { + PageRange pageRange = new PageRange(fromPage, toPage); + pagesInDocumentList.add(pageRange); + } + + PageRange[] pageInDocument = new PageRange[pagesInDocumentList.size()]; + pagesInDocumentList.toArray(pageInDocument); + + if (DEBUG) { + Log.i(LOG_TAG, "Bound pages: " + Arrays.toString(pageInDocument)); + } + + return pageInDocument; + } + + private void recyclePageView(PageContentView page, int pageIndexInAdapter) { + PageContentProvider provider = page.getPageContentProvider(); + if (provider != null) { + page.init(null, null, null); + mPageContentRepository.releasePageContentProvider(provider); + mBoundPagesInAdapter.remove(pageIndexInAdapter); + } + page.setTag(null); + } + + public void startPreloadContent(PageRange pageRangeInAdapter) { +// final int startPageInDocument = computePageIndexInDocument(pageRangeInAdapter.getStart()); +// final int startPageInFile = computePageIndexInFile(startPageInDocument); +// final int endPageInDocument = computePageIndexInDocument(pageRangeInAdapter.getEnd()); +// final int endPageInFile = computePageIndexInFile(endPageInDocument); +// if (startPageInDocument != INVALID_PAGE_INDEX && endPageInDocument != INVALID_PAGE_INDEX) { +// mPageContentRepository.startPreload(startPageInFile, endPageInFile); +// } + } + + public void stopPreloadContent() { +// mPageContentRepository.stopPreload(); + } + + private void doDestroy() { + mPageContentRepository.destroy(); + mCloseGuard.close(); + mState = STATE_DESTROYED; + if (DEBUG) { + Log.i(LOG_TAG, "STATE_DESTROYED"); + } + } + + private void throwIfNotOpened() { + if (mState != STATE_OPENED) { + throw new IllegalStateException("Not opened"); + } + } + + private void throwIfNotClosed() { + if (mState != STATE_CLOSED) { + throw new IllegalStateException("Not closed"); + } + } + + private final class MyViewHolder extends ViewHolder { + int mPageInAdapter; + + private MyViewHolder(View itemView) { + super(itemView); + } + } + + private final class PageClickListener implements OnClickListener { + @Override + public void onClick(View page) { + MyViewHolder holder = (MyViewHolder) page.getTag(); + final int pageInAdapter = holder.mPageInAdapter; + final int pageInDocument = computePageIndexInDocument(pageInAdapter); + CheckBox pageSelector = (CheckBox) page.findViewById(R.id.page_selector); + if (mConfirmedPagesInDocument.indexOfKey(pageInDocument) < 0) { + mConfirmedPagesInDocument.put(pageInDocument, null); + pageSelector.setChecked(true); + page.animate().translationZ(mSelectedPageElevation); + } else { + mConfirmedPagesInDocument.remove(pageInDocument); + pageSelector.setChecked(false); + page.animate().translationZ(mUnselectedPageElevation); + } + } + } +} diff --git a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java index 3e0d7e5..7359221 100644 --- a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java +++ b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java @@ -26,6 +26,7 @@ import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; +import android.content.res.Configuration; import android.database.DataSetObserver; import android.graphics.drawable.Drawable; import android.net.Uri; @@ -68,26 +69,39 @@ import android.widget.Spinner; import android.widget.TextView; import com.android.printspooler.R; +import com.android.printspooler.model.MutexFileProvider; import com.android.printspooler.model.PrintSpoolerProvider; import com.android.printspooler.model.PrintSpoolerService; import com.android.printspooler.model.RemotePrintDocument; +import com.android.printspooler.model.RemotePrintDocument.RemotePrintDocumentInfo; import com.android.printspooler.util.MediaSizeUtils; import com.android.printspooler.util.MediaSizeUtils.MediaSizeComparator; import com.android.printspooler.util.PageRangeUtils; import com.android.printspooler.util.PrintOptionUtils; -import com.android.printspooler.widget.ContentView; -import com.android.printspooler.widget.ContentView.OptionsStateChangeListener; - -import java.util.*; +import com.android.printspooler.widget.PrintContentView; +import com.android.printspooler.widget.PrintContentView.OptionsStateChangeListener; +import com.android.printspooler.widget.PrintContentView.OptionsStateController; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; public class PrintActivity extends Activity implements RemotePrintDocument.UpdateResultCallbacks, - PrintErrorFragment.OnActionListener, PrintProgressFragment.OnCancelRequestListener { + PrintErrorFragment.OnActionListener, PageAdapter.ContentUpdateRequestCallback, + OptionsStateChangeListener, OptionsStateController { private static final String LOG_TAG = "PrintActivity"; + private static final boolean DEBUG = true; + public static final String INTENT_EXTRA_PRINTER_ID = "INTENT_EXTRA_PRINTER_ID"; + private static final String FRAGMENT_TAG = "FRAGMENT_TAG"; + private static final int ORIENTATION_PORTRAIT = 0; private static final int ORIENTATION_LANDSCAPE = 1; @@ -106,6 +120,7 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat private static final int STATE_UPDATE_FAILED = 3; private static final int STATE_CREATE_FILE_FAILED = 4; private static final int STATE_PRINTER_UNAVAILABLE = 5; + private static final int STATE_UPDATE_SLOW = 6; private static final int UI_STATE_PREVIEW = 0; private static final int UI_STATE_ERROR = 1; @@ -120,10 +135,10 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat "(?=[]\\[+&|!(){}^\"~*?:\\\\])"); private static final Pattern PATTERN_PAGE_RANGE = Pattern.compile( - "[\\s]*[0-9]*[\\s]*[\\-]?[\\s]*[0-9]*[\\s]*?(([,])" - + "[\\s]*[0-9]*[\\s]*[\\-]?[\\s]*[0-9]*[\\s]*|[\\s]*)+"); + "[\\s]*[0-9]+[\\-]?[\\s]*[0-9]*[\\s]*?(([,])" + + "[\\s]*[0-9]+[\\s]*[\\-]?[\\s]*[0-9]*[\\s]*|[\\s]*)+"); - public static final PageRange[] ALL_PAGES_ARRAY = new PageRange[] {PageRange.ALL_PAGES}; + public static final PageRange[] ALL_PAGES_ARRAY = new PageRange[]{PageRange.ALL_PAGES}; private final PrinterAvailabilityDetector mPrinterAvailabilityDetector = new PrinterAvailabilityDetector(); @@ -134,6 +149,8 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat private PrintSpoolerProvider mSpoolerProvider; + private PrintPreviewController mPrintPreviewController; + private PrintJobInfo mPrintJob; private RemotePrintDocument mPrintedDocument; private PrinterRegistry mPrinterRegistry; @@ -158,7 +175,7 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat private Spinner mRangeOptionsSpinner; - private ContentView mOptionsContent; + private PrintContentView mOptionsContent; private TextView mSummaryCopies; private TextView mSummaryPaperSize; @@ -173,13 +190,13 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat private MediaSizeComparator mMediaSizeComparator; - private PageRange[] mRequestedPages; - private PrinterInfo mOldCurrentPrinter; + private PageRange[] mSelectedPages; + private String mCallingPackageName; - private int mState = STATE_CONFIGURING; + private int mState; private int mUiState = UI_STATE_PREVIEW; @@ -187,6 +204,8 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + setState(STATE_CONFIGURING); + Bundle extras = getIntent().getExtras(); mPrintJob = extras.getParcelable(PrintManager.EXTRA_PRINT_JOB); @@ -210,63 +229,97 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat new Runnable() { @Override public void run() { - // Now that we are bound to the print spooler service, - // create the printer registry and wait for it to get - // the first batch of results which will be delivered - // after reading historical data. This should be pretty - // fast, so just wait before showing the UI. - mPrinterRegistry = new PrinterRegistry(PrintActivity.this, - new Runnable() { - @Override - public void run() { - setTitle(R.string.print_dialog); - setContentView(R.layout.print_activity); + onConnectedToPrintSpooler(adapter); + } + }); + } + + private void onConnectedToPrintSpooler(final IBinder documentAdapter) { + // Now that we are bound to the print spooler service, + // create the printer registry and wait for it to get + // the first batch of results which will be delivered + // after reading historical data. This should be pretty + // fast, so just wait before showing the UI. + mPrinterRegistry = new PrinterRegistry(PrintActivity.this, + new Runnable() { + @Override + public void run() { + onPrinterRegistryReady(documentAdapter); + } + }); + } - mPrintedDocument = new RemotePrintDocument(PrintActivity.this, - IPrintDocumentAdapter.Stub.asInterface(adapter), - PrintSpoolerService.generateFileForPrintJob(PrintActivity.this, - mPrintJob.getId()), - new RemotePrintDocument.DocumentObserver() { - @Override - public void onDestroy() { - finish(); - } - }, PrintActivity.this); + private void onPrinterRegistryReady(IBinder documentAdapter) { + // Now that we are bound to the local print spooler service + // and the printer registry loaded the historical printers + // we can show the UI without flickering. + setTitle(R.string.print_dialog); + setContentView(R.layout.print_activity); + + final MutexFileProvider fileProvider; + try { + fileProvider = new MutexFileProvider( + PrintSpoolerService.generateFileForPrintJob( + PrintActivity.this, mPrintJob.getId())); + } catch (IOException ioe) { + // At this point we cannot recover, so just take it down. + throw new IllegalStateException("Cannot create print job file", ioe); + } + + mPrintPreviewController = new PrintPreviewController(PrintActivity.this, + fileProvider); + + mPrintedDocument = new RemotePrintDocument(PrintActivity.this, + IPrintDocumentAdapter.Stub.asInterface(documentAdapter), + fileProvider, new RemotePrintDocument.DocumentObserver() { + @Override + public void onDestroy() { + finish(); + } + }, PrintActivity.this); - mProgressMessageController = new ProgressMessageController(PrintActivity.this); + mProgressMessageController = new ProgressMessageController( + PrintActivity.this); - mMediaSizeComparator = new MediaSizeComparator(PrintActivity.this); + mMediaSizeComparator = new MediaSizeComparator(PrintActivity.this); - mDestinationSpinnerAdapter = new DestinationAdapter(); + mDestinationSpinnerAdapter = new DestinationAdapter(); - bindUi(); + bindUi(); - updateOptionsUi(); + updateOptionsUi(); - // Now show the updated UI to avoid flicker. - mOptionsContent.setVisibility(View.VISIBLE); + // Now show the updated UI to avoid flicker. + mOptionsContent.setVisibility(View.VISIBLE); - mRequestedPages = computeRequestedPages(); + mSelectedPages = computeSelectedPages(); - mPrintedDocument.start(); + mPrintedDocument.start(); - ensurePreviewUiShown(); - } - }); - } - }); + ensurePreviewUiShown(); } @Override public void onPause() { if (isFinishing()) { PrintSpoolerService spooler = mSpoolerProvider.getSpooler(); - if (mState == STATE_PRINT_CONFIRMED) { - spooler.updatePrintJobUserConfigurableOptionsNoPersistence(mPrintJob); - spooler.setPrintJobState(mPrintJob.getId(), PrintJobInfo.STATE_QUEUED, null); - } else { - spooler.setPrintJobState(mPrintJob.getId(), PrintJobInfo.STATE_CANCELED, null); + spooler.updatePrintJobUserConfigurableOptionsNoPersistence(mPrintJob); + + switch (mState) { + case STATE_PRINT_CONFIRMED: { + spooler.setPrintJobState(mPrintJob.getId(), PrintJobInfo.STATE_QUEUED, null); + } break; + + case STATE_CREATE_FILE_FAILED: { + spooler.setPrintJobState(mPrintJob.getId(), PrintJobInfo.STATE_FAILED, + getString(R.string.print_write_error_message)); + } break; + + default: { + spooler.setPrintJobState(mPrintJob.getId(), PrintJobInfo.STATE_CANCELED, null); + } break; } + mProgressMessageController.cancel(); mPrinterRegistry.setTrackedPrinter(null); mSpoolerProvider.destroy(); @@ -292,48 +345,59 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat public boolean onKeyUp(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking() && !event.isCanceled()) { - cancelPrint(); + if (mPrintPreviewController != null&&mPrintPreviewController.isOptionsOpened() && !hasErrors()) { + mPrintPreviewController.closeOptions(); + } else { + cancelPrint(); + } return true; } return super.onKeyUp(keyCode, event); } @Override - public void onActionPerformed() { - switch (mState) { - case STATE_UPDATE_FAILED: { - if (canUpdateDocument()) { - updateDocument(true, true); - ensurePreviewUiShown(); - mState = STATE_CONFIGURING; - updateOptionsUi(); - } - } break; - - case STATE_CREATE_FILE_FAILED: { - mState = STATE_CONFIGURING; - ensurePreviewUiShown(); - updateOptionsUi(); - } break; + public void requestContentUpdate() { + if (canUpdateDocument()) { + updateDocument(true, false); } } @Override - public void onCancelRequest() { - if (mPrintedDocument.isUpdating()) { - mPrintedDocument.cancel(); + public void onActionPerformed() { + if (mState == STATE_UPDATE_FAILED + && canUpdateDocument()) { + updateDocument(true, true); + ensurePreviewUiShown(); + setState(STATE_CONFIGURING); + updateOptionsUi(); } } public void onUpdateCanceled() { + if (DEBUG) { + Log.i(LOG_TAG, "onUpdateCanceled()"); + } + mProgressMessageController.cancel(); ensurePreviewUiShown(); - finishIfConfirmedOrCanceled(); - updateOptionsUi(); + + switch (mState) { + case STATE_PRINT_CONFIRMED: { + requestCreatePdfFileOrFinish(); + } break; + + case STATE_PRINT_CANCELED: { + finish(); + } break; + } } @Override - public void onUpdateCompleted(RemotePrintDocument.RemotePrintDocumentInfo document) { + public void onUpdateCompleted(RemotePrintDocumentInfo document) { + if (DEBUG) { + Log.i(LOG_TAG, "onUpdateCompleted()"); + } + mProgressMessageController.cancel(); ensurePreviewUiShown(); @@ -343,42 +407,120 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat // print service perspective which is the pages in the written PDF not the // pages in the printed document. PrintDocumentInfo info = document.info; - if (info == null) { - return; + if (info != null) { + final int pageCount = PageRangeUtils.getNormalizedPageCount(document.writtenPages, + getAdjustedPageCount(info)); + PrintDocumentInfo adjustedInfo = new PrintDocumentInfo.Builder(info.getName()) + .setContentType(info.getContentType()) + .setPageCount(pageCount) + .build(); + mPrintJob.setDocumentInfo(adjustedInfo); + mPrintJob.setPages(document.printedPages); + } + + switch (mState) { + case STATE_PRINT_CONFIRMED: { + requestCreatePdfFileOrFinish(); + } break; + + case STATE_PRINT_CANCELED: { + finish(); + } break; + + default: { + updatePrintPreviewController(document.changed); + + setState(STATE_CONFIGURING); + updateOptionsUi(); + } break; } - final int pageCount = PageRangeUtils.getNormalizedPageCount(document.writtenPages, - info.getPageCount()); - PrintDocumentInfo adjustedInfo = new PrintDocumentInfo.Builder(info.getName()) - .setContentType(info.getContentType()) - .setPageCount(pageCount) - .build(); - mPrintJob.setDocumentInfo(adjustedInfo); - mPrintJob.setPages(document.printedPages); - finishIfConfirmedOrCanceled(); - updateOptionsUi(); } @Override public void onUpdateFailed(CharSequence error) { - mState = STATE_UPDATE_FAILED; + if (DEBUG) { + Log.i(LOG_TAG, "onUpdateFailed()"); + } + + mProgressMessageController.cancel(); ensureErrorUiShown(error, PrintErrorFragment.ACTION_RETRY); + + setState(STATE_UPDATE_FAILED); + updateOptionsUi(); } @Override + public void onOptionsOpened() { + updateSelectedPagesFromPreview(); + } + + @Override + public void onOptionsClosed() { + PageRange[] selectedPages = computeSelectedPages(); + if (!Arrays.equals(mSelectedPages, selectedPages)) { + mSelectedPages = selectedPages; + + // Update preview. + updatePrintPreviewController(false); + } + + // Make sure the IME is not on the way of preview as + // the user may have used it to type copies or range. + InputMethodManager imm = (InputMethodManager) getSystemService( + Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(mDestinationSpinner.getWindowToken(), 0); + } + + private void updatePrintPreviewController(boolean contentUpdated) { + // If we have not heard from the application, do nothing. + RemotePrintDocumentInfo documentInfo = mPrintedDocument.getDocumentInfo(); + if (!documentInfo.laidout) { + return; + } + + // Update the preview controller. + mPrintPreviewController.onContentUpdated(contentUpdated, + getAdjustedPageCount(documentInfo.info), + mPrintedDocument.getDocumentInfo().writtenPages, + mSelectedPages, mPrintJob.getAttributes().getMediaSize(), + mPrintJob.getAttributes().getMinMargins()); + } + + + @Override + public boolean canOpenOptions() { + return true; + } + + @Override + public boolean canCloseOptions() { + return !hasErrors(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + mPrintPreviewController.onOrientationChanged(); + } + + @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case ACTIVITY_REQUEST_CREATE_FILE: { onStartCreateDocumentActivityResult(resultCode, data); - } break; + } + break; case ACTIVITY_REQUEST_SELECT_PRINTER: { onSelectPrinterActivityResult(resultCode, data); - } break; + } + break; case ACTIVITY_REQUEST_POPULATE_ADVANCED_PRINT_OPTIONS: { onAdvancedPrintOptionsActivityResult(resultCode, data); - } break; + } + break; } } @@ -398,15 +540,28 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat if (resultCode == RESULT_OK && data != null) { Uri uri = data.getData(); mPrintedDocument.writeContent(getContentResolver(), uri); - finish(); + // Calling finish here does not invoke lifecycle callbacks but we + // update the print job in onPause if finishing, hence post a message. + mDestinationSpinner.post(new Runnable() { + @Override + public void run() { + finish(); + } + }); } else if (resultCode == RESULT_CANCELED) { - mState = STATE_CONFIGURING; + setState(STATE_CONFIGURING); updateOptionsUi(); } else { - ensureErrorUiShown(getString(R.string.print_write_error_message), - PrintErrorFragment.ACTION_CONFIRM); - mState = STATE_CREATE_FILE_FAILED; + setState(STATE_CREATE_FILE_FAILED); updateOptionsUi(); + // Calling finish here does not invoke lifecycle callbacks but we + // update the print job in onPause if finishing, hence post a message. + mDestinationSpinner.post(new Runnable() { + @Override + public void run() { + finish(); + } + }); } } @@ -526,74 +681,105 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat } } - // Take the page range only if it is valid. + PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info; + final int pageCount = (info != null) ? getAdjustedPageCount(info) : 0; PageRange[] pageRanges = printJobInfo.getPages(); - if (pageRanges != null && pageRanges.length > 0) { - pageRanges = PageRangeUtils.normalize(pageRanges); + updateSelectedPages(pageRanges, pageCount); - PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info; - final int pageCount = (info != null) ? info.getPageCount() : 0; + // Update the content if needed. + if (canUpdateDocument()) { + updateDocument(true, false); + } + } - // Handle the case where all pages are specified explicitly - // instead of the *all pages* constant. - if (pageRanges.length == 1) { - if (pageRanges[0].getStart() == 0 && pageRanges[0].getEnd() == pageCount - 1) { - pageRanges[0] = PageRange.ALL_PAGES; - } + private void setState(int state) { + if (isFinalState(mState)) { + if (isFinalState(state)) { + mState = state; } + } else { + mState = state; + } + } - if (Arrays.equals(pageRanges, ALL_PAGES_ARRAY)) { - mPrintJob.setPages(pageRanges); + private static boolean isFinalState(int state) { + return state == STATE_PRINT_CONFIRMED + || state == STATE_PRINT_CANCELED; + } - if (mRangeOptionsSpinner.getSelectedItemPosition() != 0) { - mRangeOptionsSpinner.setSelection(0); - } - } else if (pageRanges[0].getStart() >= 0 - && pageRanges[pageRanges.length - 1].getEnd() < pageCount) { - mPrintJob.setPages(pageRanges); + private void updateSelectedPagesFromPreview() { + PageRange[] selectedPages = mPrintPreviewController.getSelectedPages(); + if (!Arrays.equals(mSelectedPages, selectedPages)) { + updateSelectedPages(selectedPages, + getAdjustedPageCount(mPrintedDocument.getDocumentInfo().info)); + } + } - if (mRangeOptionsSpinner.getSelectedItemPosition() != 1) { - mRangeOptionsSpinner.setSelection(1); - } + private void updateSelectedPages(PageRange[] selectedPages, int pageInDocumentCount) { + if (selectedPages == null || selectedPages.length <= 0) { + return; + } - StringBuilder builder = new StringBuilder(); - final int pageRangeCount = pageRanges.length; - for (int i = 0; i < pageRangeCount; i++) { - if (builder.length() > 0) { - builder.append(','); - } + selectedPages = PageRangeUtils.normalize(selectedPages); - final int shownStartPage; - final int shownEndPage; - PageRange pageRange = pageRanges[i]; - if (pageRange.equals(PageRange.ALL_PAGES)) { - shownStartPage = 1; - shownEndPage = pageCount; - } else { - shownStartPage = pageRange.getStart() + 1; - shownEndPage = pageRange.getEnd() + 1; - } + // Handle the case where all pages are specified explicitly + // instead of the *all pages* constant. + if (PageRangeUtils.isAllPages(selectedPages, pageInDocumentCount)) { + selectedPages = new PageRange[] {PageRange.ALL_PAGES}; + } - builder.append(shownStartPage); + if (Arrays.equals(mSelectedPages, selectedPages)) { + return; + } - if (shownStartPage != shownEndPage) { - builder.append('-'); - builder.append(shownEndPage); - } + mSelectedPages = selectedPages; + mPrintJob.setPages(selectedPages); + + if (Arrays.equals(selectedPages, ALL_PAGES_ARRAY)) { + if (mRangeOptionsSpinner.getSelectedItemPosition() != 0) { + mRangeOptionsSpinner.setSelection(0); + mPageRangeEditText.setText(""); + } + } else if (selectedPages[0].getStart() >= 0 + && selectedPages[selectedPages.length - 1].getEnd() < pageInDocumentCount) { + if (mRangeOptionsSpinner.getSelectedItemPosition() != 1) { + mRangeOptionsSpinner.setSelection(1); + } + + StringBuilder builder = new StringBuilder(); + final int pageRangeCount = selectedPages.length; + for (int i = 0; i < pageRangeCount; i++) { + if (builder.length() > 0) { + builder.append(','); + } + + final int shownStartPage; + final int shownEndPage; + PageRange pageRange = selectedPages[i]; + if (pageRange.equals(PageRange.ALL_PAGES)) { + shownStartPage = 1; + shownEndPage = pageInDocumentCount; + } else { + shownStartPage = pageRange.getStart() + 1; + shownEndPage = pageRange.getEnd() + 1; + } + + builder.append(shownStartPage); + + if (shownStartPage != shownEndPage) { + builder.append('-'); + builder.append(shownEndPage); } - mPageRangeEditText.setText(builder.toString()); } - } - // Update the content if needed. - if (canUpdateDocument()) { - updateDocument(true, false); + mPageRangeEditText.setText(builder.toString()); } } private void ensureProgressUiShown() { if (mUiState != UI_STATE_PROGRESS) { mUiState = UI_STATE_PROGRESS; + mPrintPreviewController.setUiShown(false); Fragment fragment = PrintProgressFragment.newInstance(); showFragment(fragment); } @@ -602,28 +788,31 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat private void ensurePreviewUiShown() { if (mUiState != UI_STATE_PREVIEW) { mUiState = UI_STATE_PREVIEW; - Fragment fragment = PrintPreviewFragment.newInstance(); - showFragment(fragment); + mPrintPreviewController.setUiShown(true); + showFragment(null); } } private void ensureErrorUiShown(CharSequence message, int action) { if (mUiState != UI_STATE_ERROR) { mUiState = UI_STATE_ERROR; + mPrintPreviewController.setUiShown(false); Fragment fragment = PrintErrorFragment.newInstance(message, action); showFragment(fragment); } } - private void showFragment(Fragment fragment) { + private void showFragment(Fragment newFragment) { FragmentTransaction transaction = getFragmentManager().beginTransaction(); - Fragment oldFragment = getFragmentManager().findFragmentById( - R.id.embedded_content_container); + Fragment oldFragment = getFragmentManager().findFragmentByTag(FRAGMENT_TAG); if (oldFragment != null) { transaction.remove(oldFragment); } - transaction.add(R.id.embedded_content_container, fragment); + if (newFragment != null) { + transaction.add(R.id.embedded_content_container, newFragment, FRAGMENT_TAG); + } transaction.commit(); + getFragmentManager().executePendingTransactions(); } private void requestCreatePdfFileOrFinish() { @@ -634,14 +823,6 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat } } - private void finishIfConfirmedOrCanceled() { - if (mState == STATE_PRINT_CONFIRMED) { - requestCreatePdfFileOrFinish(); - } else if (mState == STATE_PRINT_CANCELED) { - finish(); - } - } - private void updatePrintAttributesFromCapabilities(PrinterCapabilitiesInfo capabilities) { PrintAttributes defaults = capabilities.getDefaults(); @@ -700,21 +881,23 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat mPrintedDocument.clearUpdateError(); } - if (mRequestedPages != null && mRequestedPages.length > 0) { - final PageRange[] pages; - if (preview) { - final int firstPage = mRequestedPages[0].getStart(); - pages = new PageRange[]{new PageRange(firstPage, firstPage)}; - } else { - pages = mRequestedPages; - } - final boolean willUpdate = mPrintedDocument.update(mPrintJob.getAttributes(), - pages, preview); + final PageRange[] pages; + if (preview) { + pages = mPrintPreviewController.getRequestedPages(); + } else { + pages = mPrintPreviewController.getSelectedPages(); + } - if (willUpdate) { - mProgressMessageController.post(); - return true; - } + final boolean willUpdate = mPrintedDocument.update(mPrintJob.getAttributes(), + pages, preview); + + if (willUpdate) { + // When the update is done we update the print preview. + mProgressMessageController.post(); + return true; + } else { + // Update preview. + updatePrintPreviewController(false); } return false; @@ -735,7 +918,7 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat } private void cancelPrint() { - mState = STATE_PRINT_CANCELED; + setState(STATE_PRINT_CANCELED); updateOptionsUi(); if (mPrintedDocument.isUpdating()) { mPrintedDocument.cancel(); @@ -744,12 +927,25 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat } private void confirmPrint() { - mState = STATE_PRINT_CONFIRMED; + setState(STATE_PRINT_CONFIRMED); + updateOptionsUi(); + addCurrentPrinterToHistory(); + + PageRange[] selectedPages = computeSelectedPages(); + if (!Arrays.equals(mSelectedPages, selectedPages)) { + mSelectedPages = selectedPages; + // Update preview. + updatePrintPreviewController(false); + } + + updateSelectedPagesFromPreview(); + mPrintPreviewController.closeOptions(); + if (canUpdateDocument()) { updateDocument(false, false); } - addCurrentPrinterToHistory(); + if (!mPrintedDocument.isUpdating()) { requestCreatePdfFileOrFinish(); } @@ -761,18 +957,9 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat mSummaryPaperSize = (TextView) findViewById(R.id.paper_size_summary); // Options container - mOptionsContent = (ContentView) findViewById(R.id.options_content); - mOptionsContent.setOptionsStateChangeListener(new OptionsStateChangeListener() { - @Override - public void onOptionsOpened() { - // TODO: Update preview. - } - - @Override - public void onOptionsClosed() { - // TODO: Update preview. - } - }); + mOptionsContent = (PrintContentView) findViewById(R.id.options_content); + mOptionsContent.setOptionsStateChangeListener(this); + mOptionsContent.setOpenOptionsController(this); OnItemSelectedListener itemSelectedListener = new MyOnItemSelectedListener(); OnClickListener clickListener = new MyClickListener(); @@ -809,7 +996,7 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat mOrientationSpinnerAdapter = new ArrayAdapter<>( this, R.layout.spinner_dropdown_item, R.id.title); String[] orientationLabels = getResources().getStringArray( - R.array.orientation_labels); + R.array.orientation_labels); mOrientationSpinnerAdapter.add(new SpinnerItem<>( ORIENTATION_PORTRAIT, orientationLabels[0])); mOrientationSpinnerAdapter.add(new SpinnerItem<>( @@ -844,15 +1031,7 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat // Advanced options button. mAdvancedPrintOptionsContainer = findViewById(R.id.more_options_container); mMoreOptionsButton = (Button) findViewById(R.id.more_options_button); - mMoreOptionsButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - PrinterInfo currentPrinter = getCurrentPrinter(); - if (currentPrinter != null) { - startAdvancedPrintOptionsActivity(currentPrinter); - } - } - }); + mMoreOptionsButton.setOnClickListener(clickListener); // Print button mPrintButton = (ImageView) findViewById(R.id.print_button); @@ -883,7 +1062,7 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat && printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE; } - private void updateOptionsUi() { + void updateOptionsUi() { // Always update the summary. if (!TextUtils.isEmpty(mCopiesEditText.getText())) { mSummaryCopies.setText(mCopiesEditText.getText()); @@ -899,7 +1078,8 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat || mState == STATE_PRINT_CANCELED || mState == STATE_UPDATE_FAILED || mState == STATE_CREATE_FILE_FAILED - || mState == STATE_PRINTER_UNAVAILABLE) { + || mState == STATE_PRINTER_UNAVAILABLE + || mState == STATE_UPDATE_SLOW) { if (mState != STATE_PRINTER_UNAVAILABLE) { mDestinationSpinner.setEnabled(false); } @@ -909,14 +1089,14 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat mOrientationSpinner.setEnabled(false); mRangeOptionsSpinner.setEnabled(false); mPageRangeEditText.setEnabled(false); - mPrintButton.setEnabled(false); + mPrintButton.setVisibility(View.GONE); mMoreOptionsButton.setEnabled(false); return; } // If no current printer, or it has no capabilities, or it is not // available, we disable all print options except the destination. - PrinterInfo currentPrinter = getCurrentPrinter(); + PrinterInfo currentPrinter = getCurrentPrinter(); if (currentPrinter == null || !canPrint(currentPrinter)) { mCopiesEditText.setEnabled(false); mMediaSizeSpinner.setEnabled(false); @@ -924,7 +1104,7 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat mOrientationSpinner.setEnabled(false); mRangeOptionsSpinner.setEnabled(false); mPageRangeEditText.setEnabled(false); - mPrintButton.setEnabled(false); + mPrintButton.setVisibility(View.GONE); mMoreOptionsButton.setEnabled(false); return; } @@ -932,6 +1112,9 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat PrinterCapabilitiesInfo capabilities = currentPrinter.getCapabilities(); PrintAttributes defaultAttributes = capabilities.getDefaults(); + // Destination. + mDestinationSpinner.setEnabled(true); + // Media size. mMediaSizeSpinner.setEnabled(true); @@ -1075,8 +1258,9 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat // Range options PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info; - if (info != null && info.getPageCount() > 0) { - if (info.getPageCount() == 1) { + final int pageCount = getAdjustedPageCount(info); + if (info != null && pageCount > 0) { + if (pageCount == 1) { mRangeOptionsSpinner.setEnabled(false); } else { mRangeOptionsSpinner.setEnabled(true); @@ -1096,13 +1280,14 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat mPageRangeTitle.setVisibility(View.INVISIBLE); } } - String title = (info.getPageCount() != PrintDocumentInfo.PAGE_COUNT_UNKNOWN) - ? getString(R.string.label_pages, String.valueOf(info.getPageCount())) + String title = (pageCount != PrintDocumentInfo.PAGE_COUNT_UNKNOWN) + ? getString(R.string.label_pages, String.valueOf(pageCount)) : getString(R.string.page_count_unknown); mPageRangeOptionsTitle.setText(title); } else { if (mRangeOptionsSpinner.getSelectedItemPosition() != 0) { mRangeOptionsSpinner.setSelection(0); + mPageRangeEditText.setText(""); } mRangeOptionsSpinner.setEnabled(false); mPageRangeOptionsTitle.setText(getString(R.string.page_count_unknown)); @@ -1130,11 +1315,11 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat } if ((mRangeOptionsSpinner.getSelectedItemPosition() == 1 && (TextUtils.isEmpty(mPageRangeEditText.getText()) || hasErrors())) - || (mRangeOptionsSpinner.getSelectedItemPosition() == 0 + || (mRangeOptionsSpinner.getSelectedItemPosition() == 0 && (mPrintedDocument.getDocumentInfo() == null || hasErrors()))) { - mPrintButton.setEnabled(false); + mPrintButton.setVisibility(View.GONE); } else { - mPrintButton.setEnabled(true); + mPrintButton.setVisibility(View.VISIBLE); } // Copies @@ -1150,7 +1335,7 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat } } - private PageRange[] computeRequestedPages() { + private PageRange[] computeSelectedPages() { if (hasErrors()) { return null; } @@ -1197,16 +1382,28 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat return ALL_PAGES_ARRAY; } + private int getAdjustedPageCount(PrintDocumentInfo info) { + if (info != null) { + final int pageCount = info.getPageCount(); + if (pageCount != PrintDocumentInfo.PAGE_COUNT_UNKNOWN) { + return pageCount; + } + } + // If the app does not tell us how many pages are in the + // doc we ask for all pages and use the document page count. + return mPrintPreviewController.getFilePageCount(); + } + private boolean hasErrors() { return (mCopiesEditText.getError() != null) || (mPageRangeEditText.getVisibility() == View.VISIBLE - && mPageRangeEditText.getError() != null); + && mPageRangeEditText.getError() != null); } public void onPrinterAvailable(PrinterInfo printer) { PrinterInfo currentPrinter = getCurrentPrinter(); if (currentPrinter.equals(printer)) { - mState = STATE_CONFIGURING; + setState(STATE_CONFIGURING); if (canUpdateDocument()) { updateDocument(true, false); } @@ -1217,7 +1414,7 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat public void onPrinterUnavailable(PrinterInfo printer) { if (getCurrentPrinter().getId().equals(printer.getId())) { - mState = STATE_PRINTER_UNAVAILABLE; + setState(STATE_PRINTER_UNAVAILABLE); if (mPrintedDocument.isUpdating()) { mPrintedDocument.cancel(); } @@ -1227,6 +1424,47 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat } } + private boolean canUpdateDocument() { + if (mPrintedDocument.isDestroyed()) { + return false; + } + + if (hasErrors()) { + return false; + } + + PrintAttributes attributes = mPrintJob.getAttributes(); + + final int colorMode = attributes.getColorMode(); + if (colorMode != PrintAttributes.COLOR_MODE_COLOR + && colorMode != PrintAttributes.COLOR_MODE_MONOCHROME) { + return false; + } + if (attributes.getMediaSize() == null) { + return false; + } + if (attributes.getMinMargins() == null) { + return false; + } + if (attributes.getResolution() == null) { + return false; + } + + PrinterInfo currentPrinter = getCurrentPrinter(); + if (currentPrinter == null) { + return false; + } + PrinterCapabilitiesInfo capabilities = currentPrinter.getCapabilities(); + if (capabilities == null) { + return false; + } + if (currentPrinter.getStatus() == PrinterInfo.STATUS_UNAVAILABLE) { + return false; + } + + return true; + } + private final class SpinnerItem<T> { final T value; final CharSequence label; @@ -1266,10 +1504,10 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat mPrinter = new PrinterInfo.Builder(printer).build(); } else { notifyIfAvailable = - (mPrinter.getStatus() == PrinterInfo.STATUS_UNAVAILABLE - && printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE) - || (mPrinter.getCapabilities() == null - && printer.getCapabilities() != null); + (mPrinter.getStatus() == PrinterInfo.STATUS_UNAVAILABLE + && printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE) + || (mPrinter.getCapabilities() == null + && printer.getCapabilities() != null); mPrinter.copyFrom(printer); } @@ -1651,15 +1889,14 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat oldPrinterState.copyFrom(newPrinterState); if ((isActive && gotCapab) || (becameActive && hasCapab)) { + if (hasCapab && capabChanged) { + updatePrintAttributesFromCapabilities(newCapab); + } onPrinterAvailable(newPrinterState); - } else if ((becameInactive && hasCapab)|| (isActive && lostCapab)) { + } else if ((becameInactive && hasCapab) || (isActive && lostCapab)) { onPrinterUnavailable(newPrinterState); } - if (hasCapab && capabChanged) { - updatePrintAttributesFromCapabilities(newCapab); - } - final boolean updateNeeded = ((capabChanged && hasCapab && isActive) || (becameActive && hasCapab) || (isActive && gotCapab)); @@ -1702,11 +1939,13 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat if (mOldCurrentPrinter == currentPrinter) { return; } + mOldCurrentPrinter = currentPrinter; PrinterHolder printerHolder = mDestinationSpinnerAdapter.getPrinterHolder( currentPrinter.getId()); if (!printerHolder.removed) { + setState(STATE_CONFIGURING); mDestinationSpinnerAdapter.pruneRemovedPrinters(); ensurePreviewUiShown(); } @@ -1718,16 +1957,17 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat PrinterCapabilitiesInfo capabilities = currentPrinter.getCapabilities(); if (capabilities != null) { - updatePrintAttributesFromCapabilities(capabilities); + updatePrintAttributesFromCapabilities(capabilities); } mPrinterAvailabilityDetector.updatePrinter(currentPrinter); } else if (spinner == mMediaSizeSpinner) { SpinnerItem<MediaSize> mediaItem = mMediaSizeSpinnerAdapter.getItem(position); + PrintAttributes attributes = mPrintJob.getAttributes(); if (mOrientationSpinner.getSelectedItemPosition() == 0) { - mPrintJob.getAttributes().setMediaSize(mediaItem.value.asPortrait()); + attributes.setMediaSize(mediaItem.value.asPortrait()); } else { - mPrintJob.getAttributes().setMediaSize(mediaItem.value.asLandscape()); + attributes.setMediaSize(mediaItem.value.asLandscape()); } } else if (spinner == mColorModeSpinner) { SpinnerItem<Integer> colorModeItem = mColorModeSpinnerAdapter.getItem(position); @@ -1742,6 +1982,12 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat attributes.copyFrom(attributes.asLandscape()); } } + } else if (spinner == mRangeOptionsSpinner) { + if (mRangeOptionsSpinner.getSelectedItemPosition() == 0) { + mPageRangeEditText.setText(""); + } else if (TextUtils.isEmpty(mPageRangeEditText.getText())) { + mPageRangeEditText.setError(""); + } } if (canUpdateDocument()) { @@ -1757,47 +2003,6 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat } } - private boolean canUpdateDocument() { - if (mPrintedDocument.isDestroyed()) { - return false; - } - - if (hasErrors()) { - return false; - } - - PrintAttributes attributes = mPrintJob.getAttributes(); - - final int colorMode = attributes.getColorMode(); - if (colorMode != PrintAttributes.COLOR_MODE_COLOR - && colorMode != PrintAttributes.COLOR_MODE_MONOCHROME) { - return false; - } - if (attributes.getMediaSize() == null) { - return false; - } - if (attributes.getMinMargins() == null) { - return false; - } - if (attributes.getResolution() == null) { - return false; - } - - PrinterInfo currentPrinter = getCurrentPrinter(); - if (currentPrinter == null) { - return false; - } - PrinterCapabilitiesInfo capabilities = currentPrinter.getCapabilities(); - if (capabilities == null) { - return false; - } - if (currentPrinter.getStatus() == PrinterInfo.STATUS_UNAVAILABLE) { - return false; - } - - return true; - } - private final class SelectAllOnFocusListener implements OnFocusChangeListener { @Override public void onFocusChange(View view, boolean hasFocus) { @@ -1839,7 +2044,7 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat } PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info; - final int pageCount = (info != null) ? info.getPageCount() : 0; + final int pageCount = (info != null) ? getAdjustedPageCount(info) : 0; // The range Matcher matcher = PATTERN_DIGITS.matcher(text); @@ -1860,10 +2065,6 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat // greater than the to page. When computing the requested pages // we just swap them if necessary. - // Keep the print job up to date with the selected pages if we - // know how many pages are there in the document. - mRequestedPages = computeRequestedPages(); - mPageRangeEditText.setError(null); mPrintButton.setEnabled(true); updateOptionsUi(); @@ -1949,7 +2150,10 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat @Override public void run() { + mPosted = false; + setState(STATE_UPDATE_SLOW); ensureProgressUiShown(); + updateOptionsUi(); } } -} +}
\ No newline at end of file diff --git a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintErrorFragment.java b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintErrorFragment.java index b708356..b0b9c96 100644 --- a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintErrorFragment.java +++ b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintErrorFragment.java @@ -35,22 +35,21 @@ import com.android.printspooler.R; public final class PrintErrorFragment extends Fragment { public static final int ACTION_NONE = 0; public static final int ACTION_RETRY = 1; - public static final int ACTION_CONFIRM = 2; + + private static final String EXTRA_MESSAGE = "EXTRA_MESSAGE"; + private static final String EXTRA_ACTION = "EXTRA_ACTION"; public interface OnActionListener { public void onActionPerformed(); } - private static final String EXTRA_ERROR_MESSAGE = "EXTRA_ERROR_MESSAGE"; - private static final String EXTRA_ACTION = "EXTRA_ACTION"; - - public static PrintErrorFragment newInstance(CharSequence errorMessage, int action) { - PrintErrorFragment instance = new PrintErrorFragment(); + public static PrintErrorFragment newInstance(CharSequence message, int action) { Bundle arguments = new Bundle(); - arguments.putCharSequence(EXTRA_ERROR_MESSAGE, errorMessage); + arguments.putCharSequence(EXTRA_MESSAGE, message); arguments.putInt(EXTRA_ACTION, action); - instance.setArguments(arguments); - return instance; + PrintErrorFragment fragment = new PrintErrorFragment(); + fragment.setArguments(arguments); + return fragment; } @Override @@ -63,12 +62,11 @@ public final class PrintErrorFragment extends Fragment { public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - Bundle arguments = getArguments(); + CharSequence message = getArguments().getCharSequence(EXTRA_MESSAGE); - CharSequence error = arguments.getString(EXTRA_ERROR_MESSAGE); - if (!TextUtils.isEmpty(error)) { - TextView message = (TextView) view.findViewById(R.id.message); - message.setText(error); + if (!TextUtils.isEmpty(message)) { + TextView messageView = (TextView) view.findViewById(R.id.message); + messageView.setText(message); } Button actionButton = (Button) view.findViewById(R.id.action_button); @@ -80,11 +78,6 @@ public final class PrintErrorFragment extends Fragment { actionButton.setText(R.string.print_error_retry); } break; - case ACTION_CONFIRM: { - actionButton.setVisibility(View.VISIBLE); - actionButton.setText(android.R.string.ok); - } break; - case ACTION_NONE: { actionButton.setVisibility(View.GONE); } break; diff --git a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintPreviewController.java b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintPreviewController.java new file mode 100644 index 0000000..910818b --- /dev/null +++ b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintPreviewController.java @@ -0,0 +1,388 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.printspooler.ui; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.ParcelFileDescriptor; +import android.print.PageRange; +import android.print.PrintAttributes.MediaSize; +import android.print.PrintAttributes.Margins; +import android.print.PrintDocumentInfo; +import android.support.v7.widget.OrientationHelper; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.RecyclerView.ViewHolder; +import android.support.v7.widget.RecyclerView.LayoutManager; +import android.support.v7.widget.StaggeredGridLayoutManager; +import android.view.View; +import com.android.internal.os.SomeArgs; +import com.android.printspooler.R; +import com.android.printspooler.model.MutexFileProvider; +import com.android.printspooler.widget.PrintContentView; +import com.android.printspooler.widget.EmbeddedContentContainer; +import com.android.printspooler.widget.PrintOptionsLayout; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.ArrayList; +import java.util.List; + +class PrintPreviewController implements MutexFileProvider.OnReleaseRequestCallback, + PageAdapter.PreviewArea, EmbeddedContentContainer.OnSizeChangeListener { + + private PrintActivity mActivity; + + private final MutexFileProvider mFileProvider; + + private final MyHandler mHandler; + + private final PageAdapter mPageAdapter; + + private final StaggeredGridLayoutManager mLayoutManger; + + private PrintOptionsLayout mPrintOptionsLayout; + + private final RecyclerView mRecyclerView; + + private final PrintContentView mContentView; + + private final EmbeddedContentContainer mEmbeddedContentContainer; + + private final PreloadController mPreloadController; + + private int mDocumentPageCount; + + public PrintPreviewController(PrintActivity activity, MutexFileProvider fileProvider) { + mActivity = activity; + mHandler = new MyHandler(activity.getMainLooper()); + mFileProvider = fileProvider; + + mPrintOptionsLayout = (PrintOptionsLayout) activity.findViewById(R.id.options_container); + mPageAdapter = new PageAdapter(activity, activity, this); + + final int columnCount = mActivity.getResources().getInteger( + R.integer.preview_page_per_row_count); + + mLayoutManger = new StaggeredGridLayoutManager(columnCount, OrientationHelper.VERTICAL); + mRecyclerView = (RecyclerView) activity.findViewById(R.id.preview_content); + mRecyclerView.setLayoutManager(mLayoutManger); + mRecyclerView.setAdapter(mPageAdapter); + mPreloadController = new PreloadController(mRecyclerView); + mRecyclerView.setOnScrollListener(mPreloadController); + + mContentView = (PrintContentView) activity.findViewById(R.id.options_content); + mEmbeddedContentContainer = (EmbeddedContentContainer) activity.findViewById( + R.id.embedded_content_container); + mEmbeddedContentContainer.setOnSizeChangeListener(this); + } + + @Override + public void onSizeChanged(int width, int height) { + mPageAdapter.onPreviewAreaSizeChanged(); + } + + public boolean isOptionsOpened() { + return mContentView.isOptionsOpened(); + } + + public void closeOptions() { + mContentView.closeOptions(); + } + + public void setUiShown(boolean shown) { + if (shown) { + mRecyclerView.setVisibility(View.VISIBLE); + } else { + mRecyclerView.setVisibility(View.GONE); + } + } + + public void onOrientationChanged() { + // Adjust the print option column count. + final int optionColumnCount = mActivity.getResources().getInteger( + R.integer.print_option_column_count); + mPrintOptionsLayout.setColumnCount(optionColumnCount); + mPageAdapter.onOrientationChanged(); + } + + public int getFilePageCount() { + return mPageAdapter.getFilePageCount(); + } + + public PageRange[] getSelectedPages() { + return mPageAdapter.getSelectedPages(); + } + + public PageRange[] getRequestedPages() { + return mPageAdapter.getRequestedPages(); + } + + public void onContentUpdated(boolean documentChanged, int documentPageCount, + PageRange[] writtenPages, PageRange[] selectedPages, MediaSize mediaSize, + Margins minMargins) { + boolean contentChanged = false; + + if (documentChanged) { + contentChanged = true; + } + + if (documentPageCount != mDocumentPageCount) { + mDocumentPageCount = documentPageCount; + contentChanged = true; + } + + if (contentChanged) { + // If not closed, close as we start over. + if (mPageAdapter.isOpened()) { + Message operation = mHandler.obtainMessage(MyHandler.MSG_CLOSE); + mHandler.enqueueOperation(operation); + } + } + + // The content changed. In this case we have to invalidate + // all rendered pages and reopen the file... + if (contentChanged && writtenPages != null) { + Message operation = mHandler.obtainMessage(MyHandler.MSG_OPEN); + mHandler.enqueueOperation(operation); + } + + // Update the attributes before after closed to avoid flicker. + SomeArgs args = SomeArgs.obtain(); + args.arg1 = writtenPages; + args.arg2 = selectedPages; + args.arg3 = mediaSize; + args.arg4 = minMargins; + args.argi1 = documentPageCount; + + Message operation = mHandler.obtainMessage(MyHandler.MSG_UPDATE, args); + mHandler.enqueueOperation(operation); + + // If document changed and has pages we want to start preloading. + if (contentChanged && writtenPages != null) { + operation = mHandler.obtainMessage(MyHandler.MSG_START_PRELOAD); + mHandler.enqueueOperation(operation); + } + } + + @Override + public void onReleaseRequested(final File file) { + // This is called from the async task's single threaded executor + // thread, i.e. not on the main thread - so post a message. + mHandler.post(new Runnable() { + @Override + public void run() { + // At this point the other end will write to the file, hence + // we have to close it and reopen after the write completes. + Message operation = mHandler.obtainMessage(MyHandler.MSG_CLOSE); + mHandler.enqueueOperation(operation); + } + }); + } + + public void destroy() { + mPageAdapter.destroy(); + } + + @Override + public int getWidth() { + return mEmbeddedContentContainer.getWidth(); + } + + @Override + public int getHeight() { + return mEmbeddedContentContainer.getHeight(); + } + + @Override + public void setColumnCount(int columnCount) { + mLayoutManger.setSpanCount(columnCount); + } + + @Override + public void setPadding(int left, int top , int right, int bottom) { + mRecyclerView.setPadding(left, top, right, bottom); + } + + private final class MyHandler extends Handler { + public static final int MSG_OPEN = 1; + public static final int MSG_CLOSE = 2; + public static final int MSG_DESTROY = 3; + public static final int MSG_UPDATE = 4; + public static final int MSG_START_PRELOAD = 5; + + private boolean mAsyncOperationInProgress; + + private final Runnable mOnAsyncOperationDoneCallback = new Runnable() { + @Override + public void run() { + mAsyncOperationInProgress = false; + handleNextOperation(); + } + }; + + private final List<Message> mPendingOperations = new ArrayList<>(); + + public MyHandler(Looper looper) { + super(looper, null, false); + } + + public void enqueueOperation(Message message) { + mPendingOperations.add(message); + handleNextOperation(); + } + + public void handleNextOperation() { + while (!mPendingOperations.isEmpty() && !mAsyncOperationInProgress) { + Message operation = mPendingOperations.remove(0); + handleMessage(operation); + } + } + + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_OPEN: { + try { + File file = mFileProvider.acquireFile(PrintPreviewController.this); + ParcelFileDescriptor pfd = ParcelFileDescriptor.open(file, + ParcelFileDescriptor.MODE_READ_ONLY); + + mAsyncOperationInProgress = true; + mPageAdapter.open(pfd, new Runnable() { + @Override + public void run() { + if (mDocumentPageCount == PrintDocumentInfo.PAGE_COUNT_UNKNOWN) { + mDocumentPageCount = mPageAdapter.getFilePageCount(); + mActivity.updateOptionsUi(); + } + mOnAsyncOperationDoneCallback.run(); + } + }); + } catch (FileNotFoundException fnfe) { + /* ignore - file guaranteed to be there */ + } + } break; + + case MSG_CLOSE: { + mAsyncOperationInProgress = true; + mPageAdapter.close(new Runnable() { + @Override + public void run() { + mFileProvider.releaseFile(); + mOnAsyncOperationDoneCallback.run(); + } + }); + } break; + + case MSG_DESTROY: { + mPageAdapter.destroy(); + handleNextOperation(); + } break; + + case MSG_UPDATE: { + SomeArgs args = (SomeArgs) message.obj; + PageRange[] writtenPages = (PageRange[]) args.arg1; + PageRange[] selectedPages = (PageRange[]) args.arg2; + MediaSize mediaSize = (MediaSize) args.arg3; + Margins margins = (Margins) args.arg4; + final int pageCount = args.argi1; + args.recycle(); + + mPageAdapter.update(writtenPages, selectedPages, pageCount, + mediaSize, margins); + + } break; + + case MSG_START_PRELOAD: { + mPreloadController.startPreloadContent(); + } break; + } + } + } + + private final class PreloadController implements RecyclerView.OnScrollListener { + private final RecyclerView mRecyclerView; + + private int mOldScrollState; + + public PreloadController(RecyclerView recyclerView) { + mRecyclerView = recyclerView; + mOldScrollState = mRecyclerView.getScrollState(); + } + + @Override + public void onScrollStateChanged(int state) { + switch (mOldScrollState) { + case RecyclerView.SCROLL_STATE_SETTLING: { + if (state == RecyclerView.SCROLL_STATE_IDLE + || state == RecyclerView.SCROLL_STATE_DRAGGING){ + startPreloadContent(); + } + } break; + + case RecyclerView.SCROLL_STATE_IDLE: + case RecyclerView.SCROLL_STATE_DRAGGING: { + if (state == RecyclerView.SCROLL_STATE_SETTLING) { + stopPreloadContent(); + } + } break; + } + mOldScrollState = state; + } + + @Override + public void onScrolled(int dx, int dy) { + /* do nothing */ + } + + public void startPreloadContent() { + PageAdapter pageAdapter = (PageAdapter) mRecyclerView.getAdapter(); + + if (pageAdapter.isOpened()) { + PageRange shownPages = computeShownPages(); + if (shownPages != null) { + pageAdapter.startPreloadContent(shownPages); + } + } + } + + public void stopPreloadContent() { + PageAdapter pageAdapter = (PageAdapter) mRecyclerView.getAdapter(); + + if (pageAdapter.isOpened()) { + pageAdapter.stopPreloadContent(); + } + } + + private PageRange computeShownPages() { + final int childCount = mRecyclerView.getChildCount(); + if (childCount > 0) { + LayoutManager layoutManager = mRecyclerView.getLayoutManager(); + + View firstChild = layoutManager.getChildAt(0); + ViewHolder firstHolder = mRecyclerView.getChildViewHolder(firstChild); + + View lastChild = layoutManager.getChildAt(layoutManager.getChildCount() - 1); + ViewHolder lastHolder = mRecyclerView.getChildViewHolder(lastChild); + + return new PageRange(firstHolder.getPosition(), lastHolder.getPosition()); + } + return null; + } + } +} diff --git a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintPreviewFragment.java b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintPreviewFragment.java deleted file mode 100644 index d68a6aa..0000000 --- a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintPreviewFragment.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.android.printspooler.ui; - -import android.app.Fragment; - -public class PrintPreviewFragment extends Fragment { - - public static PrintPreviewFragment newInstance() { - return new PrintPreviewFragment(); - } - - // TODO: Implement -} diff --git a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintProgressFragment.java b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintProgressFragment.java index 96aa153..f05f8da 100644 --- a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintProgressFragment.java +++ b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintProgressFragment.java @@ -16,54 +16,24 @@ package com.android.printspooler.ui; -import android.app.Activity; import android.app.Fragment; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.view.View.OnClickListener; -import android.widget.Button; -import android.widget.TextView; import com.android.printspooler.R; /** * Fragment for showing a work in progress UI. */ public final class PrintProgressFragment extends Fragment { - - public interface OnCancelRequestListener { - public void onCancelRequest(); - } - public static PrintProgressFragment newInstance() { return new PrintProgressFragment(); } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup root, - Bundle savedInstanceState) { + public View onCreateView(LayoutInflater inflater, ViewGroup root, Bundle state) { return inflater.inflate(R.layout.print_progress_fragment, root, false); } - - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - final Button cancelButton = (Button) view.findViewById(R.id.cancel_button); - final TextView message = (TextView) view.findViewById(R.id.message); - - cancelButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View view) { - Activity activity = getActivity(); - if (activity instanceof OnCancelRequestListener) { - ((OnCancelRequestListener) getActivity()).onCancelRequest(); - } - cancelButton.setVisibility(View.GONE); - message.setVisibility(View.VISIBLE); - } - }); - } } diff --git a/packages/PrintSpooler/src/com/android/printspooler/util/PageRangeUtils.java b/packages/PrintSpooler/src/com/android/printspooler/util/PageRangeUtils.java index 33b294f..2b317b3 100644 --- a/packages/PrintSpooler/src/com/android/printspooler/util/PageRangeUtils.java +++ b/packages/PrintSpooler/src/com/android/printspooler/util/PageRangeUtils.java @@ -17,6 +17,7 @@ package com.android.printspooler.util; import android.print.PageRange; +import android.print.PrintDocumentInfo; import java.util.Arrays; import java.util.Comparator; @@ -40,13 +41,32 @@ public final class PageRangeUtils { } /** + * Gets whether page ranges contains a given page. + * + * @param pageRanges The page ranges. + * @param pageIndex The page for which to check. + * @return Whether the page is within the ranges. + */ + public static boolean contains(PageRange[] pageRanges, int pageIndex) { + final int rangeCount = pageRanges.length; + for (int i = 0; i < rangeCount; i++) { + PageRange pageRange = pageRanges[i]; + if (pageRange.contains(pageIndex)) { + return true; + } + } + return false; + } + + /** * Checks whether one page range array contains another one. * * @param ourRanges The container page ranges. * @param otherRanges The contained page ranges. + * @param pageCount The total number of pages. * @return Whether the container page ranges contains the contained ones. */ - public static boolean contains(PageRange[] ourRanges, PageRange[] otherRanges) { + public static boolean contains(PageRange[] ourRanges, PageRange[] otherRanges, int pageCount) { if (ourRanges == null || otherRanges == null) { return false; } @@ -55,6 +75,10 @@ public final class PageRangeUtils { return true; } + if (Arrays.equals(otherRanges, ALL_PAGES_RANGE)) { + otherRanges[0] = new PageRange(0, pageCount - 1); + } + ourRanges = normalize(ourRanges); otherRanges = normalize(otherRanges); @@ -77,10 +101,7 @@ public final class PageRangeUtils { } } } - if (otherRangeIdx < otherRangeCount) { - return false; - } - return true; + return (otherRangeIdx >= otherRangeCount); } /** @@ -95,28 +116,42 @@ public final class PageRangeUtils { if (pageRanges == null) { return null; } + final int oldRangeCount = pageRanges.length; if (oldRangeCount <= 1) { return pageRanges; } + Arrays.sort(pageRanges, sComparator); + int newRangeCount = 1; for (int i = 0; i < oldRangeCount - 1; i++) { - newRangeCount++; PageRange currentRange = pageRanges[i]; PageRange nextRange = pageRanges[i + 1]; if (currentRange.getEnd() + 1 >= nextRange.getStart()) { - newRangeCount--; pageRanges[i] = null; pageRanges[i + 1] = new PageRange(currentRange.getStart(), Math.max(currentRange.getEnd(), nextRange.getEnd())); + } else { + newRangeCount++; } } + if (newRangeCount == oldRangeCount) { return pageRanges; } - return Arrays.copyOfRange(pageRanges, oldRangeCount - newRangeCount, - oldRangeCount); + + int normalRangeIndex = 0; + PageRange[] normalRanges = new PageRange[newRangeCount]; + for (int i = 0; i < oldRangeCount; i++) { + PageRange normalRange = pageRanges[i]; + if (normalRange != null) { + normalRanges[normalRangeIndex] = normalRange; + normalRangeIndex++; + } + } + + return normalRanges; } /** @@ -146,14 +181,89 @@ public final class PageRangeUtils { */ public static int getNormalizedPageCount(PageRange[] pageRanges, int layoutPageCount) { int pageCount = 0; + if (pageRanges != null) { + final int pageRangeCount = pageRanges.length; + for (int i = 0; i < pageRangeCount; i++) { + PageRange pageRange = pageRanges[i]; + if (PageRange.ALL_PAGES.equals(pageRange)) { + return layoutPageCount; + } + pageCount += pageRange.getSize(); + } + } + return pageCount; + } + + public static PageRange asAbsoluteRange(PageRange pageRange, int pageCount) { + if (PageRange.ALL_PAGES.equals(pageRange)) { + return new PageRange(0, pageCount - 1); + } + return pageRange; + } + + public static boolean isAllPages(PageRange[] pageRanges) { final int pageRangeCount = pageRanges.length; for (int i = 0; i < pageRangeCount; i++) { PageRange pageRange = pageRanges[i]; - if (PageRange.ALL_PAGES.equals(pageRange)) { - return layoutPageCount; + if (isAllPages(pageRange)) { + return true; } - pageCount += pageRange.getEnd() - pageRange.getStart() + 1; } - return pageCount; + return false; + } + + public static boolean isAllPages(PageRange pageRange) { + return PageRange.ALL_PAGES.equals(pageRange); + } + + public static boolean isAllPages(PageRange[] pageRanges, int pageCount) { + final int pageRangeCount = pageRanges.length; + for (int i = 0; i < pageRangeCount; i++) { + PageRange pageRange = pageRanges[i]; + if (isAllPages(pageRange, pageCount)) { + return true; + } + } + return false; + } + + public static boolean isAllPages(PageRange pageRanges, int pageCount) { + return pageRanges.getStart() == 0 && pageRanges.getEnd() == pageCount - 1; + } + + public static PageRange[] computePrintedPages(PageRange[] requestedPages, + PageRange[] writtenPages, int pageCount) { + // Adjust the print job pages based on what was requested and written. + // The cases are ordered in the most expected to the least expected + // with a special case first where the app does not know the page count + // so we ask for all to be written. + if (Arrays.equals(requestedPages, ALL_PAGES_RANGE) + && pageCount == PrintDocumentInfo.PAGE_COUNT_UNKNOWN) { + return ALL_PAGES_RANGE; + } else if (Arrays.equals(writtenPages, requestedPages)) { + // We got a document with exactly the pages we wanted. Hence, + // the printer has to print all pages in the data. + return ALL_PAGES_RANGE; + } else if (Arrays.equals(writtenPages, ALL_PAGES_RANGE)) { + // We requested specific pages but got all of them. Hence, + // the printer has to print only the requested pages. + return requestedPages; + } else if (PageRangeUtils.contains(writtenPages, requestedPages, pageCount)) { + // We requested specific pages and got more but not all pages. + // Hence, we have to offset appropriately the printed pages to + // be based off the start of the written ones instead of zero. + // The written pages are always non-null and not empty. + final int offset = -writtenPages[0].getStart(); + PageRangeUtils.offset(requestedPages, offset); + return requestedPages; + } else if (Arrays.equals(requestedPages, ALL_PAGES_RANGE) + && isAllPages(writtenPages, pageCount)) { + // We requested all pages via the special constant and got all + // of them as an explicit enumeration. Hence, the printer has + // to print only the requested pages. + return ALL_PAGES_RANGE; + } + + return null; } } diff --git a/packages/PrintSpooler/src/com/android/printspooler/widget/EmbeddedContentContainer.java b/packages/PrintSpooler/src/com/android/printspooler/widget/EmbeddedContentContainer.java new file mode 100644 index 0000000..2afaa9b --- /dev/null +++ b/packages/PrintSpooler/src/com/android/printspooler/widget/EmbeddedContentContainer.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.printspooler.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.FrameLayout; + +public class EmbeddedContentContainer extends FrameLayout { + public interface OnSizeChangeListener { + public void onSizeChanged(int width, int height); + } + + private OnSizeChangeListener mSizeChangeListener; + + public EmbeddedContentContainer(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void setOnSizeChangeListener(OnSizeChangeListener listener) { + mSizeChangeListener = listener; + } + + @Override + protected void onSizeChanged(int newWidth, int newHeight, int oldWidth, int oldHeight) { + super.onSizeChanged(newWidth, newHeight, oldWidth, oldHeight); + if (mSizeChangeListener != null) { + mSizeChangeListener.onSizeChanged(newWidth, newHeight); + } + } +} diff --git a/packages/PrintSpooler/src/com/android/printspooler/widget/PageContentView.java b/packages/PrintSpooler/src/com/android/printspooler/widget/PageContentView.java new file mode 100644 index 0000000..bb63fb8 --- /dev/null +++ b/packages/PrintSpooler/src/com/android/printspooler/widget/PageContentView.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.printspooler.widget; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.ColorDrawable; +import android.print.PrintAttributes.MediaSize; +import android.print.PrintAttributes.Margins; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.View; +import com.android.printspooler.model.PageContentRepository; +import com.android.printspooler.model.PageContentRepository.PageContentProvider; +import com.android.printspooler.model.PageContentRepository.RenderSpec; + +/** + * This class represents a page in the print preview list. The width of the page + * is determined by stretching it to take maximal horizontal space while the height + * is computed from the width using the page aspect ratio. Note that different media + * sizes have different aspect ratios. + */ +public class PageContentView extends View + implements PageContentRepository.OnPageContentAvailableCallback { + + private final ColorDrawable mEmptyState; + + private PageContentProvider mProvider; + + private MediaSize mMediaSize; + + private Margins mMinMargins; + + private boolean mContentRequested; + + public PageContentView(Context context, AttributeSet attrs) { + super(context, attrs); + + TypedValue typedValue = new TypedValue(); + context.getTheme().resolveAttribute(com.android.internal.R.attr.textColorPrimary, + typedValue, true); + + mEmptyState = new ColorDrawable(typedValue.data); + + setBackground(mEmptyState); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + requestPageContentIfNeeded(); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + requestPageContentIfNeeded(); + } + + @Override + public void onPageContentAvailable(BitmapDrawable content) { + if (getBackground() != content) { + setBackground(content); + } + } + + public PageContentProvider getPageContentProvider() { + return mProvider; + } + + public void init(PageContentProvider provider, MediaSize mediaSize, Margins minMargins) { + if (mProvider == provider + && ((mMediaSize == null) ? mediaSize == null : mMediaSize.equals(mediaSize)) + && ((mMinMargins == null) ? minMargins == null : mMinMargins.equals(minMargins))) { + return; + } + + mProvider = provider; + mMediaSize = mediaSize; + mMinMargins = minMargins; + mContentRequested = false; + + // If there is not provider we want immediately to switch to + // the empty state, so pages with no content appear blank. + if (mProvider == null && getBackground() != mEmptyState) { + setBackground(mEmptyState); + } + + requestPageContentIfNeeded(); + } + + private void requestPageContentIfNeeded() { + if (getWidth() > 0 && getHeight() > 0 && !mContentRequested && mProvider != null) { + mContentRequested = true; + mProvider.getPageContent(new RenderSpec(getWidth(), getHeight(), mMediaSize, + mMinMargins), this); + } + } +} diff --git a/packages/PrintSpooler/src/com/android/printspooler/widget/ContentView.java b/packages/PrintSpooler/src/com/android/printspooler/widget/PrintContentView.java index 77ca541..b4a78e6 100644 --- a/packages/PrintSpooler/src/com/android/printspooler/widget/ContentView.java +++ b/packages/PrintSpooler/src/com/android/printspooler/widget/PrintContentView.java @@ -34,40 +34,55 @@ import com.android.printspooler.R; * the former is opened. */ @SuppressWarnings("unused") -public final class ContentView extends ViewGroup implements View.OnClickListener { +public final class PrintContentView extends ViewGroup implements View.OnClickListener { private static final int FIRST_POINTER_ID = 0; private final ViewDragHelper mDragger; + private final int mScrimColor; + private View mStaticContent; private ViewGroup mSummaryContent; private View mDynamicContent; private View mDraggableContent; + private View mPrintButton; private ViewGroup mMoreOptionsContainer; private ViewGroup mOptionsContainer; private View mEmbeddedContentContainer; + private View mEmbeddedContentScrim; private View mExpandCollapseHandle; private View mExpandCollapseIcon; private int mClosedOptionsOffsetY; - private int mCurrentOptionsOffsetY; + private int mCurrentOptionsOffsetY = Integer.MIN_VALUE; private OptionsStateChangeListener mOptionsStateChangeListener; + private OptionsStateController mOptionsStateController; + private int mOldDraggableHeight; + private float mDragProgress; + public interface OptionsStateChangeListener { public void onOptionsOpened(); public void onOptionsClosed(); } - public ContentView(Context context, AttributeSet attrs) { + public interface OptionsStateController { + public boolean canOpenOptions(); + public boolean canCloseOptions(); + } + + public PrintContentView(Context context, AttributeSet attrs) { super(context, attrs); mDragger = ViewDragHelper.create(this, new DragCallbacks()); + mScrimColor = context.getResources().getColor(R.color.print_preview_scrim_color); + // The options view is sliding under the static header but appears // after it in the layout, so we will draw in opposite order. setChildrenDrawingOrderEnabled(true); @@ -77,7 +92,11 @@ public final class ContentView extends ViewGroup implements View.OnClickListener mOptionsStateChangeListener = listener; } - private boolean isOptionsOpened() { + public void setOpenOptionsController(OptionsStateController controller) { + mOptionsStateController = controller; + } + + public boolean isOptionsOpened() { return mCurrentOptionsOffsetY == 0; } @@ -85,7 +104,7 @@ public final class ContentView extends ViewGroup implements View.OnClickListener return mCurrentOptionsOffsetY == mClosedOptionsOffsetY; } - private void openOptions() { + public void openOptions() { if (isOptionsOpened()) { return; } @@ -94,7 +113,7 @@ public final class ContentView extends ViewGroup implements View.OnClickListener invalidate(); } - private void closeOptions() { + public void closeOptions() { if (isOptionsClosed()) { return; } @@ -114,13 +133,14 @@ public final class ContentView extends ViewGroup implements View.OnClickListener mSummaryContent = (ViewGroup) findViewById(R.id.summary_content); mDynamicContent = findViewById(R.id.dynamic_content); mDraggableContent = findViewById(R.id.draggable_content); + mPrintButton = findViewById(R.id.print_button); mMoreOptionsContainer = (ViewGroup) findViewById(R.id.more_options_container); mOptionsContainer = (ViewGroup) findViewById(R.id.options_container); mEmbeddedContentContainer = findViewById(R.id.embedded_content_container); - mExpandCollapseIcon = findViewById(R.id.expand_collapse_icon); + mEmbeddedContentScrim = findViewById(R.id.embedded_content_scrim); mExpandCollapseHandle = findViewById(R.id.expand_collapse_handle); + mExpandCollapseIcon = findViewById(R.id.expand_collapse_icon); - mExpandCollapseIcon.setOnClickListener(this); mExpandCollapseHandle.setOnClickListener(this); // Make sure we start in a closed options state. @@ -129,12 +149,16 @@ public final class ContentView extends ViewGroup implements View.OnClickListener @Override public void onClick(View view) { - if (view == mExpandCollapseHandle || view == mExpandCollapseIcon) { - if (isOptionsClosed()) { + if (view == mExpandCollapseHandle) { + if (isOptionsClosed() && mOptionsStateController.canOpenOptions()) { openOptions(); - } else if (isOptionsOpened()) { + } else if (isOptionsOpened() && mOptionsStateController.canCloseOptions()) { closeOptions(); } // else in open/close progress do nothing. + } else if (view == mEmbeddedContentScrim) { + if (isOptionsOpened() && mOptionsStateController.canCloseOptions()) { + closeOptions(); + } } } @@ -162,6 +186,12 @@ public final class ContentView extends ViewGroup implements View.OnClickListener } } + private int computeScrimColor() { + final int baseAlpha = (mScrimColor & 0xff000000) >>> 24; + final int adjustedAlpha = (int) (baseAlpha * (1 - mDragProgress)); + return adjustedAlpha << 24 | (mScrimColor & 0xffffff); + } + private int getOpenedOptionsY() { return mStaticContent.getBottom(); } @@ -172,6 +202,8 @@ public final class ContentView extends ViewGroup implements View.OnClickListener @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final boolean wasOpened = isOptionsOpened(); + measureChild(mStaticContent, widthMeasureSpec, heightMeasureSpec); if (mSummaryContent.getVisibility() != View.GONE) { @@ -180,34 +212,34 @@ public final class ContentView extends ViewGroup implements View.OnClickListener measureChild(mDynamicContent, widthMeasureSpec, heightMeasureSpec); - final int heightSize = MeasureSpec.getSize(heightMeasureSpec); - -// // The height of the draggable content may change and if that happens -// // we have to adjust the current offset to ensure the sliding area is -// // at the same position. -// mCurrentOptionsOffsetY -= mDraggableContent.getMeasuredHeight() -// - oldDraggableHeight; - - if (mOldDraggableHeight != mDraggableContent.getMeasuredHeight()) { - mCurrentOptionsOffsetY -= mDraggableContent.getMeasuredHeight() - - mOldDraggableHeight; - mOldDraggableHeight = mDraggableContent.getMeasuredHeight(); - } + measureChild(mPrintButton, widthMeasureSpec, heightMeasureSpec); // The height of the draggable content may change and if that happens // we have to adjust the sliding area closed state offset. mClosedOptionsOffsetY = mSummaryContent.getMeasuredHeight() - mDraggableContent.getMeasuredHeight(); + if (mCurrentOptionsOffsetY == Integer.MIN_VALUE) { + mCurrentOptionsOffsetY = mClosedOptionsOffsetY; + } + + final int heightSize = MeasureSpec.getSize(heightMeasureSpec); + // The content host must be maximally large size that fits entirely // on the screen when the options are collapsed. ViewGroup.LayoutParams params = mEmbeddedContentContainer.getLayoutParams(); - if (params.height == 0) { - params.height = heightSize - mStaticContent.getMeasuredHeight() - - mSummaryContent.getMeasuredHeight() - mDynamicContent.getMeasuredHeight() - + mDraggableContent.getMeasuredHeight(); + params.height = heightSize - mStaticContent.getMeasuredHeight() + - mSummaryContent.getMeasuredHeight() - mDynamicContent.getMeasuredHeight() + + mDraggableContent.getMeasuredHeight(); - mCurrentOptionsOffsetY = mClosedOptionsOffsetY; + // The height of the draggable content may change and if that happens + // we have to adjust the current offset to ensure the sliding area is + // at the correct position. + if (mOldDraggableHeight != mDraggableContent.getMeasuredHeight()) { + if (mOldDraggableHeight != 0) { + mCurrentOptionsOffsetY = wasOpened ? 0 : mClosedOptionsOffsetY; + } + mOldDraggableHeight = mDraggableContent.getMeasuredHeight(); } // The content host can grow vertically as much as needed - we will be covering it. @@ -232,6 +264,15 @@ public final class ContentView extends ViewGroup implements View.OnClickListener mDynamicContent.layout(left, dynContentTop, right, dynContentBottom); + MarginLayoutParams params = (MarginLayoutParams) mPrintButton.getLayoutParams(); + final int rightMargin = params.rightMargin; + final int printButtonLeft = right - mPrintButton.getMeasuredWidth() - rightMargin; + final int printButtonTop = dynContentBottom - mPrintButton.getMeasuredHeight() / 2; + final int printButtonRight = printButtonLeft + mPrintButton.getMeasuredWidth(); + final int printButtonBottom = printButtonTop + mPrintButton.getMeasuredHeight(); + + mPrintButton.layout(printButtonLeft, printButtonTop, printButtonRight, printButtonBottom); + final int embContentTop = mStaticContent.getMeasuredHeight() + mClosedOptionsOffsetY + mDynamicContent.getMeasuredHeight(); final int embContentBottom = embContentTop + mEmbeddedContentContainer.getMeasuredHeight(); @@ -239,40 +280,51 @@ public final class ContentView extends ViewGroup implements View.OnClickListener mEmbeddedContentContainer.layout(left, embContentTop, right, embContentBottom); } + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return new ViewGroup.MarginLayoutParams(getContext(), attrs); + } + private void onDragProgress(float progress) { - final int summaryCount = mSummaryContent.getChildCount(); - for (int i = 0; i < summaryCount; i++) { - View child = mSummaryContent.getChildAt(i); - child.setAlpha(progress); + if (Float.compare(mDragProgress, progress) == 0) { + return; } + if ((mDragProgress == 0 && progress > 0) + || (mDragProgress == 1.0f && progress < 1.0f)) { + mSummaryContent.setLayerType(View.LAYER_TYPE_HARDWARE, null); + mDraggableContent.setLayerType(View.LAYER_TYPE_HARDWARE, null); + mMoreOptionsContainer.setLayerType(View.LAYER_TYPE_HARDWARE, null); + } + if ((mDragProgress > 0 && progress == 0) + || (mDragProgress < 1.0f && progress == 1.0f)) { + mSummaryContent.setLayerType(View.LAYER_TYPE_NONE, null); + mDraggableContent.setLayerType(View.LAYER_TYPE_NONE, null); + mMoreOptionsContainer.setLayerType(View.LAYER_TYPE_NONE, null); + } + + mDragProgress = progress; + + mSummaryContent.setAlpha(progress); + + final float inverseAlpha = 1.0f - progress; + mOptionsContainer.setAlpha(inverseAlpha); + mMoreOptionsContainer.setAlpha(inverseAlpha); + + mEmbeddedContentScrim.setBackgroundColor(computeScrimColor()); + if (progress == 0) { if (mOptionsStateChangeListener != null) { mOptionsStateChangeListener.onOptionsOpened(); } mSummaryContent.setVisibility(View.GONE); + mEmbeddedContentScrim.setOnClickListener(this); mExpandCollapseIcon.setBackgroundResource(R.drawable.ic_expand_less); } else { mSummaryContent.setVisibility(View.VISIBLE); } - final float inverseAlpha = 1.0f - progress; - - final int optionCount = mOptionsContainer.getChildCount(); - for (int i = 0; i < optionCount; i++) { - View child = mOptionsContainer.getChildAt(i); - child.setAlpha(inverseAlpha); - } - - if (mMoreOptionsContainer.getVisibility() != View.GONE) { - final int moreOptionCount = mMoreOptionsContainer.getChildCount(); - for (int i = 0; i < moreOptionCount; i++) { - View child = mMoreOptionsContainer.getChildAt(i); - child.setAlpha(inverseAlpha); - } - } - - if (inverseAlpha == 0) { + if (progress == 1.0f) { if (mOptionsStateChangeListener != null) { mOptionsStateChangeListener.onOptionsClosed(); } @@ -280,6 +332,10 @@ public final class ContentView extends ViewGroup implements View.OnClickListener mMoreOptionsContainer.setVisibility(View.INVISIBLE); } mDraggableContent.setVisibility(View.INVISIBLE); + // If we change the scrim visibility the dimming is lagging + // and is janky. Now it is there but transparent, doing nothing. + mEmbeddedContentScrim.setOnClickListener(null); + mEmbeddedContentScrim.setClickable(false); mExpandCollapseIcon.setBackgroundResource(R.drawable.ic_expand_more); } else { if (mMoreOptionsContainer.getVisibility() != View.GONE) { @@ -292,15 +348,25 @@ public final class ContentView extends ViewGroup implements View.OnClickListener private final class DragCallbacks extends ViewDragHelper.Callback { @Override public boolean tryCaptureView(View child, int pointerId) { + if (isOptionsOpened() && !mOptionsStateController.canCloseOptions() + || isOptionsClosed() && !mOptionsStateController.canOpenOptions()) { + return false; + } return child == mDynamicContent && pointerId == FIRST_POINTER_ID; } @Override public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { + if ((isOptionsClosed() || isOptionsClosed()) && dy <= 0) { + return; + } + mCurrentOptionsOffsetY += dy; final float progress = ((float) top - getOpenedOptionsY()) / (getClosedOptionsY() - getOpenedOptionsY()); + mPrintButton.offsetTopAndBottom(dy); + mDraggableContent.notifySubtreeAccessibilityStateChangedIfNeeded(); onDragProgress(progress); @@ -326,6 +392,10 @@ public final class ContentView extends ViewGroup implements View.OnClickListener invalidate(); } + public int getOrderedChildIndex(int index) { + return getChildCount() - index - 1; + } + public int getViewVerticalDragRange(View child) { return mDraggableContent.getHeight(); } diff --git a/packages/PrintSpooler/src/com/android/printspooler/widget/PrintOptionsLayout.java b/packages/PrintSpooler/src/com/android/printspooler/widget/PrintOptionsLayout.java index 23c8d08..01f4a04 100644 --- a/packages/PrintSpooler/src/com/android/printspooler/widget/PrintOptionsLayout.java +++ b/packages/PrintSpooler/src/com/android/printspooler/widget/PrintOptionsLayout.java @@ -31,7 +31,7 @@ import com.android.printspooler.R; @SuppressWarnings("unused") public final class PrintOptionsLayout extends ViewGroup { - private final int mColumnCount; + private int mColumnCount; public PrintOptionsLayout(Context context, AttributeSet attrs) { super(context, attrs); @@ -42,6 +42,13 @@ public final class PrintOptionsLayout extends ViewGroup { typedArray.recycle(); } + public void setColumnCount(int columnCount) { + if (mColumnCount != columnCount) { + mColumnCount = columnCount; + requestLayout(); + } + } + @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final int widthMode = MeasureSpec.getMode(widthMeasureSpec); |