/*
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 android.print.*;
import android.printservice.*;
import java.util.*;
import java.io.*;
import android.os.Environment;
import android.os.StatFs;
import java.net.URL;
import android.net.Uri;
public class Cups
{
static String IMG = "/img";
static String PROOT = "./proot.sh";
static String CUPSD = "/usr/sbin/cupsd";
static String LP = "/usr/bin/lp";
static String LPSTAT = "/usr/bin/lpstat";
static String LPOPTIONS = "/usr/bin/lpoptions";
static String LPINFO = "/usr/sbin/lpinfo";
static String LPADMIN = "/usr/sbin/lpadmin";
static String CANCEL = "/usr/bin/cancel";
static String CUPSACCEPT = "/usr/sbin/cupsaccept";
static String CUPSENABLE = "/usr/sbin/cupsenable";
static String DBUS = "/usr/bin/dbus-daemon";
static Process cupsd = null;
static Process dbus = null;
public static final double PointsToMillimeters = 0.35277777778;
public static final double MillimetersToPoints = 1.0 / PointsToMillimeters;
public static final double MillimetersToInches = 0.03937007874;
public static File chrootPath(Context p)
{
return new File(p.getFilesDir().getAbsolutePath() + IMG);
}
synchronized public static boolean isRunning(Context p)
{
Proc pp = new Proc(new String[] {PROOT, LPSTAT, "-r"}, chrootPath(p));
return pp.out.length > 0 && pp.out[0].equals("scheduler is running");
}
private static String[] printers = null;
private static HashSet<String> printerIsBusy = new HashSet<String>();
private static HashMap<String, HashMap<String, String[]> > printerOptions = new HashMap<String, HashMap<String, String[]> >();
synchronized private static void setPrinterList(String [] _printers, HashSet<String> _printerIsBusy, HashMap<String, HashMap<String, String[]> > _printerOptions)
{
printers = _printers;
printerIsBusy = _printerIsBusy;
printerOptions = _printerOptions;
}
public static void updatePrintersInfo(Context p)
{
ArrayList<String> printerList = new ArrayList<String>();
Proc pp = new Proc(new String[] {PROOT, LPSTAT, "-v"}, chrootPath(p));
for (String s: pp.out)
{
if (!s.startsWith("device for ") || s.indexOf(":") == -1)
continue;
printerList.add(s.substring(("device for ").length(), s.indexOf(":")));
}
HashSet<String> busy = new HashSet<String>();
for(String printer: printerList)
{
pp = new Proc(new String[] {PROOT, LPSTAT, "-p", printer}, chrootPath(p));
if (pp.out.length == 0 || pp.status != 0)
continue;
if (pp.out[0].indexOf("is idle") == -1)
busy.add(printer);
}
HashMap<String, HashMap<String, String[]> > allOptions = new HashMap<String, HashMap<String, String[]> >();
for(String printer: printerList)
{
pp = new Proc(new String[] {PROOT, LPOPTIONS, "-p", printer, "-l"}, chrootPath(p));
if (pp.out.length == 0 || pp.status != 0)
continue;
HashMap<String, String[]> options = new HashMap<String, String[]>();
for(String s: pp.out)
{
if (s.indexOf("/") == -1 || s.indexOf(": ") == -1)
continue;
String k = s.substring(0, s.indexOf("/"));
String vv[] = s.substring(s.indexOf(": ") + 2).split("\\s+");
for (int i = 0; i < vv.length; i++)
{
if (vv[i].startsWith("*"))
{
String dd = vv[i].substring(1);
vv[i] = vv[0];
vv[0] = dd;
break;
}
}
options.put(k, vv);
}
allOptions.put(printer, options);
}
setPrinterList(printerList.toArray(new String[0]), busy, allOptions);
}
synchronized public static String[] getPrinters(Context p)
{
if (printers == null)
updatePrintersInfo(p);
return printers;
}
synchronized public static int getPrinterStatus(Context p, String printer)
{
if (printers == null)
updatePrintersInfo(p);
if (!Arrays.asList(printers).contains(printer))
return PrinterInfo.STATUS_UNAVAILABLE;
if (!printerIsBusy.contains(printer))
return PrinterInfo.STATUS_IDLE;
return PrinterInfo.STATUS_BUSY;
}
synchronized public static Map<String, String[]> getPrinterOptions(Context p, String printer)
{
if (printers == null)
updatePrintersInfo(p);
if (printerOptions.containsKey(printer))
return printerOptions.get(printer);
return new HashMap<String, String[]>();
}
synchronized public static Map<String, String[]> getPrintJobs(Context p, String printer, boolean completedJobs)
{
HashMap<String, String[]> ret = new HashMap<String, String[]>();
Proc pp;
if (completedJobs)
pp = new Proc(new String[] {PROOT, LPSTAT, "-W", "completed", "-l", printer}, chrootPath(p));
else
pp = new Proc(new String[] {PROOT, LPSTAT, "-l", printer}, chrootPath(p));
if (pp.out.length == 0 || pp.status != 0)
return ret;
String currentJob = null;
ArrayList<String> jobAttrs = new ArrayList();
for (String s: pp.out)
{
if (s.trim().length() == 0)
continue;
if (s.startsWith(" ") || s.startsWith("\t"))
{
jobAttrs.add(s.trim());
}
else
{
if (currentJob != null)
ret.put(currentJob, jobAttrs.toArray(new String[0]));
currentJob = s.split("\\s+")[0];
jobAttrs = new ArrayList();
}
}
if (currentJob != null)
ret.put(currentJob, jobAttrs.toArray(new String[0]));
return ret;
}
synchronized public static Uri getPrinterAddress(Context p, String printer)
{
ArrayList<String> printerList = new ArrayList<String>();
Proc pp = new Proc(new String[] {PROOT, LPSTAT, "-v", printer}, chrootPath(p));
String addr = null;
for (String s: pp.out)
{
if (!s.startsWith("device for ") || s.indexOf(":") == -1)
continue;
addr = s.substring(s.indexOf(":") + 1);
break;
}
if (addr == null)
{
Log.d(TAG, "getPrinterAddress: addr == null");
return null;
}
addr = addr.trim();
Log.d(TAG, "getPrinterAddress: addr = " + addr);
if (!addr.startsWith("smb://"))
{
Log.d(TAG, "getPrinterAddress: addr.startsWith(smb://)");
return null;
}
addr = addr.substring("smb://".length());
String[] parts = addr.split("/");
Uri.Builder uri = new Uri.Builder();
uri.scheme(p.getResources().getString(R.string.add_printer_scheme));
uri.authority(p.getResources().getString(R.string.add_printer_host));
uri.appendQueryParameter("n", printer);
if (parts.length >= 3)
{
uri.appendQueryParameter("d", parts[0]);
uri.appendQueryParameter("s", parts[1]);
uri.appendQueryParameter("p", parts[2]);
}
else if (parts.length == 2)
{
uri.appendQueryParameter("s", parts[0]);
uri.appendQueryParameter("p", parts[1]);
}
else
{
Log.d(TAG, "getPrinterAddress: parts.length < 2");
return null;
}
pp = new Proc(new String[] {PROOT, LPOPTIONS, "-p", printer}, chrootPath(p));
String model = null;
String MODEL_STR = "printer-make-and-model='";
for (String s: pp.out)
{
if (s.indexOf(MODEL_STR) == -1)
continue;
String model1 = s.substring(s.indexOf(MODEL_STR) + MODEL_STR.length());
if (model1.indexOf("'") == -1)
continue;
model = model1.substring(0, model1.indexOf("'"));
break;
}
if (model == null)
{
Log.d(TAG, "getPrinterAddress: model == null");
return null;
}
uri.appendQueryParameter("m", model);
return uri.build();
}
synchronized public static void cancelPrintJob(Context p, String job)
{
Proc pp = new Proc(new String[] {PROOT, CANCEL, job}, chrootPath(p));
Log.d(TAG, "Cancel job status: " + pp.status + " output: " + Arrays.toString(pp.out));
}
synchronized public static void enablePrinter(Context p, String printer)
{
Proc pp = new Proc(new String[] {PROOT, CUPSACCEPT, printer}, chrootPath(p));
Log.d(TAG, "cupsaccept printer status: " + pp.status + " output: " + Arrays.toString(pp.out));
pp = new Proc(new String[] {PROOT, CUPSENABLE, printer}, chrootPath(p));
Log.d(TAG, "cupsenable printer status: " + pp.status + " output: " + Arrays.toString(pp.out));
}
private static Map<String, PrintAttributes.MediaSize> mediaSizes = null;
synchronized public static PrintAttributes.MediaSize getMediaSize(Context p, String name)
{
if (mediaSizes == null)
fillMediaSizes(p);
return mediaSizes.get(name);
}
private static void fillMediaSizes(Context p)
{
mediaSizes = new HashMap<String, PrintAttributes.MediaSize>();
try
{
BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(
chrootPath(p).getAbsolutePath() + "/usr/share/cups/ppdc/media.defs")));
String line;
while((line = in.readLine()) != null)
{
if (!line.startsWith("#media \"") || line.indexOf("/") == -1)
continue;
line = line.replace("\\\"", "”");
int slash = line.indexOf("/");
String name = line.substring("#media \"".length(), slash);
String descr = line.substring(slash + 1, line.indexOf("\"", slash + 1));
String sizes[] = line.substring(line.indexOf("\"", slash + 1) + 1).trim().split("\\s+");
//Log.d(TAG, "fillMediaSizes: dimensions: " + Arrays.toString(sizes) + " for " + name);
if (sizes.length < 2)
continue;
int w = (int)Math.round(Integer.parseInt(sizes[0]) * PointsToMillimeters * MillimetersToInches * 1000.0f);
int h = (int)Math.round(Integer.parseInt(sizes[1]) * PointsToMillimeters * MillimetersToInches * 1000.0f);
Log.d(TAG, "fillMediaSizes: " + name + " desc '" + descr + "' size " + w + "x" + h + " inches/1000" );
mediaSizes.put(name, new PrintAttributes.MediaSize(name, descr, w, h));
}
in.close();
}
catch(Exception e)
{
Log.i(TAG, "Error reading media sizes: " + e.toString());
}
}
public static PrintAttributes.Resolution getResolution(String s)
{
String rr[] = s.split("[^0-9]+");
if (rr.length == 0)
return new PrintAttributes.Resolution(s, s, 300, 300);
if (rr.length == 1)
return new PrintAttributes.Resolution(s, s, Integer.parseInt(rr[0]), Integer.parseInt(rr[0]));
return new PrintAttributes.Resolution(s, s, Integer.parseInt(rr[0]), Integer.parseInt(rr[1]));
}
synchronized public static void addPrinter(Context p, String name, String host, String printer, String model, String workgroup, String username, String password)
{
// TODO: user password will be accessbile through /proc filesystem to all processes, for several seconds while the command is executing
// lpadmin does not provide any other convenient way of passing passwords though, and I don't want to mess up with lpoptions
name = name.trim().replaceAll("[ /#]", "-");
host = host.trim();
printer = printer.trim();
model = model.trim();
workgroup = workgroup.trim();
username = username.trim();
password = password.trim();
String url = "smb://";
if (username.length() > 0 && password.length() > 0)
url = url + username + ":" + password + "@";
if (workgroup.length() > 0)
url = url + workgroup + "/";
url = url + host + "/";
url = url + printer;
Proc pp = new Proc(new String[] {PROOT, LPADMIN, "-p", name, "-v", url, "-m", model, "-o", "printer-error-policy=retry-job", "-E"}, chrootPath(p));
Log.d(TAG, "Add printer status: " + pp.status + " output: " + Arrays.toString(pp.out));
}
synchronized public static void deletePrinter(Context p, String name)
{
new Proc(new String[] {PROOT, LPADMIN, "-x", name}, chrootPath(p));
}
synchronized public static String[] printDocument( final Context p,
final android.printservice.PrintJob job,
final String printer,
final String jobLabel,
int copies,
final String mediaSize,
boolean landscape,
final String resolution,
final PageRange[] pages )
{
updateDns(p);
final String[] ret = new String[] { "", "" };
final String PIPE = "document.pdf";
File pipeFile = new File(chrootPath(p), PIPE);
pipeFile.delete();
OutputStream out = null;
try
{
Log.d(TAG, "Printing document: copying data to " + PIPE);
out = new FileOutputStream(pipeFile);
// We have to call job.getDocument().getData() right before we're starting to read from this file, otherwise we'll get crash inside PrintSpoolerService
InputStream in = new FileInputStream(job.getDocument().getData().getFileDescriptor());
int len = copyStream(in, out);
Log.d(TAG, "Printing document: finished copying data to pipe: " + len + " bytes");
}
catch(Exception e)
{
Log.i(TAG, "Error printing document: " + e.toString());
ret[1] += e.toString();
try
{
if (out != null)
out.close();
}
catch(Exception ee)
{
}
return ret;
}
ArrayList<String> params = new ArrayList<String>();
params.add(PROOT);
params.add(LP);
params.add("-d");
params.add(printer);
params.add("-n");
params.add(String.valueOf(copies));
params.add("-t");
params.add(jobLabel);
params.add("-o");
params.add("media=" + mediaSize);
if (landscape)
{
params.add("-o");
params.add("landscape");
}
if (resolution != null)
{
params.add("-o");
params.add("Resolution=" + resolution);
}
if (Options.DoubleSided.get(job).lpOption != null)
{
params.add("-o");
params.add(Options.DoubleSided.get(job).lpOption);
}
if (Options.MultiplePages.get(job).lpOption != null)
{
params.add("-o");
params.add(Options.MultiplePages.get(job).lpOption);
}
if (pages != null)
{
params.add("-P");
String pagesStr = "";
for (PageRange r: pages)
{
if (pagesStr.length() > 0)
pagesStr = pagesStr + ",";
pagesStr += String.valueOf(r.getStart() + 1);
if (r.getStart() != r.getEnd())
pagesStr += "-" + String.valueOf(r.getEnd() + 1);
}
params.add(pagesStr);
}
// Chrome tends to pass bigger image than our paper size, making printer waste two pages, so we'll resize it to fit
params.add("-o");
params.add("fit-to-page");
// if (job.getInfo().getAttributes().getMinMargins() != null) // Not supported yet
params.add("/" + PIPE);
Log.i(TAG, "Printing document command: " + Arrays.toString(params.toArray(new String[0])));
Proc lp = new Proc(params.toArray(new String[0]), chrootPath(p));
Log.i(TAG, "Printing document finished: status: " + lp.status + " msg: " + Arrays.toString(lp.out));
pipeFile.delete();
if (lp.status != 0) // There was an error
{
ret[0] = "";
ret[1] += Arrays.toString(lp.out);
}
else if (lp.out.length > 0 && lp.out[0].startsWith("request id is "))
{
ret[0] = lp.out[0].substring("request id is ".length()).trim().split("\\s+")[0];
}
return ret;
}
synchronized public static Map<String, String> getPrinterModels(Context p)
{
final String modelsFileName = "printer-models.txt";
File modelsFile = new File(chrootPath(p), modelsFileName);
if (!modelsFile.exists() || modelsFile.length() < 100000)
{
Proc pp = new Proc(new String[] {PROOT, "/bin/sh", "-c", LPINFO + " -m > " + modelsFileName}, chrootPath(p));
}
TreeMap<String, String> models = new TreeMap<String, String>();
try
{
BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(modelsFile)));
String s;
while ((s = in.readLine()) != null)
{
if (s.indexOf(" ") == -1)
continue;
models.put(s.substring(s.indexOf(" ") + 1), s.substring(0, s.indexOf(" ")));
}
}
catch(Exception e)
{
Log.i(TAG, "Cannot read " + modelsFileName + ": " + e.toString());
}
return models;
}
synchronized public static void startCupsDaemon(Context p)
{
if (cupsd != null && isDaemonRunning(p))
return;
restartCupsDaemon(p);
}
synchronized public static void stopCupsDaemon(Context p)
{
if (cupsd == null)
return;
cupsd.destroy();
dbus.destroy();
cupsd = null;
}
synchronized public static void restartCupsDaemon(Context p)
{
if (cupsd != null)
{
cupsd.destroy();
dbus.destroy();
}
cupsd = null;
dbus = null;
try
{
updateDns(p);
dbus = Runtime.getRuntime().exec(new String[] {PROOT, DBUS, "--system"}, null, chrootPath(p));
cupsd = Runtime.getRuntime().exec(new String[] {PROOT, CUPSD, "-f"}, null, chrootPath(p));
for (int i = 0; i < 10 && !isDaemonRunning(p); i++)
{
try
{
Thread.sleep(200);
}
catch(InterruptedException e)
{
}
}
}
catch(IOException e)
{
}
}
public static boolean isDaemonRunning(Context p)
{
Proc pp = new Proc(new String[] {PROOT, LPSTAT, "-r"}, chrootPath(p));
if (pp.status != 0 || pp.out.length < 1 || pp.out[0].indexOf("scheduler is running") != 0)
return false;
return true;
}
synchronized public static void updateDns(Context p)
{
Proc pp = new Proc(new String[] {"./update-dns.sh"}, chrootPath(p));
}
public static String[] getNetworkTree(Context p, String login, String password, String domain)
{
if (login.length() > 0 && password.length() > 0)
{
File auth = null;
try
{
auth = File.createTempFile("auth-", ".txt", chrootPath(p));
auth.setReadable(false, false);
auth.setReadable(true, true);
auth.setWritable(false, false);
auth.setWritable(true, true);
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(auth), "utf-8"));
out.write("username = " + login + "\n");
out.write("password = " + password + "\n");
if (domain.length() > 0)
out.write("domain = " + domain + "\n");
out.close();
}
catch(Exception e)
{
return new Proc(new String[] {PROOT, "/usr/bin/smbtree", "-N", }, chrootPath(p)).out;
}
Proc ret = new Proc(new String[] {PROOT, "/usr/bin/smbtree", "-A", "/" + auth.getName()}, chrootPath(p));
auth.delete();
return ret.out;
}
updateDns(p);
return new Proc(new String[] {PROOT, "/usr/bin/smbtree", "-N", }, chrootPath(p)).out;
}
public static int copyStream(InputStream stream, OutputStream out) throws java.io.IOException
{
byte[] buf = new byte[16384];
int len = stream.read(buf);
int totalLen = 0;
while (len >= 0)
{
if(len > 0)
out.write(buf, 0, len);
totalLen += len;
len = stream.read(buf);
}
stream.close();
out.close();
return totalLen;
}
private static final String TAG = "Cups";
}