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