soul-browser/sources/java/com/mycompany/app/web/PdfPreview.java
KaKi87 1fc1084c49 Keep PDF previews in-app when Google Drive is installed.
Skip the QUICK_VIEW fast path (and ACTION_VIEW fallback) that handed PDFs to Drive, and always open ImageViewPdf after caching.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-08 00:58:41 +02:00

342 lines
12 KiB
Java

package com.mycompany.app.web;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Intent;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.webkit.CookieManager;
import android.webkit.URLUtil;
import android.content.DialogInterface;
import com.mycompany.app.main.MainUtil;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Downloads a network PDF to cache and opens it in the in-app ImageViewPdf viewer,
* so PDF links are previewed instead of being forced through the download dialog.
*/
public final class PdfPreview {
public interface Fallback {
void run();
}
private PdfPreview() {
}
public static boolean isPdf(String url, String mimeType) {
if (!TextUtils.isEmpty(mimeType)) {
String mime = mimeType.toLowerCase(Locale.US);
if (mime.contains("application/pdf") || mime.equals("application/x-pdf")) {
return true;
}
}
return isPdfUrl(url);
}
public static boolean isPdfUrl(String url) {
if (TextUtils.isEmpty(url) || !URLUtil.isNetworkUrl(url)) {
return false;
}
String path = url;
int q = path.indexOf('?');
if (q >= 0) {
path = path.substring(0, q);
}
int h = path.indexOf('#');
if (h >= 0) {
path = path.substring(0, h);
}
return path.toLowerCase(Locale.US).endsWith(".pdf");
}
public static void open(final Activity activity, final String url, final String userAgent,
final Fallback fallback) {
if (activity == null || activity.isFinishing() || TextUtils.isEmpty(url)
|| !URLUtil.isNetworkUrl(url)) {
if (fallback != null) {
fallback.run();
}
return;
}
// Always cache + open ImageViewPdf. QUICK_VIEW resolves to Google Drive
// (or other external PDF handlers) when installed, which defeats in-app preview.
final Handler main = new Handler(Looper.getMainLooper());
final AtomicBoolean cancelled = new AtomicBoolean(false);
final ProgressDialog dialog = new ProgressDialog(activity);
try {
int loadingId = activity.getResources().getIdentifier(
"loading", "string", activity.getPackageName());
dialog.setMessage(loadingId != 0 ? activity.getString(loadingId) : "Loading…");
dialog.setIndeterminate(true);
dialog.setCancelable(true);
dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface d) {
cancelled.set(true);
}
});
dialog.show();
} catch (Exception ignored) {
}
Thread worker = new Thread(new Runnable() {
@Override
public void run() {
File out = null;
try {
String[] headerName = new String[1];
out = downloadToCache(activity, url, userAgent, headerName);
if (cancelled.get()) {
deleteQuietly(out);
return;
}
if (out == null || !out.isFile() || out.length() == 0L) {
throw new IOException("empty pdf");
}
final File pdfFile = out;
final String displayName = resolveDisplayName(url, headerName[0]);
main.post(new Runnable() {
@Override
public void run() {
dismissQuietly(dialog);
if (cancelled.get() || activity.isFinishing()) {
deleteQuietly(pdfFile);
return;
}
if (!openLocalPdf(activity, pdfFile, displayName)) {
deleteQuietly(pdfFile);
if (fallback != null) {
fallback.run();
}
}
}
});
} catch (Exception e) {
deleteQuietly(out);
main.post(new Runnable() {
@Override
public void run() {
dismissQuietly(dialog);
if (!cancelled.get() && fallback != null) {
fallback.run();
}
}
});
}
}
}, "pdf-preview");
worker.start();
}
private static boolean openLocalPdf(Activity activity, File file, String name) {
try {
Intent intent = new Intent();
intent.setClassName(activity.getPackageName(),
"com.mycompany.app.image.ImageViewPdf");
intent.putExtra("EXTRA_TYPE", 2);
intent.putExtra("EXTRA_PATH", file.getAbsolutePath());
intent.putExtra("EXTRA_INDEX", 0);
if (!TextUtils.isEmpty(name)) {
intent.putExtra("EXTRA_NAME", name);
}
activity.startActivity(intent);
return true;
} catch (Exception e) {
// Do not fall back to ACTION_VIEW: that also hands the PDF to Drive/etc.
return false;
}
}
/** Prefer Content-Disposition, then query params like file=, else URL path. */
static String resolveDisplayName(String url, String contentDispositionName) {
if (!TextUtils.isEmpty(contentDispositionName)) {
return ensurePdfSuffix(MainUtil.p3(basename(contentDispositionName)));
}
String fromQuery = nameFromQuery(url);
if (!TextUtils.isEmpty(fromQuery)) {
return ensurePdfSuffix(MainUtil.p3(fromQuery));
}
return nameFromUrl(url);
}
private static String nameFromQuery(String url) {
if (TextUtils.isEmpty(url)) {
return null;
}
try {
Uri uri = Uri.parse(url);
String[] keys = {"file", "filename", "path", "name", "f", "doc", "document"};
for (String key : keys) {
String value = uri.getQueryParameter(key);
String base = basename(value);
if (!TextUtils.isEmpty(base) && base.indexOf('.') > 0) {
return base;
}
}
} catch (Exception ignored) {
}
return null;
}
private static String basename(String value) {
if (TextUtils.isEmpty(value)) {
return null;
}
String name = value.trim();
int slash = Math.max(name.lastIndexOf('/'), name.lastIndexOf('\\'));
if (slash >= 0 && slash + 1 < name.length()) {
name = name.substring(slash + 1);
}
return TextUtils.isEmpty(name) ? null : name;
}
private static String ensurePdfSuffix(String name) {
if (TextUtils.isEmpty(name)) {
return "document.pdf";
}
if (!name.toLowerCase(Locale.US).endsWith(".pdf")) {
return name + ".pdf";
}
return name;
}
private static String nameFromUrl(String url) {
if (TextUtils.isEmpty(url)) {
return "document.pdf";
}
String path = url;
int q = path.indexOf('?');
if (q >= 0) {
path = path.substring(0, q);
}
int h = path.indexOf('#');
if (h >= 0) {
path = path.substring(0, h);
}
int slash = path.lastIndexOf('/');
String name = slash >= 0 ? path.substring(slash + 1) : path;
if (TextUtils.isEmpty(name)) {
return "document.pdf";
}
String lower = name.toLowerCase(Locale.US);
// downloadfile.php?file=book.pdf → path is downloadfile.php; don't invent downloadfile.php.pdf
if (lower.endsWith(".php") || lower.endsWith(".asp") || lower.endsWith(".aspx")
|| lower.endsWith(".jsp") || lower.endsWith(".cgi")) {
return "document.pdf";
}
if (!lower.endsWith(".pdf")) {
name = name + ".pdf";
}
return MainUtil.p3(name);
}
private static File downloadToCache(Activity activity, String url, String userAgent,
String[] contentDispositionNameOut) throws Exception {
File dir = new File(activity.getCacheDir(), "pdf_preview");
if (!dir.exists() && !dir.mkdirs()) {
throw new IOException("cache dir");
}
// Keep only a few recent previews.
File[] old = dir.listFiles();
if (old != null && old.length > 8) {
for (File f : old) {
//noinspection ResultOfMethodCallIgnored
f.delete();
}
}
String name = "preview_" + System.currentTimeMillis() + ".pdf";
File out = new File(dir, name);
HttpURLConnection conn = null;
InputStream in = null;
FileOutputStream fos = null;
try {
conn = (HttpURLConnection) new URL(url).openConnection();
conn.setConnectTimeout(15000);
conn.setReadTimeout(30000);
conn.setInstanceFollowRedirects(true);
if (!TextUtils.isEmpty(userAgent)) {
conn.setRequestProperty("User-Agent", userAgent);
}
try {
String cookie = CookieManager.getInstance().getCookie(url);
if (!TextUtils.isEmpty(cookie)) {
conn.setRequestProperty("Cookie", cookie);
}
} catch (Exception ignored) {
}
conn.connect();
int code = conn.getResponseCode();
if (code >= 400) {
throw new IOException("http " + code);
}
if (contentDispositionNameOut != null && contentDispositionNameOut.length > 0) {
try {
contentDispositionNameOut[0] = MainUtil.z2(
conn.getHeaderField("Content-Disposition"));
} catch (Exception ignored) {
contentDispositionNameOut[0] = null;
}
}
in = conn.getInputStream();
fos = new FileOutputStream(out);
byte[] buf = new byte[8192];
int n;
long total = 0L;
while ((n = in.read(buf)) >= 0) {
fos.write(buf, 0, n);
total += n;
if (total > 200L * 1024L * 1024L) {
throw new IOException("too large");
}
}
fos.flush();
return out;
} finally {
try {
if (fos != null) {
fos.close();
}
} catch (Exception ignored) {
}
try {
if (in != null) {
in.close();
}
} catch (Exception ignored) {
}
if (conn != null) {
conn.disconnect();
}
}
}
private static void dismissQuietly(ProgressDialog dialog) {
try {
if (dialog != null && dialog.isShowing()) {
dialog.dismiss();
}
} catch (Exception ignored) {
}
}
private static void deleteQuietly(File file) {
if (file != null) {
try {
//noinspection ResultOfMethodCallIgnored
file.delete();
} catch (Exception ignored) {
}
}
}
}