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.

Comments

Popular posts from this blog

Simple car racing android game in Sketchware

Simple Audio recorder app in Sketchware

How to enable upload from webview in Sketchware?

Retrieve contact list in Sketchware

Creating a Drawing View in Sketchware