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