/* * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.lemenzo.files import android.text.format.DateUtils import android.util.Log import java.io.File import java.io.FileNotFoundException import java.util.ArrayList import java.util.Locale /** * Provides support for document trash operations */ object TrashDocumentHelper { const val TRASH_LOCATION: String = ".trash-storage" private const val PREFIX_TRASHED: String = "trashed" private const val TAG: String = "TrashDocumentHelper" private const val DEFAULT_DURATION_TRASHED: Long = 30 * DateUtils.DAY_IN_MILLIS /** * Moves a file or directory to the trash. * *

The item is moved into a designated trash folder within the root and renamed to * include an expiration timestamp. * * @param originalFile The file or directory to trash. * @param rootDir The root directory where the trash folder is located. * @return A list with the trash directory and the newly trashed file. * @throws FileNotFoundException If creating the trash directory or moving the file fails. */ @Throws(FileNotFoundException::class) fun moveToTrash(originalFile: File, rootDir: File): List { val trashStorageDir = File(rootDir, TRASH_LOCATION) if (!trashStorageDir.exists()) { if (!trashStorageDir.mkdirs()) { Log.e( TAG, "Failed to create trash storage directory: " + trashStorageDir.path ) throw FileNotFoundException("Failed to create trash storage directory.") } } val dateExpires = (System.currentTimeMillis() + DEFAULT_DURATION_TRASHED) / 1000 val destinationFile = prepareDestinationFile( originalFile, trashStorageDir, rootDir, dateExpires ) if (!originalFile.renameTo(destinationFile)) { Log.e( TAG, "Failed to move file to trash: " + originalFile.path + " to " + destinationFile.path ) throw FileNotFoundException("Failed to trash document: $originalFile") } if (destinationFile.isDirectory) { prefixChildrenOnDisk(destinationFile, dateExpires) } val files: MutableList = ArrayList() files.add(trashStorageDir) files.add(destinationFile) return files } /** * Prepares the full destination path for the file within the trash directory. * * @param originalFile The file to be trashed. * @param trashBaseDirectory The base trash directory. * @param rootDir The root directory where the trash folder is located. * @param dateExpires The `dateExpires` timestamp (in seconds) of the item * @return The File object representing the final destination of the trashed item. * @throws IllegalStateException if the destination parent directory cannot be created. */ @Throws(IllegalStateException::class) private fun prepareDestinationFile( originalFile: File, trashBaseDirectory: File, rootDir: File, dateExpires: Long ): File { val updatedDisplayName = getTrashFileName(dateExpires, originalFile.name) var relPath = originalFile.absolutePath.substring(rootDir.path.length) if (relPath.startsWith(File.separator)) { relPath = relPath.substring(1) } var destParent = trashBaseDirectory if (relPath.isNotEmpty()) { val parentFile = File(relPath).parentFile if (parentFile != null) { destParent = File(trashBaseDirectory, parentFile.path) } } if (!destParent.mkdirs()) { check(destParent.exists()) { "Failed to create trash sub-directory: " + destParent.absolutePath } } return File(destParent, updatedDisplayName) } /** * Recursively prefixes the names of all children (files and directories) * within a given directory on disk, ensuring they reflect the trashed state. * This method is intended to be called on a directory that has just been * moved to trash, and its children need consistent naming. * * @param parentDir The directory whose children need to be prefixed. * @param parentTrashedDateExpires The `dateExpires` timestamp (in seconds) of the parent * trashed directory. Children will inherit this expiration. */ private fun prefixChildrenOnDisk(parentDir: File, parentTrashedDateExpires: Long) { val children = parentDir.listFiles() ?: return for (child in children) { val newChildName = getTrashFileName(parentTrashedDateExpires, child.name) val renamedChildFile = File(parentDir, newChildName) if (!child.renameTo(renamedChildFile)) { Log.w( TAG, "Failed to rename child: " + child.absolutePath + " to " + renamedChildFile.absolutePath ) } // If the renamedChildFile item is a directory, recurse into it if (renamedChildFile.isDirectory) { prefixChildrenOnDisk(renamedChildFile, parentTrashedDateExpires) } } } /** * Generates a filename for a trashed item. * * @param dateExpires The expiration timestamp of the item. * @param name The original name of the item. * @return The formatted filename to be used for the trashed item. */ private fun getTrashFileName(dateExpires: Long, name: String): String { return String.format( Locale.US, ".%s-%d-%s", PREFIX_TRASHED, dateExpires, name ) } }