Bug 1078432 - Use Android print service to enable cloud printing r=sebastian

This commit is contained in:
Mark Finkle 2015-08-31 17:54:23 -04:00
Родитель b480648efc
Коммит 3f6cf75fef
15 изменённых файлов: 262 добавлений и 37 удалений

Просмотреть файл

@ -10,6 +10,7 @@ import org.mozilla.gecko.AppConstants.Versions;
import org.mozilla.gecko.DynamicToolbar.PinReason;
import org.mozilla.gecko.DynamicToolbar.VisibilityTransition;
import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
import org.mozilla.gecko.PrintHelper;
import org.mozilla.gecko.Tabs.TabEvents;
import org.mozilla.gecko.animation.PropertyAnimator;
import org.mozilla.gecko.animation.TransitionsTracker;
@ -3167,6 +3168,7 @@ public class BrowserApp extends GeckoApp
final MenuItem share = aMenu.findItem(R.id.share);
final MenuItem quickShare = aMenu.findItem(R.id.quickshare);
final MenuItem saveAsPDF = aMenu.findItem(R.id.save_as_pdf);
final MenuItem print = aMenu.findItem(R.id.print);
final MenuItem charEncoding = aMenu.findItem(R.id.char_encoding);
final MenuItem findInPage = aMenu.findItem(R.id.find_in_page);
final MenuItem desktopMode = aMenu.findItem(R.id.desktop_mode);
@ -3192,6 +3194,7 @@ public class BrowserApp extends GeckoApp
share.setEnabled(false);
quickShare.setEnabled(false);
saveAsPDF.setEnabled(false);
print.setEnabled(false);
findInPage.setEnabled(false);
// NOTE: Use MenuUtils.safeSetEnabled because some actions might
@ -3344,10 +3347,13 @@ public class BrowserApp extends GeckoApp
final boolean privateTabVisible = RestrictedProfiles.isAllowed(this, Restriction.DISALLOW_PRIVATE_BROWSING);
MenuUtils.safeSetVisible(aMenu, R.id.new_private_tab, privateTabVisible);
// Disable save as PDF for about:home and xul pages.
saveAsPDF.setEnabled(!(isAboutHome(tab) ||
// Disable PDF generation (save and print) for about:home and xul pages.
boolean allowPDF = (!(isAboutHome(tab) ||
tab.getContentType().equals("application/vnd.mozilla.xul+xml") ||
tab.getContentType().startsWith("video/")));
saveAsPDF.setEnabled(allowPDF);
print.setEnabled(allowPDF);
print.setVisible(Versions.feature19Plus && AppConstants.NIGHTLY_BUILD);
// Disable find in page for about:home, since it won't work on Java content.
findInPage.setEnabled(!isAboutHome(tab));
@ -3471,6 +3477,11 @@ public class BrowserApp extends GeckoApp
return true;
}
if (itemId == R.id.print) {
PrintHelper.printPDF(this);
return true;
}
if (itemId == R.id.settings) {
intent = new Intent(this, GeckoPreferences.class);

Просмотреть файл

@ -6,7 +6,6 @@
package org.mozilla.gecko;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
@ -47,6 +46,7 @@ import org.mozilla.gecko.util.EventCallback;
import org.mozilla.gecko.util.GeckoRequest;
import org.mozilla.gecko.util.HardwareCodecCapabilityUtils;
import org.mozilla.gecko.util.HardwareUtils;
import org.mozilla.gecko.util.IOUtils;
import org.mozilla.gecko.util.NativeEventListener;
import org.mozilla.gecko.util.NativeJSContainer;
import org.mozilla.gecko.util.NativeJSObject;
@ -1004,13 +1004,6 @@ public class GeckoAppShell
return type + "/" + subType;
}
static void safeStreamClose(Closeable stream) {
try {
if (stream != null)
stream.close();
} catch (IOException e) {}
}
static boolean isUriSafeForScheme(Uri aUri) {
// Bug 794034 - We don't want to pass MWI or USSD codes to the
// dialer, and ensure the Uri class doesn't parse a URI
@ -2576,13 +2569,13 @@ public class GeckoAppShell
// Only alter the intent when we're sure everything has worked
intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(imageFile));
} finally {
safeStreamClose(is);
IOUtils.safeStreamClose(is);
}
}
} catch(IOException ex) {
// If something went wrong, we'll just leave the intent un-changed
} finally {
safeStreamClose(os);
IOUtils.safeStreamClose(os);
}
}

Просмотреть файл

@ -0,0 +1,115 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import org.mozilla.gecko.AppConstants.Versions;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.util.GeckoRequest;
import org.mozilla.gecko.util.IOUtils;
import org.mozilla.gecko.util.NativeJSObject;
import org.mozilla.gecko.util.ThreadUtils;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import android.content.Context;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
import android.print.PrintAttributes;
import android.print.PrintDocumentAdapter;
import android.print.PrintDocumentAdapter.LayoutResultCallback;
import android.print.PrintDocumentAdapter.WriteResultCallback;
import android.print.PrintDocumentInfo;
import android.print.PrintManager;
import android.print.PageRange;
import android.util.Log;
public class PrintHelper {
private static final String LOGTAG = "GeckoPrintUtils";
public static void printPDF(final Context context) {
GeckoAppShell.sendRequestToGecko(new GeckoRequest("Print:PDF", new JSONObject()) {
@Override
public void onResponse(NativeJSObject nativeJSObject) {
final String filePath = nativeJSObject.getString("file");
final String title = nativeJSObject.getString("title");
finish(context, filePath, title);
}
@Override
public void onError(NativeJSObject error) {
// Gecko didn't respond due to state change, javascript error, etc.
Log.d(LOGTAG, "No response from Gecko on request to generate a PDF");
}
private void finish(final Context context, final String filePath, final String title) {
PrintManager printManager = (PrintManager) context.getSystemService(Context.PRINT_SERVICE);
String jobName = title;
// The adapter methods are all called on the UI thread by the PrintManager. Put the heavyweight code
// in onWrite on the background thread.
PrintDocumentAdapter pda = new PrintDocumentAdapter() {
@Override
public void onWrite(final PageRange[] pages, final ParcelFileDescriptor destination, final CancellationSignal cancellationSignal, final WriteResultCallback callback) {
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
InputStream input = null;
OutputStream output = null;
try {
File pdfFile = new File(filePath);
input = new FileInputStream(pdfFile);
output = new FileOutputStream(destination.getFileDescriptor());
byte[] buf = new byte[8192];
int bytesRead;
while ((bytesRead = input.read(buf)) > 0) {
output.write(buf, 0, bytesRead);
}
callback.onWriteFinished(new PageRange[] { PageRange.ALL_PAGES });
// File is not really deleted until the input stream closes it
pdfFile.delete();
} catch (FileNotFoundException ee) {
Log.d(LOGTAG, "Unable to find the temporary PDF file.");
} catch (IOException ioe) {
Log.e(LOGTAG, "IOException while transferring temporary PDF file: ", ioe);
} finally {
IOUtils.safeStreamClose(input);
IOUtils.safeStreamClose(output);
}
}
});
}
@Override
public void onLayout(PrintAttributes oldAttributes, PrintAttributes newAttributes, CancellationSignal cancellationSignal, LayoutResultCallback callback, Bundle extras){
if (cancellationSignal.isCanceled()) {
callback.onLayoutCancelled();
return;
}
PrintDocumentInfo pdi = new PrintDocumentInfo.Builder(filePath).setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT).build();
callback.onLayoutFinished(pdi, true);
}
};
printManager.print(jobName, pda, null);
}
});
}
}

Просмотреть файл

@ -347,6 +347,7 @@ size. -->
<!ENTITY share_title "Share via">
<!ENTITY share_image_failed "Unable to share this image">
<!ENTITY save_as_pdf "Save as PDF">
<!ENTITY print "Print">
<!ENTITY find_in_page "Find in Page">
<!ENTITY desktop_mode "Request Desktop Site">
<!ENTITY page "Page">

Просмотреть файл

@ -419,6 +419,7 @@ gbjar.sources += [
'preferences/SearchPreferenceCategory.java',
'preferences/SyncPreference.java',
'PrefsHelper.java',
'PrintHelper.java',
'PrivateTab.java',
'prompts/ColorPickerInput.java',
'prompts/IconGridInput.java',

Просмотреть файл

@ -72,6 +72,9 @@
<item android:id="@+id/save_as_pdf"
android:title="@string/save_as_pdf"/>
<item android:id="@+id/print"
android:title="@string/print"/>
<item android:id="@+id/add_search_engine"
android:title="@string/contextmenu_add_search_engine"/>

Просмотреть файл

@ -72,6 +72,9 @@
<item android:id="@+id/save_as_pdf"
android:title="@string/save_as_pdf"/>
<item android:id="@+id/print"
android:title="@string/print"/>
<item android:id="@+id/add_search_engine"
android:title="@string/contextmenu_add_search_engine"/>

Просмотреть файл

@ -73,6 +73,9 @@
<item android:id="@+id/save_as_pdf"
android:title="@string/save_as_pdf"/>
<item android:id="@+id/print"
android:title="@string/print"/>
<item android:id="@+id/add_search_engine"
android:title="@string/contextmenu_add_search_engine"/>

Просмотреть файл

@ -39,6 +39,10 @@
<item android:id="@+id/save_as_pdf"
android:title="@string/save_as_pdf" />
<item android:id="@+id/print"
android:visible="false"
android:title="@string/print" />
<item android:id="@+id/find_in_page"
android:title="@string/find_in_page" />

Просмотреть файл

@ -95,6 +95,7 @@
<string name="share_title">&share_title;</string>
<string name="share_image_failed">&share_image_failed;</string>
<string name="save_as_pdf">&save_as_pdf;</string>
<string name="print">&print;</string>
<string name="find_in_page">&find_in_page;</string>
<string name="find_matchcase">&find_matchcase;</string>
<string name="desktop_mode">&desktop_mode;</string>

Просмотреть файл

@ -7,6 +7,7 @@ package org.mozilla.gecko.util;
import android.util.Log;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
@ -108,4 +109,11 @@ public class IOUtils {
return newBytes;
}
public static void safeStreamClose(Closeable stream) {
try {
if (stream != null)
stream.close();
} catch (IOException e) {}
}
}

Просмотреть файл

@ -0,0 +1,65 @@
// -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
var PrintHelper = {
init: function() {
Services.obs.addObserver(this, "Print:PDF", false);
},
observe: function (aSubject, aTopic, aData) {
let browser = BrowserApp.selectedBrowser;
switch (aTopic) {
case "Print:PDF":
Messaging.handleRequest(aTopic, aData, (data) => {
return this.generatePDF(browser);
});
break;
}
},
generatePDF: function(aBrowser) {
// Create the final destination file location
let fileName = ContentAreaUtils.getDefaultFileName(aBrowser.contentTitle, aBrowser.currentURI, null, null);
fileName = fileName.trim() + ".pdf";
let file = Services.dirsvc.get("TmpD", Ci.nsIFile);
file.append(fileName);
file.createUnique(file.NORMAL_FILE_TYPE, parseInt("666", 8));
let printSettings = Cc["@mozilla.org/gfx/printsettings-service;1"].getService(Ci.nsIPrintSettingsService).newPrintSettings;
printSettings.printSilent = true;
printSettings.showPrintProgress = false;
printSettings.printBGImages = false;
printSettings.printBGColors = false;
printSettings.printToFile = true;
printSettings.toFileName = file.path;
printSettings.printFrameType = Ci.nsIPrintSettings.kFramesAsIs;
printSettings.outputFormat = Ci.nsIPrintSettings.kOutputFormatPDF;
let webBrowserPrint = aBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebBrowserPrint);
return new Promise((resolve, reject) => {
webBrowserPrint.print(printSettings, {
onStateChange: function(webProgress, request, stateFlags, status) {
// We get two STATE_STOP calls, one for STATE_IS_DOCUMENT and one for STATE_IS_NETWORK
if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP && stateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
if (Components.isSuccessCode(status)) {
// Send the details to Java
resolve({ file: file.path, title: fileName });
} else {
reject();
}
}
},
onProgressChange: function () {},
onLocationChange: function () {},
onStatusChange: function () {},
onSecurityChange: function () {},
});
});
}
};

Просмотреть файл

@ -153,6 +153,7 @@ let lazilyLoadedObserverScripts = [
["SelectionHandler", ["TextSelection:Get"], "chrome://browser/content/SelectionHandler.js"],
["EmbedRT", ["GeckoView:ImportScript"], "chrome://browser/content/EmbedRT.js"],
["Reader", ["Reader:FetchContent", "Reader:Added", "Reader:Removed"], "chrome://browser/content/Reader.js"],
["PrintHelper", ["Print:PDF"], "chrome://browser/content/PrintHelper.js"],
];
if (AppConstants.NIGHTLY_BUILD) {
lazilyLoadedObserverScripts.push(

Просмотреть файл

@ -41,6 +41,7 @@ chrome.jar:
content/MemoryObserver.js (content/MemoryObserver.js)
content/ConsoleAPI.js (content/ConsoleAPI.js)
content/PluginHelper.js (content/PluginHelper.js)
content/PrintHelper.js (content/PrintHelper.js)
content/OfflineApps.js (content/OfflineApps.js)
content/MasterPassword.js (content/MasterPassword.js)
content/FindHelper.js (content/FindHelper.js)

Просмотреть файл

@ -112,6 +112,43 @@ let Messaging = {
this.sendRequest(aMessage);
});
},
/**
* Handles a request from Java, using the given listener method.
* This is mainly an internal method used by the RequestHandler object, but can be
* used in nsIObserver.observe implmentations that fall outside the normal usage
* patterns.
*
* @param aTopic The string name of the message
* @param aData The data sent to the observe method from Java
* @param aListener A function that takes a JSON data argument and returns a
* response which is sent to Java.
*/
handleRequest: Task.async(function* (aTopic, aData, aListener) {
let wrapper = JSON.parse(aData);
try {
let response = yield aListener(wrapper.data);
if (typeof response !== "object" || response === null) {
throw new Error("Gecko request listener did not return an object");
}
Messaging.sendRequest({
type: "Gecko:Request" + wrapper.id,
response: response
});
} catch (e) {
Cu.reportError("Error in Messaging handler for " + aTopic + ": " + e);
Messaging.sendRequest({
type: "Gecko:Request" + wrapper.id,
error: {
message: e.message || (e && e.toString()),
stack: e.stack || Components.stack.formattedStack,
}
});
}
})
};
let requestHandler = {
@ -139,30 +176,8 @@ let requestHandler = {
Services.obs.removeObserver(this, aMessage);
},
observe: Task.async(function* (aSubject, aTopic, aData) {
let wrapper = JSON.parse(aData);
observe: function(aSubject, aTopic, aData) {
let listener = this._listeners[aTopic];
try {
let response = yield listener(wrapper.data);
if (typeof response !== "object" || response === null) {
throw new Error("Gecko request listener did not return an object");
}
Messaging.sendRequest({
type: "Gecko:Request" + wrapper.id,
response: response
});
} catch (e) {
Cu.reportError("Error in Messaging handler for " + aTopic + ": " + e);
Messaging.sendRequest({
type: "Gecko:Request" + wrapper.id,
error: {
message: e.message || (e && e.toString()),
stack: e.stack || Components.stack.formattedStack,
}
});
}
})
Messaging.handleRequest(aTopic, aData, listener);
}
};