Pdf.js with open, print and save enabled

 This post shows how to use Pdf.js in Sketchware project with OPEN, PRINT AND SAVE enabled.


1. Create a new project in Sketchware pro (package name in my project is com.pdf.onweb). In main.xml add a WebView pdfWebView.

2. Switch On AppCompat and design.

3. Download pdf.js from following link:
or

4. Extract the contents of the downloaded zip file.

5. In Sketchware pro project, in Asset Manager, add a sample pdf file and rename it as sample.pdf. Also, create a new folder pdfjs.

6. In pdfjs folder import all the contents extracted from the downloaded zip file.

7. In assets folder, for showing custom error page, add error.html. Below is a sample error.html page.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Error Loading PDF</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        }
        
        body {
            background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
            min-height: 100vh;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            padding: 20px;
            color: #333;
        }
        
        .error-container {
            max-width: 600px;
            width: 100%;
            background-color: white;
            border-radius: 12px;
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
            padding: 40px;
            text-align: center;
            position: relative;
            overflow: hidden;
        }
        
        .error-container::before {
            content: '';
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 6px;
            background: linear-gradient(90deg, #ff6b6b, #4ecdc4, #45b7d1);
        }
        
        .error-icon {
            font-size: 80px;
            margin-bottom: 20px;
            color: #ff6b6b;
        }
        
        h1 {
            font-size: 28px;
            margin-bottom: 15px;
            color: #2c3e50;
        }
        
        .error-message {
            font-size: 18px;
            line-height: 1.6;
            margin-bottom: 25px;
            color: #555;
        }
        
        .error-details {
            background-color: #f8f9fa;
            border-left: 4px solid #4ecdc4;
            padding: 15px;
            margin: 25px 0;
            text-align: left;
            border-radius: 0 8px 8px 0;
        }
        
        .error-details h3 {
            margin-bottom: 10px;
            color: #2c3e50;
        }
        
        .error-details ul {
            padding-left: 20px;
        }
        
        .error-details li {
            margin-bottom: 8px;
        }
        
        .action-buttons {
            display: flex;
            flex-wrap: wrap;
            gap: 15px;
            justify-content: center;
            margin-top: 30px;
        }
        
        .btn {
            padding: 12px 24px;
            border: none;
            border-radius: 6px;
            font-size: 16px;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.3s ease;
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 8px;
        }
        
        .btn-primary {
            background-color: #4ecdc4;
            color: white;
        }
        
        .btn-primary:hover {
            background-color: #3db8af;
            transform: translateY(-2px);
        }
        
        .btn-secondary {
            background-color: #f8f9fa;
            color: #495057;
            border: 1px solid #dee2e6;
        }
        
        .btn-secondary:hover {
            background-color: #e9ecef;
            transform: translateY(-2px);
        }
        
        .btn-icon {
            font-size: 18px;
        }
        
        .contact-support {
            margin-top: 30px;
            font-size: 14px;
            color: #6c757d;
        }
        
        .contact-support a {
            color: #4ecdc4;
            text-decoration: none;
        }
        
        .contact-support a:hover {
            text-decoration: underline;
        }
        
        @media (max-width: 600px) {
            .error-container {
                padding: 30px 20px;
            }
            
            .action-buttons {
                flex-direction: column;
            }
            
            .btn {
                width: 100%;
            }
        }
    </style>
</head>
<body>
    <div class="error-container">
        <div class="error-icon">📄❌</div>
        <h1>We're having trouble loading this PDF</h1>
        
        <div class="error-message">
            The PDF file you're trying to access cannot be loaded. This might be due to one of the following reasons:
        </div>
        
        <div class="error-details">
            <h3>Possible causes:</h3>
            <ul>
                <li>The file has been moved or deleted</li>
                <li>There's a network connectivity issue</li>
                <li>The file is corrupted or in an unsupported format</li>
                <li>Your browser doesn't support PDF viewing</li>
            </ul>
        </div>
        
        <div class="action-buttons">
            <button class="btn btn-primary" onclick="window.location.reload()">
                <span class="btn-icon">🔄</span> Try Again
            </button>
            <button class="btn btn-secondary" onclick="history.back()">
                <span class="btn-icon">⬅️</span> Go Back
            </button>
            <button class="btn btn-secondary" onclick="goToHome()">
                <span class="btn-icon">🏠</span> Home
            </button>
        </div>
        
        <div class="contact-support">
            If the problem persists, please <a href="mailto:sanjeevk4571@gmail.com">contact our support team</a>.
        </div>
    </div>

    <script>
        function goToHome() {
            // Load the PDF viewer with sample.pdf
            window.location.href = "file:///android_asset/pdfjs/web/viewer.html?file=" + "file:///android_asset/sample.pdf";
        }
        
        // You can customize the error message based on the specific error
        function setErrorMessage(errorType) {
            const titleElement = document.querySelector('h1');
            const messageElement = document.querySelector('.error-message');
            
            if (errorType === 'not-found') {
                titleElement.textContent = "PDF File Not Found";
                messageElement.textContent = "The PDF file you're looking for doesn't exist or has been moved. Please check the URL or contact the document owner.";
            } else if (errorType === 'loading-error') {
                titleElement.textContent = "Error Loading PDF";
                messageElement.textContent = "We encountered an error while trying to load the PDF file. This might be due to a network issue or file corruption.";
            }
            // Default message is already set in HTML
        }
    </script>
</body>
</html>

8. In AndroidManifest Manager:
a. Click on Permissions and add following permission
android.permission.PRINT
b. Click on App Components and put following codes.

<provider

    android:name="androidx.core.content.FileProvider"

    android:authorities="${applicationId}.fileprovider"

    android:exported="false"

    android:grantUriPermissions="true">

    <meta-data

        android:name="android.support.FILE_PROVIDER_PATHS"

        android:resource="@xml/file_paths" />

</provider>
9. In Resource manager, add a new folder xml. Inside xml folder, create a file named file_paths.xml. In this file put following codes:

<?xml version="1.0" encoding="utf-8"?>

<paths>

    <cache-path name="cache" path="." />

    <files-path name="files" path="." />

</paths>
10. In permission manager, add following permissions.
  • android.permission.INTERNET
  • android.permission.READ_EXTERNAL_STORAGE
  • android.permission.WRITE_EXTERNAL_STORAGE
  • android.permission.ACCESS_NETWORK_STATE
  • android.permission.PRINT

11. Create a new Java file PrintDocumentAdapterFactory.java and put following codes in it.

package com.pdf.onweb;

import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
import android.print.PageRange;
import android.print.PrintAttributes;
import android.print.PrintDocumentAdapter;
import android.print.PrintDocumentInfo;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class PrintDocumentAdapterFactory {

    public static PrintDocumentAdapter createPrintDocumentAdapter(Context context, Uri pdfUri) {

        final Context ctx = context;
        final Uri fileUri = pdfUri;

        return new PrintDocumentAdapter() {

            @Override
            public void onLayout(PrintAttributes oldAttributes, PrintAttributes newAttributes,
                                 CancellationSignal cancellationSignal,
                                 LayoutResultCallback callback, Bundle extras) {

                PrintDocumentInfo info = new PrintDocumentInfo
                        .Builder("document.pdf")
                        .setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT)
                        .build();

                callback.onLayoutFinished(info, true);
            }

            @Override
            public void onWrite(PageRange[] pages, ParcelFileDescriptor destination,
                                CancellationSignal cancellationSignal,
                                WriteResultCallback callback) {

                try {

                    ParcelFileDescriptor fd =
                            ctx.getContentResolver().openFileDescriptor(fileUri, "r");

                    FileInputStream input = new FileInputStream(fd.getFileDescriptor());
                    FileOutputStream output = new FileOutputStream(destination.getFileDescriptor());

                    byte[] buf = new byte[8192];
                    int size;

                    while ((size = input.read(buf)) > 0 && !cancellationSignal.isCanceled()) {
                        output.write(buf, 0, size);
                    }

                    callback.onWriteFinished(new PageRange[]{PageRange.ALL_PAGES});

                    input.close();
                    output.close();

                } catch (IOException e) {
                    callback.onWriteFailed(e.getMessage());
                }
            }
        };
    }
}

12. Create a String variable pdfName.

13. Create three Custom Variables.
a. Modifier: private
Type: ValueCallback<Uri[]>
Name: filePathCallback

b. Modifier: private static final
Type: int
Name: FILE_CHOOSER_REQUEST_CODE
Initializer: 1000

c. Modifier: private
Type: DownloadHelper
Name: downloadHelper

14. Create a new Java file DownloadHelper.java and put following codes in it.

package com.pdf.onweb;

import android.app.DownloadManager;
import android.content.Context;
import android.database.Cursor;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.Environment;
import android.os.Handler;
import android.util.Base64;
import android.util.Log;
import android.webkit.CookieManager;
import android.webkit.JavascriptInterface;
import android.webkit.WebView;
import android.widget.Toast;

import androidx.core.content.FileProvider;

import android.print.PrintManager;
import android.print.PrintDocumentAdapter;
import android.print.PrintAttributes;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class DownloadHelper {

    private Context context;
    private WebView webView;
    private static final String JS_INTERFACE_NAME = "Android";

    public DownloadHelper(Context context, WebView webView) {
        this.context = context;
        this.webView = webView;
        setupJavaScriptInterface();
    }

    // Set up JavaScript interface for WebView communication
    private void setupJavaScriptInterface() {
        webView.addJavascriptInterface(this, JS_INTERFACE_NAME);
    }

    // Handle regular HTTP/HTTPS file downloads
    public void handleRegularDownload(String url, String userAgent, 
                                    String fileName, String mimeType, 
                                    long contentLength) {
        try {
            Uri uri = Uri.parse(url);
            DownloadManager.Request request = new DownloadManager.Request(uri);
            
            request.setMimeType(mimeType);
            String cookies = CookieManager.getInstance().getCookie(url);
            if (cookies != null) {
                request.addRequestHeader("cookie", cookies);
            }
            request.addRequestHeader("User-Agent", userAgent);
            
            request.setTitle(fileName);
            request.setDescription("Downloading file");
            request.allowScanningByMediaScanner();
            request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
            request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
            
            DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
            if (dm != null) {
                dm.enqueue(request);
                Toast.makeText(context, "Download started: " + fileName, Toast.LENGTH_LONG).show();
            }
        } catch (Exception e) {
            Log.e("Download", "Regular download failed: " + e.getMessage());
            Toast.makeText(context, "Download failed", Toast.LENGTH_SHORT).show();
        }
    }

    // Handle blob URL downloads using JavaScript fetch API
    public void handleBlobUrlDownload(String blobUrl, String fileName, String mimeType) {
        try {
            final String javascript = "javascript: (function() {" +
                "var blobUrl = '" + blobUrl + "';" +
                "fetch(blobUrl)" +
                ".then(response => response.blob())" +
                ".then(blob => {" +
                "   var reader = new FileReader();" +
                "   reader.onloadend = function() {" +
                "       var base64data = reader.result;" +
                "       " + JS_INTERFACE_NAME + ".handleBase64Data(base64data, '" + mimeType + "', '" + fileName + "');" +
                "   };" +
                "   reader.readAsDataURL(blob);" +
                "})" +
                ".catch(error => {" +
                "   " + JS_INTERFACE_NAME + ".handleBase64Data('', '" + mimeType + "', '" + fileName + "');" +
                "});" +
                "})()";
            
            webView.post(new Runnable() {
                @Override
                public void run() {
                    webView.loadUrl(javascript);
                }
            });
        } catch (Exception e) {
            Log.e("Download", "Blob URL download setup failed: " + e.getMessage());
            Toast.makeText(context, "Download setup failed", Toast.LENGTH_SHORT).show();
        }
    }

    // JavaScript interface method to handle base64 data from blob URLs
    @JavascriptInterface
    public void handleBase64Data(String base64Data, String mimeType, String fileName) {
        try {
            if (base64Data == null || base64Data.isEmpty()) {
                throw new Exception("Empty base64 data received");
            }
            
            String pureBase64 = base64Data.substring(base64Data.indexOf(",") + 1);
            byte[] fileData = Base64.decode(pureBase64, Base64.DEFAULT);
            
            saveBase64Data(fileData, fileName, mimeType);
            
        } catch (final Exception e) {
            Log.e("Download", "Blob download failed: " + e.getMessage());
            new Handler(context.getMainLooper()).post(new Runnable() {
                @Override
                public void run() {
                    Toast.makeText(context, "Download failed: " + e.getMessage(), Toast.LENGTH_LONG).show();
                }
            });
        }
    }

    // Save base64 data to Downloads directory with duplicate handling
    public void saveBase64Data(byte[] data, final String fileName, String mimeType) {
        try {
            File downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
            if (!downloadsDir.exists()) {
                downloadsDir.mkdirs();
            }
            
            File file = new File(downloadsDir, fileName);
            
            int counter = 1;
            String baseName = fileName;
            String extension = "";
            int dotIndex = fileName.lastIndexOf('.');
            if (dotIndex > 0) {
                baseName = fileName.substring(0, dotIndex);
                extension = fileName.substring(dotIndex);
            }
            
            String finalFileName = fileName;
            while (file.exists()) {
                finalFileName = baseName + " (" + counter + ")" + extension;
                file = new File(downloadsDir, finalFileName);
                counter++;
            }
            
            final String savedFileName = finalFileName;
            FileOutputStream fos = new FileOutputStream(file);
            fos.write(data);
            fos.close();
            
            MediaScannerConnection.scanFile(context, new String[]{file.getAbsolutePath()}, 
                new String[]{mimeType}, null);
            
            new Handler(context.getMainLooper()).post(new Runnable() {
                @Override
                public void run() {
                    Toast.makeText(context, "File saved: " + savedFileName, Toast.LENGTH_LONG).show();
                }
            });
            
        } catch (final Exception e) {
            Log.e("Download", "Save base64 data failed: " + e.getMessage());
            new Handler(context.getMainLooper()).post(new Runnable() {
                @Override
                public void run() {
                    Toast.makeText(context, "Save failed: " + e.getMessage(), Toast.LENGTH_LONG).show();
                }
            });
        }
    }

    // Extract filename from URI with multiple fallback methods
    public String getFileNameFromUri(Uri uri) {
        String fileName = null;
    
        try {
            if ("content".equals(uri.getScheme())) {
                Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
                if (cursor != null) {
                    try {
                        if (cursor.moveToFirst()) {
                            int nameIndex = cursor.getColumnIndex(android.provider.OpenableColumns.DISPLAY_NAME);
                            if (nameIndex != -1) {
                                fileName = cursor.getString(nameIndex);
                            }
                        }
                    } finally {
                        cursor.close();
                    }
                }
            }
            
            if (fileName == null) {
                String path = uri.getPath();
                if (path != null) {
                    int cut = path.lastIndexOf('/');
                    if (cut != -1) {
                        fileName = path.substring(cut + 1);
                    }
                }
            }
            
            if (fileName == null && "file".equals(uri.getScheme())) {
                File file = new File(uri.getPath());
                fileName = file.getName();
            }
            
            if (fileName != null && !fileName.toLowerCase().endsWith(".pdf")) {
                fileName += ".pdf";
            }
            
        } catch (Exception e) {
            Log.e("FilePicker", "Error getting file name from URI: " + e.getMessage());
        }
    
        return fileName;
    }
    
    // Print PDF file using Android Print Manager
    public void printPdf(Context context, String pdfName) {
        File pdfFile = new File(context.getCacheDir(), pdfName);
    
        Uri fileUri = FileProvider.getUriForFile(
                context,
                context.getPackageName() + ".fileprovider",
                pdfFile
        );

        PrintManager printManager = (PrintManager) context.getSystemService(Context.PRINT_SERVICE);

        PrintDocumentAdapter printAdapter =
                PrintDocumentAdapterFactory.createPrintDocumentAdapter(context, fileUri);

        printManager.print("Printing PDF", printAdapter, new PrintAttributes.Builder().build());
    }

    // Copy file from URI or assets to app cache directory
    public File copyToCache(Context context, String outputName, Uri uri, String assetName) {
        File outFile = new File(context.getCacheDir(), outputName);

        InputStream in = null;
        OutputStream out = null;

        try {
            if (uri != null) {
                in = context.getContentResolver().openInputStream(uri);
            }
            else if (assetName != null) {
                in = context.getAssets().open(assetName);
            }
            else {
                throw new IllegalArgumentException("Both uri and assetName are null");
            }

            if (in == null) {
                throw new IOException("Unable to open InputStream");
            }

            out = new FileOutputStream(outFile);

            byte[] buffer = new byte[4096];
            int len;
            while ((len = in.read(buffer)) > 0) {
                out.write(buffer, 0, len);
            }

            out.flush();
            return outFile;

        } catch (Exception e) {
            e.printStackTrace();
            return null;

        } finally {
            try { if (in != null) in.close(); } catch (Exception ignored) {}
            try { if (out != null) out.close(); } catch (Exception ignored) {}
        }
    }

    // Clean up JavaScript interface when no longer needed
    public void cleanup() {
        try {
            webView.removeJavascriptInterface(JS_INTERFACE_NAME);
        } catch (Exception e) {
            Log.e("Download", "Cleanup failed: " + e.getMessage());
        }
    }
}
15. In onCreate event, put following codes.
     // Configure WebView settings for PDF.js viewer
WebSettings settings = binding.pdfWebView.getSettings();
settings.setJavaScriptEnabled(true);
settings.setDomStorageEnabled(true);
settings.setAllowFileAccess(true);
settings.setAllowContentAccess(true);
settings.setAllowFileAccessFromFileURLs(true);
settings.setAllowUniversalAccessFromFileURLs(true);

// Initialize download helper for handling file downloads
downloadHelper = new DownloadHelper(this, binding.pdfWebView);

// Load PDF.js viewer with local PDF file
pdfName = "sample.pdf";
File cachedFile = downloadHelper.copyToCache(this, "temp.pdf", null, "sample.pdf");
String viewerUrl = "file:///android_asset/pdfjs/web/viewer.html?file=" +
        "file://" + cachedFile.getAbsolutePath();
binding.pdfWebView.loadUrl(viewerUrl);

binding.pdfWebView.setWebViewClient(new WebViewClient() {
	@Override
	public void onPageStarted(WebView _param1, String _param2, Bitmap _param3) {
		final String _url = _param2;
		super.onPageStarted(_param1, _param2, _param3);
	}
    
    @Override
    public void onPageFinished(WebView view, String url) {
        // Override window.print() to use Android's print system
        view.evaluateJavascript(
            "window.print = function() { AndroidPrint.onPrintRequested(); };",
            null
        );
    }
	
	@Override
	public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
		super.onReceivedError(view, request, error);
		Toast.makeText(view.getContext(), "Failed to load PDF: " + error.getDescription(), Toast.LENGTH_LONG).show();
		
		view.loadUrl("file:///android_asset/error.html");
	}
});

binding.pdfWebView.setWebChromeClient(new WebChromeClient() {
	@Override
	public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback,
	FileChooserParams fileChooserParams) {
		// Handle file chooser for PDF.js "Open File" feature
		if (MainActivity.this.filePathCallback != null) {
			MainActivity.this.filePathCallback.onReceiveValue(null);
		}
		MainActivity.this.filePathCallback = filePathCallback;
		
		Intent intent = fileChooserParams.createIntent();
		try {
			startActivityForResult(intent, FILE_CHOOSER_REQUEST_CODE);
		} catch (ActivityNotFoundException e) {
			MainActivity.this.filePathCallback = null;
			Toast.makeText(MainActivity.this, "Cannot open file chooser", Toast.LENGTH_SHORT).show();
			return false;
		}
		return true;
	}
});

// Handle download requests from PDF.js
binding.pdfWebView.setDownloadListener(new DownloadListener() {
	@Override
	public void onDownloadStart(String url, String userAgent, 
	String contentDisposition, String mimeType, 
	long contentLength) {
		
		if (url.startsWith("blob:") || url.startsWith("data:")) {
			// Handle blob/data URLs (PDF.js generated downloads)
			downloadHelper.handleBlobUrlDownload(url, pdfName, mimeType);
			return;
		}
		
		// Handle regular HTTP downloads
		downloadHelper.handleRegularDownload(url, userAgent, pdfName, mimeType, contentLength);
	}
});

// JavaScript interface for handling print requests from PDF.js
binding.pdfWebView.addJavascriptInterface(new Object() {
    @JavascriptInterface
    public void onPrintRequested() {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                downloadHelper.printPdf(MainActivity.this, "temp.pdf");
            }
        });
    }
}, "AndroidPrint");
16. In import, add following.

import android.print.PrintManager;

import android.print.PrintJob;

import android.print.PrintDocumentAdapter;

import android.print.PrintAttributes;
17. Add event onActivityResult and put following codes in it.


    if (_requestCode == FILE_CHOOSER_REQUEST_CODE) {
        if (filePathCallback == null) return;

        Uri[] result = null;
        if (_resultCode == Activity.RESULT_OK && _data != null) {
            if (_data.getData() != null) {
                result = new Uri[]{_data.getData()};
            }
        }

        filePathCallback.onReceiveValue(result);
        filePathCallback = null;

Uri pickedUri = result[0];
if (pickedUri == null) {
    Toast.makeText(this, "No file selected.", Toast.LENGTH_SHORT).show();
    return;
   }
pdfName = downloadHelper.getFileNameFromUri(pickedUri);

File cachedFile = downloadHelper.copyToCache(this, "temp.pdf", pickedUri, null);

if (cachedFile != null) {
    String viewerUrl = "file:///android_asset/pdfjs/web/viewer.html?file=" +
            "file://" + cachedFile.getAbsolutePath();
    binding.pdfWebView.loadUrl(viewerUrl);
}

    }

18. Add onDestroy event and put following codes in it.

if (downloadHelper != null) {
    downloadHelper.cleanup();
}

19. Save and run the project.

Post a Comment

0 Comments