1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.bips; 18 19 import android.app.Activity; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.graphics.Bitmap; 23 import android.graphics.BitmapFactory; 24 import android.graphics.Canvas; 25 import android.graphics.ColorMatrix; 26 import android.graphics.ColorMatrixColorFilter; 27 import android.graphics.ColorSpace; 28 import android.graphics.Paint; 29 import android.net.Uri; 30 import android.os.AsyncTask; 31 import android.os.Bundle; 32 import android.os.CancellationSignal; 33 import android.os.ParcelFileDescriptor; 34 import android.print.PageRange; 35 import android.print.PrintAttributes; 36 import android.print.PrintDocumentAdapter; 37 import android.print.PrintDocumentInfo; 38 import android.print.PrintJob; 39 import android.print.PrintJobId; 40 import android.print.PrintManager; 41 import android.util.DisplayMetrics; 42 import android.util.Log; 43 import android.webkit.URLUtil; 44 import android.widget.Toast; 45 46 import com.android.bips.jni.MediaSizes; 47 48 import java.io.IOException; 49 import java.io.InputStream; 50 import java.util.Arrays; 51 import java.util.HashSet; 52 import java.util.Locale; 53 import java.util.Set; 54 55 /** 56 * Activity to receive share-to-print intents for images. 57 */ 58 public class ImagePrintActivity extends Activity { 59 private static final String TAG = ImagePrintActivity.class.getSimpleName(); 60 private static final boolean DEBUG = false; 61 private static final int PRINT_DPI = 300; 62 private static final PrintAttributes.MediaSize DEFAULT_PHOTO_MEDIA = 63 PrintAttributes.MediaSize.NA_INDEX_4X6; 64 65 /** Countries where A5 is a more common photo media size. */ 66 private static final String[] ISO_A5_COUNTRY_CODES = { 67 "IQ", "SY", "YE", "VN", "MA" 68 }; 69 70 public static PrintJobId sPrintJobId; 71 72 private CancellationSignal mCancellationSignal = new CancellationSignal(); 73 private String mJobName; 74 private Bitmap mBitmap; 75 private DisplayMetrics mDisplayMetrics = new DisplayMetrics(); 76 private Runnable mOnBitmapLoaded = null; 77 private AsyncTask<?, ?, ?> mTask = null; 78 private PrintJob mPrintJob; 79 private Bitmap mGrayscaleBitmap; 80 private PrintAttributes.MediaSize mDefaultMediaSize = null; 81 82 @Override onCreate(Bundle savedInstanceState)83 protected void onCreate(Bundle savedInstanceState) { 84 super.onCreate(savedInstanceState); 85 String action = getIntent().getAction(); 86 Uri contentUri = null; 87 if (Intent.ACTION_SEND.equals(action)) { 88 contentUri = getIntent().getParcelableExtra(Intent.EXTRA_STREAM); 89 } else if (Intent.ACTION_VIEW.equals(action)) { 90 contentUri = getIntent().getData(); 91 } 92 if (contentUri == null) { 93 finish(); 94 } 95 getWindowManager().getDefaultDisplay().getMetrics(mDisplayMetrics); 96 mJobName = URLUtil.guessFileName(getIntent().getStringExtra(Intent.EXTRA_TEXT), null, 97 getIntent().resolveType(this)); 98 99 if (DEBUG) Log.d(TAG, "onCreate() uri=" + contentUri + " jobName=" + mJobName); 100 101 // Load the bitmap while we start the print 102 mTask = new LoadBitmapTask().execute(contentUri); 103 } 104 105 /** 106 * A background task to load the bitmap and start the print job. 107 */ 108 private class LoadBitmapTask extends AsyncTask<Uri, Boolean, Bitmap> { 109 @Override doInBackground(Uri... uris)110 protected Bitmap doInBackground(Uri... uris) { 111 if (DEBUG) Log.d(TAG, "Loading bitmap from stream"); 112 BitmapFactory.Options options = new BitmapFactory.Options(); 113 options.inJustDecodeBounds = true; 114 loadBitmap(uris[0], options); 115 if (options.outWidth <= 0 || options.outHeight <= 0) { 116 Log.w(TAG, "Failed to load bitmap"); 117 return null; 118 } 119 if (mCancellationSignal.isCanceled()) { 120 return null; 121 } else { 122 // Publish progress and load for real 123 publishProgress(options.outHeight > options.outWidth); 124 options.inJustDecodeBounds = false; 125 options.inPreferredColorSpace = ColorSpace.get(ColorSpace.Named.SRGB); 126 return loadBitmap(uris[0], options); 127 } 128 } 129 130 /** 131 * Return a bitmap as loaded from {@param contentUri} using {@param options}. 132 */ loadBitmap(Uri contentUri, BitmapFactory.Options options)133 private Bitmap loadBitmap(Uri contentUri, BitmapFactory.Options options) { 134 try (InputStream inputStream = getContentResolver().openInputStream(contentUri)) { 135 return BitmapFactory.decodeStream(inputStream, null, options); 136 } catch (IOException | SecurityException e) { 137 Log.w(TAG, "Failed to load bitmap", e); 138 return null; 139 } 140 } 141 142 @Override onProgressUpdate(Boolean... values)143 protected void onProgressUpdate(Boolean... values) { 144 // Once we have a portrait/landscape determination, launch the print job 145 boolean isPortrait = values[0]; 146 if (DEBUG) Log.d(TAG, "startPrint(portrait=" + isPortrait + ")"); 147 PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE); 148 if (printManager == null) { 149 finish(); 150 return; 151 } 152 153 PrintAttributes printAttributes = new PrintAttributes.Builder() 154 .setMediaSize(isPortrait ? getLocaleDefaultMediaSize() : 155 getLocaleDefaultMediaSize().asLandscape()) 156 .setColorMode(PrintAttributes.COLOR_MODE_COLOR) 157 .build(); 158 mPrintJob = printManager.print(mJobName, new ImageAdapter(), printAttributes); 159 sPrintJobId = mPrintJob.getId(); 160 } 161 162 @Override onPostExecute(Bitmap bitmap)163 protected void onPostExecute(Bitmap bitmap) { 164 if (mCancellationSignal.isCanceled()) { 165 if (DEBUG) Log.d(TAG, "LoadBitmapTask cancelled"); 166 } else if (bitmap == null) { 167 if (mPrintJob != null) { 168 mPrintJob.cancel(); 169 } 170 Toast.makeText(ImagePrintActivity.this, R.string.unreadable_input, 171 Toast.LENGTH_LONG).show(); 172 finish(); 173 } else { 174 if (DEBUG) Log.d(TAG, "LoadBitmapTask complete"); 175 mBitmap = bitmap; 176 if (mOnBitmapLoaded != null) { 177 mOnBitmapLoaded.run(); 178 } 179 } 180 } 181 } 182 getLocaleDefaultMediaSize()183 private PrintAttributes.MediaSize getLocaleDefaultMediaSize() { 184 if (mDefaultMediaSize == null) { 185 String country = getResources().getConfiguration().getLocales().get(0).getCountry(); 186 Set<String> a5Countries = new HashSet<>(Arrays.asList(ISO_A5_COUNTRY_CODES)); 187 if (Locale.JAPAN.getCountry().equals(country)) { 188 // Photo L is a more common media size in Japan 189 mDefaultMediaSize = new PrintAttributes.MediaSize(MediaSizes.OE_PHOTO_L, 190 getString(R.string.media_size_l), 3500, 5000); 191 } else if (a5Countries.contains(country)) { 192 mDefaultMediaSize = PrintAttributes.MediaSize.ISO_A5; 193 } else { 194 mDefaultMediaSize = DEFAULT_PHOTO_MEDIA; 195 } 196 } 197 return mDefaultMediaSize; 198 } 199 200 @Override onDestroy()201 protected void onDestroy() { 202 if (DEBUG) Log.d(TAG, "onDestroy()"); 203 mCancellationSignal.cancel(); 204 if (mTask != null) { 205 mTask.cancel(true); 206 mTask = null; 207 } 208 if (mBitmap != null) { 209 mBitmap.recycle(); 210 mBitmap = null; 211 } 212 if (mGrayscaleBitmap != null) { 213 mGrayscaleBitmap.recycle(); 214 mGrayscaleBitmap = null; 215 } 216 super.onDestroy(); 217 } 218 219 /** 220 * An adapter that converts the image to PDF format as requested by the print system 221 */ 222 private class ImageAdapter extends PrintDocumentAdapter { 223 private PrintAttributes mAttributes; 224 private int mDpi; 225 226 @Override onLayout(PrintAttributes oldAttributes, PrintAttributes newAttributes, CancellationSignal cancellationSignal, LayoutResultCallback callback, Bundle bundle)227 public void onLayout(PrintAttributes oldAttributes, PrintAttributes newAttributes, 228 CancellationSignal cancellationSignal, LayoutResultCallback callback, 229 Bundle bundle) { 230 if (DEBUG) Log.d(TAG, "onLayout() attrs=" + newAttributes); 231 232 if (mBitmap == null) { 233 if (DEBUG) Log.d(TAG, "waiting for bitmap..."); 234 // Try again when bitmap has arrived 235 mOnBitmapLoaded = () -> onLayout(oldAttributes, newAttributes, cancellationSignal, 236 callback, bundle); 237 return; 238 } 239 240 int oldDpi = mDpi; 241 mAttributes = newAttributes; 242 243 // Calculate required DPI (print or display) 244 if (bundle.getBoolean(EXTRA_PRINT_PREVIEW, false)) { 245 PrintAttributes.MediaSize mediaSize = mAttributes.getMediaSize(); 246 mDpi = Math.min( 247 mDisplayMetrics.widthPixels * 1000 / mediaSize.getWidthMils(), 248 mDisplayMetrics.heightPixels * 1000 / mediaSize.getHeightMils()); 249 } else { 250 mDpi = PRINT_DPI; 251 } 252 253 PrintDocumentInfo info = new PrintDocumentInfo.Builder(mJobName) 254 .setContentType(PrintDocumentInfo.CONTENT_TYPE_PHOTO) 255 .setPageCount(1) 256 .build(); 257 callback.onLayoutFinished(info, !newAttributes.equals(oldAttributes) || oldDpi != mDpi); 258 } 259 260 @Override onWrite(PageRange[] pageRanges, ParcelFileDescriptor fileDescriptor, CancellationSignal cancellationSignal, WriteResultCallback callback)261 public void onWrite(PageRange[] pageRanges, ParcelFileDescriptor fileDescriptor, 262 CancellationSignal cancellationSignal, WriteResultCallback callback) { 263 if (DEBUG) Log.d(TAG, "onWrite()"); 264 mCancellationSignal = cancellationSignal; 265 266 mTask = new ImageToPdfTask(ImagePrintActivity.this, getBitmap(mAttributes), mAttributes, 267 mDpi, cancellationSignal) { 268 @Override 269 protected void onPostExecute(Throwable throwable) { 270 if (cancellationSignal.isCanceled()) { 271 if (DEBUG) Log.d(TAG, "writeBitmap() cancelled"); 272 callback.onWriteCancelled(); 273 } else if (throwable != null) { 274 Log.w(TAG, "Failed to write bitmap", throwable); 275 callback.onWriteFailed(null); 276 } else { 277 if (DEBUG) Log.d(TAG, "Calling onWriteFinished"); 278 callback.onWriteFinished(new PageRange[] { PageRange.ALL_PAGES }); 279 } 280 mTask = null; 281 } 282 }.execute(fileDescriptor); 283 } 284 285 @Override onFinish()286 public void onFinish() { 287 if (DEBUG) Log.d(TAG, "onFinish()"); 288 finish(); 289 } 290 } 291 292 /** 293 * Return an appropriate bitmap to use when rendering {@param attributes}. 294 */ getBitmap(PrintAttributes attributes)295 private Bitmap getBitmap(PrintAttributes attributes) { 296 if (attributes.getColorMode() == PrintAttributes.COLOR_MODE_MONOCHROME) { 297 if (mGrayscaleBitmap == null) { 298 mGrayscaleBitmap = Bitmap.createBitmap(mBitmap.getWidth(), mBitmap.getHeight(), 299 Bitmap.Config.ARGB_8888); 300 Canvas canvas = new Canvas(mGrayscaleBitmap); 301 Paint paint = new Paint(); 302 ColorMatrix colorMatrix = new ColorMatrix(); 303 colorMatrix.setSaturation(0); 304 paint.setColorFilter(new ColorMatrixColorFilter(colorMatrix)); 305 canvas.drawBitmap(mBitmap, 0, 0, paint); 306 } 307 return mGrayscaleBitmap; 308 } else { 309 return mBitmap; 310 } 311 } 312 313 /** 314 * Get the print job id from PrintManager created print job. 315 * 316 * @return A PrintJobId, can be null 317 */ getLastPrintJobId()318 public static PrintJobId getLastPrintJobId() { 319 return sPrintJobId; 320 } 321 } 322