blob: 59f272ff70f601ff392d02437bd49228ee2c9cdd [file] [log] [blame]
/*
* 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.annotation.NonNull;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.app.Fragment;
import android.app.FragmentTransaction;
import android.app.LoaderManager;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.Loader;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.pm.PackageManager;
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;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.UserManager;
import android.print.IPrintDocumentAdapter;
import android.print.PageRange;
import android.print.PrintAttributes;
import android.print.PrintAttributes.MediaSize;
import android.print.PrintAttributes.Resolution;
import android.print.PrintDocumentInfo;
import android.print.PrintJobInfo;
import android.print.PrintManager;
import android.print.PrintServicesLoader;
import android.print.PrinterCapabilitiesInfo;
import android.print.PrinterId;
import android.print.PrinterInfo;
import android.printservice.PrintService;
import android.printservice.PrintServiceInfo;
import android.provider.DocumentsContract;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import android.util.TypedValue;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnFocusChangeListener;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
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.renderer.IPdfEditor;
import com.android.printspooler.renderer.PdfManipulationService;
import com.android.printspooler.util.ApprovedPrintServices;
import com.android.printspooler.util.MediaSizeUtils;
import com.android.printspooler.util.MediaSizeUtils.MediaSizeComparator;
import com.android.printspooler.util.PageRangeUtils;
import com.android.printspooler.widget.ClickInterceptSpinner;
import com.android.printspooler.widget.PrintContentView;
import com.android.printspooler.widget.PrintContentView.OptionsStateChangeListener;
import com.android.printspooler.widget.PrintContentView.OptionsStateController;
import libcore.io.IoUtils;
import libcore.io.Streams;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
public class PrintActivity extends Activity implements RemotePrintDocument.UpdateResultCallbacks,
PrintErrorFragment.OnActionListener, PageAdapter.ContentCallbacks,
OptionsStateChangeListener, OptionsStateController,
LoaderManager.LoaderCallbacks<List<PrintServiceInfo>> {
private static final String LOG_TAG = "PrintActivity";
private static final boolean DEBUG = false;
// Constants for MetricsLogger.count and MetricsLogger.histo
private static final String PRINT_PAGES_HISTO = "print_pages";
private static final String PRINT_DEFAULT_COUNT = "print_default";
private static final String PRINT_WORK_COUNT = "print_work";
private static final String FRAGMENT_TAG = "FRAGMENT_TAG";
private static final String MORE_OPTIONS_ACTIVITY_IN_PROGRESS_KEY =
PrintActivity.class.getName() + ".MORE_OPTIONS_ACTIVITY_IN_PROGRESS";
private static final String HAS_PRINTED_PREF = "has_printed";
private static final int LOADER_ID_ENABLED_PRINT_SERVICES = 1;
private static final int LOADER_ID_PRINT_REGISTRY = 2;
private static final int LOADER_ID_PRINT_REGISTRY_INT = 3;
private static final int ORIENTATION_PORTRAIT = 0;
private static final int ORIENTATION_LANDSCAPE = 1;
private static final int ACTIVITY_REQUEST_CREATE_FILE = 1;
private static final int ACTIVITY_REQUEST_SELECT_PRINTER = 2;
private static final int ACTIVITY_REQUEST_POPULATE_ADVANCED_PRINT_OPTIONS = 3;
private static final int DEST_ADAPTER_MAX_ITEM_COUNT = 9;
private static final int DEST_ADAPTER_ITEM_ID_SAVE_AS_PDF = Integer.MAX_VALUE;
private static final int DEST_ADAPTER_ITEM_ID_MORE = Integer.MAX_VALUE - 1;
private static final int STATE_INITIALIZING = 0;
private static final int STATE_CONFIGURING = 1;
private static final int STATE_PRINT_CONFIRMED = 2;
private static final int STATE_PRINT_CANCELED = 3;
private static final int STATE_UPDATE_FAILED = 4;
private static final int STATE_CREATE_FILE_FAILED = 5;
private static final int STATE_PRINTER_UNAVAILABLE = 6;
private static final int STATE_UPDATE_SLOW = 7;
private static final int STATE_PRINT_COMPLETED = 8;
private static final int UI_STATE_PREVIEW = 0;
private static final int UI_STATE_ERROR = 1;
private static final int UI_STATE_PROGRESS = 2;
// see frameworks/base/proto/src/metrics_constats.proto -> ACTION_PRINT_JOB_OPTIONS
private static final int PRINT_JOB_OPTIONS_SUBTYPE_COPIES = 1;
private static final int PRINT_JOB_OPTIONS_SUBTYPE_COLOR_MODE = 2;
private static final int PRINT_JOB_OPTIONS_SUBTYPE_DUPLEX_MODE = 3;
private static final int PRINT_JOB_OPTIONS_SUBTYPE_MEDIA_SIZE = 4;
private static final int PRINT_JOB_OPTIONS_SUBTYPE_ORIENTATION = 5;
private static final int PRINT_JOB_OPTIONS_SUBTYPE_PAGE_RANGE = 6;
private static final int MIN_COPIES = 1;
private static final String MIN_COPIES_STRING = String.valueOf(MIN_COPIES);
private boolean mIsOptionsUiBound = false;
private final PrinterAvailabilityDetector mPrinterAvailabilityDetector =
new PrinterAvailabilityDetector();
private final OnFocusChangeListener mSelectAllOnFocusListener = new SelectAllOnFocusListener();
private PrintSpoolerProvider mSpoolerProvider;
private PrintPreviewController mPrintPreviewController;
private PrintJobInfo mPrintJob;
private RemotePrintDocument mPrintedDocument;
private PrinterRegistry mPrinterRegistry;
private EditText mCopiesEditText;
private TextView mPageRangeTitle;
private EditText mPageRangeEditText;
private ClickInterceptSpinner mDestinationSpinner;
private DestinationAdapter mDestinationSpinnerAdapter;
private boolean mShowDestinationPrompt;
private Spinner mMediaSizeSpinner;
private ArrayAdapter<SpinnerItem<MediaSize>> mMediaSizeSpinnerAdapter;
private Spinner mColorModeSpinner;
private ArrayAdapter<SpinnerItem<Integer>> mColorModeSpinnerAdapter;
private Spinner mDuplexModeSpinner;
private ArrayAdapter<SpinnerItem<Integer>> mDuplexModeSpinnerAdapter;
private Spinner mOrientationSpinner;
private ArrayAdapter<SpinnerItem<Integer>> mOrientationSpinnerAdapter;
private Spinner mRangeOptionsSpinner;
private PrintContentView mOptionsContent;
private View mSummaryContainer;
private TextView mSummaryCopies;
private TextView mSummaryPaperSize;
private Button mMoreOptionsButton;
/**
* The {@link #mMoreOptionsButton} was pressed and we started the
* @link #mAdvancedPrintOptionsActivity} and it has not yet {@link #onActivityResult returned}.
*/
private boolean mIsMoreOptionsActivityInProgress;
private ImageView mPrintButton;
private ProgressMessageController mProgressMessageController;
private MutexFileProvider mFileProvider;
private MediaSizeComparator mMediaSizeComparator;
private PrinterInfo mCurrentPrinter;
private PageRange[] mSelectedPages;
private String mCallingPackageName;
private int mCurrentPageCount;
private int mState = STATE_INITIALIZING;
private int mUiState = UI_STATE_PREVIEW;
/** The ID of the printer initially set */
private PrinterId mDefaultPrinter;
/** Observer for changes to the printers */
private PrintersObserver mPrintersObserver;
/** Advances options activity name for current printer */
private ComponentName mAdvancedPrintOptionsActivity;
/** Whether at least one print services is enabled or not */
private boolean mArePrintServicesEnabled;
/** Is doFinish() already in progress */
private boolean mIsFinishing;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setTitle(R.string.print_dialog);
Bundle extras = getIntent().getExtras();
if (savedInstanceState != null) {
mIsMoreOptionsActivityInProgress =
savedInstanceState.getBoolean(MORE_OPTIONS_ACTIVITY_IN_PROGRESS_KEY);
}
mPrintJob = extras.getParcelable(PrintManager.EXTRA_PRINT_JOB);
if (mPrintJob == null) {
throw new IllegalArgumentException(PrintManager.EXTRA_PRINT_JOB
+ " cannot be null");
}
if (mPrintJob.getAttributes() == null) {
mPrintJob.setAttributes(new PrintAttributes.Builder().build());
}
final IBinder adapter = extras.getBinder(PrintManager.EXTRA_PRINT_DOCUMENT_ADAPTER);
if (adapter == null) {
throw new IllegalArgumentException(PrintManager.EXTRA_PRINT_DOCUMENT_ADAPTER
+ " cannot be null");
}
mCallingPackageName = extras.getString(Intent.EXTRA_PACKAGE_NAME);
if (savedInstanceState == null) {
MetricsLogger.action(this, MetricsEvent.PRINT_PREVIEW, mCallingPackageName);
}
// This will take just a few milliseconds, so just wait to
// bind to the local service before showing the UI.
mSpoolerProvider = new PrintSpoolerProvider(this,
() -> {
if (isFinishing() || isDestroyed()) {
if (savedInstanceState != null) {
// onPause might have not been able to cancel the job, see
// PrintActivity#onPause
// To be sure, cancel the job again. Double canceling does no harm.
mSpoolerProvider.getSpooler().setPrintJobState(mPrintJob.getId(),
PrintJobInfo.STATE_CANCELED, null);
}
} else {
if (savedInstanceState == null) {
mSpoolerProvider.getSpooler().createPrintJob(mPrintJob);
}
onConnectedToPrintSpooler(adapter);
}
});
getLoaderManager().initLoader(LOADER_ID_ENABLED_PRINT_SERVICES, null, this);
}
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 Handler(getMainLooper())).post(() -> onPrinterRegistryReady(documentAdapter));
}, LOADER_ID_PRINT_REGISTRY, LOADER_ID_PRINT_REGISTRY_INT);
}
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.
setContentView(R.layout.print_activity);
try {
mFileProvider = 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,
mFileProvider);
mPrintedDocument = new RemotePrintDocument(PrintActivity.this,
IPrintDocumentAdapter.Stub.asInterface(documentAdapter),
mFileProvider, new RemotePrintDocument.RemoteAdapterDeathObserver() {
@Override
public void onDied() {
Log.w(LOG_TAG, "Printing app died unexpectedly");
// If we are finishing or we are in a state that we do not need any
// data from the printing app, then no need to finish.
if (isFinishing() || isDestroyed() ||
(isFinalState(mState) && !mPrintedDocument.isUpdating())) {
return;
}
setState(STATE_PRINT_CANCELED);
mPrintedDocument.cancel(true);
doFinish();
}
}, PrintActivity.this);
mProgressMessageController = new ProgressMessageController(
PrintActivity.this);
mMediaSizeComparator = new MediaSizeComparator(PrintActivity.this);
mDestinationSpinnerAdapter = new DestinationAdapter();
bindUi();
updateOptionsUi();
// Now show the updated UI to avoid flicker.
mOptionsContent.setVisibility(View.VISIBLE);
mSelectedPages = computeSelectedPages();
mPrintedDocument.start();
ensurePreviewUiShown();
setState(STATE_CONFIGURING);
}
@Override
public void onStart() {
super.onStart();
if (mPrinterRegistry != null && mCurrentPrinter != null) {
mPrinterRegistry.setTrackedPrinter(mCurrentPrinter.getId());
}
}
@Override
public void onPause() {
PrintSpoolerService spooler = mSpoolerProvider.getSpooler();
if (mState == STATE_INITIALIZING) {
if (isFinishing()) {
if (spooler != null) {
spooler.setPrintJobState(mPrintJob.getId(), PrintJobInfo.STATE_CANCELED, null);
}
}
super.onPause();
return;
}
if (isFinishing()) {
spooler.updatePrintJobUserConfigurableOptionsNoPersistence(mPrintJob);
switch (mState) {
case STATE_PRINT_COMPLETED: {
if (mCurrentPrinter == mDestinationSpinnerAdapter.getPdfPrinter()) {
spooler.setPrintJobState(mPrintJob.getId(), PrintJobInfo.STATE_COMPLETED,
null);
} else {
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;
}
}
super.onPause();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(MORE_OPTIONS_ACTIVITY_IN_PROGRESS_KEY,
mIsMoreOptionsActivityInProgress);
}
@Override
protected void onStop() {
mPrinterAvailabilityDetector.cancel();
if (mPrinterRegistry != null) {
mPrinterRegistry.setTrackedPrinter(null);
}
super.onStop();
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
event.startTracking();
return true;
}
return super.onKeyDown(keyCode, event);
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (mState == STATE_INITIALIZING) {
doFinish();
return true;
}
if (mState == STATE_PRINT_CANCELED || mState == STATE_PRINT_CONFIRMED
|| mState == STATE_PRINT_COMPLETED) {
return true;
}
if (keyCode == KeyEvent.KEYCODE_BACK
&& event.isTracking() && !event.isCanceled()) {
if (mPrintPreviewController != null && mPrintPreviewController.isOptionsOpened()
&& !hasErrors()) {
mPrintPreviewController.closeOptions();
} else {
cancelPrint();
}
return true;
}
return super.onKeyUp(keyCode, event);
}
@Override
public void onRequestContentUpdate() {
if (canUpdateDocument()) {
updateDocument(false);
}
}
@Override
public void onMalformedPdfFile() {
onPrintDocumentError("Cannot print a malformed PDF file");
}
@Override
public void onSecurePdfFile() {
onPrintDocumentError("Cannot print a password protected PDF file");
}
private void onPrintDocumentError(String message) {
setState(mProgressMessageController.cancel());
ensureErrorUiShown(null, PrintErrorFragment.ACTION_RETRY);
setState(STATE_UPDATE_FAILED);
mPrintedDocument.kill(message);
}
@Override
public void onActionPerformed() {
if (mState == STATE_UPDATE_FAILED
&& canUpdateDocument() && updateDocument(true)) {
ensurePreviewUiShown();
setState(STATE_CONFIGURING);
}
}
@Override
public void onUpdateCanceled() {
if (DEBUG) {
Log.i(LOG_TAG, "onUpdateCanceled()");
}
setState(mProgressMessageController.cancel());
ensurePreviewUiShown();
switch (mState) {
case STATE_PRINT_CONFIRMED: {
requestCreatePdfFileOrFinish();
} break;
case STATE_CREATE_FILE_FAILED:
case STATE_PRINT_COMPLETED:
case STATE_PRINT_CANCELED: {
doFinish();
} break;
}
}
@Override
public void onUpdateCompleted(RemotePrintDocumentInfo document) {
if (DEBUG) {
Log.i(LOG_TAG, "onUpdateCompleted()");
}
setState(mProgressMessageController.cancel());
ensurePreviewUiShown();
// Update the print job with the info for the written document. The page
// count we get from the remote document is the pages in the document from
// the app perspective but the print job should contain the page count from
// 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) {
final int pageCount = PageRangeUtils.getNormalizedPageCount(
document.pagesWrittenToFile, getAdjustedPageCount(info));
PrintDocumentInfo adjustedInfo = new PrintDocumentInfo.Builder(info.getName())
.setContentType(info.getContentType())
.setPageCount(pageCount)
.build();
File file = mFileProvider.acquireFile(null);
try {
adjustedInfo.setDataSize(file.length());
} finally {
mFileProvider.releaseFile();
}
mPrintJob.setDocumentInfo(adjustedInfo);
mPrintJob.setPages(document.pagesInFileToPrint);
}
switch (mState) {
case STATE_PRINT_CONFIRMED: {
requestCreatePdfFileOrFinish();
} break;
case STATE_CREATE_FILE_FAILED:
case STATE_PRINT_COMPLETED:
case STATE_PRINT_CANCELED: {
updateOptionsUi();
doFinish();
} break;
default: {
updatePrintPreviewController(document.changed);
setState(STATE_CONFIGURING);
} break;
}
}
@Override
public void onUpdateFailed(CharSequence error) {
if (DEBUG) {
Log.i(LOG_TAG, "onUpdateFailed()");
}
setState(mProgressMessageController.cancel());
ensureErrorUiShown(error, PrintErrorFragment.ACTION_RETRY);
if (mState == STATE_CREATE_FILE_FAILED
|| mState == STATE_PRINT_COMPLETED
|| mState == STATE_PRINT_CANCELED) {
doFinish();
}
setState(STATE_UPDATE_FAILED);
}
@Override
public void onOptionsOpened() {
MetricsLogger.action(this, MetricsEvent.PRINT_JOB_OPTIONS);
updateSelectedPagesFromPreview();
}
@Override
public void onOptionsClosed() {
// 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 = getSystemService(InputMethodManager.class);
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().pagesWrittenToFile,
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);
if (mMediaSizeComparator != null) {
mMediaSizeComparator.onConfigurationChanged(newConfig);
}
if (mPrintPreviewController != null) {
mPrintPreviewController.onOrientationChanged();
}
}
@Override
protected void onDestroy() {
if (mPrintedDocument != null) {
mPrintedDocument.cancel(true);
}
doFinish();
super.onDestroy();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case ACTIVITY_REQUEST_CREATE_FILE: {
onStartCreateDocumentActivityResult(resultCode, data);
} break;
case ACTIVITY_REQUEST_SELECT_PRINTER: {
onSelectPrinterActivityResult(resultCode, data);
} break;
case ACTIVITY_REQUEST_POPULATE_ADVANCED_PRINT_OPTIONS: {
onAdvancedPrintOptionsActivityResult(resultCode, data);
} break;
}
}
private void startCreateDocumentActivity() {
if (!isResumed()) {
return;
}
PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info;
if (info == null) {
return;
}
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.setType("application/pdf");
intent.putExtra(Intent.EXTRA_TITLE, info.getName());
intent.putExtra(Intent.EXTRA_PACKAGE_NAME, mCallingPackageName);
try {
startActivityForResult(intent, ACTIVITY_REQUEST_CREATE_FILE);
} catch (Exception e) {
Log.e(LOG_TAG, "Could not create file", e);
Toast.makeText(this, getString(R.string.could_not_create_file),
Toast.LENGTH_SHORT).show();
onStartCreateDocumentActivityResult(RESULT_CANCELED, null);
}
}
private void onStartCreateDocumentActivityResult(int resultCode, Intent data) {
if (resultCode == RESULT_OK && data != null) {
updateOptionsUi();
final Uri uri = data.getData();
countPrintOperation(getPackageName());
// 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() {
transformDocumentAndFinish(uri);
}
});
} else if (resultCode == RESULT_CANCELED) {
if (DEBUG) {
Log.i(LOG_TAG, "[state]" + STATE_CONFIGURING);
}
mState = STATE_CONFIGURING;
// The previous update might have been canceled
updateDocument(false);
updateOptionsUi();
} else {
setState(STATE_CREATE_FILE_FAILED);
// 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() {
doFinish();
}
});
}
}
private void startSelectPrinterActivity() {
Intent intent = new Intent(this, SelectPrinterActivity.class);
startActivityForResult(intent, ACTIVITY_REQUEST_SELECT_PRINTER);
}
private void onSelectPrinterActivityResult(int resultCode, Intent data) {
if (resultCode == RESULT_OK && data != null) {
PrinterInfo printerInfo = data.getParcelableExtra(
SelectPrinterActivity.INTENT_EXTRA_PRINTER);
if (printerInfo != null) {
mCurrentPrinter = printerInfo;
mPrintJob.setPrinterId(printerInfo.getId());
mPrintJob.setPrinterName(printerInfo.getName());
if (canPrint(printerInfo)) {
updatePrintAttributesFromCapabilities(printerInfo.getCapabilities());
onPrinterAvailable(printerInfo);
} else {
onPrinterUnavailable(printerInfo);
}
mDestinationSpinnerAdapter.ensurePrinterInVisibleAdapterPosition(printerInfo);
MetricsLogger.action(this, MetricsEvent.ACTION_PRINTER_SELECT_ALL,
printerInfo.getId().getServiceName().getPackageName());
}
}
if (mCurrentPrinter != null) {
// Trigger PrintersObserver.onChanged() to adjust selection back to current printer
mDestinationSpinnerAdapter.notifyDataSetChanged();
}
}
private void startAdvancedPrintOptionsActivity(PrinterInfo printer) {
if (mAdvancedPrintOptionsActivity == null) {
return;
}
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.setComponent(mAdvancedPrintOptionsActivity);
List<ResolveInfo> resolvedActivities = getPackageManager()
.queryIntentActivities(intent, 0);
if (resolvedActivities.isEmpty()) {
Log.w(LOG_TAG, "Advanced options activity " + mAdvancedPrintOptionsActivity + " could "
+ "not be found");
return;
}
// The activity is a component name, therefore it is one or none.
if (resolvedActivities.get(0).activityInfo.exported) {
PrintJobInfo.Builder printJobBuilder = new PrintJobInfo.Builder(mPrintJob);
printJobBuilder.setPages(mSelectedPages);
intent.putExtra(PrintService.EXTRA_PRINT_JOB_INFO, printJobBuilder.build());
intent.putExtra(PrintService.EXTRA_PRINTER_INFO, printer);
intent.putExtra(PrintService.EXTRA_PRINT_DOCUMENT_INFO,
mPrintedDocument.getDocumentInfo().info);
mIsMoreOptionsActivityInProgress = true;
// This is external activity and may not be there.
try {
startActivityForResult(intent, ACTIVITY_REQUEST_POPULATE_ADVANCED_PRINT_OPTIONS);
} catch (ActivityNotFoundException anfe) {
mIsMoreOptionsActivityInProgress = false;
Log.e(LOG_TAG, "Error starting activity for intent: " + intent, anfe);
}
mMoreOptionsButton.setEnabled(!mIsMoreOptionsActivityInProgress);
}
}
private void onAdvancedPrintOptionsActivityResult(int resultCode, Intent data) {
mIsMoreOptionsActivityInProgress = false;
mMoreOptionsButton.setEnabled(true);
if (resultCode != RESULT_OK || data == null) {
return;
}
PrintJobInfo printJobInfo = data.getParcelableExtra(PrintService.EXTRA_PRINT_JOB_INFO);
if (printJobInfo == null) {
return;
}
// Take the advanced options without interpretation.
mPrintJob.setAdvancedOptions(printJobInfo.getAdvancedOptions());
if (printJobInfo.getCopies() < 1) {
Log.w(LOG_TAG, "Cannot apply return value from advanced options activity. Copies " +
"must be 1 or more. Actual value is: " + printJobInfo.getCopies() + ". " +
"Ignoring.");
} else {
mCopiesEditText.setText(String.valueOf(printJobInfo.getCopies()));
mPrintJob.setCopies(printJobInfo.getCopies());
}
PrintAttributes currAttributes = mPrintJob.getAttributes();
PrintAttributes newAttributes = printJobInfo.getAttributes();
if (newAttributes != null) {
// Take the media size only if the current printer supports is.
MediaSize oldMediaSize = currAttributes.getMediaSize();
MediaSize newMediaSize = newAttributes.getMediaSize();
if (newMediaSize != null && !oldMediaSize.equals(newMediaSize)) {
final int mediaSizeCount = mMediaSizeSpinnerAdapter.getCount();
MediaSize newMediaSizePortrait = newAttributes.getMediaSize().asPortrait();
for (int i = 0; i < mediaSizeCount; i++) {
MediaSize supportedSizePortrait = mMediaSizeSpinnerAdapter.getItem(i)
.value.asPortrait();
if (supportedSizePortrait.equals(newMediaSizePortrait)) {
currAttributes.setMediaSize(newMediaSize);
mMediaSizeSpinner.setSelection(i);
if (currAttributes.getMediaSize().isPortrait()) {
if (mOrientationSpinner.getSelectedItemPosition() != 0) {
mOrientationSpinner.setSelection(0);
}
} else {
if (mOrientationSpinner.getSelectedItemPosition() != 1) {
mOrientationSpinner.setSelection(1);
}
}
break;
}
}
}
// Take the resolution only if the current printer supports is.
Resolution oldResolution = currAttributes.getResolution();
Resolution newResolution = newAttributes.getResolution();
if (!oldResolution.equals(newResolution)) {
PrinterCapabilitiesInfo capabilities = mCurrentPrinter.getCapabilities();
if (capabilities != null) {
List<Resolution> resolutions = capabilities.getResolutions();
final int resolutionCount = resolutions.size();
for (int i = 0; i < resolutionCount; i++) {
Resolution resolution = resolutions.get(i);
if (resolution.equals(newResolution)) {
currAttributes.setResolution(resolution);
break;
}
}
}
}
// Take the color mode only if the current printer supports it.
final int currColorMode = currAttributes.getColorMode();
final int newColorMode = newAttributes.getColorMode();
if (currColorMode != newColorMode) {
final int colorModeCount = mColorModeSpinner.getCount();
for (int i = 0; i < colorModeCount; i++) {
final int supportedColorMode = mColorModeSpinnerAdapter.getItem(i).value;
if (supportedColorMode == newColorMode) {
currAttributes.setColorMode(newColorMode);
mColorModeSpinner.setSelection(i);
break;
}
}
}
// Take the duplex mode only if the current printer supports it.
final int currDuplexMode = currAttributes.getDuplexMode();
final int newDuplexMode = newAttributes.getDuplexMode();
if (currDuplexMode != newDuplexMode) {
final int duplexModeCount = mDuplexModeSpinner.getCount();
for (int i = 0; i < duplexModeCount; i++) {
final int supportedDuplexMode = mDuplexModeSpinnerAdapter.getItem(i).value;
if (supportedDuplexMode == newDuplexMode) {
currAttributes.setDuplexMode(newDuplexMode);
mDuplexModeSpinner.setSelection(i);
break;
}
}
}
}
// Handle selected page changes making sure they are in the doc.
PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info;
final int pageCount = (info != null) ? getAdjustedPageCount(info) : 0;
PageRange[] pageRanges = printJobInfo.getPages();
if (pageRanges != null && pageCount > 0) {
pageRanges = PageRangeUtils.normalize(pageRanges);
List<PageRange> validatedList = new ArrayList<>();
final int rangeCount = pageRanges.length;
for (int i = 0; i < rangeCount; i++) {
PageRange pageRange = pageRanges[i];
if (pageRange.getEnd() >= pageCount) {
final int rangeStart = pageRange.getStart();
final int rangeEnd = pageCount - 1;
if (rangeStart <= rangeEnd) {
pageRange = new PageRange(rangeStart, rangeEnd);
validatedList.add(pageRange);
}
break;
}
validatedList.add(pageRange);
}
if (!validatedList.isEmpty()) {
PageRange[] validatedArray = new PageRange[validatedList.size()];
validatedList.toArray(validatedArray);
updateSelectedPages(validatedArray, pageCount);
}
}
// Update the content if needed.
if (canUpdateDocument()) {
updateDocument(false);
}
}
private void setState(int state) {
if (isFinalState(mState)) {
if (isFinalState(state)) {
if (DEBUG) {
Log.i(LOG_TAG, "[state]" + state);
}
mState = state;
updateOptionsUi();
}
} else {
if (DEBUG) {
Log.i(LOG_TAG, "[state]" + state);
}
mState = state;
updateOptionsUi();
}
}
private static boolean isFinalState(int state) {
return state == STATE_PRINT_CANCELED
|| state == STATE_PRINT_COMPLETED
|| state == STATE_CREATE_FILE_FAILED;
}
private void updateSelectedPagesFromPreview() {
PageRange[] selectedPages = mPrintPreviewController.getSelectedPages();
if (!Arrays.equals(mSelectedPages, selectedPages)) {
updateSelectedPages(selectedPages,
getAdjustedPageCount(mPrintedDocument.getDocumentInfo().info));
}
}
private void updateSelectedPages(PageRange[] selectedPages, int pageInDocumentCount) {
if (selectedPages == null || selectedPages.length <= 0) {
return;
}
selectedPages = PageRangeUtils.normalize(selectedPages);
// 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};
}
if (Arrays.equals(mSelectedPages, selectedPages)) {
return;
}
mSelectedPages = selectedPages;
mPrintJob.setPages(selectedPages);
if (Arrays.equals(selectedPages, PageRange.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());
}
}
private void ensureProgressUiShown() {
if (isFinishing() || isDestroyed()) {
return;
}
if (mUiState != UI_STATE_PROGRESS) {
mUiState = UI_STATE_PROGRESS;
mPrintPreviewController.setUiShown(false);
Fragment fragment = PrintProgressFragment.newInstance();
showFragment(fragment);
}
}
private void ensurePreviewUiShown() {
if (isFinishing() || isDestroyed()) {
return;
}
if (mUiState != UI_STATE_PREVIEW) {
mUiState = UI_STATE_PREVIEW;
mPrintPreviewController.setUiShown(true);
showFragment(null);
}
}
private void ensureErrorUiShown(CharSequence message, int action) {
if (isFinishing() || isDestroyed()) {
return;
}
if (mUiState != UI_STATE_ERROR) {
mUiState = UI_STATE_ERROR;
mPrintPreviewController.setUiShown(false);
Fragment fragment = PrintErrorFragment.newInstance(message, action);
showFragment(fragment);
}
}
private void showFragment(Fragment newFragment) {
FragmentTransaction transaction = getFragmentManager().beginTransaction();
Fragment oldFragment = getFragmentManager().findFragmentByTag(FRAGMENT_TAG);
if (oldFragment != null) {
transaction.remove(oldFragment);
}
if (newFragment != null) {
transaction.add(R.id.embedded_content_container, newFragment, FRAGMENT_TAG);
}
transaction.commitAllowingStateLoss();
getFragmentManager().executePendingTransactions();
}
/**
* Count that a print operation has been confirmed.
*
* @param packageName The package name of the print service used
*/
private void countPrintOperation(@NonNull String packageName) {
MetricsLogger.action(this, MetricsEvent.ACTION_PRINT, packageName);
MetricsLogger.histogram(this, PRINT_PAGES_HISTO,
getAdjustedPageCount(mPrintJob.getDocumentInfo()));
if (mPrintJob.getPrinterId().equals(mDefaultPrinter)) {
MetricsLogger.histogram(this, PRINT_DEFAULT_COUNT, 1);
}
UserManager um = (UserManager) getSystemService(Context.USER_SERVICE);
if (um.isManagedProfile()) {
MetricsLogger.histogram(this, PRINT_WORK_COUNT, 1);
}
}
private void requestCreatePdfFileOrFinish() {
mPrintedDocument.cancel(false);
if (mCurrentPrinter == mDestinationSpinnerAdapter.getPdfPrinter()) {
startCreateDocumentActivity();
} else {
countPrintOperation(mCurrentPrinter.getId().getServiceName().getPackageName());
transformDocumentAndFinish(null);
}
}
/**
* Clear the selected page range and update the preview if needed.
*/
private void clearPageRanges() {
mRangeOptionsSpinner.setSelection(0);
mPageRangeEditText.setError(null);
mPageRangeEditText.setText("");
mSelectedPages = PageRange.ALL_PAGES_ARRAY;
if (!Arrays.equals(mSelectedPages, mPrintPreviewController.getSelectedPages())) {
updatePrintPreviewController(false);
}
}
private void updatePrintAttributesFromCapabilities(PrinterCapabilitiesInfo capabilities) {
boolean clearRanges = false;
PrintAttributes defaults = capabilities.getDefaults();
// Sort the media sizes based on the current locale.
List<MediaSize> sortedMediaSizes = new ArrayList<>(capabilities.getMediaSizes());
Collections.sort(sortedMediaSizes, mMediaSizeComparator);
PrintAttributes attributes = mPrintJob.getAttributes();
// Media size.
MediaSize currMediaSize = attributes.getMediaSize();
if (currMediaSize == null) {
clearRanges = true;
attributes.setMediaSize(defaults.getMediaSize());
} else {
MediaSize newMediaSize = null;
boolean isPortrait = currMediaSize.isPortrait();
// Try to find the current media size in the capabilities as
// it may be in a different orientation.
MediaSize currMediaSizePortrait = currMediaSize.asPortrait();
final int mediaSizeCount = sortedMediaSizes.size();
for (int i = 0; i < mediaSizeCount; i++) {
MediaSize mediaSize = sortedMediaSizes.get(i);
if (currMediaSizePortrait.equals(mediaSize.asPortrait())) {
newMediaSize = mediaSize;
break;
}
}
// If we did not find the current media size fall back to default.
if (newMediaSize == null) {
clearRanges = true;
newMediaSize = defaults.getMediaSize();
}
if (newMediaSize != null) {
if (isPortrait) {
attributes.setMediaSize(newMediaSize.asPortrait());
} else {
attributes.setMediaSize(newMediaSize.asLandscape());
}
}
}
// Color mode.
final int colorMode = attributes.getColorMode();
if ((capabilities.getColorModes() & colorMode) == 0) {
attributes.setColorMode(defaults.getColorMode());
}
// Duplex mode.
final int duplexMode = attributes.getDuplexMode();
if ((capabilities.getDuplexModes() & duplexMode) == 0) {
attributes.setDuplexMode(defaults.getDuplexMode());
}
// Resolution
Resolution resolution = attributes.getResolution();
if (resolution == null || !capabilities.getResolutions().contains(resolution)) {
attributes.setResolution(defaults.getResolution());
}
// Margins.
if (!Objects.equals(attributes.getMinMargins(), defaults.getMinMargins())) {
clearRanges = true;
}
attributes.setMinMargins(defaults.getMinMargins());
if (clearRanges) {
clearPageRanges();
}
}
private boolean updateDocument(boolean clearLastError) {
if (!clearLastError && mPrintedDocument.hasUpdateError()) {
return false;
}
if (clearLastError && mPrintedDocument.hasUpdateError()) {
mPrintedDocument.clearUpdateError();
}
final boolean preview = mState != STATE_PRINT_CONFIRMED;
final PageRange[] pages;
if (preview) {
pages = mPrintPreviewController.getRequestedPages();
} else {
pages = mPrintPreviewController.getSelectedPages();
}
final boolean willUpdate = mPrintedDocument.update(mPrintJob.getAttributes(),
pages, preview);
updateOptionsUi();
if (willUpdate && !mPrintedDocument.hasLaidOutPages()) {
// When the update is done we update the print preview.
mProgressMessageController.post();
return true;
} else if (!willUpdate) {
// Update preview.
updatePrintPreviewController(false);
}
return false;
}
private void addCurrentPrinterToHistory() {
if (mCurrentPrinter != null) {
PrinterId fakePdfPrinterId = mDestinationSpinnerAdapter.getPdfPrinter().getId();
if (!mCurrentPrinter.getId().equals(fakePdfPrinterId)) {
mPrinterRegistry.addHistoricalPrinter(mCurrentPrinter);
}
}
}
private void cancelPrint() {
setState(STATE_PRINT_CANCELED);
mPrintedDocument.cancel(true);
doFinish();
}
/**
* Update the selected pages from the text field.
*/
private void updateSelectedPagesFromTextField() {
PageRange[] selectedPages = computeSelectedPages();
if (!Arrays.equals(mSelectedPages, selectedPages)) {
mSelectedPages = selectedPages;
// Update preview.
updatePrintPreviewController(false);
}
}
private void confirmPrint() {
setState(STATE_PRINT_CONFIRMED);
addCurrentPrinterToHistory();
setUserPrinted();
// updateSelectedPagesFromTextField migth update the preview, hence apply the preview first
updateSelectedPagesFromPreview();
updateSelectedPagesFromTextField();
mPrintPreviewController.closeOptions();
if (canUpdateDocument()) {
updateDocument(false);
}
if (!mPrintedDocument.isUpdating()) {
requestCreatePdfFileOrFinish();
}
}
private void bindUi() {
// Summary
mSummaryContainer = findViewById(R.id.summary_content);
mSummaryCopies = findViewById(R.id.copies_count_summary);
mSummaryPaperSize = findViewById(R.id.paper_size_summary);
// Options container
mOptionsContent = findViewById(R.id.options_content);
mOptionsContent.setOptionsStateChangeListener(this);
mOptionsContent.setOpenOptionsController(this);
OnItemSelectedListener itemSelectedListener = new MyOnItemSelectedListener();
OnClickListener clickListener = new MyClickListener();
// Copies
mCopiesEditText = findViewById(R.id.copies_edittext);
mCopiesEditText.setOnFocusChangeListener(mSelectAllOnFocusListener);
mCopiesEditText.setText(MIN_COPIES_STRING);
mCopiesEditText.setSelection(mCopiesEditText.getText().length());
mCopiesEditText.addTextChangedListener(new EditTextWatcher());
// Destination.
mPrintersObserver = new PrintersObserver();
mDestinationSpinnerAdapter.registerDataSetObserver(mPrintersObserver);
mDestinationSpinner = findViewById(R.id.destination_spinner);
mDestinationSpinner.setAdapter(mDestinationSpinnerAdapter);
mDestinationSpinner.setOnItemSelectedListener(itemSelectedListener);
// Media size.
mMediaSizeSpinnerAdapter = new ArrayAdapter<>(
this, android.R.layout.simple_spinner_dropdown_item, android.R.id.text1);
mMediaSizeSpinner = findViewById(R.id.paper_size_spinner);
mMediaSizeSpinner.setAdapter(mMediaSizeSpinnerAdapter);
mMediaSizeSpinner.setOnItemSelectedListener(itemSelectedListener);
// Color mode.
mColorModeSpinnerAdapter = new ArrayAdapter<>(
this, android.R.layout.simple_spinner_dropdown_item, android.R.id.text1);
mColorModeSpinner = findViewById(R.id.color_spinner);
mColorModeSpinner.setAdapter(mColorModeSpinnerAdapter);
mColorModeSpinner.setOnItemSelectedListener(itemSelectedListener);
// Duplex mode.
mDuplexModeSpinnerAdapter = new ArrayAdapter<>(
this, android.R.layout.simple_spinner_dropdown_item, android.R.id.text1);
mDuplexModeSpinner = findViewById(R.id.duplex_spinner);
mDuplexModeSpinner.setAdapter(mDuplexModeSpinnerAdapter);
mDuplexModeSpinner.setOnItemSelectedListener(itemSelectedListener);
// Orientation
mOrientationSpinnerAdapter = new ArrayAdapter<>(
this, android.R.layout.simple_spinner_dropdown_item, android.R.id.text1);
String[] orientationLabels = getResources().getStringArray(
R.array.orientation_labels);
mOrientationSpinnerAdapter.add(new SpinnerItem<>(
ORIENTATION_PORTRAIT, orientationLabels[0]));
mOrientationSpinnerAdapter.add(new SpinnerItem<>(
ORIENTATION_LANDSCAPE, orientationLabels[1]));
mOrientationSpinner = findViewById(R.id.orientation_spinner);
mOrientationSpinner.setAdapter(mOrientationSpinnerAdapter);
mOrientationSpinner.setOnItemSelectedListener(itemSelectedListener);
// Range options
ArrayAdapter<SpinnerItem<Integer>> rangeOptionsSpinnerAdapter = new ArrayAdapter<>(
this, android.R.layout.simple_spinner_dropdown_item, android.R.id.text1);
mRangeOptionsSpinner = findViewById(R.id.range_options_spinner);
mRangeOptionsSpinner.setAdapter(rangeOptionsSpinnerAdapter);
mRangeOptionsSpinner.setOnItemSelectedListener(itemSelectedListener);
updatePageRangeOptions(PrintDocumentInfo.PAGE_COUNT_UNKNOWN);
// Page range
mPageRangeTitle = findViewById(R.id.page_range_title);
mPageRangeEditText = findViewById(R.id.page_range_edittext);
mPageRangeEditText.setVisibility(View.GONE);
mPageRangeTitle.setVisibility(View.GONE);
mPageRangeEditText.setOnFocusChangeListener(mSelectAllOnFocusListener);
mPageRangeEditText.addTextChangedListener(new RangeTextWatcher());
// Advanced options button.
mMoreOptionsButton = findViewById(R.id.more_options_button);
mMoreOptionsButton.setOnClickListener(clickListener);
// Print button
mPrintButton = findViewById(R.id.print_button);
mPrintButton.setOnClickListener(clickListener);
// The UI is now initialized
mIsOptionsUiBound = true;
// Special prompt instead of destination spinner for the first time the user printed
if (!hasUserEverPrinted()) {
mShowDestinationPrompt = true;
mSummaryCopies.setEnabled(false);
mSummaryPaperSize.setEnabled(false);
mDestinationSpinner.setPerformClickListener((v) -> {
mShowDestinationPrompt = false;
mSummaryCopies.setEnabled(true);
mSummaryPaperSize.setEnabled(true);
updateOptionsUi();
mDestinationSpinner.setPerformClickListener(null);
mDestinationSpinnerAdapter.notifyDataSetChanged();
});
}
}
@Override
public Loader<List<PrintServiceInfo>> onCreateLoader(int id, Bundle args) {
return new PrintServicesLoader((PrintManager) getSystemService(Context.PRINT_SERVICE), this,
PrintManager.ENABLED_SERVICES);
}
@Override
public void onLoadFinished(Loader<List<PrintServiceInfo>> loader,
List<PrintServiceInfo> services) {
ComponentName newAdvancedPrintOptionsActivity = null;
if (mCurrentPrinter != null && services != null) {
final int numServices = services.size();
for (int i = 0; i < numServices; i++) {
PrintServiceInfo service = services.get(i);
if (service.getComponentName().equals(mCurrentPrinter.getId().getServiceName())) {
String advancedOptionsActivityName = service.getAdvancedOptionsActivityName();
if (!TextUtils.isEmpty(advancedOptionsActivityName)) {
newAdvancedPrintOptionsActivity = new ComponentName(
service.getComponentName().getPackageName(),
advancedOptionsActivityName);
break;
}
}
}
}
if (!Objects.equals(newAdvancedPrintOptionsActivity, mAdvancedPrintOptionsActivity)) {
mAdvancedPrintOptionsActivity = newAdvancedPrintOptionsActivity;
updateOptionsUi();
}
boolean newArePrintServicesEnabled = services != null && !services.isEmpty();
if (mArePrintServicesEnabled != newArePrintServicesEnabled) {
mArePrintServicesEnabled = newArePrintServicesEnabled;
// Reload mDestinationSpinnerAdapter as mArePrintServicesEnabled changed and the adapter
// reads that in DestinationAdapter#getMoreItemTitle
if (mDestinationSpinnerAdapter != null) {
mDestinationSpinnerAdapter.notifyDataSetChanged();
}
}
}
@Override
public void onLoaderReset(Loader<List<PrintServiceInfo>> loader) {
if (!(isFinishing() || isDestroyed())) {
onLoadFinished(loader, null);
}
}
/**
* A dialog that asks the user to approve a {@link PrintService}. This dialog is automatically
* dismissed if the same {@link PrintService} gets approved by another
* {@link PrintServiceApprovalDialog}.
*/
public static final class PrintServiceApprovalDialog extends DialogFragment
implements OnSharedPreferenceChangeListener {
private static final String PRINTSERVICE_KEY = "PRINTSERVICE";
private ApprovedPrintServices mApprovedServices;
/**
* Create a new {@link PrintServiceApprovalDialog} that ask the user to approve a
* {@link PrintService}.
*
* @param printService The {@link ComponentName} of the service to approve
* @return A new {@link PrintServiceApprovalDialog} that might approve the service
*/
static PrintServiceApprovalDialog newInstance(ComponentName printService) {
PrintServiceApprovalDialog dialog = new PrintServiceApprovalDialog();
Bundle args = new Bundle();
args.putParcelable(PRINTSERVICE_KEY, printService);
dialog.setArguments(args);
return dialog;
}
@Override
public void onStop() {
super.onStop();
mApprovedServices.unregisterChangeListener(this);
}
@Override
public void onStart() {
super.onStart();
ComponentName printService = getArguments().getParcelable(PRINTSERVICE_KEY);
synchronized (ApprovedPrintServices.sLock) {
if (mApprovedServices.isApprovedService(printService)) {
dismiss();
} else {
mApprovedServices.registerChangeListenerLocked(this);
}
}
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
super.onCreateDialog(savedInstanceState);
mApprovedServices = new ApprovedPrintServices(getActivity());
PackageManager packageManager = getActivity().getPackageManager();
CharSequence serviceLabel;
try {
ComponentName printService = getArguments().getParcelable(PRINTSERVICE_KEY);
serviceLabel = packageManager.getApplicationInfo(printService.getPackageName(), 0)
.loadLabel(packageManager);
} catch (NameNotFoundException e) {
serviceLabel = null;
}
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(getString(R.string.print_service_security_warning_title,
serviceLabel))
.setMessage(getString(R.string.print_service_security_warning_summary,
serviceLabel))
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
ComponentName printService =
getArguments().getParcelable(PRINTSERVICE_KEY);
// Prevent onSharedPreferenceChanged from getting triggered
mApprovedServices
.unregisterChangeListener(PrintServiceApprovalDialog.this);
mApprovedServices.addApprovedService(printService);
((PrintActivity) getActivity()).confirmPrint();
}
})
.setNegativeButton(android.R.string.cancel, null);
return builder.create();
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
ComponentName printService = getArguments().getParcelable(PRINTSERVICE_KEY);
synchronized (ApprovedPrintServices.sLock) {
if (mApprovedServices.isApprovedService(printService)) {
dismiss();
}
}
}
}
private final class MyClickListener implements OnClickListener {
@Override
public void onClick(View view) {
if (view == mPrintButton) {
if (mCurrentPrinter != null) {
if (mDestinationSpinnerAdapter.getPdfPrinter() == mCurrentPrinter) {
confirmPrint();
} else {
ApprovedPrintServices approvedServices =
new ApprovedPrintServices(PrintActivity.this);
ComponentName printService = mCurrentPrinter.getId().getServiceName();
if (approvedServices.isApprovedService(printService)) {
confirmPrint();
} else {
PrintServiceApprovalDialog.newInstance(printService)
.show(getFragmentManager(), "approve");
}
}
} else {
cancelPrint();
}
} else if (view == mMoreOptionsButton) {
if (mPageRangeEditText.getError() == null) {
// The selected pages is only applied once the user leaves the text field. A click
// on this button, does not count as leaving.
updateSelectedPagesFromTextField();
}
if (mCurrentPrinter != null) {
startAdvancedPrintOptionsActivity(mCurrentPrinter);
}
}
}
}
private static boolean canPrint(PrinterInfo printer) {
return printer.getCapabilities() != null
&& printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE;
}
/**
* Disable all options UI elements, beside the {@link #mDestinationSpinner}
*
* @param disableRange If the range selection options should be disabled
*/
private void disableOptionsUi(boolean disableRange) {
mCopiesEditText.setEnabled(false);
mCopiesEditText.setFocusable(false);
mMediaSizeSpinner.setEnabled(false);
mColorModeSpinner.setEnabled(false);
mDuplexModeSpinner.setEnabled(false);
mOrientationSpinner.setEnabled(false);
mPrintButton.setVisibility(View.GONE);
mMoreOptionsButton.setEnabled(false);
if (disableRange) {
mRangeOptionsSpinner.setEnabled(false);
mPageRangeEditText.setEnabled(false);
}
}
void updateOptionsUi() {
if (!mIsOptionsUiBound) {
return;
}
// Always update the summary.
updateSummary();
mDestinationSpinner.setEnabled(!isFinalState(mState));
if (mState == STATE_PRINT_CONFIRMED
|| mState == STATE_PRINT_COMPLETED
|| mState == STATE_PRINT_CANCELED
|| mState == STATE_UPDATE_FAILED
|| mState == STATE_CREATE_FILE_FAILED
|| mState == STATE_PRINTER_UNAVAILABLE
|| mState == STATE_UPDATE_SLOW) {
disableOptionsUi(isFinalState(mState));
return;
}
// If no current printer, or it has no capabilities, or it is not
// available, we disable all print options except the destination.
if (mCurrentPrinter == null || !canPrint(mCurrentPrinter)) {
disableOptionsUi(false);
return;
}
PrinterCapabilitiesInfo capabilities = mCurrentPrinter.getCapabilities();
PrintAttributes defaultAttributes = capabilities.getDefaults();
// Destination.
mDestinationSpinner.setEnabled(true);
// Media size.
mMediaSizeSpinner.setEnabled(true);
List<MediaSize> mediaSizes = new ArrayList<>(capabilities.getMediaSizes());
// Sort the media sizes based on the current locale.
Collections.sort(mediaSizes, mMediaSizeComparator);
PrintAttributes attributes = mPrintJob.getAttributes();
// If the media sizes changed, we update the adapter and the spinner.
boolean mediaSizesChanged = false;
final int mediaSizeCount = mediaSizes.size();
if (mediaSizeCount != mMediaSizeSpinnerAdapter.getCount()) {
mediaSizesChanged = true;
} else {
for (int i = 0; i < mediaSizeCount; i++) {
if (!mediaSizes.get(i).equals(mMediaSizeSpinnerAdapter.getItem(i).value)) {
mediaSizesChanged = true;
break;
}
}
}
if (mediaSizesChanged) {
// Remember the old media size to try selecting it again.
int oldMediaSizeNewIndex = AdapterView.INVALID_POSITION;
MediaSize oldMediaSize = attributes.getMediaSize();
// Rebuild the adapter data.
mMediaSizeSpinnerAdapter.clear();
for (int i = 0; i < mediaSizeCount; i++) {
MediaSize mediaSize = mediaSizes.get(i);
if (oldMediaSize != null
&& mediaSize.asPortrait().equals(oldMediaSize.asPortrait())) {
// Update the index of the old selection.
oldMediaSizeNewIndex = i;
}
mMediaSizeSpinnerAdapter.add(new SpinnerItem<>(
mediaSize, mediaSize.getLabel(getPackageManager())));
}
if (oldMediaSizeNewIndex != AdapterView.INVALID_POSITION) {
// Select the old media size - nothing really changed.
if (mMediaSizeSpinner.getSelectedItemPosition() != oldMediaSizeNewIndex) {
mMediaSizeSpinner.setSelection(oldMediaSizeNewIndex);
}
} else {
// Select the first or the default.
final int mediaSizeIndex = Math.max(mediaSizes.indexOf(
defaultAttributes.getMediaSize()), 0);
if (mMediaSizeSpinner.getSelectedItemPosition() != mediaSizeIndex) {
mMediaSizeSpinner.setSelection(mediaSizeIndex);
}
// Respect the orientation of the old selection.
if (oldMediaSize != null) {
if (oldMediaSize.isPortrait()) {
attributes.setMediaSize(mMediaSizeSpinnerAdapter
.getItem(mediaSizeIndex).value.asPortrait());
} else {
attributes.setMediaSize(mMediaSizeSpinnerAdapter
.getItem(mediaSizeIndex).value.asLandscape());
}
}
}
}
// Color mode.
mColorModeSpinner.setEnabled(true);
final int colorModes = capabilities.getColorModes();
// If the color modes changed, we update the adapter and the spinner.
boolean colorModesChanged = false;
if (Integer.bitCount(colorModes) != mColorModeSpinnerAdapter.getCount()) {
colorModesChanged = true;
} else {
int remainingColorModes = colorModes;
int adapterIndex = 0;
while (remainingColorModes != 0) {
final int colorBitOffset = Integer.numberOfTrailingZeros(remainingColorModes);
final int colorMode = 1 << colorBitOffset;
remainingColorModes &= ~colorMode;
if (colorMode != mColorModeSpinnerAdapter.getItem(adapterIndex).value) {
colorModesChanged = true;
break;
}
adapterIndex++;
}
}
if (colorModesChanged) {
// Remember the old color mode to try selecting it again.
int oldColorModeNewIndex = AdapterView.INVALID_POSITION;
final int oldColorMode = attributes.getColorMode();
// Rebuild the adapter data.
mColorModeSpinnerAdapter.clear();
String[] colorModeLabels = getResources().getStringArray(R.array.color_mode_labels);
int remainingColorModes = colorModes;
while (remainingColorModes != 0) {
final int colorBitOffset = Integer.numberOfTrailingZeros(remainingColorModes);
final int colorMode = 1 << colorBitOffset;
if (colorMode == oldColorMode) {
// Update the index of the old selection.
oldColorModeNewIndex = mColorModeSpinnerAdapter.getCount();
}
remainingColorModes &= ~colorMode;
mColorModeSpinnerAdapter.add(new SpinnerItem<>(colorMode,
colorModeLabels[colorBitOffset]));
}
if (oldColorModeNewIndex != AdapterView.INVALID_POSITION) {
// Select the old color mode - nothing really changed.
if (mColorModeSpinner.getSelectedItemPosition() != oldColorModeNewIndex) {
mColorModeSpinner.setSelection(oldColorModeNewIndex);
}
} else {
// Select the default.
final int selectedColorMode = colorModes & defaultAttributes.getColorMode();
final int itemCount = mColorModeSpinnerAdapter.getCount();
for (int i = 0; i < itemCount; i++) {
SpinnerItem<Integer> item = mColorModeSpinnerAdapter.getItem(i);
if (selectedColorMode == item.value) {
if (mColorModeSpinner.getSelectedItemPosition() != i) {
mColorModeSpinner.setSelection(i);
}
attributes.setColorMode(selectedColorMode);
break;
}
}
}
}
// Duplex mode.
mDuplexModeSpinner.setEnabled(true);
final int duplexModes = capabilities.getDuplexModes();
// If the duplex modes changed, we update the adapter and the spinner.
// Note that we use bit count +1 to account for the no duplex option.
boolean duplexModesChanged = false;
if (Integer.bitCount(duplexModes) != mDuplexModeSpinnerAdapter.getCount()) {
duplexModesChanged = true;
} else {
int remainingDuplexModes = duplexModes;
int adapterIndex = 0;
while (remainingDuplexModes != 0) {
final int duplexBitOffset = Integer.numberOfTrailingZeros(remainingDuplexModes);
final int duplexMode = 1 << duplexBitOffset;
remainingDuplexModes &= ~duplexMode;
if (duplexMode != mDuplexModeSpinnerAdapter.getItem(adapterIndex).value) {
duplexModesChanged = true;
break;
}
adapterIndex++;
}
}
if (duplexModesChanged) {
// Remember the old duplex mode to try selecting it again. Also the fallback
// is no duplexing which is always the first item in the dropdown.
int oldDuplexModeNewIndex = AdapterView.INVALID_POSITION;
final int oldDuplexMode = attributes.getDuplexMode();
// Rebuild the adapter data.
mDuplexModeSpinnerAdapter.clear();
String[] duplexModeLabels = getResources().getStringArray(R.array.duplex_mode_labels);
int remainingDuplexModes = duplexModes;
while (remainingDuplexModes != 0) {
final int duplexBitOffset = Integer.numberOfTrailingZeros(remainingDuplexModes);
final int duplexMode = 1 << duplexBitOffset;
if (duplexMode == oldDuplexMode) {
// Update the index of the old selection.
oldDuplexModeNewIndex = mDuplexModeSpinnerAdapter.getCount();
}
remainingDuplexModes &= ~duplexMode;
mDuplexModeSpinnerAdapter.add(new SpinnerItem<>(duplexMode,
duplexModeLabels[duplexBitOffset]));
}
if (oldDuplexModeNewIndex != AdapterView.INVALID_POSITION) {
// Select the old duplex mode - nothing really changed.
if (mDuplexModeSpinner.getSelectedItemPosition() != oldDuplexModeNewIndex) {
mDuplexModeSpinner.setSelection(oldDuplexModeNewIndex);
}
} else {
// Select the default.
final int selectedDuplexMode = defaultAttributes.getDuplexMode();
final int itemCount = mDuplexModeSpinnerAdapter.getCount();
for (int i = 0; i < itemCount; i++) {
SpinnerItem<Integer> item = mDuplexModeSpinnerAdapter.getItem(i);
if (selectedDuplexMode == item.value) {
if (mDuplexModeSpinner.getSelectedItemPosition() != i) {
mDuplexModeSpinner.setSelection(i);
}
attributes.setDuplexMode(selectedDuplexMode);
break;
}
}
}
}
mDuplexModeSpinner.setEnabled(mDuplexModeSpinnerAdapter.getCount() > 1);
// Orientation
mOrientationSpinner.setEnabled(true);
MediaSize mediaSize = attributes.getMediaSize();
if (mediaSize != null) {
if (mediaSize.isPortrait()
&& mOrientationSpinner.getSelectedItemPosition() != 0) {
mOrientationSpinner.setSelection(0);
} else if (!mediaSize.isPortrait()
&& mOrientationSpinner.getSelectedItemPosition() != 1) {
mOrientationSpinner.setSelection(1);
}
}
// Range options
PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info;
final int pageCount = getAdjustedPageCount(info);
if (pageCount > 0) {
if (info != null) {
if (pageCount == 1) {
mRangeOptionsSpinner.setEnabled(false);
} else {
mRangeOptionsSpinner.setEnabled(true);
if (mRangeOptionsSpinner.getSelectedItemPosition() > 0) {
if (!mPageRangeEditText.isEnabled()) {
mPageRangeEditText.setEnabled(true);
mPageRangeEditText.setVisibility(View.VISIBLE);
mPageRangeTitle.setVisibility(View.VISIBLE);
mPageRangeEditText.requestFocus();
InputMethodManager imm = (InputMethodManager)
getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(mPageRangeEditText, 0);
}
} else {
mPageRangeEditText.setEnabled(false);
mPageRangeEditText.setVisibility(View.GONE);
mPageRangeTitle.setVisibility(View.GONE);
}
}
} else {
if (mRangeOptionsSpinner.getSelectedItemPosition() != 0) {
mRangeOptionsSpinner.setSelection(0);
mPageRangeEditText.setText("");
}
mRangeOptionsSpinner.setEnabled(false);
mPageRangeEditText.setEnabled(false);
mPageRangeEditText.setVisibility(View.GONE);
mPageRangeTitle.setVisibility(View.GONE);
}
}
final int newPageCount = getAdjustedPageCount(info);
if (newPageCount != mCurrentPageCount) {
mCurrentPageCount = newPageCount;
updatePageRangeOptions(newPageCount);
}
// Advanced print options
if (mAdvancedPrintOptionsActivity != null) {
mMoreOptionsButton.setVisibility(View.VISIBLE);
mMoreOptionsButton.setEnabled(!mIsMoreOptionsActivityInProgress);
} else {
mMoreOptionsButton.setVisibility(View.GONE);
mMoreOptionsButton.setEnabled(false);
}
// Print
if (mDestinationSpinnerAdapter.getPdfPrinter() != mCurrentPrinter) {
mPrintButton.setImageResource(com.android.internal.R.drawable.ic_print);
mPrintButton.setContentDescription(getString(R.string.print_button));
} else {
mPrintButton.setImageResource(R.drawable.ic_menu_savetopdf);
mPrintButton.setContentDescription(getString(R.string.savetopdf_button));
}
if (!mPrintedDocument.getDocumentInfo().updated
||(mRangeOptionsSpinner.getSelectedItemPosition() == 1
&& (TextUtils.isEmpty(mPageRangeEditText.getText()) || hasErrors()))
|| (mRangeOptionsSpinner.getSelectedItemPosition() == 0
&& (mPrintedDocument.getDocumentInfo() == null || hasErrors()))) {
mPrintButton.setVisibility(View.GONE);
} else {
mPrintButton.setVisibility(View.VISIBLE);
}
// Copies
if (mDestinationSpinnerAdapter.getPdfPrinter() != mCurrentPrinter) {
mCopiesEditText.setEnabled(true);
mCopiesEditText.setFocusableInTouchMode(true);
} else {
CharSequence text = mCopiesEditText.getText();
if (TextUtils.isEmpty(text) || !MIN_COPIES_STRING.equals(text.toString())) {
mCopiesEditText.setText(MIN_COPIES_STRING);
}
mCopiesEditText.setEnabled(false);
mCopiesEditText.setFocusable(false);
}
if (mCopiesEditText.getError() == null
&& TextUtils.isEmpty(mCopiesEditText.getText())) {
mCopiesEditText.setText(MIN_COPIES_STRING);
mCopiesEditText.requestFocus();
}
if (mShowDestinationPrompt) {
disableOptionsUi(false);
}
}
private void updateSummary() {
if (!mIsOptionsUiBound) {
return;
}
CharSequence copiesText = null;
CharSequence mediaSizeText = null;
if (!TextUtils.isEmpty(mCopiesEditText.getText())) {
copiesText = mCopiesEditText.getText();
mSummaryCopies.setText(copiesText);
}
final int selectedMediaIndex = mMediaSizeSpinner.getSelectedItemPosition();
if (selectedMediaIndex >= 0) {
SpinnerItem<MediaSize> mediaItem = mMediaSizeSpinnerAdapter.getItem(selectedMediaIndex);
mediaSizeText = mediaItem.label;
mSummaryPaperSize.setText(mediaSizeText);
}
if (!TextUtils.isEmpty(copiesText) && !TextUtils.isEmpty(mediaSizeText)) {
String summaryText = getString(R.string.summary_template, copiesText, mediaSizeText);
mSummaryContainer.setContentDescription(summaryText);
}
}
private void updatePageRangeOptions(int pageCount) {
@SuppressWarnings("unchecked")
ArrayAdapter<SpinnerItem<Integer>> rangeOptionsSpinnerAdapter =
(ArrayAdapter<SpinnerItem<Integer>>) mRangeOptionsSpinner.getAdapter();
rangeOptionsSpinnerAdapter.clear();
final int[] rangeOptionsValues = getResources().getIntArray(
R.array.page_options_values);
String pageCountLabel = (pageCount > 0) ? String.valueOf(pageCount) : "";
String[] rangeOptionsLabels = new String[] {
getString(R.string.template_all_pages, pageCountLabel),
getString(R.string.template_page_range, pageCountLabel)
};
final int rangeOptionsCount = rangeOptionsLabels.length;
for (int i = 0; i < rangeOptionsCount; i++) {
rangeOptionsSpinnerAdapter.add(new SpinnerItem<>(
rangeOptionsValues[i], rangeOptionsLabels[i]));
}
}
private PageRange[] computeSelectedPages() {
if (hasErrors()) {
return null;
}
if (mRangeOptionsSpinner.getSelectedItemPosition() > 0) {
PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info;
final int pageCount = (info != null) ? getAdjustedPageCount(info) : 0;
return PageRangeUtils.parsePageRanges(mPageRangeEditText.getText(), pageCount);
}
return PageRange.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);
}
public void onPrinterAvailable(PrinterInfo printer) {
if (mCurrentPrinter != null && mCurrentPrinter.equals(printer)) {
setState(STATE_CONFIGURING);
if (canUpdateDocument()) {
updateDocument(false);
}
ensurePreviewUiShown();
}
}
public void onPrinterUnavailable(PrinterInfo printer) {
if (mCurrentPrinter == null || mCurrentPrinter.getId().equals(printer.getId())) {
setState(STATE_PRINTER_UNAVAILABLE);
mPrintedDocument.cancel(false);
ensureErrorUiShown(getString(R.string.print_error_printer_unavailable),
PrintErrorFragment.ACTION_NONE);
}
}
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;
}
if (mCurrentPrinter == null) {
return false;
}
PrinterCapabilitiesInfo capabilities = mCurrentPrinter.getCapabilities();
if (capabilities == null) {
return false;
}
if (mCurrentPrinter.getStatus() == PrinterInfo.STATUS_UNAVAILABLE) {
return false;
}
return true;
}
private void transformDocumentAndFinish(final Uri writeToUri) {
// If saving to PDF, apply the attibutes as we are acting as a print service.
PrintAttributes attributes = mDestinationSpinnerAdapter.getPdfPrinter() == mCurrentPrinter
? mPrintJob.getAttributes() : null;
new DocumentTransformer(this, mPrintJob, mFileProvider, attributes, error -> {
if (error == null) {
if (writeToUri != null) {
mPrintedDocument.writeContent(getContentResolver(), writeToUri);
}
setState(STATE_PRINT_COMPLETED);
doFinish();
} else {
onPrintDocumentError(error);
}
}).transform();
}
private void doFinish() {
if (mPrintedDocument != null && mPrintedDocument.isUpdating()) {
// The printedDocument will call doFinish() when the current command finishes
return;
}
if (mIsFinishing) {
return;
}
mIsFinishing = true;
if (mPrinterRegistry != null) {
mPrinterRegistry.setTrackedPrinter(null);
mPrinterRegistry.setOnPrintersChangeListener(null);
}
if (mPrintersObserver != null) {
mDestinationSpinnerAdapter.unregisterDataSetObserver(mPrintersObserver);
}
if (mSpoolerProvider != null) {
mSpoolerProvider.destroy();
}
if (mProgressMessageController != null) {
setState(mProgressMessageController.cancel());
}
if (mState != STATE_INITIALIZING) {
mPrintedDocument.finish();
mPrintedDocument.destroy();
mPrintPreviewController.destroy(new Runnable() {
@Override
public void run() {
finish();
}
});
} else {
finish();
}
}
private final class SpinnerItem<T> {
final T value;
final CharSequence label;
public SpinnerItem(T value, CharSequence label) {
this.value = value;
this.label = label;
}
@Override
public String toString() {
return label.toString();
}
}
private final class PrinterAvailabilityDetector implements Runnable {
private static final long UNAVAILABLE_TIMEOUT_MILLIS = 10000; // 10sec
private boolean mPosted;
private boolean mPrinterUnavailable;
private PrinterInfo mPrinter;
public void updatePrinter(PrinterInfo printer) {
if (printer.equals(mDestinationSpinnerAdapter.getPdfPrinter())) {
return;
}
final boolean available = printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE
&& printer.getCapabilities() != null;
final boolean notifyIfAvailable;
if (mPrinter == null || !mPrinter.getId().equals(printer.getId())) {
notifyIfAvailable = true;
unpostIfNeeded();
mPrinterUnavailable = false;
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 = printer;
}
if (available) {
unpostIfNeeded();
mPrinterUnavailable = false;
if (notifyIfAvailable) {
onPrinterAvailable(mPrinter);
}
} else {
if (!mPrinterUnavailable) {
postIfNeeded();
}
}
}
public void cancel() {
unpostIfNeeded();
mPrinterUnavailable = false;
}
private void postIfNeeded() {
if (!mPosted) {
mPosted = true;
mDestinationSpinner.postDelayed(this, UNAVAILABLE_TIMEOUT_MILLIS);
}
}
private void unpostIfNeeded() {
if (mPosted) {
mPosted = false;
mDestinationSpinner.removeCallbacks(this);
}
}
@Override
public void run() {
mPosted = false;
mPrinterUnavailable = true;
onPrinterUnavailable(mPrinter);
}
}
private static final class PrinterHolder {
PrinterInfo printer;
boolean removed;
public PrinterHolder(PrinterInfo printer) {
this.printer = printer;
}
}
/**
* Check if the user has ever printed a document
*
* @return true iff the user has ever printed a document
*/
private boolean hasUserEverPrinted() {
SharedPreferences preferences = getSharedPreferences(HAS_PRINTED_PREF, MODE_PRIVATE);
return preferences.getBoolean(HAS_PRINTED_PREF, false);
}
/**
* Remember that the user printed a document
*/
private void setUserPrinted() {
SharedPreferences preferences = getSharedPreferences(HAS_PRINTED_PREF, MODE_PRIVATE);
if (!preferences.getBoolean(HAS_PRINTED_PREF, false)) {
SharedPreferences.Editor edit = preferences.edit();
edit.putBoolean(HAS_PRINTED_PREF, true);
edit.apply();
}
}
private final class DestinationAdapter extends BaseAdapter
implements PrinterRegistry.OnPrintersChangeListener {
private final List<PrinterHolder> mPrinterHolders = new ArrayList<>();
private final PrinterHolder mFakePdfPrinterHolder;
private boolean mHistoricalPrintersLoaded;
/**
* Has the {@link #mDestinationSpinner} ever used a view from printer_dropdown_prompt
*/
private boolean hadPromptView;
public DestinationAdapter() {
mHistoricalPrintersLoaded = mPrinterRegistry.areHistoricalPrintersLoaded();
if (mHistoricalPrintersLoaded) {
addPrinters(mPrinterHolders, mPrinterRegistry.getPrinters());
}
mPrinterRegistry.setOnPrintersChangeListener(this);
mFakePdfPrinterHolder = new PrinterHolder(createFakePdfPrinter());
}
public PrinterInfo getPdfPrinter() {
return mFakePdfPrinterHolder.printer;
}
public int getPrinterIndex(PrinterId printerId) {
for (int i = 0; i < getCount(); i++) {
PrinterHolder printerHolder = (PrinterHolder) getItem(i);
if (printerHolder != null && printerHolder.printer.getId().equals(printerId)) {
return i;
}
}
return AdapterView.INVALID_POSITION;
}
public void ensurePrinterInVisibleAdapterPosition(PrinterInfo printer) {
final int printerCount = mPrinterHolders.size();
boolean isKnownPrinter = false;
for (int i = 0; i < printerCount; i++) {
PrinterHolder printerHolder = mPrinterHolders.get(i);
if (printerHolder.printer.getId().equals(printer.getId())) {
isKnownPrinter = true;
// If already in the list - do nothing.
if (i < getCount() - 2) {
break;
}
// Else replace the last one (two items are not printers).
final int lastPrinterIndex = getCount() - 3;
mPrinterHolders.set(i, mPrinterHolders.get(lastPrinterIndex));
mPrinterHolders.set(lastPrinterIndex, printerHolder);
break;
}
}
if (!isKnownPrinter) {
PrinterHolder printerHolder = new PrinterHolder(printer);
printerHolder.removed = true;
mPrinterHolders.add(Math.max(0, getCount() - 3), printerHolder);
}
// Force reload to adjust selection in PrintersObserver.onChanged()
notifyDataSetChanged();
}
@Override
public int getCount() {
if (mHistoricalPrintersLoaded) {
return Math.min(mPrinterHolders.size() + 2, DEST_ADAPTER_MAX_ITEM_COUNT);
}
return 0;
}
@Override
public boolean isEnabled(int position) {
Object item = getItem(position);
if (item instanceof PrinterHolder) {
PrinterHolder printerHolder = (PrinterHolder) item;
return !printerHolder.removed
&& printerHolder.printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE;
}
return true;
}
@Override
public Object getItem(int position) {
if (mPrinterHolders.isEmpty()) {
if (position == 0) {
return mFakePdfPrinterHolder;
}
} else {
if (position < 1) {
return mPrinterHolders.get(position);
}
if (position == 1) {
return mFakePdfPrinterHolder;
}
if (position < getCount() - 1) {
return mPrinterHolders.get(position - 1);
}
}
return null;
}
@Override
public long getItemId(int position) {
if (mPrinterHolders.isEmpty()) {
if (position == 0) {
return DEST_ADAPTER_ITEM_ID_SAVE_AS_PDF;
} else if (position == 1) {
return DEST_ADAPTER_ITEM_ID_MORE;
}
} else {
if (position == 1) {
return DEST_ADAPTER_ITEM_ID_SAVE_AS_PDF;
}
if (position == getCount() - 1) {
return DEST_ADAPTER_ITEM_ID_MORE;
}
}
return position;
}
@Override
public View getDropDownView(int position, View convertView, ViewGroup parent) {
View view = getView(position, convertView, parent);
view.setEnabled(isEnabled(position));
return view;
}
private String getMoreItemTitle() {
if (mArePrintServicesEnabled) {
return getString(R.string.all_printers);
} else {
return getString(R.string.print_add_printer);
}
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (mShowDestinationPrompt) {
if (convertView == null) {
convertView = getLayoutInflater().inflate(
R.layout.printer_dropdown_prompt, parent, false);
hadPromptView = true;
}
return convertView;
} else {
// We don't know if we got an recyled printer_dropdown_prompt, hence do not use it
if (hadPromptView || convertView == null) {
convertView = getLayoutInflater().inflate(
R.layout.printer_dropdown_item, parent, false);
}
}
CharSequence title = null;
CharSequence subtitle = null;
Drawable icon = null;
if (mPrinterHolders.isEmpty()) {
if (position == 0 && getPdfPrinter() != null) {
PrinterHolder printerHolder = (PrinterHolder) getItem(position);
title = printerHolder.printer.getName();
icon = getResources().getDrawable(R.drawable.ic_pdf_printer, null);
} else if (position == 1) {
title = getMoreItemTitle();
}
} else {
if (position == 1 && getPdfPrinter() != null) {
PrinterHolder printerHolder = (PrinterHolder) getItem(position);
title = printerHolder.printer.getName();
icon = getResources().getDrawable(R.drawable.ic_pdf_printer, null);
} else if (position == getCount() - 1) {
title = getMoreItemTitle();
} else {
PrinterHolder printerHolder = (PrinterHolder) getItem(position);
PrinterInfo printInfo = printerHolder.printer;
title = printInfo.getName();
icon = printInfo.loadIcon(PrintActivity.this);
subtitle = printInfo.getDescription();
}
}
TextView titleView = (TextView) convertView.findViewById(R.id.title);
titleView.setText(title);
TextView subtitleView = (TextView) convertView.findViewById(R.id.subtitle);
if (!TextUtils.isEmpty(subtitle)) {
subtitleView.setText(subtitle);
subtitleView.setVisibility(View.VISIBLE);
} else {
subtitleView.setText(null);
subtitleView.setVisibility(View.GONE);
}
ImageView iconView = (ImageView) convertView.findViewById(R.id.icon);
if (icon != null) {
iconView.setVisibility(View.VISIBLE);
if (!isEnabled(position)) {
icon.mutate();
TypedValue value = new TypedValue();
getTheme().resolveAttribute(android.R.attr.disabledAlpha, value, true);
icon.setAlpha((int)(value.getFloat() * 255));
}
iconView.setImageDrawable(icon);
} else {
iconView.setVisibility(View.INVISIBLE);
}
return convertView;
}
@Override
public void onPrintersChanged(List<PrinterInfo> printers) {
// We rearrange the printers if the user selects a printer
// not shown in the initial short list. Therefore, we have
// to keep the printer order.
// Check if historical printers are loaded as this adapter is open
// for busyness only if they are. This member is updated here and
// when the adapter is created because the historical printers may
// be loaded before or after the adapter is created.
mHistoricalPrintersLoaded = mPrinterRegistry.areHistoricalPrintersLoaded();
// No old printers - do not bother keeping their position.
if (mPrinterHolders.isEmpty()) {
addPrinters(mPrinterHolders, printers);
notifyDataSetChanged();
return;
}
// Add the new printers to a map.
ArrayMap<PrinterId, PrinterInfo> newPrintersMap = new ArrayMap<>();
final int printerCount = printers.size();
for (int i = 0; i < printerCount; i++) {
PrinterInfo printer = printers.get(i);
newPrintersMap.put(printer.getId(), printer);
}
List<PrinterHolder> newPrinterHolders = new ArrayList<>();
// Update printers we already have which are either updated or removed.
// We do not remove the currently selected printer.
final int oldPrinterCount = mPrinterHolders.size();
for (int i = 0; i < oldPrinterCount; i++) {
PrinterHolder printerHolder = mPrinterHolders.get(i);
PrinterId oldPrinterId = printerHolder.printer.getId();
PrinterInfo updatedPrinter = newPrintersMap.remove(oldPrinterId);
if (updatedPrinter != null) {
printerHolder.printer = updatedPrinter;
printerHolder.removed = false;
if (canPrint(printerHolder.printer)) {
onPrinterAvailable(printerHolder.printer);
} else {
onPrinterUnavailable(printerHolder.printer);
}
newPrinterHolders.add(printerHolder);
} else if (mCurrentPrinter != null && mCurrentPrinter.getId().equals(oldPrinterId)){
printerHolder.removed = true;
onPrinterUnavailable(printerHolder.printer);
newPrinterHolders.add(printerHolder);
}
}
// Add the rest of the new printers, i.e. what is left.
addPrinters(newPrinterHolders, newPrintersMap.values());
mPrinterHolders.clear();
mPrinterHolders.addAll(newPrinterHolders);
notifyDataSetChanged();
}
@Override
public void onPrintersInvalid() {
mPrinterHolders.clear();
notifyDataSetInvalidated();
}
public PrinterHolder getPrinterHolder(PrinterId printerId) {
final int itemCount = getCount();
for (int i = 0; i < itemCount; i++) {
Object item = getItem(i);
if (item instanceof PrinterHolder) {
PrinterHolder printerHolder = (PrinterHolder) item;
if (printerId.equals(printerHolder.printer.getId())) {
return printerHolder;
}
}
}
return null;
}
/**
* Remove a printer from the holders if it is marked as removed.
*
* @param printerId the id of the printer to remove.
*
* @return true iff the printer was removed.
*/
public boolean pruneRemovedPrinter(PrinterId printerId) {
final int holderCounts = mPrinterHolders.size();
for (int i = holderCounts - 1; i >= 0; i--) {
PrinterHolder printerHolder = mPrinterHolders.get(i);
if (printerHolder.printer.getId().equals(printerId) && printerHolder.removed) {
mPrinterHolders.remove(i);
return true;
}
}
return false;
}
private void addPrinters(List<PrinterHolder> list, Collection<PrinterInfo> printers) {
for (PrinterInfo printer : printers) {
PrinterHolder printerHolder = new PrinterHolder(printer);
list.add(printerHolder);
}
}
private PrinterInfo createFakePdfPrinter() {
ArraySet<MediaSize> allMediaSizes = MediaSize.getAllPredefinedSizes();
MediaSize defaultMediaSize = MediaSizeUtils.getDefault(PrintActivity.this);
PrinterId printerId = new PrinterId(getComponentName(), "PDF printer");
PrinterCapabilitiesInfo.Builder builder =
new PrinterCapabilitiesInfo.Builder(printerId);
final int mediaSizeCount = allMediaSizes.size();
for (int i = 0; i < mediaSizeCount; i++) {
MediaSize mediaSize = allMediaSizes.valueAt(i);
builder.addMediaSize(mediaSize, mediaSize.equals(defaultMediaSize));
}
builder.addResolution(new Resolution("PDF resolution", "PDF resolution", 300, 300),
true);
builder.setColorModes(PrintAttributes.COLOR_MODE_COLOR
| PrintAttributes.COLOR_MODE_MONOCHROME, PrintAttributes.COLOR_MODE_COLOR);
return new PrinterInfo.Builder(printerId, getString(R.string.save_as_pdf),
PrinterInfo.STATUS_IDLE).setCapabilities(builder.build()).build();
}
}
private final class PrintersObserver extends DataSetObserver {
@Override
public void onChanged() {
PrinterInfo oldPrinterState = mCurrentPrinter;
if (oldPrinterState == null) {
return;
}
PrinterHolder printerHolder = mDestinationSpinnerAdapter.getPrinterHolder(
oldPrinterState.getId());
PrinterInfo newPrinterState = printerHolder.printer;
if (printerHolder.removed) {
onPrinterUnavailable(newPrinterState);
}
if (mDestinationSpinner.getSelectedItem() != printerHolder) {
mDestinationSpinner.setSelection(
mDestinationSpinnerAdapter.getPrinterIndex(newPrinterState.getId()));
}
if (oldPrinterState.equals(newPrinterState)) {
return;
}
PrinterCapabilitiesInfo oldCapab = oldPrinterState.getCapabilities();
PrinterCapabilitiesInfo newCapab = newPrinterState.getCapabilities();
final boolean hadCabab = oldCapab != null;
final boolean hasCapab = newCapab != null;
final boolean gotCapab = oldCapab == null && newCapab != null;
final boolean lostCapab = oldCapab != null && newCapab == null;
final boolean capabChanged = capabilitiesChanged(oldCapab, newCapab);
final int oldStatus = oldPrinterState.getStatus();
final int newStatus = newPrinterState.getStatus();
final boolean isActive = newStatus != PrinterInfo.STATUS_UNAVAILABLE;
final boolean becameActive = (oldStatus == PrinterInfo.STATUS_UNAVAILABLE
&& oldStatus != newStatus);
final boolean becameInactive = (newStatus == PrinterInfo.STATUS_UNAVAILABLE
&& oldStatus != newStatus);
mPrinterAvailabilityDetector.updatePrinter(newPrinterState);
mCurrentPrinter = newPrinterState;
final boolean updateNeeded = ((capabChanged && hasCapab && isActive)
|| (becameActive && hasCapab) || (isActive && gotCapab));
if (capabChanged && hasCapab) {
updatePrintAttributesFromCapabilities(newCapab);
}
if (updateNeeded) {
updatePrintPreviewController(false);
}
if ((isActive && gotCapab) || (becameActive && hasCapab)) {
onPrinterAvailable(newPrinterState);
} else if ((becameInactive && hadCabab) || (isActive && lostCapab)) {
onPrinterUnavailable(newPrinterState);
}
if (updateNeeded && canUpdateDocument()) {
updateDocument(false);
}
// Force a reload of the enabled print services to update mAdvancedPrintOptionsActivity
// in onLoadFinished();
getLoaderManager().getLoader(LOADER_ID_ENABLED_PRINT_SERVICES).forceLoad();
updateOptionsUi();
updateSummary();
}
private boolean capabilitiesChanged(PrinterCapabilitiesInfo oldCapabilities,
PrinterCapabilitiesInfo newCapabilities) {
if (oldCapabilities == null) {
if (newCapabilities != null) {
return true;
}
} else if (!oldCapabilities.equals(newCapabilities)) {
return true;
}
return false;
}
}
private final class MyOnItemSelectedListener implements AdapterView.OnItemSelectedListener {
@Override
public void onItemSelected(AdapterView<?> spinner, View view, int position, long id) {
boolean clearRanges = false;
if (spinner == mDestinationSpinner) {
if (position == AdapterView.INVALID_POSITION) {
return;
}
if (id == DEST_ADAPTER_ITEM_ID_MORE) {
startSelectPrinterActivity();
return;
}
PrinterHolder currentItem = (PrinterHolder) mDestinationSpinner.getSelectedItem();
PrinterInfo currentPrinter = (currentItem != null) ? currentItem.printer : null;
// Why on earth item selected is called if no selection changed.
if (mCurrentPrinter == currentPrinter) {
return;
}
if (mDefaultPrinter == null) {
mDefaultPrinter = currentPrinter.getId();
}
PrinterId oldId = null;
if (mCurrentPrinter != null) {
oldId = mCurrentPrinter.getId();
}
mCurrentPrinter = currentPrinter;
if (oldId != null) {
boolean printerRemoved = mDestinationSpinnerAdapter.pruneRemovedPrinter(oldId);
if (printerRemoved) {
// Trigger PrinterObserver.onChanged to adjust selection. This will call
// this function again.
mDestinationSpinnerAdapter.notifyDataSetChanged();
return;
}
if (mState != STATE_INITIALIZING) {
if (currentPrinter != null) {
MetricsLogger.action(PrintActivity.this,
MetricsEvent.ACTION_PRINTER_SELECT_DROPDOWN,
currentPrinter.getId().getServiceName().getPackageName());
} else {
MetricsLogger.action(PrintActivity.this,
MetricsEvent.ACTION_PRINTER_SELECT_DROPDOWN, "");
}
}
}
PrinterHolder printerHolder = mDestinationSpinnerAdapter.getPrinterHolder(
currentPrinter.getId());
if (!printerHolder.removed) {
setState(STATE_CONFIGURING);
ensurePreviewUiShown();
}
mPrintJob.setPrinterId(currentPrinter.getId());
mPrintJob.setPrinterName(currentPrinter.getName());
mPrinterRegistry.setTrackedPrinter(currentPrinter.getId());
PrinterCapabilitiesInfo capabilities = currentPrinter.getCapabilities();
if (capabilities != null) {
updatePrintAttributesFromCapabilities(capabilities);
}
mPrinterAvailabilityDetector.updatePrinter(currentPrinter);
// Force a reload of the enabled print services to update
// mAdvancedPrintOptionsActivity in onLoadFinished();
getLoaderManager().getLoader(LOADER_ID_ENABLED_PRINT_SERVICES).forceLoad();
} else if (spinner == mMediaSizeSpinner) {
SpinnerItem<MediaSize> mediaItem = mMediaSizeSpinnerAdapter.getItem(position);
PrintAttributes attributes = mPrintJob.getAttributes();
MediaSize newMediaSize;
if (mOrientationSpinner.getSelectedItemPosition() == 0) {
newMediaSize = mediaItem.value.asPortrait();
} else {
newMediaSize = mediaItem.value.asLandscape();
}
if (newMediaSize != attributes.getMediaSize()) {
if (!newMediaSize.equals(attributes.getMediaSize())
&& !attributes.getMediaSize().equals(MediaSize.UNKNOWN_LANDSCAPE)
&& !attributes.getMediaSize().equals(MediaSize.UNKNOWN_PORTRAIT)
&& mState != STATE_INITIALIZING) {
MetricsLogger.action(PrintActivity.this,
MetricsEvent.ACTION_PRINT_JOB_OPTIONS,
PRINT_JOB_OPTIONS_SUBTYPE_MEDIA_SIZE);
}
clearRanges = true;
attributes.setMediaSize(newMediaSize);
}
} else if (spinner == mColorModeSpinner) {
SpinnerItem<Integer> colorModeItem = mColorModeSpinnerAdapter.getItem(position);
int newMode = colorModeItem.value;
if (mPrintJob.getAttributes().getColorMode() != newMode
&& mState != STATE_INITIALIZING) {
MetricsLogger.action(PrintActivity.this, MetricsEvent.ACTION_PRINT_JOB_OPTIONS,
PRINT_JOB_OPTIONS_SUBTYPE_COLOR_MODE);
}
mPrintJob.getAttributes().setColorMode(newMode);
} else if (spinner == mDuplexModeSpinner) {
SpinnerItem<Integer> duplexModeItem = mDuplexModeSpinnerAdapter.getItem(position);
int newMode = duplexModeItem.value;
if (mPrintJob.getAttributes().getDuplexMode() != newMode
&& mState != STATE_INITIALIZING) {
MetricsLogger.action(PrintActivity.this, MetricsEvent.ACTION_PRINT_JOB_OPTIONS,
PRINT_JOB_OPTIONS_SUBTYPE_DUPLEX_MODE);
}
mPrintJob.getAttributes().setDuplexMode(newMode);
} else if (spinner == mOrientationSpinner) {
SpinnerItem<Integer> orientationItem = mOrientationSpinnerAdapter.getItem(position);
PrintAttributes attributes = mPrintJob.getAttributes();
if (mMediaSizeSpinner.getSelectedItem() != null) {
boolean isPortrait = attributes.isPortrait();
boolean newIsPortrait = orientationItem.value == ORIENTATION_PORTRAIT;
if (isPortrait != newIsPortrait) {
if (mState != STATE_INITIALIZING) {
MetricsLogger.action(PrintActivity.this,
MetricsEvent.ACTION_PRINT_JOB_OPTIONS,
PRINT_JOB_OPTIONS_SUBTYPE_ORIENTATION);
}
clearRanges = true;
if (newIsPortrait) {
attributes.copyFrom(attributes.asPortrait());
} else {
attributes.copyFrom(attributes.asLandscape());
}
}
}
} else if (spinner == mRangeOptionsSpinner) {
if (mRangeOptionsSpinner.getSelectedItemPosition() == 0) {
clearRanges = true;
mPageRangeEditText.setText("");
if (mPageRangeEditText.getVisibility() == View.VISIBLE &&
mState != STATE_INITIALIZING) {
MetricsLogger.action(PrintActivity.this,
MetricsEvent.ACTION_PRINT_JOB_OPTIONS,
PRINT_JOB_OPTIONS_SUBTYPE_PAGE_RANGE);
}
} else if (TextUtils.isEmpty(mPageRangeEditText.getText())) {
mPageRangeEditText.setError("");
if (mPageRangeEditText.getVisibility() != View.VISIBLE &&
mState != STATE_INITIALIZING) {
MetricsLogger.action(PrintActivity.this,
MetricsEvent.ACTION_PRINT_JOB_OPTIONS,
PRINT_JOB_OPTIONS_SUBTYPE_PAGE_RANGE);
}
}
}
if (clearRanges) {
clearPageRanges();
}
updateOptionsUi();
if (canUpdateDocument()) {
updateDocument(false);
}
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
/* do nothing*/
}
}
private final class SelectAllOnFocusListener implements OnFocusChangeListener {
@Override
public void onFocusChange(View view, boolean hasFocus) {
EditText editText = (EditText) view;
if (!TextUtils.isEmpty(editText.getText())) {
editText.setSelection(editText.getText().length());
}
if (view == mPageRangeEditText && !hasFocus && mPageRangeEditText.getError() == null) {
updateSelectedPagesFromTextField();
}
}
}
private final class RangeTextWatcher implements TextWatcher {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
/* do nothing */
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
/* do nothing */
}
@Override
public void afterTextChanged(Editable editable) {
final boolean hadErrors = hasErrors();
PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info;
final int pageCount = (info != null) ? getAdjustedPageCount(info) : 0;
PageRange[] ranges = PageRangeUtils.parsePageRanges(editable, pageCount);
if (ranges.length == 0) {
if (mPageRangeEditText.getError() == null) {
mPageRangeEditText.setError("");
updateOptionsUi();
}
return;
}
if (mPageRangeEditText.getError() != null) {
mPageRangeEditText.setError(null);
updateOptionsUi();
}
if (hadErrors && canUpdateDocument()) {
updateDocument(false);
}
}
}
private final class EditTextWatcher implements TextWatcher {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
/* do nothing */
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
/* do nothing */
}
@Override
public void afterTextChanged(Editable editable) {
final boolean hadErrors = hasErrors();
if (editable.length() == 0) {
if (mCopiesEditText.getError() == null) {
mCopiesEditText.setError("");
updateOptionsUi();
}
return;
}
int copies = 0;
try {
copies = Integer.parseInt(editable.toString());
} catch (NumberFormatException nfe) {
/* ignore */
}
if (mState != STATE_INITIALIZING) {
MetricsLogger.action(PrintActivity.this, MetricsEvent.ACTION_PRINT_JOB_OPTIONS,
PRINT_JOB_OPTIONS_SUBTYPE_COPIES);
}
if (copies < MIN_COPIES) {
if (mCopiesEditText.getError() == null) {
mCopiesEditText.setError("");
updateOptionsUi();
}
return;
}
mPrintJob.setCopies(copies);
if (mCopiesEditText.getError() != null) {
mCopiesEditText.setError(null);
updateOptionsUi();
}
if (hadErrors && canUpdateDocument()) {
updateDocument(false);
}
}
}
private final class ProgressMessageController implements Runnable {
private static final long PROGRESS_TIMEOUT_MILLIS = 1000;
private final Handler mHandler;
private boolean mPosted;
/** State before run was executed */
private int mPreviousState = -1;
public ProgressMessageController(Context context) {
mHandler = new Handler(context.getMainLooper(), null, false);
}
public void post() {
if (mState == STATE_UPDATE_SLOW) {
setState(STATE_UPDATE_SLOW);
ensureProgressUiShown();
return;
} else if (mPosted) {
return;
}
mPreviousState = -1;
mPosted = true;
mHandler.postDelayed(this, PROGRESS_TIMEOUT_MILLIS);
}
private int getStateAfterCancel() {
if (mPreviousState == -1) {
return mState;
} else {
return mPreviousState;
}
}
public int cancel() {
int state;
if (!mPosted) {
state = getStateAfterCancel();
} else {
mPosted = false;
mHandler.removeCallbacks(this);
state = getStateAfterCancel();
}
mPreviousState = -1;
return state;
}
@Override
public void run() {
mPosted = false;
mPreviousState = mState;
setState(STATE_UPDATE_SLOW);
ensureProgressUiShown();
}
}
private static final class DocumentTransformer implements ServiceConnection {
private static final String TEMP_FILE_PREFIX = "print_job";
private static final String TEMP_FILE_EXTENSION = ".pdf";
private final Context mContext;
private final MutexFileProvider mFileProvider;
private final PrintJobInfo mPrintJob;
private final PageRange[] mPagesToShred;
private final PrintAttributes mAttributesToApply;
private final Consumer<String> mCallback;
private boolean mIsTransformationStarted;
public DocumentTransformer(Context context, PrintJobInfo printJob,
MutexFileProvider fileProvider, PrintAttributes attributes,
Consumer<String> callback) {
mContext = context;
mPrintJob = printJob;
mFileProvider = fileProvider;
mCallback = callback;
mPagesToShred = computePagesToShred(mPrintJob);
mAttributesToApply = attributes;
}
public void transform() {
// If we have only the pages we want, done.
if (mPagesToShred.length <= 0 && mAttributesToApply == null) {
mCallback.accept(null);
return;
}
// Bind to the manipulation service and the work
// will be performed upon connection to the service.
Intent intent = new Intent(PdfManipulationService.ACTION_GET_EDITOR);
intent.setClass(mContext, PdfManipulationService.class);
mContext.bindService(intent, this, Context.BIND_AUTO_CREATE);
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
// We might get several onServiceConnected if the service crashes and restarts.
// mIsTransformationStarted makes sure that we only try once.
if (!mIsTransformationStarted) {
final IPdfEditor editor = IPdfEditor.Stub.asInterface(service);
new AsyncTask<Void, Void, String>() {
@Override
protected String doInBackground(Void... params) {
// It's OK to access the data members as they are
// final and this code is the last one to touch
// them as shredding is the very last step, so the
// UI is not interactive at this point.
try {
doTransform(editor);
updatePrintJob();
return null;
} catch (IOException | RemoteException | IllegalStateException e) {
return e.toString();
}
}
@Override
protected void onPostExecute(String error) {
mContext.unbindService(DocumentTransformer.this);
mCallback.accept(error);
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
mIsTransformationStarted = true;
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
/* do nothing */
}
private void doTransform(IPdfEditor editor) throws IOException, RemoteException {
File tempFile = null;
ParcelFileDescriptor src = null;
ParcelFileDescriptor dst = null;
InputStream in = null;
OutputStream out = null;
try {
File jobFile = mFileProvider.acquireFile(null);
src = ParcelFileDescriptor.open(jobFile, ParcelFileDescriptor.MODE_READ_WRITE);
// Open the document.
editor.openDocument(src);
// We passed the fd over IPC, close this one.
src.close();
// Drop the pages.
editor.removePages(mPagesToShred);
// Apply print attributes if needed.
if (mAttributesToApply != null) {
editor.applyPrintAttributes(mAttributesToApply);
}
// Write the modified PDF to a temp file.
tempFile = File.createTempFile(TEMP_FILE_PREFIX, TEMP_FILE_EXTENSION,
mContext.getCacheDir());
dst = ParcelFileDescriptor.open(tempFile, ParcelFileDescriptor.MODE_READ_WRITE);
editor.write(dst);
dst.close();
// Close the document.
editor.closeDocument();
// Copy the temp file over the print job file.
jobFile.delete();
in = new FileInputStream(tempFile);
out = new FileOutputStream(jobFile);
Streams.copy(in, out);
} finally {
IoUtils.closeQuietly(src);
IoUtils.closeQuietly(dst);
IoUtils.closeQuietly(in);
IoUtils.closeQuietly(out);
if (tempFile != null) {
tempFile.delete();
}
mFileProvider.releaseFile();
}
}
private void updatePrintJob() {
// Update the print job pages.
final int newPageCount = PageRangeUtils.getNormalizedPageCount(
mPrintJob.getPages(), 0);
mPrintJob.setPages(new PageRange[]{PageRange.ALL_PAGES});
// Update the print job document info.
PrintDocumentInfo oldDocInfo = mPrintJob.getDocumentInfo();
PrintDocumentInfo newDocInfo = new PrintDocumentInfo
.Builder(oldDocInfo.getName())
.setContentType(oldDocInfo.getContentType())
.setPageCount(newPageCount)
.build();
File file = mFileProvider.acquireFile(null);
try {
newDocInfo.setDataSize(file.length());
} finally {
mFileProvider.releaseFile();
}
mPrintJob.setDocumentInfo(newDocInfo);
}
private static PageRange[] computePagesToShred(PrintJobInfo printJob) {
List<PageRange> rangesToShred = new ArrayList<>();
PageRange previousRange = null;
PageRange[] printedPages = printJob.getPages();
final int rangeCount = printedPages.length;
for (int i = 0; i < rangeCount; i++) {
PageRange range = printedPages[i];
if (previousRange == null) {
final int startPageIdx = 0;
final int endPageIdx = range.getStart() - 1;
if (startPageIdx <= endPageIdx) {
PageRange removedRange = new PageRange(startPageIdx, endPageIdx);
rangesToShred.add(removedRange);
}
} else {
final int startPageIdx = previousRange.getEnd() + 1;
final int endPageIdx = range.getStart() - 1;
if (startPageIdx <= endPageIdx) {
PageRange removedRange = new PageRange(startPageIdx, endPageIdx);
rangesToShred.add(removedRange);
}
}
if (i == rangeCount - 1) {
if (range.getEnd() != Integer.MAX_VALUE) {
rangesToShred.add(new PageRange(range.getEnd() + 1, Integer.MAX_VALUE));
}
}
previousRange = range;
}
PageRange[] result = new PageRange[rangesToShred.size()];
rangesToShred.toArray(result);
return result;
}
}
}