/* Copyright (C) 2014 Sergii Pylypenko. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.cups.android; import android.app.Activity; import android.app.Service; import android.content.Context; import android.os.Bundle; import android.os.IBinder; import android.view.MotionEvent; import android.view.KeyEvent; import android.view.Window; import android.view.WindowManager; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import android.widget.EditText; import android.text.Editable; import android.widget.Button; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.FrameLayout; import android.graphics.drawable.Drawable; import android.graphics.Color; import android.content.res.Configuration; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Intent; import android.view.View.OnKeyListener; import android.view.MenuItem; import android.view.Menu; import android.view.Gravity; import android.text.method.TextKeyListener; import java.util.LinkedList; import java.io.SequenceInputStream; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.FileOutputStream; import java.io.File; import java.io.FileInputStream; import java.util.Set; import android.text.SpannedString; import java.io.BufferedReader; import java.io.BufferedInputStream; import java.io.InputStreamReader; import android.view.inputmethod.InputMethodManager; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Handler; import android.os.Message; import android.os.SystemClock; import java.util.concurrent.Semaphore; import android.content.pm.ActivityInfo; import android.view.Display; import android.text.InputType; import android.util.Log; import android.view.Surface; import android.app.ProgressDialog; import java.util.ArrayList; import android.print.*; import android.printservice.*; import java.util.*; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; public class CupsPrintService extends PrintService { public static boolean pluginEnabled = false; @Override public void onCreate() { Log.d(TAG, "onCreate()"); super.onCreate(); pluginEnabled = true; PrintJobs.init(this); // Initialize cached paper sizes Cups.getMediaSize(this, "A4"); } @Override public void onDestroy() { Log.d(TAG, "onDestroy()"); super.onDestroy(); pluginEnabled = false; PrintJobs.destroy(); } @Override public void onConnected() { Log.d(TAG, "onConnected()"); super.onConnected(); if (!Installer.isInstalled(this)) { Intent dialogIntent = new Intent(getBaseContext(), MainActivity.class); dialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); getApplication().startActivity(dialogIntent); } else { Cups.startCupsDaemon(this); // Initialize cached printer list Cups.updatePrintersInfo(this); } } @Override public void onDisconnected() { Log.d(TAG, "onDisconnected()"); super.onDisconnected(); Cups.stopCupsDaemon(this); } class CupsPrinterDiscoverySession extends PrinterDiscoverySession implements Runnable { private boolean shouldExit = false; private HashSet<PrinterId> trackedPrinters = new HashSet<PrinterId>(); private Semaphore sem = new Semaphore(0); private Handler mainThread = null; CupsPrinterDiscoverySession() { mainThread = new Handler(CupsPrintService.this.getMainLooper()); new Thread(this).start(); } public synchronized void onDestroy() { Log.d(TAG, "onDestroy()"); shouldExit = true; sem.release(); } public synchronized void onStartPrinterDiscovery(List<PrinterId> priorityList) { // TODO: cache printer info, because invoking commandline tools is slow Log.d(TAG, "onStartPrinterDiscovery()"); final ArrayList<PrinterInfo> ret = new ArrayList<PrinterInfo>(); for (String pr: Cups.getPrinters(CupsPrintService.this)) { PrinterId id = generatePrinterId(pr); ret.add(getPrinterInfoFull(id)); } addPrinters(ret); } public synchronized void onStopPrinterDiscovery() { Log.d(TAG, "onStopPrinterDiscovery()"); } public synchronized void onStartPrinterStateTracking(PrinterId id) { Log.d(TAG, "onStartPrinterTracking(): " + id.getLocalId()); trackedPrinters.add(id); ArrayList<PrinterInfo> ret = new ArrayList<PrinterInfo>(); ret.add(getPrinterInfoFull(id)); addPrinters(ret); sem.release(); } public synchronized void onStopPrinterStateTracking(PrinterId id) { Log.d(TAG, "onStopPrinterStateTracking(): " + id.getLocalId()); trackedPrinters.remove(id); } public synchronized void onValidatePrinters(List<PrinterId> printerIds) { Log.d(TAG, "onValidatePrinters()"); ArrayList<PrinterInfo> ret = new ArrayList<PrinterInfo>(); for (PrinterId id: printerIds) { Log.d(TAG, "onValidatePrinters(): " + id.getLocalId()); ret.add(getPrinterInfoFull(id)); } addPrinters(ret); Log.d(TAG, "onValidatePrinters(): exit"); } public void run() { while (!shouldExit) { try { sem.tryAcquire(8, TimeUnit.SECONDS); } catch(Exception e) { } synchronized(this) { if (shouldExit) return; } Cups.updatePrintersInfo(CupsPrintService.this); synchronized(this) { if (shouldExit) return; if (!trackedPrinters.isEmpty()) { final ArrayList<PrinterInfo> ret = new ArrayList<PrinterInfo>(); for (PrinterId id: trackedPrinters) { ret.add(getPrinterInfoFull(id)); } Log.d(TAG, "onStartPrinterTracking(): finishing from discover thread"); mainThread.post(new Runnable() { public void run() { addPrinters(ret); } }); } } } } private PrinterInfo getPrinterInfoBasic(PrinterId id) { return getPrinterInfo(id, false); } private PrinterInfo getPrinterInfoFull(PrinterId id) { return getPrinterInfo(id, true); } private PrinterInfo getPrinterInfo(PrinterId id, boolean updateCaps) { String pr = id.getLocalId(); PrinterInfo.Builder pi = new PrinterInfo.Builder(id, pr, Cups.getPrinterStatus(CupsPrintService.this, pr)); pi.setDescription(""); pi.setName(pr); if (!updateCaps) return pi.build(); PrinterCapabilitiesInfo.Builder pc = new PrinterCapabilitiesInfo.Builder(id); Map<String, String[]> options = Cups.getPrinterOptions(CupsPrintService.this, pr); boolean hasPageSize = false; if (options.containsKey("PageSize")) { String pagesize[] = options.get("PageSize"); if (pagesize.length > 0 && Cups.getMediaSize(CupsPrintService.this, pagesize[0]) != null) { pc.addMediaSize(Cups.getMediaSize(CupsPrintService.this, pagesize[0]), true); hasPageSize = true; } for (int i = 1; i < pagesize.length; i++) if (Cups.getMediaSize(CupsPrintService.this, pagesize[i]) != null) pc.addMediaSize(Cups.getMediaSize(CupsPrintService.this, pagesize[i]), false); } if (!hasPageSize) { // Just so it won't crash pc.addMediaSize(PrintAttributes.MediaSize.ISO_A4, true); pc.addMediaSize(PrintAttributes.MediaSize.NA_LETTER, false); } boolean hasResolution = false; if (options.containsKey("Resolution")) { String res[] = options.get("Resolution"); if (res.length > 0) { pc.addResolution(Cups.getResolution(res[0]), true); hasResolution = true; } for (int i = 1; i < res.length; i++) pc.addResolution(Cups.getResolution(res[i]), false); } if (!hasResolution) { // Just so it won't crash pc.addResolution(new PrintAttributes.Resolution("Default", "Default", 300, 300), true); } pc.setColorModes(PrintAttributes.COLOR_MODE_COLOR | PrintAttributes.COLOR_MODE_MONOCHROME, PrintAttributes.COLOR_MODE_COLOR); pc.setMinMargins(PrintAttributes.Margins.NO_MARGINS); pi.setCapabilities(pc.build()); return pi.build(); } public static final String TAG = "CupsPrinterDiscoverySession"; } @Override public PrinterDiscoverySession onCreatePrinterDiscoverySession() { Log.d(TAG, "onCreatePrinterDiscoverySession()"); return new CupsPrinterDiscoverySession(); } @Override public void onPrintJobQueued(android.printservice.PrintJob job) { Log.d(TAG, "=============== onPrintJobQueued() ==============="); Map<String, String[]> options = Cups.getPrinterOptions(this, job.getInfo().getPrinterId().getLocalId()); HashSet<String> pageSizes = new HashSet(Arrays.asList(options.containsKey("PageSize") ? options.get("PageSize") : new String[] {"A4", "Letter"})); HashSet<String> resolutions = new HashSet(Arrays.asList(options.containsKey("Resolution") ? options.get("Resolution") : new String[] {})); boolean landscape = false; String mediaSize = "A4"; if (job.getInfo().getAttributes().getMediaSize() != null) { if (pageSizes.contains(job.getInfo().getAttributes().getMediaSize().getId())) mediaSize = job.getInfo().getAttributes().getMediaSize().getId(); else mediaSize = "Custom." + Math.round(job.getInfo().getAttributes().getMediaSize().asPortrait().getWidthMils() / 1000.0 / Cups.MillimetersToInches) + "x" + Math.round(job.getInfo().getAttributes().getMediaSize().asPortrait().getHeightMils() / 1000.0 / Cups.MillimetersToInches) + "mm"; landscape = !job.getInfo().getAttributes().getMediaSize().isPortrait(); } if (job.isQueued() && !job.isStarted()) job.start(); String[] jobId = Cups.printDocument( this, job, job.getInfo().getPrinterId().getLocalId(), job.getInfo().getLabel().length() > 0 ? job.getInfo().getLabel() : "PrintJob", job.getInfo().getCopies(), mediaSize, landscape, job.getInfo().getAttributes().getResolution() != null && resolutions.contains(job.getInfo().getAttributes().getResolution().getId()) ? job.getInfo().getAttributes().getResolution().getId() : null, job.getInfo().getPages() != null && job.getInfo().getPages().length > 0 && job.getInfo().getPages()[0].getStart() > 0 && job.getInfo().getPages()[0].getEnd() > 0 ? job.getInfo().getPages() : null ); if (jobId[0].length() > 0) { // TODO: do not complete job immediately, use getPrinterJobs() and report job actual status back to Android Log.d(TAG, "Printing document: job started: job ID " + jobId[0]); job.setTag(jobId[0]); PrintJobs.trackJob(job); } else { Log.d(TAG, "Printing document: job failed: " + jobId[1]); job.fail(jobId[1]); } } @Override public void onRequestCancelPrintJob(android.printservice.PrintJob job) { Log.d(TAG, "=============== onRequestCancelPrintJob() ==============="); PrintJobs.stopTrackingJob(job); if (job.getTag() != null && job.getTag().length() > 0) Cups.cancelPrintJob(this, job.getTag()); job.cancel(); } public static final String TAG = "CupsPrintService"; }