diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a945b0b..1add5dd 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -128,6 +128,13 @@ android { targetCompatibility = Versions.jvm } + packaging { + resources { + excludes.add("META-INF/INDEX.LIST") + excludes.add("META-INF/DEPENDENCIES") + } + } + // Always show the result of every unit test, even if it passes. /* testOptions.unitTests.all { @@ -182,6 +189,13 @@ dependencies { //ffmpeg implementation("com.arthenica:ffmpeg-kit-full:6.0-2.LTS") + + //Apache POI + implementation ("org.apache.poi:poi:5.3.0") + implementation ("org.apache.poi:poi-ooxml:5.3.0") + implementation ("org.apache.poi:poi-scratchpad:5.2.2") + implementation ("org.apache.odftoolkit:simple-odf:0.8.2-incubating") + implementation ("com.itextpdf:itext7-core:7.1.16") } diff --git a/app/src/main/java/rocks/poopjournal/metadataremover/metadata/handlers/DocumentMetadataHandler.kt b/app/src/main/java/rocks/poopjournal/metadataremover/metadata/handlers/DocumentMetadataHandler.kt new file mode 100644 index 0000000..02818c4 --- /dev/null +++ b/app/src/main/java/rocks/poopjournal/metadataremover/metadata/handlers/DocumentMetadataHandler.kt @@ -0,0 +1,551 @@ +package rocks.poopjournal.metadataremover.metadata.handlers + +import android.content.Context +import android.net.Uri +import androidx.core.net.toUri +import com.itextpdf.kernel.pdf.PdfDocument +import com.itextpdf.kernel.pdf.PdfName +import com.itextpdf.kernel.pdf.PdfReader +import com.itextpdf.kernel.pdf.PdfWriter +import org.apache.poi.hssf.usermodel.HSSFWorkbook +import org.apache.poi.hwpf.HWPFDocument +import org.apache.poi.poifs.filesystem.OfficeXmlFileException +import org.apache.poi.xssf.usermodel.XSSFWorkbook +import org.apache.poi.xwpf.usermodel.XWPFDocument +import org.odftoolkit.simple.Document +import org.odftoolkit.simple.SpreadsheetDocument +import org.odftoolkit.simple.TextDocument +import rocks.poopjournal.metadataremover.R +import rocks.poopjournal.metadataremover.model.metadata.Metadata +import rocks.poopjournal.metadataremover.model.metadata.MetadataHandler +import rocks.poopjournal.metadataremover.model.resources.Image +import rocks.poopjournal.metadataremover.model.resources.MediaType +import rocks.poopjournal.metadataremover.model.resources.MediaTypes +import rocks.poopjournal.metadataremover.model.resources.Text +import rocks.poopjournal.metadataremover.util.extensions.toCalendar +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.text.SimpleDateFormat +import java.util.Date +import java.util.GregorianCalendar +import java.util.Locale +import java.util.Optional + +class DocumentMetadataHandler(private val context: Context) : MetadataHandler { + + override val readableMimeTypes = MediaTypes[MediaTypes.MICROSOFT_WORD] + + MediaTypes[MediaTypes.OOXML_DOCUMENT] + + MediaTypes[MediaTypes.MICROSOFT_EXCEL] + + MediaTypes[MediaTypes.OOXML_SHEET] + + MediaTypes[MediaTypes.OPENDOCUMENT_TEXT] + + MediaTypes[MediaTypes.OPENDOCUMENT_SPREADSHEET] + + MediaTypes[MediaTypes.PDF] + + override val writableMimeTypes = MediaTypes[MediaTypes.MICROSOFT_WORD] + + MediaTypes[MediaTypes.OOXML_DOCUMENT] + + MediaTypes[MediaTypes.MICROSOFT_EXCEL] + + MediaTypes[MediaTypes.OOXML_SHEET] + + MediaTypes[MediaTypes.OPENDOCUMENT_TEXT] + + MediaTypes[MediaTypes.OPENDOCUMENT_SPREADSHEET] + + MediaTypes[MediaTypes.PDF] + + + override suspend fun loadMetadata(mediaType: MediaType, inputFile: File): Metadata? { + check(mediaType in readableMimeTypes) + + return Metadata( + thumbnail = Image(inputFile), + attributes = readDocumentMetadata(context, mediaType, inputFile.toUri()).toSet() + ) + } + + override suspend fun removeMetadata( + mediaType: MediaType, + inputFile: File, + outputFile: File, + ): Boolean { + check(mediaType in writableMimeTypes) + + println(mediaType) + + when (mediaType) { + MediaTypes.OOXML_DOCUMENT -> removeOOXMLMetadata(inputFile, outputFile, MediaTypes.OOXML_DOCUMENT) + MediaTypes.OOXML_SHEET -> removeOOXMLMetadata(inputFile, outputFile, MediaTypes.OOXML_SHEET) + MediaTypes.MICROSOFT_WORD -> removeHWPFMetadata(inputFile, outputFile) + MediaTypes.MICROSOFT_EXCEL -> removeHSSFMetadata(inputFile, outputFile) + MediaTypes.OPENDOCUMENT_TEXT -> removeODTMetadata(inputFile, outputFile) + MediaTypes.OPENDOCUMENT_SPREADSHEET -> removeODSMetadata(inputFile, outputFile) + MediaTypes.PDF -> removePDFMetadata(inputFile, outputFile) + } + + return true + } + + private fun readDocumentMetadata(context: Context, mediaType: MediaType, uri: Uri): List { + val metadataList = mutableListOf() + + when (mediaType) { + MediaTypes.OOXML_DOCUMENT -> readOOXMLMetadata(context, uri, MediaTypes.OOXML_DOCUMENT, metadataList) + MediaTypes.OOXML_SHEET -> readOOXMLMetadata(context, uri, MediaTypes.OOXML_SHEET, metadataList) + MediaTypes.MICROSOFT_WORD -> readHWPFMetadata(context, uri, metadataList) + MediaTypes.MICROSOFT_EXCEL -> readHSSFMetadata(context, uri, metadataList) + MediaTypes.OPENDOCUMENT_TEXT, MediaTypes.OPENDOCUMENT_SPREADSHEET -> readODFMetadata(context, uri, metadataList) + MediaTypes.PDF -> readPDFMetadata(context, uri, metadataList) + else -> metadataList.add(Metadata.Attribute(label = Text("Unsupported file type"), icon = Image(R.drawable.ic_error), primaryValue = Text(mediaType.type))) + } + + return metadataList + } + + //docx, xlsx + private fun readOOXMLMetadata(context: Context, uri: Uri, mediaType: MediaType, metadataList: MutableList) { + context.contentResolver.openInputStream(uri)?.use { inputStream -> + val document = when(mediaType) { + MediaTypes.OOXML_DOCUMENT -> XWPFDocument(inputStream) + MediaTypes.OOXML_SHEET, MediaTypes.MICROSOFT_EXCEL -> XSSFWorkbook(inputStream) + else -> throw IllegalArgumentException("Unsupported file type") + } + + val properties = document.properties + val coreProperties = properties.coreProperties + val extendedProperties = properties.extendedProperties + + coreProperties.creator?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Author"), icon = Image(R.drawable.ic_author), primaryValue = Text(coreProperties.creator ))) + } + + coreProperties.lastModifiedByUser?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Last Modified By"), icon = Image(R.drawable.ic_edit), primaryValue = Text(coreProperties.lastModifiedByUser ))) + } + + + coreProperties.description?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Description"), icon = Image(R.drawable.ic_description), primaryValue = Text(coreProperties.description ))) + } + + coreProperties.subject?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Subject"), icon = Image(R.drawable.ic_subject), primaryValue = Text(coreProperties.subject ))) + } + + coreProperties.created?.let { + metadataList.add(Metadata.Attribute(label = Text("Created"), icon = Image(R.drawable.ic_calendar_today), primaryValue = Text(convertDate(coreProperties.created)))) + } + + coreProperties.modified?.let { + metadataList.add(Metadata.Attribute(label = Text("Modified"), icon = Image(R.drawable.ic_update), primaryValue = Text(convertDate(coreProperties.modified)))) + } + + coreProperties.lastPrinted?.let { + metadataList.add(Metadata.Attribute(label = Text("Last Printed"), icon = Image(R.drawable.ic_update), primaryValue = Text(convertDate(coreProperties.lastPrinted)))) + } + + coreProperties.revision?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Revision"), icon = Image(R.drawable.ic_history), primaryValue = Text(coreProperties.revision ))) + } + + coreProperties.keywords?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Keywords"), icon = Image(R.drawable.ic_label), primaryValue = Text(coreProperties.keywords ))) + } + + coreProperties.category?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Category"), icon = Image(R.drawable.ic_category), primaryValue = Text(coreProperties.category ))) + } + + coreProperties.contentStatus?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Content Status"), icon = Image(R.drawable.ic_info), primaryValue = Text(coreProperties.contentStatus ))) + } + + coreProperties.contentType?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Content Type"), icon = Image(R.drawable.ic_file_type), primaryValue = Text(coreProperties.contentType ))) + } + + extendedProperties.company?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Company"), icon = Image(R.drawable.ic_business), primaryValue = Text(extendedProperties.company ))) + } + + extendedProperties.manager?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Manager"), icon = Image(R.drawable.ic_supervisor_account), primaryValue = Text(extendedProperties.manager ))) + } + + document.close() + } + } + + private fun removeOOXMLMetadata(inputFile: File, outputFile: File, mediaType: MediaType){ + val document = when (mediaType) { + MediaTypes.OOXML_DOCUMENT -> XWPFDocument(FileInputStream(inputFile)) + MediaTypes.OOXML_SHEET, MediaTypes.MICROSOFT_EXCEL-> XSSFWorkbook(FileInputStream(inputFile)) + else -> throw IllegalArgumentException("Unsupported file format") + } + + val properties = document.properties + val coreProperties = properties.coreProperties + val extendedProperties = properties.extendedProperties + val date = getStartOfTimeDate() + + coreProperties.title = "" + coreProperties.creator = "" + coreProperties.lastModifiedByUser = "" + coreProperties.description = "" + coreProperties.setSubjectProperty("") + coreProperties.setCreated(Optional.of(date)) + coreProperties.setModified(Optional.of(date)) + coreProperties.setLastPrinted(Optional.of(date)) + coreProperties.revision = "" + coreProperties.keywords = "" + coreProperties.category = "" + coreProperties.contentStatus = "" + coreProperties.contentType = "" + extendedProperties.company = "" + extendedProperties.manager = "" + + FileOutputStream(outputFile).use { out -> document.write(out) } + document.close() + } + + //Doc + private fun readHWPFMetadata(context: Context, uri: Uri, metadataList: MutableList) { + context.contentResolver.openInputStream(uri)?.use { inputStream -> + val document = HWPFDocument(inputStream) + val summaryInformation = document.summaryInformation + val documentSummaryInformation = document.documentSummaryInformation + + summaryInformation.author?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Author"), icon = Image(R.drawable.ic_person), primaryValue = Text(summaryInformation.author ))) + } + + summaryInformation.subject?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Subject"), icon = Image(R.drawable.ic_subject), primaryValue = Text(summaryInformation.subject ))) + } + + summaryInformation.keywords?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Keywords"), icon = Image(R.drawable.ic_label), primaryValue = Text(summaryInformation.keywords ))) + } + + summaryInformation.comments?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Comments"), icon = Image(R.drawable.ic_comment), primaryValue = Text(summaryInformation.comments ))) + } + + summaryInformation.template?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Template"), icon = Image(R.drawable.ic_description), primaryValue = Text(summaryInformation.template ))) + } + + summaryInformation.revNumber?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Revision Number"), icon = Image(R.drawable.ic_history), primaryValue = Text(summaryInformation.revNumber ))) + } + + summaryInformation.lastAuthor?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Last Author"), icon = Image(R.drawable.ic_edit), primaryValue = Text(summaryInformation.lastAuthor ))) + } + + summaryInformation.applicationName?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Application Name"), icon = Image(R.drawable.ic_apps), primaryValue = Text(summaryInformation.applicationName ))) + } + + summaryInformation.createDateTime?.let { + metadataList.add(Metadata.Attribute(label = Text("Created Date"), icon = Image(R.drawable.ic_calendar_today), primaryValue = Text(convertDate(summaryInformation.createDateTime) ))) + } + + summaryInformation.lastSaveDateTime?.let { + metadataList.add(Metadata.Attribute(label = Text("Last Saved Date"), icon = Image(R.drawable.ic_update), primaryValue = Text(convertDate(summaryInformation.lastSaveDateTime)))) + } + + documentSummaryInformation.company?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Company"), icon = Image(R.drawable.ic_business), primaryValue = Text(documentSummaryInformation.company ))) + } + + documentSummaryInformation.category?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Category"), icon = Image(R.drawable.ic_category), primaryValue = Text(documentSummaryInformation.category ))) + } + + documentSummaryInformation.manager?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Manager"), icon = Image(R.drawable.ic_supervisor_account), primaryValue = Text(documentSummaryInformation.manager ))) + } + + document.close() + } + } + + private fun removeHWPFMetadata(inputFile: File, outputFile: File) { + val document = HWPFDocument(FileInputStream(inputFile)) + val summaryInformation = document.summaryInformation + val documentSummaryInformation = document.documentSummaryInformation + val date = getStartOfTimeDate() + + summaryInformation.title = "" + summaryInformation.author = "" + summaryInformation.subject = "" + summaryInformation.keywords = "" + summaryInformation.comments = "" + summaryInformation.template = "" + summaryInformation.revNumber = "" + summaryInformation.lastAuthor = "" + summaryInformation.applicationName = "" + summaryInformation.createDateTime = date + summaryInformation.lastSaveDateTime = date + documentSummaryInformation.company = "" + documentSummaryInformation.category = "" + documentSummaryInformation.manager = "" + + + FileOutputStream(outputFile).use { out -> document.write(out) } + document.close() + } + + //xls + private fun readHSSFMetadata(context: Context, uri: Uri, metadataList: MutableList) { + try { + context.contentResolver.openInputStream(uri)?.use { inputStream -> + val workbook = HSSFWorkbook(inputStream) + val summaryInformation = workbook.summaryInformation + val documentSummaryInformation = workbook.documentSummaryInformation + + summaryInformation.author?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Author"), icon = Image(R.drawable.ic_person), primaryValue = Text(summaryInformation.author))) + } + + summaryInformation.lastAuthor?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Last Author"), icon = Image(R.drawable.ic_edit), primaryValue = Text(summaryInformation.lastAuthor))) + } + + summaryInformation.subject?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Subject"), icon = Image(R.drawable.ic_subject), primaryValue = Text(summaryInformation.subject))) + } + + summaryInformation.keywords?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Keywords"), icon = Image(R.drawable.ic_label), primaryValue = Text(summaryInformation.keywords))) + } + + documentSummaryInformation.category?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Category"), icon = Image(R.drawable.ic_category), primaryValue = Text(documentSummaryInformation.category))) + } + + summaryInformation.comments?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Comments"), icon = Image(R.drawable.ic_comment), primaryValue = Text(summaryInformation.comments))) + } + + summaryInformation.template?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Template"), icon = Image(R.drawable.ic_description), primaryValue = Text(summaryInformation.template))) + } + + summaryInformation.revNumber?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Revision Number"), icon = Image(R.drawable.ic_history), primaryValue = Text(summaryInformation.revNumber))) + } + + summaryInformation.applicationName?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Application Name"), icon = Image(R.drawable.ic_apps), primaryValue = Text(summaryInformation.applicationName))) + } + + summaryInformation.createDateTime?.let { + metadataList.add(Metadata.Attribute(label = Text("Created Date"), icon = Image(R.drawable.ic_calendar_today), primaryValue = Text(convertDate(summaryInformation.createDateTime)))) + } + + summaryInformation.lastSaveDateTime?.let { + metadataList.add(Metadata.Attribute(label = Text("Last Saved Date"), icon = Image(R.drawable.ic_update), primaryValue = Text(convertDate(summaryInformation.lastSaveDateTime)))) + } + + summaryInformation.lastPrinted?.let { + metadataList.add(Metadata.Attribute(label = Text("Last Printed"), icon = Image(R.drawable.ic_print), primaryValue = Text(convertDate(summaryInformation.lastPrinted)))) + } + + documentSummaryInformation.company?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Company"), icon = Image(R.drawable.ic_business), primaryValue = Text(documentSummaryInformation.company))) + } + + documentSummaryInformation.manager?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Manager"), icon = Image(R.drawable.ic_supervisor_account), primaryValue = Text(documentSummaryInformation.manager))) + } + + workbook.close() + } + } catch (e: OfficeXmlFileException){ + readOOXMLMetadata(context, uri, MediaTypes.MICROSOFT_EXCEL, metadataList) + } + } + + private fun removeHSSFMetadata(inputFile: File, outputFile: File){ + try { + val workbook = HSSFWorkbook(FileInputStream(inputFile)) + val summaryInformation = workbook.summaryInformation + val documentSummaryInformation = workbook.documentSummaryInformation + val date = getStartOfTimeDate() + + summaryInformation.title = "" + summaryInformation.author = "" + summaryInformation.lastAuthor = "" + summaryInformation.subject = "" + summaryInformation.keywords = "" + documentSummaryInformation.category = "" + summaryInformation.comments = "" + summaryInformation.template = "" + summaryInformation.revNumber = "" + summaryInformation.applicationName = "" + summaryInformation.createDateTime = date + summaryInformation.lastSaveDateTime = date + summaryInformation.lastPrinted = date + documentSummaryInformation.company = "" + documentSummaryInformation.manager = "" + + FileOutputStream(outputFile).use { out -> workbook.write(out) } + workbook.close() + } catch (e: OfficeXmlFileException){ + removeOOXMLMetadata(inputFile, outputFile, MediaTypes.MICROSOFT_EXCEL) + } + } + + //Open office + private fun readODFMetadata(context: Context, uri: Uri, metadataList: MutableList) { + context.contentResolver.openInputStream(uri)?.use { inputStream -> + val document = Document.loadDocument(inputStream) + val metadata = document.officeMetadata + + metadata.initialCreator?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Initial Creator"), icon = Image(R.drawable.ic_person), primaryValue = Text(metadata.initialCreator))) + } + + metadata.creator?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Creator"), icon = Image(R.drawable.ic_edit), primaryValue = Text(metadata.creator))) + } + + metadata.editingCycles.toString().takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Editing Cycles"), icon = Image(R.drawable.ic_loop), primaryValue = Text(metadata.editingCycles.toString()))) + } + + metadata.printedBy?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Printed By"), icon = Image(R.drawable.ic_print), primaryValue = Text(metadata.printedBy))) + } + + metadata.printDate?.let { + metadataList.add(Metadata.Attribute(label = Text("Print Date"), icon = Image(R.drawable.ic_print), primaryValue = Text(convertDate(metadata.printDate.time)))) + } + + metadata.dcdate?.let { + metadataList.add(Metadata.Attribute(label = Text("DC Date"), icon = Image(R.drawable.ic_update), primaryValue = Text(convertDate(metadata.dcdate.time)))) + } + + metadata.creationDate?.let { + metadataList.add(Metadata.Attribute(label = Text("Created Date"), icon = Image(R.drawable.ic_calendar_today), primaryValue = Text(convertDate(metadata.creationDate.time)))) + } + + metadata.language?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Language"), icon = Image(R.drawable.ic_language), primaryValue = Text(metadata.language))) + } + + metadata.keywords.toString().takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Keywords"), icon = Image(R.drawable.ic_label), primaryValue = Text(metadata.keywords.toString()))) + } + + metadata.subject?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Subject"), icon = Image(R.drawable.ic_subject), primaryValue = Text(metadata.subject))) + } + + metadata.description?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Description"), icon = Image(R.drawable.ic_description), primaryValue = Text(metadata.description))) + } + + metadata.generator?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Generator"), icon = Image(R.drawable.ic_build), primaryValue = Text(metadata.generator))) + } + + document.close() + } + } + + private fun removeODSMetadata(inputFile: File, outputFile: File) { + val spreadsheet = SpreadsheetDocument.loadDocument(inputFile) + val metadata = spreadsheet.officeMetadata + val date = getStartOfTimeDate() + + metadata.title = "" + metadata.initialCreator = "" + metadata.creator = "" + metadata.editingCycles = 0 + metadata.printedBy = "" + metadata.printDate = date.toCalendar() + metadata.dcdate = date.toCalendar() + metadata.creationDate = date.toCalendar() + metadata.language = "" + metadata.keywords = emptyList() + metadata.subject = "" + metadata.description = "" + metadata.generator = "" + + spreadsheet.save(outputFile) + } + + private fun removeODTMetadata(inputFile: File, outputFile: File) { + val document = TextDocument.loadDocument(inputFile) + val metadata = document.officeMetadata + val date = getStartOfTimeDate() + + metadata.title = "" + metadata.initialCreator = "" + metadata.creator = "" + metadata.editingCycles = 0 + metadata.printedBy = "" + metadata.printDate = date.toCalendar() + metadata.dcdate = date.toCalendar() + metadata.creationDate = date.toCalendar() + metadata.language = "" + metadata.keywords = emptyList() + metadata.subject = "" + metadata.description = "" + metadata.generator = "" + + document.save(outputFile) + } + + + //PDF + private fun readPDFMetadata(context: Context, uri: Uri, metadataList: MutableList) { + context.contentResolver.openInputStream(uri)?.use { inputStream -> + val reader = PdfReader(inputStream) + val pdfDocument = PdfDocument(reader) + val info = pdfDocument.documentInfo + + info.author?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Author"), icon = Image(R.drawable.ic_person), primaryValue = Text(info.author))) + } + + info.subject?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Subject"), icon = Image(R.drawable.ic_subject), primaryValue = Text(info.subject))) + } + + info.keywords?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Keywords"), icon = Image(R.drawable.ic_label), primaryValue = Text(info.keywords))) + } + + info.creator?.takeIf { it.isNotBlank() }?.let { + metadataList.add(Metadata.Attribute(label = Text("Creator"), icon = Image(R.drawable.ic_build), primaryValue = Text(info.creator))) + } + + pdfDocument.close() + reader.close() + } + } + + private fun removePDFMetadata(inputFile: File, outputFile: File) { + val tempFile = File(outputFile.absolutePath + ".temp") + PdfDocument(PdfReader(inputFile), PdfWriter(tempFile)).use { pdfDoc -> + pdfDoc.documentInfo.apply { + author = "" + title = "" + subject = "" + keywords = "" + creator = "" + } + } + tempFile.renameTo(outputFile) + } + + private fun convertDate(date: Date): String { + val formatter = SimpleDateFormat("dd MMM yyyy, HH:mm:ss", Locale.getDefault()) + return formatter.format(date) + } + + private fun getStartOfTimeDate(): Date { + val calendar = GregorianCalendar(1970, 0, 1) + return calendar.time + } +} \ No newline at end of file diff --git a/app/src/main/java/rocks/poopjournal/metadataremover/metadata/handlers/VideoMetadataHandler.kt b/app/src/main/java/rocks/poopjournal/metadataremover/metadata/handlers/VideoMetadataHandler.kt index 2e0604c..2223f75 100644 --- a/app/src/main/java/rocks/poopjournal/metadataremover/metadata/handlers/VideoMetadataHandler.kt +++ b/app/src/main/java/rocks/poopjournal/metadataremover/metadata/handlers/VideoMetadataHandler.kt @@ -49,8 +49,7 @@ class VideoMetadataHandler(private val context: Context): MetadataHandler { inputFile: File, outputFile: File, ): Boolean { - - println(outputFile.absolutePath) + check(mediaType in writableMimeTypes) val command = "-y -i ${inputFile.absolutePath} -map 0 -map_metadata -1 -c copy ${outputFile.absolutePath}" val session = FFmpegKit.execute(command) diff --git a/app/src/main/java/rocks/poopjournal/metadataremover/model/util/SupportedTypes.kt b/app/src/main/java/rocks/poopjournal/metadataremover/model/util/SupportedTypes.kt index c885f36..724789b 100644 --- a/app/src/main/java/rocks/poopjournal/metadataremover/model/util/SupportedTypes.kt +++ b/app/src/main/java/rocks/poopjournal/metadataremover/model/util/SupportedTypes.kt @@ -2,5 +2,6 @@ package rocks.poopjournal.metadataremover.model.util enum class SupportedTypes { IMAGE, - VIDEO + VIDEO, + DOCUMENTS } \ No newline at end of file diff --git a/app/src/main/java/rocks/poopjournal/metadataremover/ui/MainActivity.kt b/app/src/main/java/rocks/poopjournal/metadataremover/ui/MainActivity.kt index 5136896..c3e6454 100644 --- a/app/src/main/java/rocks/poopjournal/metadataremover/ui/MainActivity.kt +++ b/app/src/main/java/rocks/poopjournal/metadataremover/ui/MainActivity.kt @@ -93,6 +93,16 @@ class MainActivity : AppCompatActivity(), OnLastItemClickedListener { } } + private val pickMultipleDocuments = + registerForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { uris -> + if (uris.isNotEmpty()) { + viewModel.getPicketUris(uris) + adapter.setUris(uris, SupportedTypes.DOCUMENTS) + currentSupportedType = SupportedTypes.DOCUMENTS + preparePager(true) + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) @@ -174,6 +184,7 @@ class MainActivity : AppCompatActivity(), OnLastItemClickedListener { showRestartConfirmationDialog(this@MainActivity, onConfirmed = { metadata.clear() viewModel.restart() + adapter.restart() binding.bottomSheet.listMetadata.apply { val adapter = adapter if (adapter is MetaAttributeAdapter) { @@ -300,10 +311,16 @@ class MainActivity : AppCompatActivity(), OnLastItemClickedListener { } override fun onLastItemClicked() { - if (currentSupportedType == SupportedTypes.IMAGE){ - launchPhotoPicker() - } else { - launchVideoPicker() + when (currentSupportedType) { + SupportedTypes.IMAGE -> { + launchPhotoPicker() + } + SupportedTypes.VIDEO -> { + launchVideoPicker() + } + else -> { + launchDocumentPicker() + } } } @@ -315,6 +332,20 @@ class MainActivity : AppCompatActivity(), OnLastItemClickedListener { pickMultipleVideos.launch(arrayOf("video/*")) } + private fun launchDocumentPicker(){ + val mimeTypes = arrayOf( + "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.ms-excel", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/vnd.oasis.opendocument.text", + "application/vnd.oasis.opendocument.spreadsheet", + "application/pdf" + ) + + pickMultipleDocuments.launch(mimeTypes) + } + private fun handleSendImage(intent: Intent) { (intent.parcelable(Intent.EXTRA_STREAM) as? Uri)?.let { uri -> val uris = arrayListOf() @@ -371,5 +402,10 @@ class MainActivity : AppCompatActivity(), OnLastItemClickedListener { launchVideoPicker() dialog.dismiss() } + + dialogBinding.documentSelection.setOnClickListener { + launchDocumentPicker() + dialog.dismiss() + } } } diff --git a/app/src/main/java/rocks/poopjournal/metadataremover/ui/adapter/PageAdapter.kt b/app/src/main/java/rocks/poopjournal/metadataremover/ui/adapter/PageAdapter.kt index 1358342..eb3e84d 100644 --- a/app/src/main/java/rocks/poopjournal/metadataremover/ui/adapter/PageAdapter.kt +++ b/app/src/main/java/rocks/poopjournal/metadataremover/ui/adapter/PageAdapter.kt @@ -10,6 +10,7 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageButton import android.widget.ImageView +import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide import rocks.poopjournal.metadataremover.R @@ -26,6 +27,9 @@ class PageAdapter : RecyclerView.Adapter() { val addNewBody: View = itemView.findViewById(R.id.addNewBody) val addNewButton : ImageButton = itemView.findViewById(R.id.addNewButton) val playIcon: ImageView = itemView.findViewById(R.id.playIcon) + val documentBackground: View = itemView.findViewById(R.id.documentBackground) + val documentIcon: ImageView = itemView.findViewById(R.id.documentIcon) + val documentType: TextView = itemView.findViewById(R.id.documentType) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PageViewHolder { @@ -39,6 +43,11 @@ class PageAdapter : RecyclerView.Adapter() { if (isLastItem(position)){ holder.addNewBody.visibility = View.VISIBLE holder.addNewButton.visibility = View.VISIBLE + + holder.documentBackground.visibility = View.GONE + holder.documentIcon.visibility = View.GONE + holder.documentType.visibility = View.GONE + holder.addNewBody.setOnClickListener { lastItemClickedListener?.onLastItemClicked() } @@ -53,14 +62,36 @@ class PageAdapter : RecyclerView.Adapter() { Glide.with(holder.imageView) .load(uri) .into(holder.imageView) - } else { + } else if (mediaType == SupportedTypes.VIDEO) { val thumbnail = getThumbnail(holder.imageView.context, uri) holder.imageView.setImageBitmap(thumbnail) holder.playIcon.visibility = View.VISIBLE + } else { + holder.documentBackground.visibility = View.VISIBLE + holder.documentIcon.visibility = View.VISIBLE + holder.documentType.visibility = View.VISIBLE + + val docType = getMimeTypeFromUri(holder.itemView.context, uri) + holder.documentType.text = docType } } } + private fun getMimeTypeFromUri(context: Context, uri: Uri): String { + val mimeType = context.contentResolver.getType(uri) ?: return "Unknown type" + + return when { + mimeType.startsWith("application/vnd.openxmlformats-officedocument.wordprocessingml.document") -> "DOCX" + mimeType.startsWith("application/msword") -> "DOC" + mimeType.startsWith("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") -> "XLSX" + mimeType.startsWith("application/vnd.ms-excel") -> "XLS" + mimeType.startsWith("application/vnd.oasis.opendocument.text") -> "ODT" + mimeType.startsWith("application/vnd.oasis.opendocument.spreadsheet") -> "ODS" + mimeType.startsWith("application/pdf") -> "PDF" + else -> "Other ($mimeType)" + } + } + override fun getItemCount(): Int { return imageUris.size } diff --git a/app/src/main/java/rocks/poopjournal/metadataremover/viewmodel/usecases/MetadataHandler.kt b/app/src/main/java/rocks/poopjournal/metadataremover/viewmodel/usecases/MetadataHandler.kt index 6f4013f..1fdb7c0 100644 --- a/app/src/main/java/rocks/poopjournal/metadataremover/viewmodel/usecases/MetadataHandler.kt +++ b/app/src/main/java/rocks/poopjournal/metadataremover/viewmodel/usecases/MetadataHandler.kt @@ -2,6 +2,7 @@ package rocks.poopjournal.metadataremover.viewmodel.usecases import android.content.Context import rocks.poopjournal.metadataremover.metadata.handlers.ApplyAllMetadataHandler +import rocks.poopjournal.metadataremover.metadata.handlers.DocumentMetadataHandler import rocks.poopjournal.metadataremover.metadata.handlers.ExifMetadataHandler import rocks.poopjournal.metadataremover.metadata.handlers.FirstMatchMetadataHandler import rocks.poopjournal.metadataremover.metadata.handlers.PngMetadataHandler @@ -17,7 +18,8 @@ class MetadataHandler @Inject constructor( ExifMetadataHandler(context), // DrewMetadataReader.toMetadataHandler(), PngMetadataHandler, - VideoMetadataHandler(context) + VideoMetadataHandler(context), + DocumentMetadataHandler(context) ) // , NopMetadataHandler // For testing purposes only. TODO Remove after testing. ) diff --git a/app/src/main/java/rocks/poopjournal/metadataremover/viewmodel/usecases/SaveFiles.kt b/app/src/main/java/rocks/poopjournal/metadataremover/viewmodel/usecases/SaveFiles.kt index 6ad40c1..a046bad 100644 --- a/app/src/main/java/rocks/poopjournal/metadataremover/viewmodel/usecases/SaveFiles.kt +++ b/app/src/main/java/rocks/poopjournal/metadataremover/viewmodel/usecases/SaveFiles.kt @@ -25,7 +25,7 @@ class SaveFiles @Inject constructor( val collection = when { mimeType?.startsWith("image/") == true -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI mimeType?.startsWith("video/") == true -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI - else -> throw IllegalArgumentException("Unsupported MIME type: $mimeType") + else -> MediaStore.Files.getContentUri("external") } val uriToInsert = contentResolver.insert(collection, contentValues) diff --git a/app/src/main/res/drawable/ic_apps.xml b/app/src/main/res/drawable/ic_apps.xml new file mode 100644 index 0000000..24a9f32 --- /dev/null +++ b/app/src/main/res/drawable/ic_apps.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_build.xml b/app/src/main/res/drawable/ic_build.xml new file mode 100644 index 0000000..cbc92c8 --- /dev/null +++ b/app/src/main/res/drawable/ic_build.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_business.xml b/app/src/main/res/drawable/ic_business.xml new file mode 100644 index 0000000..b0d35b6 --- /dev/null +++ b/app/src/main/res/drawable/ic_business.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_calendar_today.xml b/app/src/main/res/drawable/ic_calendar_today.xml new file mode 100644 index 0000000..45cefd6 --- /dev/null +++ b/app/src/main/res/drawable/ic_calendar_today.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_category.xml b/app/src/main/res/drawable/ic_category.xml new file mode 100644 index 0000000..76b250d --- /dev/null +++ b/app/src/main/res/drawable/ic_category.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_description.xml b/app/src/main/res/drawable/ic_description.xml new file mode 100644 index 0000000..25e9929 --- /dev/null +++ b/app/src/main/res/drawable/ic_description.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_document.xml b/app/src/main/res/drawable/ic_document.xml new file mode 100644 index 0000000..172f23c --- /dev/null +++ b/app/src/main/res/drawable/ic_document.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_edit.xml b/app/src/main/res/drawable/ic_edit.xml new file mode 100644 index 0000000..3c53db7 --- /dev/null +++ b/app/src/main/res/drawable/ic_edit.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_error.xml b/app/src/main/res/drawable/ic_error.xml new file mode 100644 index 0000000..aef9dd2 --- /dev/null +++ b/app/src/main/res/drawable/ic_error.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_factory.xml b/app/src/main/res/drawable/ic_factory.xml new file mode 100644 index 0000000..dcf30c7 --- /dev/null +++ b/app/src/main/res/drawable/ic_factory.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_file_type.xml b/app/src/main/res/drawable/ic_file_type.xml new file mode 100644 index 0000000..172f23c --- /dev/null +++ b/app/src/main/res/drawable/ic_file_type.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_info.xml b/app/src/main/res/drawable/ic_info.xml new file mode 100644 index 0000000..ef3a1fe --- /dev/null +++ b/app/src/main/res/drawable/ic_info.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_label.xml b/app/src/main/res/drawable/ic_label.xml new file mode 100644 index 0000000..02371e7 --- /dev/null +++ b/app/src/main/res/drawable/ic_label.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_language.xml b/app/src/main/res/drawable/ic_language.xml new file mode 100644 index 0000000..643d3fc --- /dev/null +++ b/app/src/main/res/drawable/ic_language.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_loop.xml b/app/src/main/res/drawable/ic_loop.xml new file mode 100644 index 0000000..a3180f8 --- /dev/null +++ b/app/src/main/res/drawable/ic_loop.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_print.xml b/app/src/main/res/drawable/ic_print.xml new file mode 100644 index 0000000..0ca3195 --- /dev/null +++ b/app/src/main/res/drawable/ic_print.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_subject.xml b/app/src/main/res/drawable/ic_subject.xml new file mode 100644 index 0000000..d5fb76b --- /dev/null +++ b/app/src/main/res/drawable/ic_subject.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_supervisor_account.xml b/app/src/main/res/drawable/ic_supervisor_account.xml new file mode 100644 index 0000000..516ced2 --- /dev/null +++ b/app/src/main/res/drawable/ic_supervisor_account.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_update.xml b/app/src/main/res/drawable/ic_update.xml new file mode 100644 index 0000000..72b030c --- /dev/null +++ b/app/src/main/res/drawable/ic_update.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/layout/image_child.xml b/app/src/main/res/layout/image_child.xml index 73b630b..c3b1652 100644 --- a/app/src/main/res/layout/image_child.xml +++ b/app/src/main/res/layout/image_child.xml @@ -43,5 +43,30 @@ android:background="@drawable/add_circle" android:layout_gravity="center"/> + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/type_selector_dialog.xml b/app/src/main/res/layout/type_selector_dialog.xml index 2fa68e5..b324bcd 100644 --- a/app/src/main/res/layout/type_selector_dialog.xml +++ b/app/src/main/res/layout/type_selector_dialog.xml @@ -12,7 +12,7 @@ android:layout_height="wrap_content" android:layout_margin="16dp" app:cardCornerRadius="8dp" - app:layout_constraintBottom_toTopOf="@id/pictureSelection" + app:layout_constraintBottom_toTopOf="@id/videoSelection" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> @@ -34,12 +34,11 @@ android:layout_height="wrap_content" android:layout_margin="16dp" app:cardCornerRadius="8dp" - app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintBottom_toTopOf="@id/documentSelection" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/pictureSelection"> - + + + + + + \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 966b534..9550145 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -38,7 +38,7 @@ object Versions { object Sdk { const val compile = 34 - const val min = 21 + const val min = 26 const val target = 34 }