Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Third-party notices

ClearPDF uses open-source components and keeps their notices with the project. Release packaging must preserve the applicable copyright and license text.

## Apache PDFBox Android

- Artifact: `com.tom-roush:pdfbox-android:2.0.27.0`
- License: Apache License 2.0
- Use: PDF parsing, editing, merging, splitting, and password removal.
- Notice practice: keep the Apache 2.0 license and upstream attribution available with redistributed builds. Do not imply that the Apache Software Foundation endorses ClearPDF.

## Apache POI

- Artifacts: `org.apache.poi:poi:3.17`, `org.apache.poi:poi-scratchpad:3.17`
- License: Apache License 2.0
- Use: text extraction from legacy `.doc`, `.xls`, and `.ppt` files. Modern Office Open XML files use the platform ZIP/XML parser for a smaller footprint.
- Notice practice: retain the Apache 2.0 license and any bundled dependency notices when distributing an APK or source package.

## AndroidLiquidGlass / Backdrop

- Component: `AndroidLiquidGlass` / the local `backdrop` module
- License: Apache License 2.0 (as attributed in the Settings screen)
- Use: the app's translucent glass surfaces, backdrop effects, and shared UI components.

The app does not add GPL or LGPL components for document rendering. If a future dependency changes that, its license and redistribution obligations must be reviewed before release.
8 changes: 7 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ android {
targetSdk = 36
versionCode = 2
versionName = "1.1.0"
androidResources.localeFilters += arrayOf("en")
// Keep every locale declared by the app. Filtering this to English
// removes values-pt-rBR from the packaged APK, so the language picker
// can appear to work while the app continues to resolve English.
}

signingConfigs {
Expand Down Expand Up @@ -126,6 +128,10 @@ dependencies {
implementation(libs.kotlinx.serialization.json)
implementation(project(":backdrop"))
implementation(project(":pdf-core"))
// Apache POI provides legacy .doc/.xls/.ppt text extraction. It is Apache-2.0
// licensed; see THIRD_PARTY_NOTICES.md for redistribution requirements.
implementation("org.apache.poi:poi:3.17")
implementation("org.apache.poi:poi-scratchpad:3.17")

// ML Kit Document Scanner & Camera
implementation(libs.play.services.mlkit.scanner)
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,13 @@
<data android:scheme="content" />
<data android:mimeType="application/vnd.openxmlformats-officedocument.presentationml.presentation" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="content" />
<data android:mimeType="application/vnd.ms-powerpoint" />
</intent-filter>

<!-- Open plain text files -->
<intent-filter>
Expand Down
22 changes: 20 additions & 2 deletions app/src/main/java/com/chethan616/clearpdf/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ class MainActivity : ComponentActivity() {
}
}

// Handle PDF file opened from other apps (VIEW/SEND)
// Handle supported documents opened from other apps (VIEW/SEND).
// The viewer converts office/text/image sources into a local PDF preview.
val incomingPdfUri: Uri? = when (intent?.action) {
Intent.ACTION_VIEW -> intent.data
Intent.ACTION_SEND -> {
Expand All @@ -57,7 +58,7 @@ class MainActivity : ComponentActivity() {
}
}
else -> null
}?.takeIf { intent?.type == "application/pdf" || intent?.data?.toString()?.endsWith(".pdf") == true }
}?.takeIf { uri -> isSupportedDocumentIntent(intent?.type, uri.toString()) }

// If a PDF was shared/opened, navigate to viewer with that URI
val effectiveRoute = if (incomingPdfUri != null) "pdf_viewer" else shortcutRoute
Expand All @@ -67,6 +68,23 @@ class MainActivity : ComponentActivity() {
}
}

private fun isSupportedDocumentIntent(mimeType: String?, uriString: String): Boolean {
val lower = uriString.lowercase()
val supportedExtension = listOf(
".pdf", ".doc", ".docx", ".ppt", ".pptx", ".xls", ".xlsx", ".csv", ".txt", ".rtf",
".odt", ".ods", ".odp", ".png", ".jpg", ".jpeg", ".webp", ".bmp", ".gif", ".heic"
).any(lower::contains)
return mimeType == null ||
mimeType == "application/pdf" ||
mimeType?.startsWith("image/") == true ||
mimeType?.startsWith("text/") == true ||
mimeType?.contains("word", ignoreCase = true) == true ||
mimeType?.contains("excel", ignoreCase = true) == true ||
mimeType?.contains("spreadsheet", ignoreCase = true) == true ||
mimeType?.contains("presentation", ignoreCase = true) == true ||
supportedExtension
}

private fun requestHighRefreshRate() {
// Prefer the display mode with the highest refresh rate
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,37 @@ object SignatureManager {
?.sortedByDescending { it.lastModified() }
?: emptyList()

/** Saves a transparent-background signature bitmap; returns the saved file. */
fun saveSignature(context: Context, bitmap: Bitmap): File {
val file = File(dir(context), "sig_${System.currentTimeMillis()}.png")
/** Saves a transparent-background signature bitmap using a human-readable name. */
fun saveSignature(context: Context, bitmap: Bitmap, name: String = "Signature"): File {
val safeName = name
.trim()
.ifBlank { "Signature" }
.replace(Regex("[^\\p{L}\\p{N}\\-_ ]"), "")
.trim()
.replace(Regex("\\s+"), "_")
.take(48)
.ifBlank { "Signature" }
val file = File(dir(context), "${safeName}_${System.currentTimeMillis()}.png")
FileOutputStream(file).use { out ->
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
out.flush()
}
return file
}

/** Returns the display name stored in a signature filename. */
fun displayName(file: File): String {
val base = file.nameWithoutExtension
val withoutTimestamp = base.substringBeforeLast('_').takeIf {
base.substringAfterLast('_').toLongOrNull() != null
} ?: base
return withoutTimestamp
.removePrefix("sig")
.replace('_', ' ')
.trim()
.ifBlank { "Signature" }
}

/** Loads a signature from file. Returns null if the file is missing or corrupt. */
fun loadSignature(file: File): Bitmap? = try {
val options = BitmapFactory.Options().apply {
Expand Down
35 changes: 24 additions & 11 deletions app/src/main/java/com/chethan616/clearpdf/ui/DocsApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
Expand Down Expand Up @@ -67,6 +69,9 @@ import kotlinx.coroutines.flow.collectLatest
fun DocsApp(shortcutRoute: String? = null, incomingPdfUri: android.net.Uri? = null) {
val context = LocalContext.current
var selectedLocale by rememberSaveable { mutableStateOf<String>(OnboardingManager.getSelectedLocale(context)) }
val localizedContext = remember(context, selectedLocale) {
com.chethan616.clearpdf.ui.utils.LocaleHelper.getLocalizedContext(context, selectedLocale)
}

var themeMode by rememberSaveable { mutableIntStateOf(AppSettingsManager.getThemeMode(context)) }
var showStarPrompt by rememberSaveable { mutableStateOf(false) }
Expand Down Expand Up @@ -154,13 +159,8 @@ fun DocsApp(shortcutRoute: String? = null, incomingPdfUri: android.net.Uri? = nu
contentScale = ContentScale.Crop
)

LaunchedEffect(selectedLocale) {
if (selectedLocale.isNotBlank()) {
com.chethan616.clearpdf.ui.utils.LocaleHelper.applyLocale(context, selectedLocale, recreate = false)
}
}

CompositionLocalProvider(
LocalResources provides localizedContext.resources,
LocalIsDarkMode provides isDarkMode
) {
Box(Modifier.fillMaxSize()) {
Expand All @@ -176,6 +176,19 @@ fun DocsApp(shortcutRoute: String? = null, incomingPdfUri: android.net.Uri? = nu
themeMode = it
AppSettingsManager.setThemeMode(context, it)
},
selectedLocale = selectedLocale,
onLocaleChanged = { code ->
val normalized = com.chethan616.clearpdf.ui.utils.LocaleHelper.normalizeForUi(code)
if (normalized != selectedLocale) {
selectedLocale = normalized
com.chethan616.clearpdf.ui.utils.LocaleHelper.applyLocale(
context = context,
languageTag = normalized,
recreate = false,
updateAppCompat = false
)
}
},
incomingPdfUri = incomingPdfUri
)

Expand Down Expand Up @@ -247,11 +260,11 @@ fun DocsApp(shortcutRoute: String? = null, incomingPdfUri: android.net.Uri? = nu
Modifier.size(52.dp), starAccent
)
BasicText(
"Support ClearPDF",
stringResource(R.string.star_prompt_title),
style = TextStyle(starText, 20.sp, FontWeight.Bold, textAlign = TextAlign.Center)
)
BasicText(
"ClearPDF is open source on GitHub.\nWould you like to star the project?",
stringResource(R.string.star_prompt_message),
style = TextStyle(starSub, 14.sp, textAlign = TextAlign.Center)
)
Spacer(Modifier.height(4.dp))
Expand All @@ -270,7 +283,7 @@ fun DocsApp(shortcutRoute: String? = null, incomingPdfUri: android.net.Uri? = nu
verticalAlignment = Alignment.CenterVertically
) {
Icon(Icons.Rounded.Star, null, Modifier.size(18.dp), Color.White)
BasicText("Yes, Star It", style = TextStyle(Color.White, 15.sp, FontWeight.SemiBold))
BasicText(stringResource(R.string.star_prompt_accept), style = TextStyle(Color.White, 15.sp, FontWeight.SemiBold))
}
}
Row(
Expand All @@ -279,7 +292,7 @@ fun DocsApp(shortcutRoute: String? = null, incomingPdfUri: android.net.Uri? = nu
verticalAlignment = Alignment.CenterVertically
) {
BasicText(
"Not Now",
stringResource(R.string.star_prompt_later),
style = TextStyle(starSub, 13.sp, FontWeight.Medium, textAlign = TextAlign.Center),
modifier = Modifier
.clickable {
Expand All @@ -293,7 +306,7 @@ fun DocsApp(shortcutRoute: String? = null, incomingPdfUri: android.net.Uri? = nu
style = TextStyle(starSub.copy(0.4f), 13.sp)
)
BasicText(
"Don't Ask Again",
stringResource(R.string.star_prompt_never),
style = TextStyle(starSub.copy(0.7f), 13.sp, FontWeight.Medium, textAlign = TextAlign.Center),
modifier = Modifier
.clickable {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import androidx.compose.material.icons.rounded.GridView
import androidx.compose.material.icons.rounded.Tune
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.chethan616.clearpdf.ui.theme.LiquidGlassColors
import com.chethan616.clearpdf.ui.theme.LocalIsDarkMode
import com.chethan616.clearpdf.R
import com.kyant.backdrop.Backdrop

@Composable
Expand All @@ -32,17 +34,17 @@ fun DocsBottomTabs(
modifier = modifier
) {
LiquidBottomTab(onClick = { onTabSelected(0) }) {
Icon(Icons.Rounded.Home, contentDescription = "Home", tint = tint,
Icon(Icons.Rounded.Home, contentDescription = stringResource(R.string.nav_home), tint = tint,
modifier = Modifier.size(22.dp))
}

LiquidBottomTab(onClick = { onTabSelected(1) }) {
Icon(Icons.Rounded.GridView, contentDescription = "Tools", tint = tint,
Icon(Icons.Rounded.GridView, contentDescription = stringResource(R.string.nav_tools), tint = tint,
modifier = Modifier.size(22.dp))
}

LiquidBottomTab(onClick = { onTabSelected(2) }) {
Icon(Icons.Rounded.Tune, contentDescription = "Settings", tint = tint,
Icon(Icons.Rounded.Tune, contentDescription = stringResource(R.string.nav_settings), tint = tint,
modifier = Modifier.size(22.dp))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import kotlin.math.tanh
import androidx.compose.material3.Icon
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.PathParser
import androidx.compose.ui.res.stringResource
import com.chethan616.clearpdf.R

private const val CLOSE_CROSS_SVG_PATH =
"M480-424 284-228q-11 11-28 11t-28-11q-11-11-11-28t11-28l196-196-196-196q-11-11-11-28t11-28q11-11 28-11t28 11l196 196 196-196q11-11 28-11t28 11q11 11 11 28t-11 28L536-480l196 196q11 11 11 28t-11 28q-11 11-28 11t-28-11L480-424Z"
Expand All @@ -55,7 +57,7 @@ fun CloseCrossIcon(modifier: Modifier = Modifier, tint: Color = Color.White) {
fill = androidx.compose.ui.graphics.SolidColor(tint)
).build()
}
Icon(vector, contentDescription = "Close", modifier = modifier, tint = tint)
Icon(vector, contentDescription = stringResource(R.string.close), modifier = modifier, tint = tint)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.chethan616.clearpdf.data.repository.SaveLocationManager
import com.chethan616.clearpdf.R
import com.chethan616.clearpdf.ui.theme.LocalIsDarkMode
import com.chethan616.clearpdf.ui.utils.UISensor
import com.kyant.backdrop.Backdrop
Expand Down Expand Up @@ -59,7 +61,7 @@ fun LiquidSaveDialog(
context.contentResolver.takePersistableUriPermission(uri, flags)
} catch (_: Exception) {}
locationUri = uri
locationDisplay = uri.lastPathSegment?.replace("primary:", "") ?: "Selected Folder"
locationDisplay = uri.lastPathSegment?.replace("primary:", "") ?: context.getString(R.string.selected_folder)
}
}

Expand All @@ -75,13 +77,13 @@ fun LiquidSaveDialog(
verticalArrangement = Arrangement.spacedBy(20.dp)
) {
BasicText(
"Save Document",
stringResource(R.string.save_document),
style = TextStyle(color = text, fontSize = 20.sp, fontWeight = FontWeight.SemiBold)
)

// File Name Input
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
BasicText("File Name", style = TextStyle(color = sub, fontSize = 13.sp, fontWeight = FontWeight.Medium))
BasicText(stringResource(R.string.file_name), style = TextStyle(color = sub, fontSize = 13.sp, fontWeight = FontWeight.Medium))
BasicTextField(
value = fileName,
onValueChange = { fileName = it },
Expand All @@ -94,7 +96,7 @@ fun LiquidSaveDialog(
.padding(14.dp),
decorationBox = { inner ->
if (fileName.isEmpty()) {
BasicText("Document.pdf", style = TextStyle(color = sub.copy(0.5f), fontSize = 16.sp))
BasicText(stringResource(R.string.document_pdf), style = TextStyle(color = sub.copy(0.5f), fontSize = 16.sp))
}
inner()
}
Expand All @@ -103,7 +105,7 @@ fun LiquidSaveDialog(

// Save Location Picker
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
BasicText("Save Location", style = TextStyle(color = sub, fontSize = 13.sp, fontWeight = FontWeight.Medium))
BasicText(stringResource(R.string.settings_save_location), style = TextStyle(color = sub, fontSize = 13.sp, fontWeight = FontWeight.Medium))
LiquidButton(
onClick = { folderPicker.launch(null) },
backdrop = backdrop,
Expand All @@ -119,7 +121,7 @@ fun LiquidSaveDialog(
BasicText(locationDisplay, style = TextStyle(color = text, fontSize = 14.sp), maxLines = 1)
}
Spacer(Modifier.width(8.dp))
Icon(Icons.Rounded.FolderOpen, "Change Folder", Modifier.size(20.dp), accent)
Icon(Icons.Rounded.FolderOpen, stringResource(R.string.change_folder), Modifier.size(20.dp), accent)
}
}
}
Expand All @@ -135,7 +137,7 @@ fun LiquidSaveDialog(
backdrop = backdrop,
surfaceColor = Color.Transparent
) {
BasicText("Cancel", style = TextStyle(color = sub, fontSize = 14.sp, fontWeight = FontWeight.Medium))
BasicText(stringResource(R.string.cancel), style = TextStyle(color = sub, fontSize = 14.sp, fontWeight = FontWeight.Medium))
}
Spacer(Modifier.width(8.dp))
LiquidButton(
Expand All @@ -147,8 +149,8 @@ fun LiquidSaveDialog(
tint = accent
) {
Row(horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Rounded.Save, "Save", Modifier.size(16.dp), Color.White)
BasicText("Save", style = TextStyle(color = Color.White, fontSize = 14.sp, fontWeight = FontWeight.SemiBold))
Icon(Icons.Rounded.Save, stringResource(R.string.save), Modifier.size(16.dp), Color.White)
BasicText(stringResource(R.string.save), style = TextStyle(color = Color.White, fontSize = 14.sp, fontWeight = FontWeight.SemiBold))
}
}
}
Expand Down
Loading
Loading